This document covers everything you need to develop, test, and build the Tableau Card Engine (TCE) project. For a high-level overview, see the README.
- Environment Setup
- Running Locally
- Building for Production
- Testing
- Project Structure
- Path Aliases
- Adding an Example Game
- Transcript Persistence
- Replay Tool
- Managing Assets
- Keeping Docs Up to Date
- Work-Item Tracking
- Troubleshooting
Prerequisites:
- Node.js 18+ (LTS recommended)
- npm 9+ (ships with Node.js 18+)
- Git
Install dependencies:
npm installThis installs Phaser 3.x as a runtime dependency and TypeScript, Vite, and Vitest as dev dependencies.
npm run devStarts the Vite dev server at http://localhost:3000 with hot module replacement (HMR). The root index.html loads the Game Selector landing page, which displays all available example games as clickable cards. Click a game to launch it.
The project uses a unified entry point (main.ts at the project root) that registers a GameSelectorScene as the initial Phaser scene alongside all example game scenes. Navigation works as follows:
- Game Selector -> Game: Clicking a game card calls
scene.start(sceneKey)to transition to the selected game's scene. - Game -> Game Selector: Each game scene has a
[ Menu ]button in its title bar (top-left) and in its end-game overlays that callsscene.start('GameSelectorScene')to return to the selector.
The game catalogue is stored in the Phaser registry (key: gameSelector.games) via a preBoot callback, so game scenes don't need to know about the catalogue to return to the selector.
Each example game also retains its own standalone main.ts entry point and createXxxGame.ts factory function for independent testing and browser test use.
npm run buildThis runs two steps:
tsc --noEmit-- TypeScript type-checking (strict mode, no output files)vite build-- production bundle todist/
To preview the production build locally:
npm run previewNote: The Phaser library produces a ~1.4 MB chunk. This is expected and can be addressed with code-splitting when needed.
The project deploys automatically to GitHub Pages at:
A GitHub Actions workflow (.github/workflows/deploy.yml) runs on every push to main:
- Installs dependencies (
npm ci) - Installs Playwright Chromium (required for browser tests)
- Runs
npm test-- deployment stops if any test fails - Runs
npm run build-- deployment stops if the build fails - Uploads the
dist/directory as a Pages artifact - Deploys to GitHub Pages via the
actions/deploy-pages@v4action
Only one deployment runs at a time; if a new push arrives while a deployment is in progress, the previous run is cancelled.
GitHub Pages serves the site at /<repo>/ rather than /. The Vite config sets the base option conditionally:
- Production (
npm run build):base: '/Tableau-Card-Engine/' - Development (
npm run dev):base: '/'
This is handled automatically via Vite's mode parameter in vite.config.ts. Game scenes use relative asset paths (e.g., assets/cards/card_back.svg), which resolve correctly under either base.
A repository administrator must enable GitHub Pages once:
- Go to Settings > Pages in the repository
- Under Source, select GitHub Actions
- No custom domain is needed -- the default URL is used
After this, every push to main will automatically deploy.
- Check the Actions tab in the repository for the latest workflow run
- Visit
https://thewizardscode.github.io/Tableau-Card-Engine/to confirm the site loads - Verify all games (9-Card Golf, Beleaguered Castle, Sushi Go!, Feudalism, Lost Cities, The Mind) are playable and card assets load correctly
npm test # run all tests once (unit + browser)Tests use Vitest configured inline in vite.config.ts with two test projects:
| Project | Environment | File Pattern | Purpose |
|---|---|---|---|
unit |
Node.js | tests/**/*.test.ts |
Logic, data, and integration tests |
browser |
Chromium (Playwright) | tests/**/*.browser.test.ts |
Phaser UI and rendering tests |
Both projects run together via npm test. The browser project runs in headless Chromium using @vitest/browser with the Playwright provider.
- Place test files in
tests/following the*.test.tspattern - Import from
vitestdirectly:import { describe, it, expect } from 'vitest' - Vitest globals are enabled --
describe,it,expectare available without imports in test files
Browser tests verify Phaser UI rendering and interactions in a real browser environment. Phaser requires WebGL/Canvas and cannot run in JSDOM or happy-dom.
- Use the
*.browser.test.tspattern to mark tests for the browser project - Tests run in headless Chromium via Playwright -- no visible browser window
- Import
createGolfGamefrom the game's factory module to boot Phaser inside the test - Wait for the scene to become active before making assertions
- Clean up the game instance in
afterEachto avoid resource leaks - Access Phaser game objects via
game.scene.getScene('SceneKey').children.list
Example:
import { describe, it, expect, afterEach } from 'vitest';
import Phaser from 'phaser';
import { createGolfGame } from '../../example-games/golf/createGolfGame';
describe('MyScene browser tests', () => {
let game: Phaser.Game | null = null;
afterEach(() => {
if (game) game.destroy(true, false);
game = null;
});
it('should render a canvas', async () => {
const container = document.createElement('div');
container.id = 'game-container';
document.body.appendChild(container);
game = createGolfGame();
// wait for scene, then assert...
});
});Browser test dependencies:
@vitest/browser(matches vitest version)playwright(provides Chromium browser)- Install Chromium:
npx playwright install chromium
src/
├── core-engine/ Game loop, state management, turn sequencing, utilities
│ ├── GameState.ts GameState<T>, createGameState (deprecated for setup — use SetupOptions)
│ ├── SetupOptions.ts BaseSetupOptions, MultiplayerSetupOptions, resolveSetupOptions
│ ├── SeededRng.ts createSeededRng — deterministic PRNG (LCG) for shuffles and AI
│ ├── TranscriptRecorder.ts BaseTranscript interface, TranscriptRecorderBase<T> abstract base class
│ ├── TurnSequencer.ts advanceTurn, getCurrentPlayer, startGame, endGame
│ └── index.ts Barrel file / public API
├── card-system/ Card, Deck, Pile abstractions
│ ├── Card.ts Rank, Suit, Card type, createCard
│ ├── Deck.ts createStandardDeck, shuffle, draw, drawOrThrow
│ ├── Pile.ts Pile class (push, pop, peek, isEmpty, size)
│ └── index.ts Barrel file / public API
├── rule-engine/index.ts Rule definitions (stub -- game-specific rules live with games)
├── ai/ Shared AI strategy abstractions and utilities
│ ├── AiStrategy.ts AiStrategyBase interface, AiPlayer<TStrategy> generic base class
│ ├── AiUtils.ts pickRandom<T>, pickBest<T> utility functions
│ └── index.ts Barrel file / public API
└── ui/
├── GameSelectorScene.ts Game selector landing page (GameEntry, REGISTRY_KEY_GAMES)
├── HelpPanel.ts Reusable help panel component
├── HelpButton.ts Help button component
└── index.ts Barrel file / public API
example-games/
├── hello-world/
│ ├── main.ts Game entry point (Phaser.Game config)
│ └── scenes/
│ └── HelloWorldScene.ts Phaser.Scene subclass
├── golf/
│ ├── main.ts Game entry point (Phaser.Game config)
│ ├── createGolfGame.ts Factory function (used by main.ts and tests)
│ ├── GolfGrid.ts 3x3 grid type and utilities
│ ├── GolfRules.ts Turn legality, move application, round-end detection
│ ├── GolfScoring.ts Card point values, grid scoring, column matching
│ ├── GolfGame.ts Game orchestration (session setup, turn execution)
│ ├── AiStrategy.ts AI strategies (RandomStrategy, GreedyStrategy)
│ ├── GameTranscript.ts Transcript recording (TranscriptRecorder)
│ └── scenes/
│ └── GolfScene.ts Phaser scene (full visual interface)
├── beleaguered-castle/
│ ├── main.ts Game entry point
│ ├── createBeleagueredCastleGame.ts Factory function (used by main.ts)
│ ├── BeleagueredCastleState.ts State types, move types, constants
│ ├── BeleagueredCastleRules.ts Pure game logic (deal, moves, win/loss)
│ ├── GameTranscript.ts Transcript recording (BCTranscriptRecorder)
│ ├── help-content.json Help panel content (rules, controls, tips)
│ └── scenes/
│ └── BeleagueredCastleScene.ts Phaser scene (full visual interface)
├── sushi-go/
│ ├── main.ts Game entry point
│ ├── createSushiGoGame.ts Factory function (used by main.ts and tests)
│ ├── SushiGoCards.ts Card types, deck creation, card-back texture generation
│ ├── SushiGoGame.ts Game orchestration (drafting rounds, scoring)
│ ├── SushiGoScoring.ts Set-collection scoring rules (Maki, Tempura, etc.)
│ ├── AiStrategy.ts AI strategies (RandomStrategy, GreedyStrategy)
│ ├── help-content.json Help panel content
│ └── scenes/
│ └── SushiGoScene.ts Phaser scene (drafting UI, card picking)
├── feudalism/
│ ├── main.ts Game entry point
│ ├── createFeudalismGame.ts Factory function (used by main.ts and tests)
│ ├── FeudalismCards.ts Development cards, nobles, gem types, tier data
│ ├── FeudalismGame.ts Game orchestration (token collection, purchases, nobles)
│ ├── AiStrategy.ts AI strategies (RandomStrategy, GreedyStrategy)
│ ├── help-content.json Help panel content
│ └── scenes/
│ └── FeudalismScene.ts Phaser scene (gem tokens, card market, purchases)
└── lost-cities/
├── LostCitiesCards.ts Card types, deck factory, 5 expedition colors, card helpers
├── LostCitiesRules.ts Two-phase turn model, ascending-play validation, legality checks
├── LostCitiesScoring.ts Expedition scoring (-20 base, investments, 8-card bonus)
├── LostCitiesGame.ts Match manager (3-round session, executeAction, state queries)
├── AiStrategy.ts AI strategies (RandomStrategy, GreedyStrategy)
├── GameTranscript.ts Transcript recording (LCTranscriptRecorder)
├── help-content.json Help panel content (rules, scoring, controls)
└── scenes/
├── LostCitiesMockScene.ts Static layout mockup (development aid)
└── LostCitiesScene.ts Phaser scene (interactive play with animations)
├── the-mind/
├── MindCards.ts Card types and deck creation
├── MindGame.ts Game orchestration (levels, lives, card play, penalties)
├── AiStrategy.ts AI strategies (timing-based play decisions)
├── GameTranscript.ts Transcript types and MindTranscriptRecorder (event-based)
├── headlessGame.ts Headless AI-vs-AI runner for fixture generation
├── help-content.json Help panel content
└── scenes/
└── TheMindScene.ts Phaser scene (real-time card play interface)
scripts/
├── replay.ts Replay CLI (Playwright-driven transcript replay + screenshots)
├── generate-thumbnail.ts Thumbnail generator (midpoint frame -> 120x68 PNG)
├── refresh-thumbnails.sh Batch thumbnail refresh for all games
├── generate-*-fixture-transcript.ts Per-game fixture transcript generators
└── adapters/
├── ReplayAdapter.ts ReplayAdapter interface (contract for all adapters)
├── AdapterRegistry.ts Singleton adapter registry
├── index.ts Barrel file (imports and registers all adapters)
├── BeleagueredCastleReplayAdapter.ts
├── LostCitiesReplayAdapter.ts
├── TheMindReplayAdapter.ts
├── SushiGoReplayAdapter.ts
├── FeudalismReplayAdapter.ts
└── GolfReplayAdapter.ts (structural detection fallback -- registered last)
public/assets/
├── cards/ 52 standard card SVGs + card_back.svg (140x190px, CC0)
│ └── lost-cities/ 60 Lost Cities expedition card SVGs + lc-back.svg (140x190px)
├── games/ Per-game assets (thumbnails)
│ ├── golf/thumbnail.png
│ ├── beleaguered-castle/thumbnail.png
│ ├── lost-cities/thumbnail.png
│ ├── the-mind/thumbnail.png
│ ├── sushi-go/thumbnail.png
│ └── feudalism/thumbnail.png
└── CREDITS.md Asset attribution
tests/
├── smoke.test.ts Toolchain smoke test
├── fixtures/transcripts/ Fixture transcripts for replay tests (one per game)
├── ai/ AiPlayer, pickRandom, pickBest, barrel export tests
├── card-system/ Card, Deck, Pile unit tests
├── core-engine/ GameState, TurnSequencer, UndoRedoManager, SeededRng, TranscriptRecorder unit tests
├── golf/ Golf game unit + integration + browser tests
├── beleaguered-castle/ Beleaguered Castle unit + integration tests
├── sushi-go/ Sushi Go! cards, scoring, game, AI tests
├── feudalism/ Feudalism cards, game, AI tests
├── the-mind/ The Mind cards, game state, AI, transcript, auto-play, integration tests
├── lost-cities/ Lost Cities cards, scoring, rules, game, AI, transcript tests
└── replay/ Replay CLI validation tests
Each src/ module has a barrel file (index.ts) that serves as its public API. Import engine modules using path aliases (see below).
The project defines path aliases in both tsconfig.json and vite.config.ts:
| Alias | Resolves To |
|---|---|
@core-engine/* |
src/core-engine/* |
@card-system/* |
src/card-system/* |
@rule-engine/* |
src/rule-engine/* |
@ai/* |
src/ai/* |
@ui/* |
src/ui/* |
Usage in code:
import { ENGINE_VERSION } from '@core-engine/index';- Create a directory:
example-games/<game-name>/ - Add a standalone entry point:
example-games/<game-name>/main.ts - Add a factory function:
example-games/<game-name>/createXxxGame.ts(for browser tests) - Add scenes:
example-games/<game-name>/scenes/<SceneName>.ts(extendPhaser.Scene) - Place assets in
public/assets/<game-name>/and document attribution inpublic/assets/CREDITS.md - Add game-specific tests under
tests/<game-name>/ - Register the game in the unified entry point (
main.tsat the project root):- Import the scene class
- Add it to the
scenearray in the Phaser config - Add a
GameEntryto theGAMEScatalogue array (includethumbnailonce available)
- Add a
[ Menu ]button to the game scene that callsthis.scene.start('GameSelectorScene')for navigation back to the selector - Add transcript recording:
- Create
example-games/<game-name>/GameTranscript.tswith transcript types and aTranscriptRecorderextendingTranscriptRecorderBase<T>fromsrc/core-engine/TranscriptRecorder.ts - Integrate recording into the scene: create the recorder after game setup, record each turn/action, finalize on game over, and auto-save to
TranscriptStore
- Create
- Add replay support:
- Add
loadBoardState(stateJson: string)to the scene to reconstruct visual state from a transcript snapshot - Emit a
state-settledevent (viaGameEventEmitter) afterloadBoardState()completes rendering - Handle
?mode=replayURL parameter in the scene to skip normal game initialization - Expose
window.__GAME_EVENTS__in replay mode for adapter communication
- Add
- Create a replay adapter:
- Create
scripts/adapters/<GameName>ReplayAdapter.tsimplementing theReplayAdapterinterface - Register the adapter in
scripts/adapters/index.ts(before Golf, which uses structural detection) - Include a
gameTypefield in the transcript for explicit adapter matching
- Create
- Generate fixture and thumbnail:
- Create a fixture generator script at
scripts/generate-<game>-fixture-transcript.ts - Generate and commit the fixture transcript at
tests/fixtures/transcripts/<game-name>/fixture-game.json - Generate and commit the thumbnail at
public/assets/games/<game-name>/thumbnail.pngusing./scripts/refresh-thumbnails.sh <game-name>
- Create a fixture generator script at
Follow the Golf (original reference) and Sushi Go (most recent) examples as reference implementations.
The Golf game is the first full spike built on the engine. It demonstrates:
- Card system: Card, Deck, and Pile abstractions from
src/card-system/ - Core engine: GameState and TurnSequencer from
src/core-engine/ - Game rules: Golf-specific scoring (A=1, 2=-2, 3-10=face, J/Q=10, K=0, column-of-three=0), turn legality, and round-end detection
- AI strategies: RandomStrategy (uniform random legal moves) and GreedyStrategy (minimizes total score)
- Transcript recording: JSON game transcripts capturing all turns, board states, and results
- Phaser UI: Full visual interface with 3x3 grids, draw/discard piles, card flip animations, score display, and end-of-round screen
npm run devOpen http://localhost:3000 and click the 9-Card Golf card on the game selector page. Click the stock or discard pile to draw, click a grid card to swap, or click the discard pile after drawing to discard and flip a face-down card. Use the [ Menu ] button in the top-left to return to the game selector.
| File | Purpose |
|---|---|
example-games/golf/main.ts |
Phaser game config entry point |
example-games/golf/createGolfGame.ts |
Factory function to create a Golf game instance |
example-games/golf/GolfGrid.ts |
3x3 grid type and utilities |
example-games/golf/GolfRules.ts |
Turn legality, move application, round-end detection |
example-games/golf/GolfScoring.ts |
Scoring rules (card values, column matching) |
example-games/golf/GolfGame.ts |
Game orchestration (setup, turn execution, move enumeration) |
example-games/golf/AiStrategy.ts |
AI strategies and AiPlayer wrapper |
example-games/golf/GameTranscript.ts |
Transcript types and TranscriptRecorder |
example-games/golf/scenes/GolfScene.ts |
Phaser scene (visual interface) |
Tests are in tests/golf/:
| File | Tests |
|---|---|
GolfGrid.test.ts |
Grid creation, indexing, face-up counting |
GolfRules.test.ts |
Turn legality, move application, round-end detection |
GolfScoring.test.ts |
Card values, grid scoring, column matching |
GolfGame.test.ts |
Game setup, legal move enumeration, turn execution |
AiStrategy.test.ts |
RandomStrategy, GreedyStrategy, AiPlayer |
GameTranscript.test.ts |
Transcript recording, snapshots, finalization |
Integration.test.ts |
Full AI-vs-AI games, transcript validation, game invariants |
GolfScene.browser.test.ts |
Phaser UI rendering, canvas, game objects, interactions (browser) |
Beleaguered Castle is the second full spike. It is a single-player open solitaire game demonstrating:
- Single-player support: Core engine's
createGameState()accepts 1 player (deprecated for setup — useresolveSetupOptions()orresolveBaseSetupOptions()fromSetupOptions.tsinstead) - Undo/Redo: Reusable
UndoRedoManagerinsrc/core-engine/with Command pattern and compound commands - Drag-and-drop: Phaser drag events with drop zone highlighting and snap-back animation
- Click-to-move: Select a card then click a destination; co-exists with drag-and-drop
- Auto-move to foundations: Safe-move heuristic automatically plays cards to foundations when no tableau column needs them
- Auto-complete: Detects trivially winnable states and plays all remaining moves to completion
- Win/loss detection: Recognises when all 52 cards are on foundations (win) or no legal moves remain (loss)
- Game transcript: JSON transcript recording compatible with the Visual Replay Dev Tool
- Help panel: In-game rules and keyboard shortcut reference using the reusable HelpPanel/HelpButton components
npm run devOpen http://localhost:3000 and click the Beleaguered Castle card on the game selector page. Use the [ Menu ] button in the top-left to return to the game selector.
| File | Purpose |
|---|---|
example-games/beleaguered-castle/main.ts |
Phaser game config entry point |
example-games/beleaguered-castle/createBeleagueredCastleGame.ts |
Factory function to create a BC game instance |
example-games/beleaguered-castle/BeleagueredCastleState.ts |
State types, move types, constants (FOUNDATION_COUNT, TABLEAU_COUNT, etc.) |
example-games/beleaguered-castle/BeleagueredCastleRules.ts |
Pure game logic: deal, applyMove, undoMove, getLegalMoves, isWon, hasNoMoves, findSafeAutoMoves, isTriviallyWinnable, getAutoCompleteMoves |
example-games/beleaguered-castle/GameTranscript.ts |
Transcript types and BCTranscriptRecorder |
example-games/beleaguered-castle/help-content.json |
Help panel content (8 sections: overview, setup, foundations, tableau, winning/losing, controls, keyboard shortcuts, tips) |
example-games/beleaguered-castle/scenes/BeleagueredCastleScene.ts |
Phaser scene with drag-and-drop, click-to-move, undo/redo, auto-move, auto-complete, win/loss overlays, help panel, and transcript recording |
Tests are in tests/beleaguered-castle/:
| File | Tests |
|---|---|
BeleagueredCastleRules.test.ts |
Deal correctness, move legality, foundation builds, win/loss detection, undo, auto-move heuristics, auto-complete (70 tests) |
Integration.test.ts |
Full greedy game play across seeds, game invariants, undo/redo across moves, transcript recording and validation, auto-complete verification, snapshot utilities (30 tests) |
Sushi Go! is a card drafting game demonstrating:
- Card drafting: Pick one card from a hand, pass the rest, repeat until hands are exhausted
- Custom card types: Non-standard cards representing sushi dishes (Tempura, Sashimi, Maki Rolls, Nigiri, Wasabi, Dumplings, Pudding, Chopsticks)
- Multi-round match: 3-round game with inter-round scoring and Pudding scored only at game end
- Set-collection scoring: Different scoring functions per card type
- AI strategies: Random (valid picks) and Greedy (highest-value pick)
npm run devOpen http://localhost:3000 and click the Sushi Go! card on the game selector page.
| File | Purpose |
|---|---|
example-games/sushi-go/main.ts |
Phaser game config entry point |
example-games/sushi-go/createSushiGoGame.ts |
Factory function for tests |
example-games/sushi-go/SushiGoCards.ts |
Card types, deck creation, procedural card-back textures |
example-games/sushi-go/SushiGoGame.ts |
Game orchestration (drafting rounds, hand passing, scoring) |
example-games/sushi-go/SushiGoScoring.ts |
Set-collection scoring rules per card type |
example-games/sushi-go/AiStrategy.ts |
AI strategies (RandomStrategy, GreedyStrategy) |
example-games/sushi-go/GameTranscript.ts |
Transcript types and SushiGoTranscriptRecorder |
example-games/sushi-go/help-content.json |
Help panel content |
example-games/sushi-go/scenes/SushiGoScene.ts |
Phaser scene (drafting interface) |
Tests are in tests/sushi-go/:
| File | Tests |
|---|---|
SushiGoCards.test.ts |
Card types, deck creation, card counts (19 tests) |
SushiGoScoring.test.ts |
All card type scoring rules (56 tests) |
SushiGoGame.test.ts |
Game setup, drafting, round progression (21 tests) |
AiStrategy.test.ts |
Random and Greedy strategy validation (15 tests) |
Feudalism is an engine-building card game demonstrating:
- Resource management: Collect gem tokens to purchase development cards
- Custom card types: Development cards with costs, gem bonuses, and prestige points across 3 tiers
- Noble attraction: Automatically attract noble tiles when development card bonuses meet thresholds
- Multi-action turns: Take gems, reserve cards, or purchase developments
- AI strategies: Random (valid actions) and Greedy (prioritizes purchases and high-value cards)
npm run devOpen http://localhost:3000 and click the Feudalism card on the game selector page.
| File | Purpose |
|---|---|
example-games/feudalism/main.ts |
Phaser game config entry point |
example-games/feudalism/createFeudalismGame.ts |
Factory function for tests |
example-games/feudalism/FeudalismCards.ts |
Development cards, nobles, gem types, tier data |
example-games/feudalism/FeudalismGame.ts |
Game orchestration (token collection, purchases, nobles, win detection) |
example-games/feudalism/AiStrategy.ts |
AI strategies (RandomStrategy, GreedyStrategy) |
example-games/feudalism/GameTranscript.ts |
Transcript types and FeudalismTranscriptRecorder |
example-games/feudalism/help-content.json |
Help panel content |
example-games/feudalism/scenes/FeudalismScene.ts |
Phaser scene (gem market, card tiers, token UI) |
Tests are in tests/feudalism/:
| File | Tests |
|---|---|
FeudalismCards.test.ts |
Card data, noble data, gem types (50 tests) |
FeudalismGame.test.ts |
Game setup, turn execution, win detection, noble attraction (55 tests) |
AiStrategy.test.ts |
Random and Greedy strategy validation (14 tests) |
Lost Cities is a two-player expedition card game demonstrating:
- Custom card types: 60 cards across 5 expedition colors (yellow, blue, white, green, red) with investment cards and ranks 2-10
- Custom SVG assets: 61 procedurally generated SVG card images (60 cards + card back) at 140x190px
- Two-phase turn model: Each turn consists of a play/discard phase followed by a draw phase
- Ascending-play rules: Cards must be played in ascending order on expedition lanes; investment cards must be played before any numbered cards
- Multi-round match: 3-round match with cumulative scoring across rounds
- Investment multipliers: 1/2/3 investment cards multiply expedition score by x2/x3/x4
- AI strategies: Random (valid moves) and Greedy (discard-aware, avoids giving opponent useful cards)
- Transcript recording: Multi-round JSON transcripts capturing all actions and board states
npm run devOpen http://localhost:3000 and click the Lost Cities card on the game selector page. Select a card from your hand, then click an expedition lane to play or a discard pile to discard. Draw from the draw pile or a discard pile to complete your turn. The AI opponent plays automatically between your turns.
| File | Purpose |
|---|---|
example-games/lost-cities/LostCitiesCards.ts |
Card types, deck factory, expedition colors, card helpers |
example-games/lost-cities/LostCitiesRules.ts |
Two-phase turn model, ascending-play validation, legality checks |
example-games/lost-cities/LostCitiesScoring.ts |
Expedition scoring (-20 base, card values, investment multiplier, 8-card bonus) |
example-games/lost-cities/LostCitiesGame.ts |
Match manager (3-round session, executeAction, visible state, match lifecycle) |
example-games/lost-cities/AiStrategy.ts |
AI strategies (RandomStrategy, GreedyStrategy, LostCitiesAiPlayer wrapper) |
example-games/lost-cities/GameTranscript.ts |
Transcript recording (LCTranscriptRecorder, multi-round structure) |
example-games/lost-cities/help-content.json |
Help panel content (8 sections: overview, scoring, controls, strategy tips) |
example-games/lost-cities/scenes/LostCitiesScene.ts |
Phaser scene (interactive play with two-phase turns, animations, overlays) |
example-games/lost-cities/scenes/LostCitiesMockScene.ts |
Static layout mockup (development aid) |
The 61 SVG card images are generated by scripts/generate-lost-cities-cards.ts:
npx tsx scripts/generate-lost-cities-cards.tsAssets are output to public/assets/cards/lost-cities/ and documented in public/assets/CREDITS.md.
Tests are in tests/lost-cities/:
| File | Tests |
|---|---|
lost-cities-cards.test.ts |
Card types, deck factory, helpers, uniqueness (44 tests) |
lost-cities-scoring.test.ts |
Expedition scoring, round scoring, match scoring (26 tests) |
lost-cities-rules.test.ts |
Turn phases, legality checks, legal action enumeration (32 tests) |
lost-cities-game.test.ts |
Session setup, executeAction, round/match lifecycle (40 tests) |
lost-cities-ai.test.ts |
Random and Greedy strategies, AI player wrapper (22 tests) |
lost-cities-transcript.test.ts |
Transcript recording, full AI-vs-AI match validation (21 tests) |
The Mind is a cooperative real-time card game demonstrating:
- Real-time gameplay: Players play numbered cards (1-100) onto a shared ascending pile without communicating
- Event-based transcript: Unlike turn-based games, The Mind records real-time events (card plays, penalties, level completions) rather than discrete turns
- Cooperative AI: AI players use timing-based strategies to decide when to play cards
- Level progression: Survive 8 levels with increasing card counts per player
- Headless runner:
headlessGame.tsenables AI-vs-AI games without Phaser for fixture generation
npm run devOpen http://localhost:3000 and click the The Mind card on the game selector page. Click your cards to play them onto the shared pile. The AI partner plays automatically based on timing. Survive all 8 levels to win.
| File | Purpose |
|---|---|
example-games/the-mind/main.ts |
Phaser game config entry point |
example-games/the-mind/createTheMindGame.ts |
Factory function for tests |
example-games/the-mind/MindCards.ts |
Card types and deck creation |
example-games/the-mind/MindGame.ts |
Game orchestration (levels, lives, card play, penalties) |
example-games/the-mind/AiStrategy.ts |
AI strategies (timing-based play decisions) |
example-games/the-mind/GameTranscript.ts |
Transcript types and MindTranscriptRecorder (event-based) |
example-games/the-mind/headlessGame.ts |
Headless AI-vs-AI runner for fixture generation |
example-games/the-mind/help-content.json |
Help panel content |
example-games/the-mind/scenes/TheMindScene.ts |
Phaser scene (real-time card play interface) |
Tests are in tests/the-mind/:
| File | Tests |
|---|---|
mind-card.test.ts |
Card types and deck creation (27 tests) |
mind-card-renderer.test.ts |
Card rendering utilities (37 tests) |
game-state.test.ts |
Game state, level progression, lives (72 tests) |
ai-strategy.test.ts |
AI timing strategies (56 tests) |
transcript.test.ts |
Event-based transcript recording (32 tests) |
auto-play.test.ts |
Headless auto-play across seeds (33 tests) |
integration.test.ts |
Full game invariants across seeds (33 tests) |
Game transcripts are automatically recorded by the engine's TranscriptStore and saved to the browser's IndexedDB. Two additional mechanisms allow transcripts to be persisted to disk for debugging, replay, and analysis.
When running npm run dev, a Vite plugin intercepts POST /api/transcripts requests and writes each transcript as a timestamped JSON file:
data/transcripts/<gameType>/<gameType>-<ISO-timestamp>.json
This happens via a fire-and-forget POST from TranscriptStore.save(). If the POST fails (e.g. the production build is being served instead of the dev server), a console.warn is emitted but gameplay is not disrupted.
The data/ directory is gitignored, so persisted transcripts remain local to your machine.
To export all transcripts currently stored in IndexedDB to disk, use:
npm run transcripts:export -- <game>For example:
npm run transcripts:export -- golfThis launches a headless Chromium browser via Playwright, navigates to the game, reads all transcripts from IndexedDB, and writes them to data/transcripts/<game>/. The dev server is started automatically if it is not already running.
Requirements: Playwright's Chromium must be installed (npx playwright install chromium).
Each game has a fixture transcript used by replay tests and thumbnail generation:
tests/fixtures/transcripts/<game-name>/fixture-game.json
All six games have fixture transcripts checked into version control:
| Game | Fixture Path |
|---|---|
| Golf | tests/fixtures/transcripts/golf/fixture-game.json |
| Beleaguered Castle | tests/fixtures/transcripts/beleaguered-castle/fixture-game.json |
| Lost Cities | tests/fixtures/transcripts/lost-cities/fixture-game.json |
| The Mind | tests/fixtures/transcripts/the-mind/fixture-game.json |
| Sushi Go | tests/fixtures/transcripts/sushi-go/fixture-game.json |
| Feudalism | tests/fixtures/transcripts/feudalism/fixture-game.json |
These are generated by game-specific fixture generator scripts (e.g. scripts/generate-golf-fixture-transcript.ts) that run deterministic AI-vs-AI games using a fixed seed.
The replay tool (scripts/replay.ts) replays a fixture transcript through the game's Phaser scene in a headless browser, capturing per-turn screenshots. It is the foundation for thumbnail generation and visual regression testing.
npm run replay -- <transcript-path> [--output <dir>] [--stop-at <turn>]transcript-path-- Path to a fixture transcript JSON file--output <dir>-- Output directory for screenshots (defaults todata/screenshots/<game-type>/)--stop-at <turn>-- Stop replay at a specific turn number (for interactive takeover in headed mode)
Examples:
# Replay Golf fixture and capture all screenshots
npm run replay -- tests/fixtures/transcripts/golf/fixture-game.json
# Replay Feudalism with custom output directory
npm run replay -- tests/fixtures/transcripts/feudalism/fixture-game.json --output data/screenshots/feudalism-testScreenshots are written as turn-000.png, turn-001.png, etc. in the output directory. A replay-summary.json is also written with metadata.
- The replay tool parses the transcript and resolves a
ReplayAdapterfrom the adapter registry (scripts/adapters/index.ts) - It launches a headless Chromium browser via Playwright and navigates to the game with
?mode=replay&game=<game-type> - For each turn in the transcript, it calls
adapter._injectBoardState()which usespage.evaluate()to call the scene'sloadBoardState(stateJson)method - The scene reconstructs visual state from the snapshot and emits a
state-settledevent when rendering is complete - The tool captures a screenshot of the canvas after each
state-settledevent
Each game has a ReplayAdapter implementation in scripts/adapters/ that bridges the replay tool to the game's scene:
| Game | Adapter | Game Type |
|---|---|---|
| Beleaguered Castle | BeleagueredCastleReplayAdapter |
beleaguered-castle |
| Lost Cities | LostCitiesReplayAdapter |
lost-cities |
| The Mind | TheMindReplayAdapter |
the-mind |
| Sushi Go | SushiGoReplayAdapter |
sushi-go |
| Feudalism | FeudalismReplayAdapter |
feudalism |
| Golf | GolfReplayAdapter |
(structural detection) |
Adapters are registered in scripts/adapters/index.ts. Registration order matters: adapters with explicit gameType fields are registered before Golf, which uses structural shape-matching as a fallback.
- Create
scripts/adapters/<GameName>ReplayAdapter.tsimplementing theReplayAdapterinterface fromscripts/adapters/ReplayAdapter.ts - Implement all 14 interface methods (see
SushiGoReplayAdapteras the most recent reference) - Register the adapter in
scripts/adapters/index.tsbefore the Golf adapter - Ensure the game scene implements
loadBoardState()and emitsstate-settledevents - Test with:
npm run replay -- tests/fixtures/transcripts/<game>/fixture-game.json
- All assets go in
public/assets/and are served by Vite at the/assets/URL path - Assets must be CC0, MIT, Apache 2.0, or similarly permissive -- no restrictive licenses
- Document every asset source and license in
public/assets/CREDITS.md - Prefer SVG for card art (resolution-independent, small file size)
Each game can have a thumbnail image displayed on its card in the Game Selector. Thumbnails are committed to the repo at:
public/assets/games/<game-name>/thumbnail.png
Generating a thumbnail from replay screenshots:
npx tsx scripts/generate-thumbnail.ts <game-name> [source-dir]game-name-- The game identifier (e.g.golf)source-dir-- Optional path to a directory containingturn-NNN.pngreplay screenshots. Defaults todata/screenshots/<game-name>/
The script selects the midpoint frame from the replay output, resizes it to 120x68 PNG, and writes it to public/assets/games/<game-name>/thumbnail.png.
Wiring up a thumbnail:
After generating the thumbnail PNG, add a thumbnail field to the game's entry in main.ts:
{
sceneKey: 'GolfScene',
title: '9-Card Golf',
description: '...',
thumbnail: 'games/golf/thumbnail', // asset key (no .png extension)
}The GameSelectorScene will preload and display the thumbnail automatically. Games without a thumbnail field fall back to the text-only card layout.
Refreshing all thumbnails at once:
Use the scripts/refresh-thumbnails.sh script to replay fixture transcripts and regenerate thumbnails for all supported games in a single command:
bash scripts/refresh-thumbnails.shThe script iterates over every game in example-games/, runs the replay tool to capture screenshots, then invokes the thumbnail generator. Games that lack a fixture transcript or replay adapter are skipped with a warning (not a failure). A summary table is printed at the end showing which games were refreshed and which were skipped. The script exits non-zero if any supported game fails during replay or thumbnail generation.
When to regenerate thumbnails:
Thumbnails are static assets. Regenerate them when a game's visual appearance changes significantly. Use scripts/refresh-thumbnails.sh to regenerate all thumbnails at once, or use the individual commands above for a single game.
Policy: Any change that alters developer workflows must include a documentation update. Specifically:
- Changes to npm scripts, dependencies, or
package.jsonstructure - Changes to
tsconfig.json,vite.config.ts, or build configuration - Changes to directory structure or path aliases
- New tooling, CI/CD, or developer-facing infrastructure
How to comply:
- Update this file (
docs/DEVELOPER.md) and the relevant section ofAGENTS.mdin the same commit or PR - If the doc update cannot be done in the same commit, create a child work item in Worklog:
wl create --title "Update docs for <change>" --parent <parent-id> --priority medium --issue-type task --json
- The parent work item cannot be closed until the doc-update child is also closed
This project uses Worklog (wl) for all task tracking. Key commands:
wl next --json # what should I work on?
wl create --title "..." --json # create a work item
wl update <id> --status in_progress --json # claim a task
wl close <id> --reason "..." --json # close when done
wl sync # sync with remoteSee the Worklog section in AGENTS.md for full documentation.
Vite dev server won't start:
- Check port 3000 is not already in use:
lsof -i :3000 - Try
npm run dev -- --port 3001for an alternate port
TypeScript errors on build:
- Run
npx tsc --noEmitto see detailed errors - Check that path aliases match between
tsconfig.jsonandvite.config.ts
Tests fail to find modules:
- Ensure Vitest config in
vite.config.tsincludes thetest.projectsblock - Verify unit test files match
tests/**/*.test.ts - Verify browser test files match
tests/**/*.browser.test.ts
Browser tests fail or time out:
- Ensure Playwright's Chromium is installed:
npx playwright install chromium - Check that
@vitest/browserversion matchesvitestversion - Browser tests boot a real Phaser game and may take 8-10 seconds each
- If tests hang, check for unresolved game instances (ensure
afterEachdestroys the game)
Large bundle warning:
- The Phaser library is ~1.4 MB minified -- this is expected
- Code-splitting can be added later via
build.rollupOptions.output.manualChunksinvite.config.ts