extends SceneTree const Model = preload("res://games/pacman/pacman_model.gd") const Maze = preload("res://games/pacman/pacman_maze.gd") const Rules = preload("res://games/pacman/pacman_rules.gd") var checks := 0 var failures := 0 func check(condition: bool, label: String) -> void: checks += 1 if not condition: failures += 1 push_error("FAIL: " + label) func _initialize() -> void: call_deferred("run") func run() -> void: for row in Maze.ROWS: check(row.length() == 28, "28-column maze") check(Maze.pellets().size() == 244, "244 pellets") var powers := 0 for value in Maze.pellets().values(): if value: powers += 1 check(powers == 4, "four energizers") var seen := {Maze.PLAYER_START: true} var queue := [Maze.PLAYER_START] while not queue.is_empty(): var tile: Vector2i = queue.pop_front() for direction in Maze.DIRS: var next: Vector2i = Maze.neighbor(tile, direction) if Maze.walkable(next) and not seen.has(next): seen[next] = true queue.append(next) for tile in Maze.pellets(): check(seen.has(tile), "reachable pellet %s" % tile) check(not Maze.walkable(Vector2i(13, 12)), "player cannot enter ghost door") check(Maze.neighbor(Vector2i(0,14), Vector2i.LEFT) == Vector2i(27,14), "tunnel wrap") check(Rules.target(0, Vector2i(5,5), Vector2i.UP, Vector2i.ZERO, Vector2i.ZERO) == Vector2i(5,5), "Blinky target") check(Rules.target(1, Vector2i(5,5), Vector2i.UP, Vector2i.ZERO, Vector2i.ZERO) == Vector2i(1,1), "Pinky upward quirk") check(Rules.target(2, Vector2i(5,5), Vector2i.RIGHT, Vector2i(3,5), Vector2i.ZERO) == Vector2i(11,5), "Inky target") check(Rules.target(3, Vector2i(5,5), Vector2i.RIGHT, Vector2i.ZERO, Vector2i(6,5)) == Rules.SCATTER[3], "Clyde retreats") var m = Model.new() m.start() m.update(5.0) check(m.state == "playing", "ready becomes playing") # Movement stops at the corridor wall, despite a held direction. for i in range(180): m._move_player(0.1) m.pellet_pause = 0.0 check(Maze.walkable(m.player.tile), "movement stays in corridor") # Energizer changes active ghosts; capture returns eyes and adds score. m.player = Maze.actor(Vector2i(1,3), Vector2i.DOWN) m._consume() check(m.fright_time > 0.0, "energizer starts frightened timer") var before: int = m.score m.ghosts[0].tile = m.player.tile m.ghosts[0].next = m.player.tile m.ghosts[0].progress = 0.0 m._check_collisions() check(m.ghosts[0].status == "eyes" and m.score == before + 200, "ghost capture score and eyes") m.reset() check(m.state == "idle" and m.score == 0 and m.dots.size() == 244, "reset restores board") m.start() m.state = "playing" m.dots = {m.player.tile: false} m._consume() check(m.state == "clear", "last pellet clears level") m.update(3.0) check(m.state == "won", "default room objective wins") m.boards_to_win = 0 m.start() m.state = "clear" m.phase_time = 0.0 m.update(0.1) check(m.level == 2 and m.state == "ready" and m.dots.size() == 244, "arcade progression") m.state = "clear" m.phase_time = 0.0 m.update(0.1) check(m.state == "intermission", "second board starts intermission") m.update(6.0) check(m.state == "ready" and m.level == 3, "intermission continues to next level") m.lives = 1 m.state = "dying" m.phase_time = 0.0 m.update(0.1) assert(m.state == "death_pop", "death pop before final loss") m.phase_time = 0.0 m.update(0.1) check(m.state == "lost", "final life loses") m.start() m.state = "playing" m.score_to_win = 100 m._add_score(100) check(m.state == "won", "score objective") # Traverse the entire board through the actual movement and consumption code. m.score_to_win = 0 m.boards_to_win = 1 m.start() m.state = "playing" var moves := 0 while not m.dots.is_empty() and moves < 3000: var frontier: Array = [m.player.tile] var parent := {m.player.tile: null} var destination: Vector2i = m.player.tile while not frontier.is_empty(): var current: Vector2i = frontier.pop_front() if m.dots.has(current): destination = current break for direction in Maze.DIRS: var next: Vector2i = Maze.neighbor(current, direction) if Maze.walkable(next) and not parent.has(next): parent[next] = current frontier.append(next) var path: Array[Vector2i] = [] while destination != m.player.tile: path.push_front(destination) destination = parent[destination] if path.is_empty(): m._consume() for tile in path: var direction: Vector2i = tile - m.player.tile if abs(direction.x) > 1: direction.x = -signi(direction.x) m.requested = direction for step in range(50): m.pellet_pause = 0.0 m._move_player(0.1) if m.player.tile == tile: break check(m.player.tile == tile, "buffered turn reaches next corridor tile") moves += 1 check(m.dots.is_empty() and m.state == "clear", "full maze cleared through movement") # Every returning ghost must be able to reach the house and emerge again. m.start() m.state = "playing" for tile in seen: var g: Dictionary = m.ghosts[0] g.tile = tile g.next = tile g.progress = 0.0 g.dir = Vector2i.LEFT g.status = "eyes" g.reverse = false for step in range(1600): m._move_ghost(0, 0.5) if g.status == "active": break check(g.status == "active", "eyes return from %s" % tile) # Headless random walk checks ghost movement stays in legal corridors. m.score_to_win = 0 m.start() m.state = "playing" for frame in range(12000): m.requested = Maze.DIRS[(frame / 30) % 4] m.update(1.0 / 60.0) if m.state == "lost": m.start() for g in m.ghosts: if g.status == "active": check(Maze.walkable(g.tile), "ghost stays on map") var game = load("res://games/pacman/Pacman.tscn").instantiate() root.add_child(game) var counts := {"start": 0, "won": 0, "lost": 0, "reset": 0} game.game_started.connect(func(): counts.start += 1) game.game_won.connect(func(): counts.won += 1) game.game_lost.connect(func(): counts.lost += 1) game.game_reset.connect(func(): counts.reset += 1) check(game.start_game(), "ArcadeGame starts") check(not game.start_game(), "duplicate start ignored") game.win_game() game.win_game() check(counts.won == 1 and counts.start == 1, "win event exactly once") game.reset_game() game.start_game() game.lose_game() game.lose_game() check(counts.lost == 1 and counts.reset == 1, "loss and reset events") var loop = game.audio._loop_stream("siren0", game.audio.stream("siren0_firstloop")) check(loop.loop_begin > 0 and loop.loop_end > loop.loop_begin, "intro plays once before PCM loop") game.audio.set_background("fright") game.reset_game() check(game.audio.background == "" and not game.audio.ambience.playing, "reset stops ambience") game.queue_free() print("PACMAN: %d checks, %d failures" % [checks, failures]) quit(1 if failures else 0)