From a7cedad3b7ecf36352e46aad4a1e9c671d2cf08b Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 23 Aug 2026 13:18:09 +0200 Subject: [PATCH] feat: first playable prototype with tile art and entities --- game/__main__.py | 25 +++++++++ game/art.py | 143 +++++++++++++++++++++++++++++++++++++++++++++++ game/entities.py | 129 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 297 insertions(+) create mode 100644 game/__main__.py create mode 100644 game/art.py create mode 100644 game/entities.py diff --git a/game/__main__.py b/game/__main__.py new file mode 100644 index 0000000..a18f908 --- /dev/null +++ b/game/__main__.py @@ -0,0 +1,25 @@ +import argparse +import os +import sys + + +def main(argv): + parser = argparse.ArgumentParser(prog="game") + parser.add_argument("--screenshot") + parser.add_argument("--frames", type=int, default=60) + args = parser.parse_args(argv) + + if args.screenshot: + os.environ.setdefault("SDL_VIDEODRIVER", "dummy") + os.environ.setdefault("SDL_AUDIODRIVER", "dummy") + from .render.app import runScreenshot + + runScreenshot(args.screenshot, args.frames) + else: + from .render.app import runPlay + + runPlay() + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/game/art.py b/game/art.py new file mode 100644 index 0000000..f414629 --- /dev/null +++ b/game/art.py @@ -0,0 +1,143 @@ +import pygame + +from .constants import tileSize + +transparent = (0, 0, 0, 0) + +playerPalette = { + "r": (216, 40, 0), + "s": (252, 188, 140), + "h": (100, 52, 16), + "o": (32, 56, 236), + "k": (0, 0, 0), + "y": (247, 213, 29), +} + +playerPattern = [ + "................", + "....rrrrrr......", + "...rrrrrrrrr....", + "...hhhssskss....", + "..hshsssksss....", + "..hhhssssksss...", + "..hhsssssssss...", + "....sssssss.....", + "...rrrrrrrr.....", + "..rrrrrrrrrr....", + ".ssrroooorrss...", + ".ssrooooooorss..", + "...ooyooyoo.....", + "...oooooooo.....", + "...ooo..ooo.....", + "..hhh....hhh....", +] + +goombaPalette = { + "d": (60, 30, 10), + "m": (150, 75, 30), + "w": (255, 255, 255), + "k": (0, 0, 0), + "f": (20, 12, 8), +} + +goombaPattern = [ + "................", + ".....dddddd.....", + "...ddmmmmmmdd...", + "..dmmmmmmmmmmd..", + ".dmmmmmmmmmmmmd.", + ".dmmwwmmmmwwmmd.", + ".dmmwkmmmmwkmmd.", + ".dmmwwmmmmwwmmd.", + ".dmmmmmmmmmmmmd.", + ".dmmmmmmmmmmmmd.", + "..dmmmmmmmmmmd..", + "...dmmmmmmmmd...", + "....dddddddd....", + "...ff....ff.....", + "..ffff..ffff....", + "................", +] + + +def patternSurface(pattern, palette): + surf = pygame.Surface((tileSize, tileSize), pygame.SRCALPHA) + for y, line in enumerate(pattern): + for x, code in enumerate(line): + if code == ".": + continue + surf.set_at((x, y), palette[code]) + return surf + + +def coinSurface(): + surf = pygame.Surface((tileSize, tileSize), pygame.SRCALPHA) + pygame.draw.ellipse(surf, (200, 160, 20), (3, 1, 10, 14)) + pygame.draw.ellipse(surf, (247, 213, 29), (4, 2, 8, 12)) + pygame.draw.rect(surf, (255, 245, 170), (6, 4, 1, 8)) + return surf + + +def groundSurface(): + surf = pygame.Surface((tileSize, tileSize)) + surf.fill((198, 100, 29)) + pygame.draw.rect(surf, (222, 140, 80), (0, 0, tileSize, 4)) + pygame.draw.line(surf, (120, 60, 15), (0, tileSize - 1), (tileSize - 1, tileSize - 1)) + pygame.draw.line(surf, (120, 60, 15), (tileSize - 1, 0), (tileSize - 1, tileSize - 1)) + for spot in ((4, 8), (11, 6), (7, 12), (13, 11)): + surf.set_at(spot, (150, 74, 18)) + return surf + + +def brickSurface(): + surf = pygame.Surface((tileSize, tileSize)) + surf.fill((181, 94, 41)) + mortar = (74, 38, 18) + pygame.draw.line(surf, mortar, (0, 0), (tileSize - 1, 0)) + pygame.draw.line(surf, mortar, (0, 8), (tileSize - 1, 8)) + pygame.draw.line(surf, mortar, (8, 1), (8, 7)) + pygame.draw.line(surf, mortar, (0, 9), (0, tileSize - 1)) + pygame.draw.line(surf, mortar, (15, 9), (15, tileSize - 1)) + return surf + + +def platformSurface(): + surf = pygame.Surface((tileSize, tileSize)) + surf.fill((217, 164, 65)) + pygame.draw.rect(surf, (150, 100, 30), (0, 0, tileSize, tileSize), 1) + pygame.draw.rect(surf, (247, 213, 29), (2, 2, tileSize - 4, 3)) + for spot in ((2, 2), (13, 2), (2, 13), (13, 13)): + surf.set_at(spot, (90, 58, 18)) + return surf + + +def pipeSurface(top, leftSide): + surf = pygame.Surface((tileSize, tileSize)) + surf.fill((0, 168, 0)) + if leftSide: + pygame.draw.rect(surf, (128, 232, 128), (2, 0, 3, tileSize)) + pygame.draw.line(surf, (0, 96, 0), (0, 0), (0, tileSize - 1)) + else: + pygame.draw.rect(surf, (72, 200, 72), (8, 0, 3, tileSize)) + pygame.draw.rect(surf, (0, 96, 0), (13, 0, 3, tileSize)) + if top: + pygame.draw.rect(surf, (0, 200, 0), (0, 0, tileSize, 6)) + pygame.draw.rect(surf, (0, 96, 0), (0, 0, tileSize, 6), 1) + if leftSide: + pygame.draw.rect(surf, (160, 255, 160), (2, 1, 3, 4)) + return surf + + +def buildSprites(): + return { + "player": patternSurface(playerPattern, playerPalette), + "goomba": patternSurface(goombaPattern, goombaPalette), + "coin": coinSurface(), + "ground": groundSurface(), + "brick": brickSurface(), + "platform": platformSurface(), + "pipeTL": pipeSurface(True, True), + "pipeTR": pipeSurface(True, False), + "pipeBL": pipeSurface(False, True), + "pipeBR": pipeSurface(False, False), + } diff --git a/game/entities.py b/game/entities.py new file mode 100644 index 0000000..b88aa75 --- /dev/null +++ b/game/entities.py @@ -0,0 +1,129 @@ +import pygame + +from .constants import ( + tileSize, + gravity, + maxFallSpeed, + moveAccel, + moveSpeed, + friction, + jumpVelocity, + enemySpeed, +) + + +class Entity: + def __init__(self, x, y, width, height): + self.x = float(x) + self.y = float(y) + self.vx = 0.0 + self.vy = 0.0 + self.width = width + self.height = height + self.onGround = False + + def rect(self): + return pygame.Rect(int(self.x), int(self.y), self.width, self.height) + + def moveAndCollideX(self, world): + self.x += self.vx + box = self.rect() + for tile in world.solidCellsOverlapping(box): + if box.colliderect(tile): + if self.vx > 0: + self.x = tile.left - self.width + elif self.vx < 0: + self.x = tile.right + self.vx = 0.0 + box = self.rect() + + def moveAndCollideY(self, world): + self.y += self.vy + self.onGround = False + box = self.rect() + for tile in world.solidCellsOverlapping(box): + if box.colliderect(tile): + if self.vy > 0: + self.y = tile.top - self.height + self.onGround = True + elif self.vy < 0: + self.y = tile.bottom + self.vy = 0.0 + box = self.rect() + + if not self.onGround and self.vy >= 0: + footSensor = pygame.Rect(int(self.x), int(self.y) + self.height, self.width, 1) + for tile in world.solidCellsOverlapping(footSensor): + if footSensor.colliderect(tile): + self.y = tile.top - self.height + self.vy = 0.0 + self.onGround = True + break + + +class Player(Entity): + def __init__(self, x, y): + super().__init__(x, y, 12, 16) + self.facing = 1 + self.alive = True + + def update(self, world, moveInput): + if moveInput.left: + self.vx -= moveAccel + self.facing = -1 + if moveInput.right: + self.vx += moveAccel + self.facing = 1 + if not moveInput.left and not moveInput.right: + self.vx *= friction + if abs(self.vx) < 0.1: + self.vx = 0.0 + if self.vx > moveSpeed: + self.vx = moveSpeed + if self.vx < -moveSpeed: + self.vx = -moveSpeed + + if moveInput.jump and self.onGround: + self.vy = jumpVelocity + + self.vy += gravity + if self.vy > maxFallSpeed: + self.vy = maxFallSpeed + + self.moveAndCollideX(world) + self.moveAndCollideY(world) + + +class Enemy(Entity): + def __init__(self, x, y): + super().__init__(x, y, 16, 16) + self.direction = -1 + self.alive = True + + def update(self, world): + self.vx = enemySpeed * self.direction + self.moveAndCollideX(world) + if self.vx == 0.0: + self.direction *= -1 + + self.vy += gravity + if self.vy > maxFallSpeed: + self.vy = maxFallSpeed + self.moveAndCollideY(world) + + if self.onGround: + aheadX = self.x + (self.width if self.direction > 0 else -1) + aheadCol = int(aheadX) // tileSize + belowRow = int(self.y + self.height + 1) // tileSize + if not world.solidAt(aheadCol, belowRow): + self.direction *= -1 + + +class Coin: + def __init__(self, x, y): + self.x = x + self.y = y + self.collected = False + + def rect(self): + return pygame.Rect(self.x, self.y, tileSize, tileSize)