from typing import Optional, Union import Vec from OptionalUtils import exists from Snake import Snake from Vec import Vec2 import Vec from Box import Box from Door import Door from PressurePlate import PressurePlate from Trail import Trail from Walls import Walls Static = Union[PressurePlate, Trail, Door] class Level: def __init__(self, levelString: str): self.boxes: list[Box] = [] self.statics: list[Static] = [] self.walls = Walls.fromString(levelString) self.statics = [] self.levelIn: Optional['Vec2'] = None self.boxes = [] y = 0 for line in levelString.split('\n'): x = 0 for char in line: if char == "b": self.boxes.append(Box(Vec.Vec2(x, y))) elif char == "D": if self.doorAt(Vec.Vec2(x, y)) == None: self.statics.append(Door(Vec.Vec2(x, y))) elif char == "_": self.parseSwitchTrail(levelString, Vec.Vec2(x, y)) elif char == "I": self.levelIn = Vec.Vec2(x, y) x += 1 y += 1 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 doorAt(self, pos: Vec2) -> Optional[Door]: for static in self.statics: match static: case Door(): if static.pos == pos: return static return None def parseSwitchTrail(self, levelStr: str, platePos: Vec2) -> None: level = levelStr.split('\n') startSwitch = None if level[platePos.y][platePos.x] == "_": startSwitch = PressurePlate(platePos) self.statics.append(startSwitch) else: raise ValueError visited = [] def inner(pos: Vec2) -> Optional[Static]: visited.append(pos) if level[pos.y][pos.x] == "+": directions = filter(lambda p: not p in visited, map(lambda p: pos + p, [Vec.up, Vec.down, Vec.left, Vec.right] ) ) nextIter = filter(lambda x: x != None, map(lambda x: inner(x), directions)) nextPart = next(nextIter) #assert len(nextPart) == 1 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] ) ) 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: return door door = Door(pos) self.statics.append(door) return door return None inner(platePos) for static in self.statics: match static: case PressurePlate(): trailString = map( lambda x: x.pos.toString(), static._trails) def width(self) -> int: return self.walls.width() def height(self) -> int: return self.walls.height() def _movableSolidAt(self, pos: Vec2) -> bool: match self.snake: case Snake(): if pos in self.snake.cells: return True return pos in map(lambda box: box.pos, self.boxes) def closedDoorAt(self, pos: Vec2) -> bool: res = False for static in self.statics: match static: case Door(): res = res or (static.pos == pos and not static.isOpen()) return res def switchAt(self, pos: Vec2) -> Optional[PressurePlate]: for static in self.statics: match static: case PressurePlate(): if static.pos == pos: return static return None def enter(self, pos: Vec2, inDir: Vec2) -> bool: boxAt = next(filter(lambda box: box.pos == pos, self.boxes), None) if self.walls.wallAt(pos) or self.closedDoorAt(pos): return False elif exists(self.snake, lambda s: pos in s.cells): return False else: match boxAt: case None: return True case _: if self.enter(pos + inDir, inDir): boxAt.pos = pos + inDir return True else: return False def isCompleted(self) -> bool: snakeHead = self.snake.cells[0] return (snakeHead.x < 0 or snakeHead.x >= self.width()) or (snakeHead.y < 0 or snakeHead.y >= self.height()) class Game: """Class responsible for the main logic of a game. For a game, this will probably be a singleton.""" def __init__(self, levels: list[str]): self.levelIndex = 0 self.levels = levels self.level = Level(self.levels[self.levelIndex]) def nextLevel(self) -> None: self.levelIndex += 1 self.level = Level(self.levels[self.levelIndex]) def restartLevel(self) -> None: self.level = Level(self.levels[self.levelIndex]) def tick(self) -> None: match self.level.snake: case Snake(): self.level.snake.move() for static in self.level.statics: match static: case PressurePlate(): if static.isActive() and not self.level._movableSolidAt(static.pos): static.deactivate() elif (not static.isActive()) and self.level._movableSolidAt(static.pos): static.activate() for static in self.level.statics: match static: case Door(): if static.isOpen() and not static.isActive(): if not self.level._movableSolidAt(static.pos): static.close() def isLost(self) -> bool: match self.level.snake: case Snake(): return self.level.snake.hasCollided case _: return False