The pack
mazeChase
function mazeChase(): Pack<{ mazeChase: typeof kit }>
Install the renderer-independent maze chase API.
Helpers
classicLevel
function classicLevel(level: number): MazeTuning
One row of the arcade's level table (Pittman, "The Pac-Man Dossier", table A.1): speeds as fractions of full speed, frightened time and flashes, Elroy thresholds, scatter/chase timings, fruit, house release counters and the release timer. Levels beyond 21 repeat level 21.
createMazeChase
function createMazeChase(config: MazeConfig)
Create a deterministic maze chase from an authored or generated definition.
gardenLevels
function gardenLevels(count = 12): MazeTuning[]
A gentler table for generated mazes: each level a little faster, with shorter frightened time.
generateMaze
function generateMaze(options: MazeGeneration = {})
Seeded connected maze: depth-first carve, loop openings, optional wrap tunnels and ghost house.
mazePreset
function mazePreset( kind: "classic" | "garden" = "classic", options: MazePresetOptions = {}, ): MazeConfig
classic: the arcade arrangement and level table (start level via level). garden: a seeded generated
maze (dimensions, ghosts, loops, tunnels and house from generation) regenerated every level, with its own
table. Any field can be overridden; both are ordinary MazeConfig data.
nextMazeLevel
function nextMazeLevel( game: MazeChase, definition: MazeConfig, ): MazeChase
Continue a finished board in a different definition (another authored maze, say), keeping score, lives and
extra-life progress. For the same definition, game.nextLevel() is enough; it runs automatically unless
autoAdvance is false.
resolveMazeLevel
function resolveMazeLevel( config: MazeConfig, level: number, ): ResolvedMaze
Merge one level-table entry over a definition. Pure; exported so a presenter can preview any level.
Classes
MazeChase
class MazeChase
Owns buffered maze movement, ghost AI, the house, collectibles, fruit, lives and level progression.
readonly config: MazeConfig— The definition as authored (all levels).cols: number— Maze width in tiles; set from the current level's map.rows: number— Maze height in tiles; set from the current level's map.constructor(cfg: MazeConfig)get state(): MazeState— A copy of the complete state.get definition(): ResolvedMaze— The definition in force for the current level (level table applied, generated maze included).get mode(): "scatter" | "chase"— Current scatter/chase mode (frightened is per ghost).levelFruit(level: number): { kind: string; score: number } | null— Fruit kind and score shown for a level, from the level table.input(d: Direction)— Buffer a direction; it is taken as soon as it is legal, including early/late cornering.setGate(id: string, open: boolean)— Open or close a gate; never closes a gate on an actor.target(id: string): Cell— The tile a ghost is currently steering toward (inspectable for debugging and tests).nextLevel()— Advance to the next level now: new level table entry (and new maze with a generator); score and lives carry over.tick(dt: number)— Fixed 120 Hz rules, independent of presentation frame cadence.drain()— Take and clear the pending events.snapshot()— The complete state and the unspent fixed-step time, to save.restore(snapshot: ReturnType<MazeChase["snapshot"]>)— Replaces the game with a saved snapshot. Throws if its status, ghosts, collectibles or numbers are invalid for the current level.reset()— Start the whole game again from the configured level.
Constants
CLASSIC_FULL_SPEED
const CLASSIC_FULL_SPEED: number
100% speed in the arcade: 75.75757625 pixels per second over 8-pixel tiles.
CLASSIC_LEVELS
const CLASSIC_LEVELS: readonly MazeTuning[]
The arcade's level table, levels 1..21 (the last repeats).
CLASSIC_MAZE
const CLASSIC_MAZE: readonly string[]
The arcade maze, 28x31 tiles (the 36-row screen less three score rows and two lives rows): 240 dots, four energizers, the wrap tunnel on row 14 and the ghost house with its door. Data, not an engine limit.
Types
GhostDefinition
interface GhostDefinition
One pursuer. Coordinates are tile centres; half-tile values (e.g. 13.5) are allowed for spawn points.
id: string— Unique name for the ghost, used bytarget, events, anchors and per-level patches; not empty.at: Cell— Spawn point. Inside a house (H) the ghost waits in the pen; elsewhere it starts roaming.home?: Cell— Where eaten eyes revive: a house slot, or a maze tile when there is no house. Defaultat(or the house home for a ghost spawned outside).direction?: Direction— Heading at the start; default left.personality: Personality— How it picks its target tile; seePersonality.scatter: Cell— Scatter target tile; may lie outside the board, as the arcade's corner targets do.releaseDots?: number— Personal dot counter limit before this ghost leaves the house.releaseAfterDeath?: number— Global dot counter value that releases this ghost after the player has lost a life.ahead?: number— Tiles ahead of the player forambush(default 4) andvector(default 2).nearDistance?: number—shydistance in tiles (default 8).anchor?: string—vectoranchor ghost id (default: the first ghost).speed?: number— Normal speed in tiles per second; replacesghostSpeedfor this ghost.elroy?: { remaining: number; speed: number }[]— "Cruise Elroy": once remaining dots fall toremaining, this ghost moves atspeedand ignores scatter. Later entries take priority when also satisfied.
GhostPhase
type GhostPhase = "pen" | "exiting" | "roaming" | "eyes" | "entering" | "reviving"
Ghost lifecycle. pen: waiting inside the house. exiting: following the scripted path out of the house.
roaming: in the maze (scatter, chase or frightened). eyes: eaten, routing back. entering: eyes
descending into the house. reviving: back home, about to leave again (or, with no house, to roam again).
GhostState
interface GhostState
Serializable ghost pose and lifecycle.
id: string— The ghost's id, matching aGhostDefinition.motion: GridWalkerState— The ghost's pose: position in tile units, heading and buffered turn, as for the grid walker.phase: GhostPhase— Where the ghost is in its lifecycle; seeGhostPhase.frightened: boolean— Whether the ghost is frightened now: it turns blue after an energizer and can be eaten.releaseCount: number— Personal dot counter.revive: number— Seconds left inreviving.planned: Direction— Direction chosen for the tile being entered (one-tile lookahead).reverse: boolean— A forced reversal is pending.exitReverse: boolean— The mode changed while inside the house, so this ghost leaves the other way.path: Cell[]— Remaining scripted waypoints for pen exit/entry.
MazeConfig
interface MazeConfig extends Omit<MazeTuning, "ghosts" | "fruit">
A complete authored maze game. Map characters: # wall, . dot, o energizer, space floor,
H ghost-house interior, = house door. Every count, position, speed, table and timing is data.
map: string[]— The maze as rows of text, all the same width, at most a million tiles in all.#wall,.dot,oenergizer, space floor,Hhouse interior,=house door.generatorreplaces it each level.player: Cell— Player start; half-tile x is allowed (classic 13.5).direction?: Direction— The player's heading at the start; default left.ghosts: GhostDefinition[]— The pursuers, each with its own start, scatter corner, personality and release rules; may be empty. Ids must be unique.house?: MazeHouse— The ghost house; required when any ghost starts on anHtile. Without one, ghosts revive at their home tile in the maze.portals?: { from: Cell; direction: Direction; to: Cell }[]— Directed wrap portals: leavingfromindirectionarrives atto. Actors slide off the tile edge and in at the far one.noUp?: Cell[]— Tiles where chasing/scattering ghosts may not choose up (frightened ghosts and eyes may).tunnelCells?: Cell[]— Tiles where ghosts slow totunnelSpeed.gates?: MazeGate[]— Barriers on open tiles that keys open andsetGatetoggles.keys?: MazeKey[]— Keys on open tiles, each opening named gates when the player enters its tile.fruit?: MazeFruit— Bonus fruit: where it appears, when and what it is worth.levels?: MazeTuning[]— Level table: level N uses entry min(N, length) - 1 merged over this config.generator?: MazeGeneration— Regenerate the maze for every level with these parameters (seed advances with the level).turnBefore?: number— Pre-turn window in tiles (< 0.5).turnAfter?: number— Post-turn window in tiles (< 0.5).arcadeCornering?: boolean— Swap pre/post windows when moving left or up, as the arcade does.upTargetBug?: boolean— Reproduce the arcade overflow: "ahead" targets moving up are also shifted left.lives?: number— Lives including the one in play.seed?: number— Seed for frightened-ghost choices and fruit timing; default 1. It is restored at the start of every life and level.pelletScore?: number— Points per dot; default 10.powerScore?: number— Points per energizer; default 50.ghostScores?: number[]— Score for the 1st, 2nd, ... ghost eaten on one energizer; the last entry repeats.pelletPause?: number— Freeze after eating a dot / energizer (the player only).powerPause?: number— Seconds the player freezes after eating an energizer; default 3/60.capturePause?: number— Freeze after eating a ghost (all but eyes).reviveSeconds?: number— Seconds an eaten ghost waits at home, revived, before it leaves again; default 0.25.startSeconds?: number— READY! time at the very start, and after a lost life or a new level.readySeconds?: number— READY! time after a lost life or a new level, in seconds; default 2.deathSeconds?: number— Total time from a fatal contact to the next READY!; the firstdeathFreezeSecondshold the scene still.deathFreezeSeconds?: number— Seconds at the start of the dying phase that hold the scene still. The rules only validate it; a presenter reads it fromdefinition(the gallery game uses 1 by default).levelCompleteSeconds?: number— Maze flash after the last dot, before the next level.autoAdvance?: boolean— Advance to the next level automatically after the flash (default true).fruitScoreSeconds?: number— How long an eaten fruit's score stays on screen.collisionRadius?: number— Combined player/ghost contact distance in tiles (default 0.5, about one shared tile).collision?: "distance" | "tile"—distance(default) or the arcade'stile(same rounded tile).extraLifeScore?: number— First extra life score; 0 disables.extraLifeEvery?: number— Further extra lives every N points after the first; 0 (default) awards only one.goal?: "clear" | "score"—clear(default) ends a level when every dot and energizer is eaten;scoreends it when the score reachestargetScore.targetScore?: number— Score that ends a level whengoalisscore; default 1000.level?: number— Starting level.
MazeEvent
interface MazeEvent
Ordered notifications for a renderer or audio adapter.
kind: | "pellet" | "power" | "ghost-eaten" | "fruit-spawn" | "fruit-eaten" | "fruit-expired" | "ghost-release" | "ghost-revived" | "mode" | "start" | "death" | "game-over" | "level-complete" | "level-start" | "extra-life" | "gate" | "portal"— What happened:pelletorpowereaten,ghost-eaten,fruit-spawn,fruit-eaten,fruit-expired,ghost-release,ghost-revived,mode(scatter and chase swapped),startof play,death,game-over,level-complete,level-start,extra-life,gateopened or closed, orportalcrossed.id?: string— Ghost id; omitted for the player.from?: Cell— Forportal: the tile the actor left.to?: Cell— Forportal: the tile it arrived at.score?: number— Points involved, forpellet,power,ghost-eatenandfruit-eaten.level?: number— The level, forstart,level-startandlevel-complete.
MazeFruit
interface MazeFruit
Bonus fruit: where, after how many eaten dots, what it is worth and how long it stays (a seeded value in seconds).
at: Cell— Where the fruit appears, in tiles; half-tile positions are allowed.thresholds: number[]— Dots eaten (energizers included) at which a fruit appears, one entry per fruit; whole numbers from 1, strictly increasing.score: number— Points for eating it.kind?: string— Name for a renderer to pick the picture by; defaultcherry.seconds?: [number, number]— Shortest and longest time it stays, in seconds; each fruit stays a seeded value between them. Default 9.33 and 10.
MazeGate
interface MazeGate
A controllable tile barrier with an independent ghost passage policy.
id: string— Unique name that keys andsetGateuse to open or close it.at: Cell— The tile it blocks, as column and row. It must be an open tile.open?: boolean— Whether it starts open; default false.ghostsPass?: boolean— Whether ghosts can pass it while it is closed; default false.
MazeGeneration
interface MazeGeneration
Seeded generated-maze parameters (see generateMaze). With MazeConfig.generator, each level gets a fresh maze.
seed?: number— Seed for the first level's maze; level N usesseed + N - 1. Default 1.cols?: number— Odd width in tiles, 9..199.rows?: number— Odd height in tiles, 9..199.ghostCount?: number— Ghosts to place in the generated maze, from 0 to 64. Default 2.loops?: number— Extra wall openings that turn the perfect maze into loops.tunnels?: number— Number of wrap-around tunnel rows (each opens both side walls).house?: boolean— Carve a central ghost house with a door (needs cols and rows >= 13).energizers?: number— Energizer count, placed in corners first.
MazeHouse
interface MazeHouse
The ghost house. Ghosts leave through exit, eyes descend to home and then to their own slot.
exit: Cell— The point just outside the door where ghosts join the maze. Half-tile x is allowed (classic 13.5).home: Cell— The house centre the eyes descend to.door?: Cell— The door tile (informational; the map's=tiles are the door).exitDirection?: Direction— Direction a ghost takes on leaving, unless the mode changed while it was inside (then the reverse). Default left.
MazeKey
interface MazeKey
An obtainable key that opens named gates and optionally awards points.
at: Cell— The tile the key lies on, as column and row; it is collected when the player enters it.gates: string[]— Ids of theMazeGates it opens when collected.score?: number— Points awarded when collected; default 0.
MazePhase
interface MazePhase
A scatter/chase phase. The last phase lasts for ever.
mode: "scatter" | "chase"—scatter: ghosts head for their scatter corners.chase: they pursue the player by their personalities.seconds: number— How long the phase lasts, in seconds; above 0.
MazePresetOptions
type MazePresetOptions = Partial<MazeConfig> & { generation?: MazeGeneration; }
Overrides plus, for the generated preset, the maze generator's own parameters.
MazeState
interface MazeState
Complete serializable board progress.
player: GridWalkerState— The player's pose: position in tile units, heading and buffered turn, as for the grid walker.playerMoving: boolean— Whether the player moved during the last tick (renderers stop the chomp when blocked).ghosts: GhostState[]— Every ghost's state, in the order ofconfig.ghosts.pellets: Cell[]— The dots still to eat, as tile columns and rows.powers: Cell[]— The energizers still to eat, as tile columns and rows.keys: MazeKey[]— The keys not yet collected.gates: Record<string, boolean>— Whether each gate is open, by gate id.score: number— Points scored in this game; it carries over between levels.lives: number— Lives left, including the one in play.eaten: number— Dots and energizers eaten on this level; the fruit thresholds count against it.dotsTotal: number— Dots and energizers the level started with.level: number— The level being played, from 1.time: number— Seconds of play so far, carried over between levels; it does not run while the game is over.modeIndex: number— Index into the scatter/chase schedule of the current phase.modeRemaining: number— Seconds left in the current scatter/chase phase.frightened: number— Frightened seconds left.flash: boolean— Frightened ghosts are currently in the white half of a flash.combo: number— Ghosts eaten on the current energizer; it picks the next entry ofghostScores.fruit: { kind: string; at: Cell; score: number; remaining: number } | null— The bonus fruit on the board, with its remaining seconds, or null.fruitIndex: number— How many of the fruit's thresholds have already been reached.fruitScore: { score: number; at: Cell; remaining: number } | null— An eaten fruit's score, shown where it was.capture: { id: string; score: number; at: Cell; remaining: number } | null— A ghost just eaten: everything but eyes is frozen and the score is shown in its place.status: MazeStatus— Where the round stands:ready,playing,dying,level-completeorlost.statusTime: number— Seconds since the status last changed.statusRemaining: number— Seconds left in a timed status (ready, dying, level-complete).playerPause: number— Seconds the player stays frozen after eating a dot or energizer.starvation: number— Seconds since the player last ate or a ghost was forced out; atstarvationSeconds(default 4) the next waiting ghost is forced out of the house.afterDeath: boolean— The global dot counter governs releases (after a lost life).globalDots: number— Dots eaten since the last lost life, while the global counter governs releases.elroyHold: boolean— Elroy is suspended after a lost life until the house empties.extraLives: number— Extra lives awarded so far in this game.random: number[]— The ghost-choice and fruit stream: four words of the kernel'screateRngstate, advanced in place.
MazeStatus
type MazeStatus = "ready" | "playing" | "dying" | "level-complete" | "lost"
Lifecycle of a round.
MazeTuning
interface MazeTuning
Everything a level table can change. Speeds are tiles per second, times are seconds.
playerSpeed?: number— Player speed in tiles per second; default 7.6.frightenedPlayerSpeed?: number— Player speed while ghosts are frightened.ghostSpeed?: number— Ghost speed in tiles per second; default 7.1. A ghost's ownspeedor a Cruise Elroy entry replaces it.frightenedSpeed?: number— Frightened ghost speed in tiles per second; default 4.7.tunnelSpeed?: number— Ghost speed ontunnelCells(the player is never slowed there).eyesSpeed?: number— Speed of eaten ghosts returning home, in tiles per second; default 18.9.houseSpeed?: number— Ghost speed inside the house and while leaving it.frightenedSeconds?: number— Frightened time after an energizer. 0 still reverses the ghosts but never turns them blue.flashes?: number— White flashes at the end of frightened time, eachflashPeriodseconds long.flashPeriod?: number— Seconds each white flash at the end of frightened time lasts; default 0.4.schedule?: MazePhase[]— The scatter and chase phases in order; the last lasts for ever. Default the arcade's: 7, 20, 7, 20, 5, 20, 5 seconds, then chase.starvationSeconds?: number— Seconds without eating that force the next waiting ghost out of the house.fruit?: { kind?: string; score?: number }— Fruit kind and score for this level (position and thresholds come fromMazeConfig.fruit).ghosts?: Record< string, Partial< Pick< GhostDefinition, "releaseDots" | "releaseAfterDeath" | "speed" | "elroy" > > >— Per-ghost patches for this level, by ghost id: release counters, speed, Elroy thresholds.
Personality
type Personality = "chase" | "ambush" | "vector" | "shy" | "random"
Target policies. chase targets the player's tile; ambush targets ahead tiles in front of the player;
vector doubles the vector from the anchor ghost to the tile ahead of the player; shy chases while
at least nearDistance tiles away and otherwise heads for its scatter target; random wanders as if
frightened.
ResolvedMaze
type ResolvedMaze = MazeConfig & { fruit?: MazeFruit }
The definition actually in force for one level: the config with its level-table entry applied.


