Files
room_glitch/tests/test_pacman.gd
T

181 lines
6.6 KiB
GDScript

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)