> **Objective:** Make all four ghosts leave the pen and roam the maze by aiming them at the door while inside it and releasing them one at a time, 60 frames apart.
## Why this spec exists
SPEC 01 replaced the two MVP ghosts with the four classic archetypes. During implementation it was verified — by simulating `maze.js` and `game.js` outside the browser — that **none of the four ghosts ever leaves the pen**. Blinky walks a closed 6-cell loop forever:
The cause is that Pac-Man starts at `(13,23)`, directly below the pen, and the pen's only exit is the door above. With greedy Manhattan pursuit, `up` is always the worst-scoring direction while the ghost is inside: it moves away from row 23 and costs `+1`. Every lateral or downward move scores strictly better, so the door is never chosen. The pen is a local minimum for any pursuer.
This bug predates SPEC 01. The original `hunter` ghost was trapped identically (12 cells visited, never escaped); the MVP's `random` ghost escaped by luck and masked the problem. SPEC 01 removed the `random` kind, which removed the only accidental escape route and made all four ghosts stuck.
Two statements in SPEC 01 are therefore falsified by measurement:
- Its acceptance criterion *"Within a few seconds of starting, all four ghosts have left the pen and are roaming the maze"* is unreachable with the algorithm it specifies.
- Its risk mitigation — *"every ghost can leave through the door at any intersection … a stuck ghost still picks a valid exit"* — assumes that an available exit gets chosen. Under greedy scoring, availability is not selection.
This spec fixes the defect without touching the four archetypes, and adds the staged exit that SPEC 01 deliberately deferred to a future spec.
## Scope
**In:**
- A `PEN` box constant in `src/js/maze.js` describing the pen interior **and** the two door cells, plus a `PEN_DOOR` cell.
- One branch at the top of `ghostTarget` (`src/js/game.js`): while a ghost is inside the `PEN` box, its target is the door cell instead of its archetype target.
- A `frame` counter in the game state, incremented by `update`.
- A release gate: the ghost at index `i` does not move until `game.frame >= i * GHOST_RELEASE_FRAMES`.
-`resetPositions` resets `game.frame` to `0`, so the stagger repeats after a death.
**Out of scope (for future specs):**
- Power-pellets, frightened mode, blue ghosts.
- Pathfinding beyond the next intersection (no A*, no BFS).
- Any visual animation of the pen: a door that lifts, ghosts bobbing inside. This spec only gates movement.
- Scatter/chase phase timers and per-ghost scatter tiles.
- Per-ghost speed.
- Reintroducing randomness anywhere. The game stays fully deterministic.
- Ghosts returning to the pen once they have left it.
- The index-based colour-to-kind mapping fragility in `render.js`, observed during SPEC 01.
- Sounds, levels beyond the first, high-scores, settings.
## Data model
```js
// src/js/maze.js
// Box of the pen. Deliberately includes the two door cells at row 12.
constPEN={x0:11,x1:16,y0:12,y1:15};
constPEN_DOOR={x:13,y:12};
// src/js/game.js
constGHOST_RELEASE_FRAMES=60;// 1 s at 60 fps between each exit
```
```js
// src/js/game.js — the object returned by createGame()
{
state:'start',
frame:0,// frames elapsed; governs the staged release
-`PEN` is an inclusive box. A ghost is inside the pen when `x0 <= x <= x1 && y0 <= y <= y1`.
-`PEN_DOOR` is `(13, 12)`. `(14, 12)` is the symmetric door and stays passable for ghosts, but is not used as a target.
- Ghost index `i` is its position in `game.ghosts`, which mirrors the order of `GHOST_STARTS` (`blinky`, `pinky`, `inky`, `clyde`) and therefore the order of `GHOST_COLORS`.
-`game.frame` advances by one per `update()` call and returns to `0` in `resetPositions`.
`game.ghosts[]` keeps the shape `{ x, y, dir, speed, kind }`. This spec adds **no per-ghost field** — the release state is fully derived from the index and the frame counter.
## Implementation plan
1.**Edit `src/js/maze.js`** — add the `PEN` and `PEN_DOOR` constants next to `GHOST_STARTS` and export both on `window`, alongside the existing exports. No other change in this file.
2.**Edit `src/js/game.js`** — add `GHOST_RELEASE_FRAMES = 60` next to `GHOST_SPEED`, and add `frame: 0` to the object literal returned by `createGame`.
3.**Edit `ghostTarget` in `src/js/game.js`** — insert a first branch, before the four archetype branches, that returns `PEN_DOOR` when the ghost is inside the `PEN` box. The archetype branches are untouched below it.
4.**Thread the ghost index into `moveGhost`** — change the signature to accept `i` and update the single call site in `update` to `.forEach( ( g, i ) => moveGhost( game, g, i ) )`. At the top of `moveGhost`, return early while `game.frame < i * GHOST_RELEASE_FRAMES`, so a gated ghost neither moves nor decides a direction and keeps the `dir` it was reset to.
5.**Drive and reset the counter** — increment `game.frame` at the top of `update`, and set `game.frame = 0` in `resetPositions`. Manual test: die on purpose, and watch the four ghosts re-stagger.
6.**Manual playtest** — open `src/index.html`, press Start, and confirm the ghosts leave one at a time and then behave per archetype. The console must stay clean.
## Acceptance criteria
- [ ]`PEN` and `PEN_DOOR` exist in `src/js/maze.js` and are exported on `window`.
- [ ]`PEN` includes row `12`, so both door cells `(13,12)` and `(14,12)` are inside the box.
- [ ] All four `GHOST_STARTS` cells lie inside the `PEN` box.
- [ ] The ghost at index `i` does not change position before frame `i * GHOST_RELEASE_FRAMES`.
- [ ] With Pac-Man at his start cell and never pressing a key, all four ghosts are outside the `PEN` box within 300 frames of pressing Start.
- [ ] No ghost is still inside the `PEN` box after frame 300.
- [ ] After a death, `game.frame` is `0` and the ghost at index `i` again waits until frame `i * GHOST_RELEASE_FRAMES`.
- [ ] Once a ghost is outside the `PEN` box, it uses its archetype target: `blinky` pursues Pac-Man, `pinky` aims four tiles ahead, `inky` flanks off Blinky, `clyde` retreats to `(0,30)` within 8 tiles.
- [ ] The tunnel on row 14 still wraps all four ghosts.
- [ ] Pressing Start after `lost` or `won` resets to a fresh state with all four ghosts back at their start cells.
- [ ] The game is deterministic: two runs with identical key input produce identical ghost positions.
- [ ]`src/js/render.js`, `src/js/main.js`, `src/index.html` and `src/css/style.css` are unchanged.
- [ ] No new files, no new dependencies, no ES module conversion, no build artefacts.
- [ ] The browser console shows no errors or warnings during a full play (start, eat dots, die, restart).
## Decisions
- **Yes:** an explicit coordinate box in `maze.js` rather than deriving the pen region from `MAZE`. The pen is a fixed region in this single level; a box is readable, costs nothing at runtime, and has no failure mode on a one-cell corridor.
- **Yes:** the box includes row `12`, the two door cells. This is the part that makes the fix work. If the box stopped at row 13, a ghost standing in the doorway would leave the box and immediately hand control back to its archetype, whose target (Pac-Man, below) would send it back down into the pen, oscillating in the doorway.
- **Yes:** the door cell itself, `(13,12)`, as the target — not the cell above it. Both work; verified by simulation, all four ghosts leave in 31–52 frames with the door cell and in 21–47 frames with `(13,11)`. The door cell states the intent directly and matches the spec language.
- **Yes:** a staged release. SPEC 01 listed "staged exits" as explicitly out of scope for a future spec; this is that spec.
- **Yes:** release order is the position in `GHOST_STARTS` (`blinky`, `pinky`, `inky`, `clyde`). It matches `GHOST_COLORS` and the canonical order, and needs no new data.
- **Yes:** 60 frames between releases, one second at 60 fps. All four are out by frame 300.
- **Yes:** `game.frame` resets on death, so the stagger replays after every life.
- **Yes:** the release gate lives in `moveGhost`, so a gated ghost neither moves nor calls `decideGhost`. Its `dir` stays `up` and it starts moving already aimed at the door.
- **No:** a per-ghost `waiting` field. The index and the frame counter already carry the whole meaning; a field would be redundant state to keep in sync.
- **No:** `Math.random` anywhere. The game stays deterministic, as SPEC 01 established. If randomness is wanted later, it is a scatter-mode spec, not a patch here.
- **No:** a visual pen animation (lifting door, bobbing ghosts). This spec gates movement only. A separate animation would need a render change SPEC 02 explicitly avoids.
- **No:** scatter/chase phase timers. Those only become meaningful together with per-ghost scatter tiles.
- **No:** allowing a ghost to re-enter the pen once it has left. Nothing in the archetypes pulls a ghost back in, and Pac-Man cannot enter either, so the rule would be dead code.
| `PEN` drifts out of sync with the maze if the pen is ever redrawn. | `PEN` is a single constant sitting next to `GHOST_STARTS` in `maze.js`. An acceptance criterion asserts all four start cells lie inside the box. |
| A ghost oscillates in the doorway because its archetype target is already outside the box. | The box includes row `12`, so the ghost keeps targeting the door until it steps onto row `11`. Verified by simulation: all four leave in 31–52 frames. |
| Ghosts bunched at the door after a death look wrong. | Identical to the opening of a game. The 60-frame stagger applies the same way in both cases, so the two situations are indistinguishable. |
| The release desynchronises from the animation if `update` is not called every frame. | `game.frame` only advances inside `update()`, and `main.js` only calls `update` while `state === 'playing'`. Pausing the game pauses the release, which is the intended behaviour. |
| Inky computes its flank off Blinky's position while Blinky is still in the pen. | Cannot happen: Inky is index 2 and releases at frame 120, while Blinky is index 0 and is outside the pen by frame 51. Verified by the exit-frame measurements. |
| The `frame` counter changes how SPEC 01's archetypes feel, shifting difficulty. | `GHOST_RELEASE_FRAMES` is a single named constant. Tuning it to taste is a one-line change. |
## What is **not** in this spec
- Power-pellets, frightened mode, blue ghosts.
- Pathfinding beyond the next intersection.
- Visual animation of the ghost house: a lifting door, bobbing ghosts.
// Caja de la pen. Incluye a proposito la fila 12 (las dos celdas de la puerta,
// (13,12) y (14,12)): si terminara en la fila 13, un fantasma de pie en el
// doorway recuperaria su arquetipo, cuyo target esta abajo, y volveria a entrar.
constPEN={x0:11,x1:16,y0:12,y1:15};
constPEN_DOOR={x:13,y:12};
window.MAZE=MAZE;
window.TUNNEL_ROW=TUNNEL_ROW;
window.PACMAN_START=PACMAN_START;
window.GHOST_STARTS=GHOST_STARTS;
window.PEN=PEN;
window.PEN_DOOR=PEN_DOOR;
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.