diff options
| author | Joel Kronqvist <joel.kronqvist@iki.fi> | 2025-11-03 23:10:04 +0200 |
|---|---|---|
| committer | Joel Kronqvist <joel.kronqvist@iki.fi> | 2025-11-03 23:10:04 +0200 |
| commit | d1c404fe8eac3c743004a9a48a683e9361c8f7b3 (patch) | |
| tree | f5df16492fd5cfc3a2915c678306b53c212edb5e | |
| parent | ef6abc27cec35e32acef66c5077ffcc6bedde983 (diff) | |
| download | SnakePuzzle-d1c404fe8eac3c743004a9a48a683e9361c8f7b3.tar.gz SnakePuzzle-d1c404fe8eac3c743004a9a48a683e9361c8f7b3.zip | |
fix: added typing
| -rw-r--r-- | Box.py | 2 | ||||
| -rw-r--r-- | Door.py | 23 | ||||
| -rw-r--r-- | Game.py | 124 | ||||
| -rw-r--r-- | GameView.py | 61 | ||||
| -rw-r--r-- | OptionalUtils.py | 19 | ||||
| -rw-r--r-- | PressurePlate.py | 16 | ||||
| -rw-r--r-- | Snake.py | 8 | ||||
| -rw-r--r-- | Trail.py | 10 | ||||
| -rw-r--r-- | Vec.py | 24 | ||||
| -rw-r--r-- | Walls.py | 18 | ||||
| -rwxr-xr-x | main.py | 27 |
11 files changed, 214 insertions, 118 deletions
@@ -3,5 +3,5 @@ from Vec import Vec2 class Box: - def __init__(self, pos: Vec2): + def __init__(self, pos: Vec2) -> None: self.pos = pos @@ -1,24 +1,35 @@ +from __future__ import annotations +from Vec import Vec2 + + class Door: - def __init__(self, pos, isOpen = False, isActive = False): + def __init__( + self, + pos: Vec2, + isOpen: bool = False, + isActive: bool = False + ) -> None: self.pos = pos self._open = isOpen self._active = isActive - def isOpen(self): return self._open + def isOpen(self) -> bool: + return self._open - def isActive(self): return self._active + def isActive(self) -> bool: + return self._active - def activate(self): + def activate(self) -> None: self._open = True self._active = True - def deactivate(self): + def deactivate(self) -> None: self._active = False - def close(self): + def close(self) -> None: self._open = False @@ -1,4 +1,9 @@ +from typing import Optional, Union + +import Vec + +from OptionalUtils import exists from Snake import Snake from Vec import Vec2 import Vec @@ -7,27 +12,28 @@ from Door import Door from PressurePlate import PressurePlate from Trail import Trail from Walls import Walls +Static = Union[PressurePlate, Trail, Door] + class Game: """Class responsible for the main logic of a game. For a game, this will probably be a singleton.""" - def __init__(self, levels): - self.snake = None - self.boxes = [] - self.walls = None - self.statics = [] - self.levelIn = None + def __init__(self, levels: list[str]): + self.snake: Optional[Snake] = None + self.boxes: list[Box] = [] + self.statics: list[Static] = [] + self.walls: Walls = Walls.empty() + self.levelIn: Optional['Vec2'] = None self.level = -1 self.levels = [] self.levels = levels self.nextLevel() - def nextLevel(self): + def nextLevel(self) -> None: self.level += 1 - self.walls = Walls.fromString(self.levels[self.level]) self.snake = None self.statics = [] @@ -38,24 +44,27 @@ class Game: x = 0 for char in line: if char == "b": - self.boxes.append(Box(Vec2(x, y))) + self.boxes.append(Box(Vec.Vec2(x, y))) elif char == "D": - if self.doorAt(Vec2(x, y)) == None: - self.statics.append(Door(Vec2(x, y))) + if self.doorAt(Vec.Vec2(x, y)) == None: + self.statics.append(Door(Vec.Vec2(x, y))) elif char == "_": - self.parseSwitchTrail(self.levels[self.level], Vec2(x, y)) + self.parseSwitchTrail(self.levels[self.level], Vec.Vec2(x, y)) elif char == "I": - self.levelIn = Vec2(x, y) + self.levelIn = Vec.Vec2(x, y) x += 1 y += 1 - self.snake = Snake([self.levelIn, self.levelIn, self.levelIn, self.levelIn], self) + match self.levelIn: + case None: raise ValueError("level must have entrance") + case Vec2: self.snake = Snake([self.levelIn, self.levelIn, self.levelIn, self.levelIn], self) + self.statics.append(Door(self.levelIn)) self.snake.heading = Vec.up - def parseSwitchTrail(self, level: str, platePos: Vec2): - level = level.split('\n') + def parseSwitchTrail(self, levelStr: str, platePos: Vec2) -> None: + level = levelStr.split('\n') startSwitch = None if level[platePos.y][platePos.x] == "_": startSwitch = PressurePlate(platePos) @@ -64,7 +73,7 @@ class Game: raise ValueError visited = [] - def inner(pos: Vec2): + def inner(pos: Vec2) -> Optional[Static]: visited.append(pos) if level[pos.y][pos.x] == "+": directions = filter(lambda p: not p in visited, @@ -72,20 +81,27 @@ class Game: [Vec.up, Vec.down, Vec.left, Vec.right] ) ) - nextPart = filter(lambda x: x != None, map(lambda x: inner(x), directions)) + nextIter = filter(lambda x: x != None, map(lambda x: inner(x), directions)) + nextPart = next(nextIter) #assert len(nextPart) == 1 - trail = Trail(pos, next(nextPart)) - self.statics.append(trail) - return trail + match nextPart: + case Trail() | Door(): + trail = Trail(pos, nextPart) + self.statics.append(trail) + return trail + case _: raise ValueError(f"trails must be followed by doors or trails, not {nextPart}") elif level[pos.y][pos.x] == "_" and pos == platePos: directions = filter(lambda p: not p in visited, map(lambda p: pos + p, [Vec.up, Vec.down, Vec.left, Vec.right] ) ) - nextPart = filter(lambda x: x != None, map(lambda x: inner(x), directions)) - for trail in nextPart: - startSwitch.addTrail(trail) + nextIter = filter(lambda x: x != None, map(lambda x: inner(x), directions)) + for tr in nextIter: + match tr: + case Door() | Trail(): startSwitch.addTrail(tr) + case _: raise ValueError(f"plates must be followed by doors or trails, not {tr}") + elif level[pos.y][pos.x] == "D": door = self.doorAt(pos) if door != None: @@ -94,6 +110,8 @@ class Game: door = Door(pos) self.statics.append(door) return door + return None + inner(platePos) for static in self.statics: match static: @@ -101,20 +119,28 @@ class Game: trailString = map( lambda x: x.pos.toString(), static._trails) - def width(self): return self.walls.width() - def height(self): return self.walls.height() + def width(self) -> int: + return self.walls.width() + def height(self) -> int: + return self.walls.height() def _movableSolidAt(self, pos: Vec2) -> bool: - return (pos in self.snake.cells) or (pos in map(lambda box: box.pos, self.boxes)) - - def tick(self): - self.snake.move() - - lastSnakeCell = self.snake.cells[0]#[len(self.snake.cells) - 1] - if (lastSnakeCell.x < 0 or lastSnakeCell.x >= self.width()) or (lastSnakeCell.y < 0 or lastSnakeCell.y >= self.height()): - self.nextLevel() - return + match self.snake: + case Snake(): + if pos in self.snake.cells: + return True + return pos in map(lambda box: box.pos, self.boxes) + + def tick(self) -> None: + match self.snake: + case Snake(): + self.snake.move() + + lastSnakeCell = self.snake.cells[0]#[len(self.snake.cells) - 1] + if (lastSnakeCell.x < 0 or lastSnakeCell.x >= self.width()) or (lastSnakeCell.y < 0 or lastSnakeCell.y >= self.height()): + self.nextLevel() + return for static in self.statics: match static: @@ -131,11 +157,13 @@ class Game: static.close() - def isLost(self): - return self.snake.hasCollided + def isLost(self) -> bool: + match self.snake: + case Snake(): return self.snake.hasCollided + case _: return False - def doorAt(self, pos: Vec2): + def doorAt(self, pos: Vec2) -> Optional[Door]: for static in self.statics: match static: case Door(): @@ -152,7 +180,7 @@ class Game: res = res or (static.pos == pos and not static.isOpen()) return res - def switchAt(self, pos: Vec2): + def switchAt(self, pos: Vec2) -> Optional[PressurePlate]: for static in self.statics: match static: case PressurePlate(): @@ -165,14 +193,14 @@ class Game: boxAt = next(filter(lambda box: box.pos == pos, self.boxes), None) if self.walls.wallAt(pos) or self.closedDoorAt(pos): return False - elif pos in self.snake.cells: + elif exists(self.snake, lambda s: pos in s.cells): return False - elif boxAt != None: - if self.enter(pos + inDir, inDir): - boxAt.pos = pos + inDir - return True - else: - return False else: - return True - + match boxAt: + case None: return True + case _: + if self.enter(pos + inDir, inDir): + boxAt.pos = pos + inDir + return True + else: + return False diff --git a/GameView.py b/GameView.py index 10fba7b..0303445 100644 --- a/GameView.py +++ b/GameView.py @@ -1,16 +1,19 @@ +from typing import Optional +from Vec import Vec2 from Game import Game from Box import Box from Door import Door from PressurePlate import PressurePlate from Trail import Trail +from pygame import Surface import pygame class GameView: - def __init__(self, timeBased = False): + def __init__(self, timeBased: bool = True): self.timeBased = timeBased self.game = Game([ @@ -91,21 +94,24 @@ class GameView: self._tickTime = 700 - self._previousTick = None + self._previousTick = -10*self._tickTime - self.nextControlDirection = None + self.nextControlDirection: Optional[Vec2] = None - def isRunning(self): return not self.game.isLost() + def isRunning(self) -> bool: + return not self.game.isLost() - def width(self): return self.game.width() * self.cellWidth + def width(self) -> int: + return self.game.width() * self.cellWidth - def height(self): return self.game.height() * self.cellWidth + def height(self) -> int: + return self.game.height() * self.cellWidth - def render(self, surface): + def render(self, surface: Surface) -> None: surface.fill("black") @@ -130,10 +136,13 @@ class GameView: for box in self.game.boxes: self._drawBoxOfColor(surface, "brown", box.pos.x, box.pos.y) - for cell in self.game.snake.cells: - self._drawBoxOfColor(surface, "red", cell.x, cell.y) + match self.game.snake: + case None: raise ValueError("snake not found") + case _: + for cell in self.game.snake.cells: + self._drawBoxOfColor(surface, "red", cell.x, cell.y) - def _drawBoxOfColor(self, surface, color, x, y): + def _drawBoxOfColor(self, surface: Surface, color: str, x: int, y: int) -> None: pygame.draw.rect(surface, color, pygame.Rect( x*self.cellWidth, y*self.cellWidth, @@ -142,14 +151,28 @@ class GameView: )) - def update(self, time): - if self.timeBased and ((self._previousTick == None) or (self._previousTick + self._tickTime <= time)): + def update(self, time: int) -> None: + if self.timeBased and (self._previousTick + self._tickTime <= time): self._previousTick = time - if self.nextControlDirection != None: - self.game.snake.heading = self.nextControlDirection - self.nextControlDirection = None - self.game.tick() - elif (not self.timeBased) and self.nextControlDirection != None: - self.game.snake.heading = self.nextControlDirection - self.nextControlDirection = None + match self.nextControlDirection: + case None: return None + case _: + match self.game.snake: + case None: return None + case _: + self.game.snake.heading = self.nextControlDirection + self.nextControlDirection = None self.game.tick() + else: + match self.nextControlDirection: + case None: return None + case _: + self._previousTick = time + match self.game.snake: + case None: return None + case _: + self.game.snake.heading = self.nextControlDirection + self.nextControlDirection = None + self.game.tick() + + diff --git a/OptionalUtils.py b/OptionalUtils.py new file mode 100644 index 0000000..51828bb --- /dev/null +++ b/OptionalUtils.py @@ -0,0 +1,19 @@ + +from typing import Optional, TypeVar, Callable + + + +T = TypeVar('T') +def exists(opt: Optional[T], cond: Callable[[T], bool]) -> bool: + match opt: + case None: + return False + case _: + return cond(opt) + +def foreach(opt: Optional[T], call: Callable[[T], None]) -> None: + match opt: + case None: + return None + case _: + return call(opt) diff --git a/PressurePlate.py b/PressurePlate.py index acc0d42..fb3a938 100644 --- a/PressurePlate.py +++ b/PressurePlate.py @@ -1,28 +1,32 @@ +from Vec import Vec2 +from Trail import Trail +from Door import Door +from typing import Union class PressurePlate: - def __init__(self, pos): + def __init__(self, pos: Vec2) -> None: self.pos = pos - self._trails = [] + self._trails: list[Union[Trail, Door]] = [] self._isActive = False - def addTrail(self, trail): + def addTrail(self, trail: Union[Trail, Door]) -> None: self._trails.append(trail) - def isActive(self): + def isActive(self) -> bool: return self._isActive - def activate(self): + def activate(self) -> None: for trail in self._trails: trail.activate() self._isActive = True - def deactivate(self): + def deactivate(self) -> None: for trail in self._trails: trail.deactivate() self._isActive = False @@ -6,15 +6,13 @@ import Game class Snake: - def __init__(self, cells: list[Vec2], game: Game): - self.cells = [] + def __init__(self, cells: list[Vec2], game: "Game.Game") -> None: self.heading = Vec.up - self.game = None self.hasCollided = False self.cells = cells self.game = game - def move(self): + def move(self) -> None: nextPos = self.cells[0] + self.heading last = self.cells.pop() if self.game.enter(nextPos, self.heading): @@ -23,5 +21,5 @@ class Snake: self.cells.append(last) self.hasCollided = True - def head(self): + def head(self) -> Vec2: return self.cells[0] @@ -1,10 +1,12 @@ +from typing import Union + from Vec import Vec2 -import Trail +from Door import Door class Trail: - def __init__(self, pos: Vec2, nextTrail: Trail): + def __init__(self, pos: Vec2, nextTrail: Union['Trail', 'Door']): self._isOn = False self.pos = pos self._nextTrail = nextTrail @@ -14,11 +16,11 @@ class Trail: return self._isOn - def activate(self): + def activate(self) -> None: self._isOn = True self._nextTrail.activate() - def deactivate(self): + def deactivate(self) -> None: self._isOn = False self._nextTrail.deactivate() @@ -1,23 +1,27 @@ +from __future__ import annotations + class Vec2: - def __init__(self, x, y): + def __init__(self, x: int, y: int) -> None: self.x = x self.y = y - def __add__(self, other): + def __add__(self, other: 'Vec2') -> 'Vec2': return Vec2(self.x + other.x, self.y + other.y) - def __eq__(self, other): - return (other != None) and (self.x == other.x) and (self.y == other.y) + def __eq__(self, other: object) -> bool: + match other: + case Vec2(): return self.x == other.x and self.y == other.y + case _: return False - def neg(self): + def neg(self) -> 'Vec2': return Vec2(-self.x, -self.y) - def toString(self): + def toString(self) -> str: return f"({self.x}, {self.y})" -right = Vec2(1, 0) -up = Vec2(0, -1) -left = Vec2(-1, 0) -down = Vec2(0, 1) +right: Vec2 = Vec2(1, 0) +up: Vec2 = Vec2(0, -1) +left: Vec2 = Vec2(-1, 0) +down: Vec2 = Vec2(0, 1) @@ -8,11 +8,11 @@ class Walls: Useful as a wrapper to later increase performance of these operations.""" - def __init__(self, walls): - _walls = [] + def __init__(self, walls: list[Vec2]) -> None: self._walls = walls - def fromString(wallString): + @staticmethod + def fromString(wallString: str) -> 'Walls': walls = [] y = 0 for line in wallString.split('\n'): @@ -24,15 +24,19 @@ class Walls: y += 1 return Walls(walls) - def walls(self): + @staticmethod + def empty() -> 'Walls': + return Walls([]) + + def walls(self) -> list[Vec2]: return self._walls - def wallAt(self, pos): + def wallAt(self, pos: Vec2) -> bool: return (pos in self._walls) - def width(self): + def width(self) -> int: return max(self._walls, key=lambda p: p.x).x + 1 - def height(self): + def height(self) -> int: return max(self._walls, key=lambda p: p.y).y + 1 @@ -15,18 +15,21 @@ running = True while running: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - running = False - elif event.type == pygame.KEYDOWN: - if event.key == pygame.K_w and view.game.snake.heading != Vec.up.neg(): - view.nextControlDirection = Vec.up - elif event.key == pygame.K_a and view.game.snake.heading != Vec.left.neg(): - view.nextControlDirection = Vec.left - elif event.key == pygame.K_r and view.game.snake.heading != Vec.down.neg(): - view.nextControlDirection = Vec.down - elif event.key == pygame.K_s and view.game.snake.heading != Vec.right.neg(): - view.nextControlDirection = Vec.right + match view.game.snake: + case None: raise ValueError("no snake") + case _: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_w and view.game.snake.heading != Vec.up.neg(): + view.nextControlDirection = Vec.up + elif event.key == pygame.K_a and view.game.snake.heading != Vec.left.neg(): + view.nextControlDirection = Vec.left + elif event.key == pygame.K_r and view.game.snake.heading != Vec.down.neg(): + view.nextControlDirection = Vec.down + elif event.key == pygame.K_s and view.game.snake.heading != Vec.right.neg(): + view.nextControlDirection = Vec.right if view.isRunning(): view.update(pygame.time.get_ticks()) |
