Concept. Ghost Squad is a reversed Pac-Man: instead of guiding Pac-Man away from the ghosts, the player commands the four ghosts to hunt an AI-controlled Pac-Man. It is the second title in the reversed-games series, following minelayer (reversed Minesweeper, where the player lays mines and an AI sweeper solves the board).
Why this game, why now. Design research within the series established three selection criteria, all met by this concept:
Differentiating mechanic. All four ghosts are human-controllable at once, each bound to its own keyboard cluster (WASD, ↑←↓→, IJKL, numpad 8456). One player realistically attends to about two ghosts at a time; uncontrolled ghosts fall back to the original arcade chase AI. Two to four friends sharing one keyboard bring more ghosts under human control. Difficulty therefore emerges from human attention bandwidth rather than from AI knobs alone, and the game gains a couch co-op mode at zero netcode cost.
Goals (observable outcomes).
| Element | Definition |
|---|---|
| Primary user | A casual player in a browser, alone or with friends physically present |
| Trigger | Opening the game's HTML file (double-click or browser URL); the game boots straight into the title/attract screen with no loading of external resources |
| Core job | Command the four ghosts to catch the AI Pac-Man before he clears the maze of dots |
| Completion condition (round won) | Pac-Man is caught 3 times (his lives exhausted) before all dots are eaten |
| Failure condition (round lost) | Pac-Man eats every dot in the maze |
| Trust boundaries | Everything runs locally in one HTML file; no network calls, no external assets, no accounts; best scores persist only in localStorage |
| Object | Canonical definition |
|---|---|
| Maze | The single fixed playfield: tile grid, walls, dots, 4 power pellets, ghost house, side tunnel. Replicated from the arcade original's layout. |
| Pac-Man (AI) | The autonomous opponent. Goal: eat all dots. Has 3 lives per round. Behavior = shared movement core + one active Personality (SR-3). |
| Ghost × 4 | Player-controllable hunters. Each has its arcade identity, color, spawn point in the ghost house, scatter corner, and fallback chase behavior: |
|
Shadow (Blinky) — fallback: targets Pac-Man's current tile Speedy (Pinky) — fallback: targets 4 tiles ahead of Pac-Man Bashful (Inky) — fallback: flank vector off Shadow Pokey (Clyde) — fallback: chases when far, retreats to corner when close | |
| Dots | 234 regular dots + 4 power pellets (count follows the replicated arcade maze). Pac-Man's win progress; visible on the maze at all times. |
| Control mode (per ghost) | Each ghost is independently in HUMAN or AUTO mode at any moment (SR-2). Displayed above each ghost and on the HUD. |
| Personality | A named parameter bundle for the AI Pac-Man, drawn once per round and telegraphed to the player (SR-3). |
| Difficulty tier | One of Beginner / Intermediate / Expert, selected before the round; tunes only the AI Pac-Man's physical parameters (speed, cornering boost, empowerment duration, personality pool) — decision quality is identical on all tiers (SR-4). |
States in capitals are the only legal states; transitions not shown are prohibited (e.g., no path from ATTRACT directly into PLAY, no restart mid-catch animation).
Purpose. The reversal only reads instantly if the original is reproduced faithfully. This series' convention (set by minelayer's Windows XP fidelity) applies here to the 1980 arcade original.
#2121DE, black background, canonical character colors). Sprites are drawn to match the arcade silhouettes; no invented decorative elements, backgrounds, or effects.| Aspect | Rule |
|---|---|
| Trigger | Player selects a difficulty tier on TIER SELECT (click or 1/2/3) |
| Ready phase | ~2.5 s: maze shown with dots refilled, characters at spawn, active Personality revealed by name, icon, and trail tint (SR-3). No input accepted. |
| Catch | A ghost in HUMAN or AUTO mode overlaps Pac-Man while he is not empowered → Pac-Man loses a life. If lives remain: brief catch animation, all characters return to spawn, sub-ready (~1 s), play resumes with dots as they were. If none remain: WIN. |
| Clear | Pac-Man eats the final dot → LOSS. |
| Round end | WIN or LOSS banner with final score, best score for the tier, and a prompt: Enter = same tier again, Esc = back to TIER SELECT. Dots and positions fully reset on replay. |
| Persistence | Best score per tier saved to localStorage; shown on TIER SELECT and round end. |
| Exceptions | Browser tab hidden or window blurred during PLAY → automatic PAUSED (fair-play and input-safety rule, SR-5). |
Purpose. Let one keyboard seat one to four players; make human attention the difficulty dial.
| Ghost | Primary cluster | Alternate cluster |
|---|---|---|
| Shadow | WASD | — |
| Speedy | ↑←↓→ | — |
| Bashful | IJKL | — |
| Pokey | numpad 8456 | TFGH (laptops without numpad) |
Focus the dark panel (click it), then press any cluster. A ghost you touch becomes HUMAN; leave it unattended and it reverts to AUTO (fallback AI) — the exact SR-2 rule.
Purpose. Make the full game playable on devices without a keyboard, preserving the exact SR-2 claim/idle semantics of FR-03.
claimGhost() path as a keypress — instant HUMAN claim (even mid-round) plus a queued turn applied at the next legal intersection; opposite direction reverses immediately per arcade rules.IDLE_S after the final release, identical to keyboard. Pads are independent and multi-touch capable, so several ghosts can be steered simultaneously — the touch equivalent of several players on separate clusters.pointerId on the PointerEvent path, Touch#identifier on the legacy TouchEvent path) in a per-ghost press stack; the stack top (most recently pressed, still held) is the effective direction. Lifting any finger releases only that finger: two fingers may share one button, and lifting the first never cancels the second's hold; lifting an older finger after a second finger chose a new direction keeps the newer one. pointercancel/touchcancel (OS gesture, incoming call) and window blur release all fingers, so no input can ever stick. Compatibility events the browser synthesizes around a real press (duplicate mousedown/touchstart/click within 400 ms) are deduplicated.pointer: coarse media query — phones/tablets), not merely when a touchscreen exists, so touch laptops keep the desktop layout; appending ?touch=1 to the URL forces the pads on any device (testing, mouse play, touch laptops).Purpose. Supply a convincing, learnable, varied opponent with zero network dependency.
food-value − threat. Food-value combines a linear pull toward the nearest remaining food (constant gradient at any range — it never loses track of the last dots), dot density within 18 tiles (steep at short range so an adjacent dot always beats the keep-direction commitment), dots lying ahead in the corridor, and a large bonus for a dot on the immediate next step taken forward/sideways (never bypassing free food); threat sums per-ghost terms with a 1/d² falloff.localStorage unavailable (private mode) → best scores silently become session-only; no error is shown to the player.localStorage. Audio starts only after the first user gesture, per browser autoplay policy.title_brand + " — " + title_suffix, both localized per language), a hook line with the translated tier name and catch count (win/loss variants share_hook_win/share_hook_loss), a compact emoji stat row (👻× catches + 🏆 score), and a call-to-action line with the page URL.public/shared/share.js. Clicking SHARE RESULT always opens a retro CRT preview modal first — the player sees the exact card before anything leaves the device. From the modal: COPY (clipboard + execCommand legacy fallback + retro toast), native SHARE via the Web Share API sheet where available (a dismissed sheet is not an error), CLOSE. Modal button labels are localized in all 8 languages by the engine's own dictionary. The button click stops propagation so the overlay's tap-to-retry does not fire. Purely additive: no server, no account, no change to the round loop.share_btn, share_copied, share_hook_win, share_hook_loss, share_cta dictionary keys in all 8 languages; card assembly (buildShareCard) is a pure function pinned byte-for-byte by the 11-share test suite.Inputs are taps queued per ghost; no action requires holding keys. All four clusters plus Esc (pause), Enter (confirm), M (mute), and 1–3 (tier select) form the complete input set. Any key press on ATTRACT advances to TIER SELECT. On touch devices the virtual d-pads (FR-03a) provide the identical input set through on-screen controls — same claim path, same queueing, same fairness rules; touch input never grants abilities keyboard lacks.
Each ghost is in exactly one mode. Cluster key press → HUMAN immediately. No key press on its cluster for 2.0 s of gameplay time → AUTO. Mode changes never interrupt movement and never cost score. The mode of every ghost is always visible on the sprite outline and HUD. Coordination bonus: when 2 or more HUMAN-mode active ghosts exist simultaneously, each receives a +15% speed bonus. This rewards multi-ghost coordination and team play without making single-ghost play easier — the bonus does not activate for solo 1-ghost control.
| Personality | Behavior signature | Counter-play for hunters | Tier pool |
|---|---|---|---|
| Glutton | High dot greed, short flee radius — takes dots even with ghosts nearby | Punish greed: collapse on his feeding lane | All tiers |
| Skittish | Large flee radius, high threat weight, extra caution vs HUMAN ghosts | Herd him: drive him toward a waiting ghost | All tiers |
| Pellet Hunter | Lunges for power pellets whenever a ghost is near; hunts aggressively during empowerment | Guard pellet corners; retreat on empowerment | Intermediate+ |
| Tunnel Rat | Prefers corridors dense with dots; commits to eating through a lane | Cut off his lane exits ahead of him | Intermediate+ |
| Trickster | Baits: approaches a committed chaser (4–6 tiles), then doubles back the moment separation strictly grows; occasionally jukes to a near-equal route | Hold intersections; don't over-commit | Expert only |
One personality is drawn uniformly from the tier's pool at READY and stays fixed for the whole round. Name, hint line, and trail tint are shown at reveal; tint persists during play.
| Tier | Pac-Man speed | Cornering boost | Empowerment | Personality pool |
|---|---|---|---|---|
| Beginner | 85% of ghost speed | ×1.08 | 4 s | Glutton, Skittish |
| Intermediate | 95% | ×1.12 | 5 s | + Pellet Hunter, Tunnel Rat |
| Expert | 100% | ×1.22 | 6 s | All five |
Decision logic (FR-05) is identical on every tier — tiers alter only the opponent's physical parameters, never ghost speed, maze, or pellet count.
Esc toggles PAUSED; gameplay time (including the SR-2 idle timer) freezes. Window blur or tab hidden forces PAUSED. PAUSED dims the maze and shows controls; no game state is lost.
| Case | Behavior |
|---|---|
| Small viewport / mobile portrait | Playfield scales proportionally; below a usable minimum, show "rotate or enlarge your window" notice (touch is P1, so phones are not P0 targets) |
| Keyboard without numpad | Pokey's alternate cluster TFGH always active |
| Hardware key ghosting in 3–4 player co-op | Not detectable; mitigated by tap-based input and matrix-distant cluster placement (FR-03) |
| Autoplay-blocked audio | Audio engine initializes on first user gesture; game is fully playable silent |
localStorage blocked | Session-only best scores, no error surfaced (FR-08) |
| Mid-round cluster conflict (two friends fight over one ghost) | Most recent press wins; no lockout — physical negotiation is part of couch co-op |
A dedicated test suite lives at tests/ghost-squad/ with 40 tests across 9 files. The harness loads the real index.html in-memory with stubbed browser APIs (DOM, canvas, audio) and drives the game loop frame-by-frame with deterministic entity placement. The game file is never modified.
| File | Tests | Covers |
|---|---|---|
01-ai-movement.test.js | 7 | Pincer trap escape, U-turns for dots behind (near & far), 1-dot endgame routing (all-personality sweep), calm dithering, turn-into-dots overshoot |
02-state-machine.test.js | 6 | Boot→attract→ready→play, catch→respawn, 3 catches = ghosts win, all dots cleared = end, Enter restarts / Esc → tier |
03-combat-empowerment.test.js | 5 | Empowerment flip, eating ghosts, re-emergence in AUTO, normal contact costs a life, empowerment expiry |
04-input-claim-idle.test.js | 4 | WASD/arrow cluster claim, per-ghost independence, idle→AUTO fallback, queued direction applied |
05-tunnel-waves.test.js | 4 | Side-tunnel wrap for Pac-Man and ghosts, ~0.55× tunnel slowdown, scatter/chase wave alternation |
06-empowerment-hunt.test.js | 4 | Hunting frightened ghosts, pellet weaponization, guarded-pellet safety, resume clearing after eating all four |
07-ghost-house.test.js | 3 | Eyes return path, house release timers, ~3 s out-of-play minimum after being eaten |
08-misc-soak.test.js | 4 | Pause/resume immutability, 4-player simultaneous claims, 400/800/1200 catch scoring, 5-sim-minute Expert soak with per-frame invariants |
09-difficulty-sim.test.js | 3 | Full-game baseline simulation: all-AUTO ghosts × 3 runs per tier, tracks win rate, catch count, dot-clear %, timing |
Conventions: every AI movement test corresponds to a bug observed and fixed during development. Scenario tests pin one personality via G().setPersonality(name) to eliminate RNG-draw flakiness; behavior-sweep tests iterate all five personalities explicitly. Ghost control uses mode:'human' with no queued input so ghosts march deterministically.
Purpose: verify that the AI Pac-Man is strong enough that all-AUTO ghosts (no human input) cannot trivially beat him. If AUTO ghosts win easily, the human player has no reason to participate — the game plays itself.
Method: run a full round with all 4 ghosts on AUTO (no keyboard input) for 5 runs per tier. Track catch count, dot-clear percentage, and outcome.
| Tier | All-AUTO win rate | Typical catches | Pac-Man clear % | Verdict |
|---|---|---|---|---|
| Beginner | 100% (5/5) | 3 | 57–89% | Ghosts too strong without human — acceptable on easiest tier |
| Intermediate | 60% (3/5) | 1–3 | 70–100% | Balanced — human input matters |
| Expert | 0% (0/5) | 1–2 | 100% | Pac-Man dominant — human required, as designed |
Expert at 0% all-AUTO win rate is intentional: it confirms the AI Pac-Man is strong enough that a human player is strictly necessary to win on the hardest tier. The lower bound validates Pac-Man's decision quality, not difficulty tuning.
Purpose: verify that a human player controlling 1 ghost can realistically beat the AI Pac-Man. Record all moves, measure APM, and evaluate whether a real human — especially a new player — could repeat the performance.
Method: simulate a human player claiming Shadow (ghost 0, WASD). Every 300 ms (average human) or 200 ms (skilled), re-evaluate direction via BFS pathfind toward Pac-Man's predicted position (3 tiles ahead in current heading). Other 3 ghosts stay on AUTO. Record every keypress and outcome. 5 runs per tier to account for personality variance.
| Tier | 300 ms reaction (avg human) | 200 ms (skilled) | APM required | Reclaims |
|---|---|---|---|---|
| Beginner | 5/5 (100%) | 5/5 (100%) | ~114/min (1.9 keys/s) | 0 |
| Intermediate | 3/5 (60%) | 4/5 (80%) | ~114–150/min | 0 |
| Expert | 1/5 (20%) | 1/5 (20%) | ~120–144/min | 0 |
Key findings (single-ghost):
Purpose: verify that controlling more ghosts rewards skill — the win rate should increase (or at least not decrease) when players add ghost control, for both team play (N humans) and solo multi-ghost (1 human cycling through N ghosts).
Method: each controlled ghost uses a role-specific BFS interception target: ghost 0 chases Pac-Man's current tile, ghost 1 cuts off 4 tiles ahead, ghost 2 traps from 4 tiles behind, ghost 3 guards the nearest pellet. Decisions every 300 ms. 5 runs per configuration to account for personality variance.
| Team play (N humans, 300 ms reaction each) | ||||
|---|---|---|---|---|
| Tier | 1 ghost | 2 ghosts | 3 ghosts | 4 ghosts |
| Beginner | 100% | 100% | 80% | 100% |
| Intermediate | 60% | 60% | 40% | 80% |
| Expert | 20% | 0% | 20% | 20% |
| Solo multi-ghost (1 human cycling N ghosts) | |||
|---|---|---|---|
| Tier | 2 ghosts | 3 ghosts | 4 ghosts |
| Beginner | 100% | 80% | 100% |
| Intermediate | 0% | 60% | 40% |
| Expert | 0% | 0% | 0% |
Key findings:
Conclusion: The baseline curve had Expert flat at 10–20% (multi-ghost skill not rewarded). A comprehensive sweep of 11 configurations × 10 runs × 3 tiers × 4 ghost counts (1,320 simulated games) tested every option individually and in combination. None of the flat speed bonuses produced the desired wide range — they all pushed 1-ghost Expert too high (50–60%) while keeping 4-ghost at the same level, resulting in a narrow 50–70% range.
The solution is a coordination bonus: when 2+ HUMAN-mode active ghosts exist simultaneously, each receives +15% speed. This is the only mechanism that creates a wide Expert range without making 1-ghost too easy, because the bonus does not activate for solo 1-ghost control. Combined with IDLE_S increased from 1.5 to 2.0 s (for solo multi-ghost viability), the final configuration is:
| Team play — final verified curve (20 runs) | ||||
|---|---|---|---|---|
| Tier | 1 ghost | 2 ghosts | 3 ghosts | 4 ghosts |
| Beginner | 100% | 100% | 100% | 100% |
| Intermediate | 80% | 85% | 55% | 80% |
| Expert | 25% | 40% | 35% | 55% |
Beginner is perfectly flat at 100%. Intermediate ranges 55–90% — viable for any team size. Expert ranges 25–55% — a significant improvement over the baseline 10–20% flat curve. The 1-ghost rate (25%) is close to the design target of ~20%, and the 4-ghost rate (55%) rewards team coordination. The 3-ghost dip (35%) is a known limitation of the BFS simulation — real human teams would coordinate better with 3 players. The coordination bonus is the key innovation: it rewards multi-ghost skill without inflating 1-ghost difficulty.
Tuning methodology: all individual options and combinations were tested with 10–20 runs per configuration per tier per ghost-count (1,320+ simulated games total across 11 configs, plus 20-run verification of the final config). The coordination bonus was tested at +10%, +15%, and +18% — +15% produced the smoothest curve. See tests/ghost-squad/curve_sweep.js, tests/ghost-squad/expert_tune.js, tests/ghost-squad/expert_tune2.js, tests/ghost-squad/expert_tune3.js, tests/ghost-squad/expert_tune4.js, and tests/ghost-squad/verify_curve2.js for the full sweep scripts and tests/ghost-squad/README.md for documentation.
Engine. All user-visible text is served by the shared, offline i18n engine public/shared/i18n.js plus a per-page dictionary. Ghost Squad is registered as page id ghost and its translations live in public/shared/lang/ghost.js (namespace window.RG_I18N_DICT.ghost). English is stored in the dictionary like any other language (the en block) and doubles as the default: when the active language has no entry for a key, the en entry is served. The markup's own text and the T(key, EnglishDefault) second argument remain as last-resort fallbacks.
| Aspect | Definition |
|---|---|
| Languages | 8: en, zh, es, de, fr, ru, ja, ko |
| Detection order | localStorage 'rg.lang' → browser language (navigator.languages / navigator.language; exact id, then 2-letter prefix) → en |
| Static markup pattern | data-i18n="ghost.<key>" on an element holding the pre-init English text; RG_I18N.init('ghost') swaps in the translation for the active language — English included, from the dictionary — and captures the original once as the last-resort fallback; covers the document <title> as well (key doc_title) |
| Dynamic text pattern | T = window.RG_I18N ? function(k,d){ return RG_I18N.t('ghost',k,d); } : function(k,d){ return d!=null?d:k; }, always called as T('ghost.<key>', 'English default') — used for the canvas HUD, tier cards, the attract best table, the personality banner, and the end overlay |
| Re-render on language change | JS-built DOM registers RG_I18N.onChange(...) which re-renders the attract best table, the tier cards, the "TONIGHT'S OPPONENT" banner (while visible) and the end overlay (while visible); the engine re-applies every data-i18n element itself |
| Switcher | The engine injects a fixed top-right <select> language switcher; the choice persists in localStorage 'rg.lang' |
The test harness is unaffected. The deterministic suite in §9.1 loads only the inline <script> block with a stubbed DOM, so window.RG_I18N is undefined there. The guarded boot if(window.RG_I18N) RG_I18N.init('ghost') is skipped and T() returns its English default, so all 40 tests keep passing and the game logic, tuning constants, RNG, boot sequence, and canvas drawing math are untouched.