summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Kronqvist <joel.kronqvist@iki.fi>2025-11-03 23:10:04 +0200
committerJoel Kronqvist <joel.kronqvist@iki.fi>2025-11-03 23:10:04 +0200
commitd1c404fe8eac3c743004a9a48a683e9361c8f7b3 (patch)
treef5df16492fd5cfc3a2915c678306b53c212edb5e
parentef6abc27cec35e32acef66c5077ffcc6bedde983 (diff)
downloadSnakePuzzle-d1c404fe8eac3c743004a9a48a683e9361c8f7b3.tar.gz
SnakePuzzle-d1c404fe8eac3c743004a9a48a683e9361c8f7b3.zip
fix: added typing
-rw-r--r--Box.py2
-rw-r--r--Door.py23
-rw-r--r--Game.py124
-rw-r--r--GameView.py61
-rw-r--r--OptionalUtils.py19
-rw-r--r--PressurePlate.py16
-rw-r--r--Snake.py8
-rw-r--r--Trail.py10
-rw-r--r--Vec.py24
-rw-r--r--Walls.py18
-rwxr-xr-xmain.py27
11 files changed, 214 insertions, 118 deletions
diff --git a/Box.py b/Box.py
index 9641a44..5e06ad0 100644
--- a/Box.py
+++ b/Box.py
@@ -3,5 +3,5 @@ from Vec import Vec2
class Box:
- def __init__(self, pos: Vec2):
+ def __init__(self, pos: Vec2) -> None:
self.pos = pos
diff --git a/Door.py b/Door.py
index 111bb5a..6a53516 100644
--- a/Door.py
+++ b/Door.py
@@ -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
diff --git a/Game.py b/Game.py
index af9b233..7e5cb1d 100644
--- a/Game.py
+++ b/Game.py
@@ -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
diff --git a/Snake.py b/Snake.py
index 7127326..84f3505 100644
--- a/Snake.py
+++ b/Snake.py
@@ -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]
diff --git a/Trail.py b/Trail.py
index 08450d3..5dd024d 100644
--- a/Trail.py
+++ b/Trail.py
@@ -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()
diff --git a/Vec.py b/Vec.py
index 31a67da..a6e0dcd 100644
--- a/Vec.py
+++ b/Vec.py
@@ -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)
diff --git a/Walls.py b/Walls.py
index d591291..a365b89 100644
--- a/Walls.py
+++ b/Walls.py
@@ -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
diff --git a/main.py b/main.py
index a0659f3..688589c 100755
--- a/main.py
+++ b/main.py
@@ -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())