Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
109 commits
Select commit Hold shift + click to select a range
07a1169
CG-0MQ6IEM920091HF6: Port Golf stock/discard piles to PileView
Jun 10, 2026
e4b3532
CG-0MQ6IEM920091HF6: Add CardTextureResolver to HandView for non-stan…
Jun 10, 2026
a84ba33
CG-0MQ6IEM920091HF6: The Mind - migrate to shared HandView/PileView
Jun 10, 2026
2caed66
CG-0MQ6IEM920091HF6: Fix Golf PileView discard pile crash and add int…
Jun 10, 2026
842d53d
CG-0MQ6IEM920091HF6: Fix 3 browser test assertion failures in TheMind…
Jun 10, 2026
76289c7
CG-0MQ7XYA8O005DWLC: Fix played cards disappearing after animation
Jun 10, 2026
af32327
CG-0MQ7B7HU400147P9: Fix tutorial tooltip buttons unresponsive at T3 …
Jun 10, 2026
324b6fb
CG-0MQ7B7HU400147P9: Fix Continue button not working for action-requi…
Jun 10, 2026
acbfe16
Revert "CG-0MQ7B7HU400147P9: Fix Continue button not working for acti…
Jun 10, 2026
b9d117a
CG-0MQ7B7HU400147P9: Improve tutorial instructions and fix button vis…
Jun 10, 2026
25de08f
CG-0MQ7B7HU400147P9: Add missing onTutorialActionComplete for select-…
Jun 10, 2026
0fc16b8
CG-0MQ7B7HU400147P9: Move incidents step before end turn step
Jun 10, 2026
bb89282
CG-0MQ7B7HU400147P9: Show Continue button for acknowledge-queue steps
Jun 10, 2026
f2264c8
CG-0MQ7B7HU400147P9: Fix T7 buy-event tutorial advancement and instru…
Jun 10, 2026
0dbc904
CG-0MQ8MWTK7003FF9S: Update browser overlay tests for unified 13-step…
Jun 11, 2026
048fcd7
CG-0MQ8MX4XK002PN6Q: update layout resolution tests for unified tutor…
Jun 11, 2026
b2197f8
CG-0MQ8MXYN8008GC22: Unify tutorial: merge step definitions into unif…
Jun 11, 2026
d22f7c8
CG-0MQ8MZH310009T3Q: Unify tutorial rendering with single showStep() …
Jun 11, 2026
d67dd75
CG-0MQ8MZZPY002LG6V: Unify tutorial: remove replay tutorial and updat…
Jun 11, 2026
818badd
CG-0MQ8N0EY0006IYSD: Unify tutorial documentation and cleanup
Jun 11, 2026
1493866
Add ?tutorial=1 URL parameter to force tutorial display
Jun 11, 2026
6d04028
Fix tutorial: market business row highlight and action predicate
Jun 11, 2026
7f0e9cd
Fix Continue button to re-evaluate predicate on click
Jun 11, 2026
327c5c9
Update T3 step text: 'Market Business Row' with clearer description
Jun 11, 2026
f85e9de
Update marketBusinessRow layout height to match corrected test expect…
Jun 11, 2026
b5b3d36
Fix confirmTutorialStep to handle action steps after completion
Jun 11, 2026
9f54962
Show tutorial overlay immediately after action completion
Jun 11, 2026
e14a279
Fix confirmTutorialStep to advance action steps when clicked
Jun 11, 2026
951f068
Fix Continue button to check predicate before advancing action step
Jun 11, 2026
d014236
Add Playwright E2E browser test for Main Street tutorial flow
Jun 11, 2026
3a2bce7
Add Playwright E2E browser test for Main Street tutorial flow
Jun 11, 2026
7a73fbd
CG-0MQ7VY9XU003T5DT: Fix buttons on Main Street end-of-game overlay n…
Jun 12, 2026
40ff0a9
CG-0MQBKTJY6003996N: Add tutorial seed constant (TUTORIAL_SEED) and f…
Jun 12, 2026
f2e246f
CG-0MQBKTPD1003OVMI: Add requiredCardId to TutorialFlow and update st…
Jun 12, 2026
09e4853
CG-0MQBKTPD1003OVMI: Remove discovery utility script
Jun 12, 2026
c16786f
CG-0MQBKTPD300873F5: Add card enforcement logic to MainStreetTurnCont…
Jun 12, 2026
f4fdf97
CG-0MQBKTPDA00652E8: Add invalid purchase messaging with 2-second aut…
Jun 12, 2026
9910cf5
CG-0MQBKTPDZ0081YGZ: Update E2E test for deterministic tutorial behavior
Jun 12, 2026
85b0609
CG-0MQAVSS700037Y9H: Fix card enforcement - use full card pool for tu…
Jun 13, 2026
7ad7fbf
CG-0MQAVSS700037Y9H: Fix tutorial end flow - T10 now advances to T11-…
Jun 13, 2026
dc01853
CG-0MQAVSS700037Y9H: Remove T10 'Help + Hint Tools' tutorial step
Jun 13, 2026
3a13b05
CG-0MQAVSS700037Y9H: Split Challenges & Scoring into two steps with p…
Jun 13, 2026
c5b2448
CG-0MQAVSS700037Y9H: Fix tutorial completion not deactivating controller
Jun 13, 2026
8c67a47
CG-0MQBN9XV1002IVCA: Upgrade cards now show target business on card a…
Jun 13, 2026
5ded87c
CG-0MPDWKITM006Y08I: Migrate Beleaguered Castle foundation piles to s…
Jun 13, 2026
25d4759
CG-0MQBOJZJI006YX8Q: Implement HandView vertical/cascade layout exten…
Jun 13, 2026
b75a2da
CG-0MQBOJZJI006YX8Q: Add vertical layout toggle demo to Gym HandPile …
Jun 13, 2026
94cd3c6
CG-0MQBOJZJI006YX8Q: Move toggle button to bottom, hide arc/rotation …
Jun 13, 2026
334cc28
CG-0MPDWYP9X004O37P: Port Beleaguered Castle tableau columns to share…
Jun 13, 2026
483066d
CG-0MQ6IFGJY006COIT: Add HandView/PileView migration smoke tests
Jun 13, 2026
b63d21f
CG-0MQCVVMDQ005XGOT: Fix main-street-tutorial-e2e 'require is not def…
Jun 13, 2026
0393b5b
CG-0MQBPALHT001D2H5: Add drag-and-drop support to HandView
Jun 14, 2026
61a509c
CG-0MQBPALHT001D2H5: Add drag-and-drop demo to Gym HandPile scene
Jun 14, 2026
efd51e8
CG-0MQBPALHT001D2H5: Fix drag validator registration in Gym drag demo
Jun 14, 2026
fa1a4f2
CG-0MQBPALHT001D2H5: Fix drag-and-drop acceptance in Gym demo
Jun 14, 2026
18ce527
CG-0MQBPALHT001D2H5: Fix drag-and-drop Gym demo + add browser test
Jun 14, 2026
0f29c11
CG-0MQBPALHT001D2H5: Remove deck as drop target in Gym drag demo
Jun 14, 2026
57e3954
CG-0MPDWYUMC007YNN5: Document feudalism HandView/PileView migration d…
Jun 14, 2026
9997732
CG-0MPDWZ8OI0021TSQ: Port Lost Cities to HandView/PileView
Jun 14, 2026
3e57bfe
CG-0MPDWZ8OI0021TSQ: Add Lost Cities README documenting HandView/Pile…
Jun 14, 2026
51953b2
CG-0MPDWZ8OI0021TSQ: Fix TypeScript errors in Lost Cities smoke tests
Jun 14, 2026
ea9003f
CG-0MQ6IFGK2008CYJ5: Add smoke tests for Lost Cities HandView/PileVie…
Jun 14, 2026
64cd6c8
CG-0MQ6IFGP5001YJTQ: Create PileView-compatible adapter for non-stand…
Jun 14, 2026
80dde70
CG-0MQ6IFGP5001YJTQ: Fix test mock to pass TypeScript strict checks
Jun 14, 2026
89c1ddc
CG-0MQ6IFGK00089SXI: Add Feudalism HandView/PileView migration smoke …
Jun 14, 2026
2dae673
CG-0MQBOKB540040Q60: Port Lost Cities expedition piles to PileView wi…
Jun 14, 2026
7f3cda6
CG-0MQEH6A6T008EDNE: Fix HandView sprite type errors, wire up customH…
Jun 15, 2026
589fce3
CG-0MQEH6GZP001U032: Integrate Sushi Go hand rendering with HandView …
Jun 15, 2026
d87f33e
CG-0MQEH6GZP002EB6J: Integrate Main Street hand rendering with HandVi…
Jun 15, 2026
986ce85
CG-0MQEH6H07004A66I: Document HandView renderCard API and game integr…
Jun 15, 2026
40a157b
CG-0MQ6IEM9F001JTQD: Fix Lost Cities card sizing, spacing, positionin…
Jun 15, 2026
255c365
CG-0MQF4A2MS000CUCQ: Tests and implementation for CommunitySpaceCard …
Jun 15, 2026
c8439ce
CG-0MQF4A8QG004EJRC: Tests for Development market row
Jun 15, 2026
512de51
CG-0MQF4AJGP00328M9: Implement Development market row
Jun 15, 2026
0af36e1
CG-0MQF4AJGU003VIHB: Update tutorial text for Development row
Jun 15, 2026
61ff588
CG-0MQF4AJIL005T7WU: Add serialized state migration for old-format saves
Jun 15, 2026
1796ddc
CG-0MQF4AJH70092S0A: Update documentation for community space cards
Jun 15, 2026
1fa29f1
CG-0MQBLGQ2Z0066UGP: Fix TypeScript compilation issues in community-s…
Jun 15, 2026
39e760c
CG-0MPE5J9CN006XSHU: Centralize draw animation in HandView.animateAdd…
Jun 15, 2026
6998a24
CG-0MQFFSX59009QTRQ: Fix TypeScript build errors in animateAddCard mi…
Jun 15, 2026
f047384
CG-0MQFK85CX0007M5L: Add insertAtIndex option to animateAddCard for s…
Jun 15, 2026
62611b5
CG-0MQCVVMDQ005XGOT: Fix market.business -> market.development rename…
Jun 15, 2026
999df70
CG-0MPK8XS5A00345OT: Add autosave and load feature for Beleaguered Ca…
Jun 15, 2026
1f203b1
CG-0MQFTTLLA004W351: Offer resume from saved checkpoint in Beleaguere…
Jun 15, 2026
d5feeea
CG-0MQFTTLLA004W351: Fix game-breaking bugs in resume checkpoint feature
Jun 15, 2026
91b77bb
CG-0MQFTTLLA004W351: Start deal only after checkpoint check, no false…
Jun 16, 2026
4e7680b
CG-0MPNWGG8H008E5K4: Fix card state inconsistency on discard animatio…
Jun 16, 2026
afce69a
CG-0MPNWGL7G005DFRZ: Optimize slider pointermove listeners to be self…
Jun 16, 2026
3c030cf
CG-0MPNWGQJ0003034V: Add proper shutdown lifecycle cleanup to GymHand…
Jun 16, 2026
112d447
CG-0MPNWGV7N003LYOB: Remove 'as any' casts for GameEventEmitter in Gy…
Jun 16, 2026
2c2d62e
CG-0MPNWH0X1002OQKN: Extract reusable Slider class from GymSceneUtils…
Jun 16, 2026
a14cce2
CG-0MPNWH6HC006X41U: Create centralized HighlightManager and migrate …
Jun 16, 2026
4270956
CG-0MPXU27PI009P7JX: Add optional centerX to HandView for fixed-cente…
Jun 16, 2026
4bdee3a
CG-0MPXU6IFN005CSSN: Add Prev/Next navigation buttons to Gym demo scenes
Jun 16, 2026
2c44887
chore: update feudalism README
Jun 17, 2026
4aad867
CG-0MLTFRU211Y0Z2WU: Add in-game transcript export button
Jun 17, 2026
2c2a84d
CG-0MLTFU5EK1XCH545: Add screenshot contact sheet generator
Jun 17, 2026
f7b4aea
CG-0MLTFUXI713N7W44: Update replay tool documentation
Jun 17, 2026
5821d93
Remove accidentally deleted Main Street thumbnail asset
Jun 17, 2026
29d57fc
CG-0MQHAR5AF002XPYI CG-0MQHARGYN000K81I: Add reusable undo/redo butto…
Jun 17, 2026
380a836
CG-0MQHAR57W005P8UN CG-0MQHARHKA008UY5J: Migrate Beleaguered Castle t…
Jun 17, 2026
8a6c9ce
CG-0MQHAR5O30029W27 CG-0MQHARH7J004XP4V: Migrate Main Street to share…
Jun 17, 2026
86d8511
CG-0MQHARHHW003PVJD: Document undo/redo button mechanism
Jun 17, 2026
df91907
CG-0MQI02FWL006Y0ZZ: Update Gym undo/redo scene to use shared action …
Jun 17, 2026
c91ddd2
CG-0MQI02FWL006Y0ZZ: Refactor undo/redo into standalone createStandar…
Jun 17, 2026
0eb1627
CG-0MM1OQN4E153GJY3: Standardize SFX key naming across all games
Jun 17, 2026
fe995b5
CG-0MM2DD29P0GRV6M4: Add animated card-to-foundation visual and sound…
Jun 17, 2026
8e6634c
Bump version to v0.1.1
Jun 17, 2026
787bb74
Merge origin/dev into main (automated)
Jun 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .ralph/event.pending
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"event_type": "pi_started",
"timestamp": "2026-06-14T22:46:53.758097+00:00",
"work_item_ids": [],
"cmd": "pi -p --session-id ralph-no-target-implementation-ae302828 --mode json --model Proxy/qwen3 'implement-single CG-0MQBOK2SQ0067ZBP\nComplete only this work item.\nContinue until the work item is completed, but do not merge.\nDo not ask the producer questions or pause for interactive input.\nIf you cannot continue safely without explicit producer input, stop and return a structured no_safe_path response with the missing decision.\nIMPORTANT: Use the existing feature branch '\"'\"'wl-CG-0MQ6IEM9F001JTQD-phase-3-port-high-risk-games-to-shared-handview-pi'\"'\"' for all commits. Run '\"'\"'git checkout wl-CG-0MQ6IEM9F001JTQD-phase-3-port-high-risk-games-to-shared-handview-pi'\"'\"' if not already on this branch. Do NOT create a new branch.\nWhen creating commit messages, include a '\"'\"'Related-Work: <child-id>'\"'\"' trailer where <child-id> is '\"'\"'CG-0MQBOK2SQ0067ZBP'\"'\"'. Example format:\n CG-0MQBOK2SQ0067ZBP: <concise summary of changes>\n\n Related-Work: CG-0MQBOK2SQ0067ZBP'"
}
139 changes: 138 additions & 1 deletion docs/DEVELOPER.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,13 @@ This runs `scripts/tf-generate-synths.sh` and writes generated outputs under `bu

See `docs/the-build/audio.md` for full details (module shape, mapping, runtime wiring, CI guidance).

### SFX Key Naming Convention

All sound effects use the `sfx-` prefix with no game identifier. Common cross-game
keys are defined in `COMMON_SFX_KEYS` (exported from `src/core-engine/SoundManager.ts`).
Audio assets are organized in `public/assets/audio/<game>/` with a fallback to
`public/assets/audio/default/`. See `docs/SFX_CONVENTION.md` for the full convention.

## Project Structure

```
Expand Down Expand Up @@ -526,6 +533,22 @@ Screenshots are written as `turn-000.png`, `turn-001.png`, etc. in the output di
4. The scene reconstructs visual state from the snapshot and emits a `state-settled` event when rendering is complete
5. The tool captures a screenshot of the canvas after each `state-settled` event

### Contact Sheet

After a replay completes, a contact sheet image is automatically generated showing all per-turn screenshots arranged in a grid. The contact sheet is written to `contact-sheet.png` in the output directory.

- Thumbnails are 225x175px arranged in 4 columns
- Each thumbnail is labeled with its turn number
- Generated using `sharp` (MIT-licensed, already a dependency)
- The contact sheet path is included in `replay-summary.json` as `contactSheetPath`

### In-Game Transcript Export Button

During gameplay, an **Export Transcript** button appears on the end-of-round results screen, allowing you to download the current game transcript as a JSON file directly from the browser.

- **End-of-round screen:** After the game ends, click `[ Export Transcript ]` to download the transcript as `golf-transcript-<timestamp>.json`
- **Error-triggered export:** If an unhandled JavaScript error occurs during gameplay, an overlay appears with an `[ Export Transcript ]` button so the transcript can be saved for debugging before reloading

### Replay Adapters

Each game has a `ReplayAdapter` implementation in `scripts/adapters/` that bridges the replay tool to the game's scene:
Expand All @@ -550,6 +573,76 @@ Adapters are registered in `scripts/adapters/index.ts`. Registration order matte
4. Ensure the game scene implements `loadBoardState()` and emits `state-settled` events
5. Test with: `npm run replay -- tests/fixtures/transcripts/<game>/fixture-game.json`

## Engine Event System

The core engine provides a typed event system for turn lifecycle events. It consists of two parts:

- **`GameEventEmitter`** (`src/core-engine/GameEventEmitter.ts`) — A type-safe event emitter that works in both Node.js and browser environments. Events are defined with typed payloads.
- **`PhaserEventBridge`** (`src/core-engine/PhaserEventBridge.ts`) — Bridges `GameEventEmitter` events to Phaser's scene event system and vice versa, allowing Phaser-based consumers (scenes, UI components) to subscribe to engine events using Phaser's native `scene.events`.

### Event Types

| Event | Payload | Fires When |
|-------|---------|------------|
| `turn-started` | `{ turnNumber: number, playerIndex: number, phase: string }` | A player's turn begins |
| `turn-completed` | `{ turnNumber: number, playerIndex: number }` | A move is applied and recorded |
| `animation-complete` | `{ turnNumber: number }` | All tween animations for a turn finish |
| `state-settled` | `{ turnNumber: number, phase: string }` | The board is visually stable and safe to screenshot |
| `game-ended` | `{ finalTurnNumber: number, winnerIndex: number, reason: string }` | The game ends after scoring |
| `resume-replay` | (none) | Signals the replay tool to resume after takeover |

### Subscribing to Events

```typescript
import { GameEventEmitter } from '@core-engine';

const emitter = new GameEventEmitter();

// Subscribe with full type safety
emitter.on('state-settled', (payload) => {
console.log(`Turn ${payload.turnNumber} settled, phase: ${payload.phase}`);
});

// Unsubscribe
const handler = (p: StateSettledPayload) => {};
emitter.on('state-settled', handler);
emitter.off('state-settled', handler);
```

### Emitting Events

```typescript
emitter.emit('state-settled', { turnNumber: 5, phase: 'draw' });
```

### Global Access

During gameplay, the emitter is exposed globally as `window.__GAME_EVENTS__` so that tools (replay, testing) can subscribe from outside the Phaser scene:

```typescript
const emitter = (window as any).__GAME_EVENTS__;
emitter.on('state-settled', (payload) => {
// e.g., capture screenshot
});
```

### PhaserEventBridge

When using Phaser scenes, the `PhaserEventBridge` forwards engine events to Phaser's scene events and vice versa:

```typescript
import { GameEventEmitter, PhaserEventBridge } from '@core-engine';

const emitter = new GameEventEmitter();
const bridge = new PhaserEventBridge(emitter, scene.events);

// Now scene.events receives forwarded engine events:
this.events.on('state-settled', (payload) => { /* ... */ });

// Destroy on scene shutdown:
bridge.destroy();
```

## Managing Assets

- All assets go in `public/assets/` and are served by Vite at the `/assets/` URL path
Expand Down Expand Up @@ -1359,7 +1452,7 @@ reusing base layout zones through composition.
| `example-games/main-street/layouts/main-street.layout.json` | Canonical base layout (8 zones, position-only) |
| `example-games/main-street/layouts/main-street-tutorial.layout.json` | Tutorial-specific layout (7 zones, position + dimensions) |
| `example-games/main-street/scenes/MainStreetTutorialHints.ts` | Tutorial overlay manager |
| `example-games/main-street/TutorialFlow.ts` | T1-T10 step definitions with `TutorialHighlightZone` type |
| `example-games/main-street/TutorialFlow.ts` | T1-T13 unified step definitions with `TutorialHighlightZone` / `TutorialActionType` types |

#### How composition works

Expand Down Expand Up @@ -1608,6 +1701,8 @@ The `CardGameScene` abstract class (at `src/ui/CardGameScene.ts`) provides share
- Event system setup (`GameEventEmitter` + `PhaserEventBridge`)
- Sound system setup (`SoundManager` + SFX registration)
- Help and Settings panel initialization via `initHelpPanel()` and `initSettingsPanel()`
- Undo/redo button creation via `initUndoRedoButtons()` with resolution-independent positioning
- Undo/redo button state updates via `refreshUndoRedoButtons(canUndo, canRedo)`
- Replay mode detection
- Standard shutdown/cleanup via `shutdownBase()`

Expand All @@ -1624,6 +1719,10 @@ export class MyGameScene extends CardGameScene {
if (!this.replayMode) {
this.initHelpPanel(helpContent as HelpSection[]);
this.initSettingsPanel();
this.initUndoRedoButtons(
() => this.turnController.performUndo(),
() => this.turnController.performRedo(),
);
}
// ... game-specific setup ...
}
Expand All @@ -1636,6 +1735,23 @@ export class MyGameScene extends CardGameScene {

The `initHelpPanel()` method creates both `HelpPanel` and `HelpButton`. The `initSettingsPanel()` method creates both `SettingsPanel` and `SettingsButton`. These are accessed via `this.helpPanel`, `this.helpButton`, `this.settingsPanel`, and `this.settingsButton` respectively.

### Undo/Redo Buttons

The `initUndoRedoButtons(onUndo, onRedo)` method creates standard undo/redo
action buttons positioned to avoid overlap with the settings and help toggle
buttons. The positioning is resolution-independent — computed dynamically from
the scene viewport using the same formula as the settings button's default
position.

- **Undo button** is placed to the left of the settings button
- **Redo button** is placed to the right of the undo button
- Both buttons are parented into `hudContainer` for consistent depth ordering
- Use `refreshUndoRedoButtons(canUndo, canRedo)` to update enabled/disabled
state (alpha 1.0 when enabled, 0.5 when disabled)
- Both buttons are destroyed in `shutdownBase()`
- This method is **opt-in**: only scenes that explicitly call it get undo/redo
buttons (games without undo/redo are unaffected)

### HUD Container Pattern

Games that need to separate persistent overlay elements (help/settings buttons, panel input blockers) from transient HUD elements (score text, status bars) should use a two-container pattern:
Expand Down Expand Up @@ -1701,3 +1817,24 @@ wl close <id> --reason "..." --json # close when done
**Large bundle warning:**
- The Phaser library is ~1.4 MB minified -- this is expected
- Code-splitting can be added later via `build.rollupOptions.output.manualChunks` in `vite.config.ts`

**Replay tool: Dev server not running:**
- The replay tool (`npm run replay`) and transcript export (`npm run transcripts:export`) auto-start the dev server if `localhost:3000` is not responding
- If auto-start fails, start the dev server manually: `npm run dev`
- Check port 3000 availability: `lsof -i :3000`

**Replay tool: Unsupported transcript version error:**
- The transcript schema includes a `version` field; the replay tool validates this and exits with a clear error if the version is unsupported
- Re-record the game to generate a transcript with the current version
- Transcripts evolve independently per game type; check the game's adapter for supported versions

**Transcript persistence: IndexedDB storage quota:**
- The `TranscriptStore` uses IndexedDB with a rolling window of the last 10 transcripts per game type
- If IndexedDB is unavailable (private browsing, storage quota exceeded), it falls back to localStorage with a console warning
- Individual large transcripts can exceed localStorage's ~5-10MB limit; a size warning is logged to console
- Use `npm run transcripts:export -- <game>` to offload transcripts to disk

**Playwright not installed:**
- The replay tool and transcript export use Playwright's Chromium browser
- Install it: `npx playwright install chromium`
- Verify installation: `npx playwright install --list`
115 changes: 115 additions & 0 deletions docs/SFX_CONVENTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# SFX Key Naming Convention

> **Last updated:** 2026-06-17
> **Related work-item:** CG-0MM1OQN4E153GJY3 — SFX key naming inconsistency and potential collision

## Overview

All sound effects (SFX) across all games in the Tableau Card Engine use the `sfx-` prefix with **no game identifier**. This ensures a consistent, collision-safe naming convention.

### Examples

| ✅ Correct | ❌ Incorrect |
|-----------|-------------|
| `sfx-card-draw` | `bc-sfx-card-draw` |
| `sfx-ui-click` | `ms-click` |
| `sfx-turn-change` | `lc-sfx-turn-change` |

## Shared Constants

Common cross-game SFX keys are defined as a shared constants object (`COMMON_SFX_KEYS`) exported from `src/core-engine/SoundManager.ts`. Games import and use these constants instead of defining duplicate strings.

```ts
import { COMMON_SFX_KEYS } from '@core-engine/SoundManager';

const MY_SFX_KEYS = {
UI_CLICK: COMMON_SFX_KEYS.UI_CLICK,
CARD_DRAW: 'sfx-card-draw',
} as const;
```

### Available common keys

| Constant | Value | Usage |
|----------|-------|-------|
| `COMMON_SFX_KEYS.UI_CLICK` | `sfx-ui-click` | Generic UI click / tap feedback |
| `COMMON_SFX_KEYS.TURN_CHANGE` | `sfx-turn-change` | Active player changes |
| `COMMON_SFX_KEYS.ROUND_END` | `sfx-round-end` | A round has ended |
| `COMMON_SFX_KEYS.SCORE_REVEAL` | `sfx-score-reveal` | Scores are being revealed |

## Audio Asset Organization

Audio files are organized per game with a default fallback:

```
public/assets/audio/
├── default/ # Fallback sounds for common SFX keys
│ ├── card-draw.wav
│ ├── card-flip.wav
│ ├── ui-click.wav
│ └── ...
├── golf/ # Game-specific audio
│ ├── card-draw.wav
│ └── ...
├── sushi-go/
├── feudalism/
├── beleaguered-castle/
├── lost-cities/
├── the-mind/
└── main-street/ # (Main Street uses assets/games/main-street/audio/)
```

When loading audio, use the `audioPathWithFallback()` helper from `src/ui/CardGameScene.ts`:

```ts
import { audioPathWithFallback } from '@ui/CardGameScene';

// Tries assets/audio/golf/card-draw.wav first,
// then assets/audio/default/card-draw.wav
this.load.audio('golf:sfx-card-draw', audioPathWithFallback('golf', 'card-draw.wav'));
```

## Collision Protection

To prevent Phaser audio key collisions when multiple games are loaded:

1. **Namespace-scoped audio keys**: Each game loads audio with a namespace-prefixed key: `game-name:sfx-card-draw`. This is transparent to game code — SoundManager handles the namespace mapping automatically via the `namespace` option.

2. **Scene-scoped cleanup**: When a game scene shuts down, `SoundManager.destroy()` unsubscribes event listeners and `clearRegistrations()` removes registered keys.

### Setting up namespace in a game scene

```ts
// In your scene's preload():
const ns = 'my-game';
this.load.audio(`${ns}:sfx-card-draw`, audioPathWithFallback('my-game', 'card-draw.wav'));

// In your scene's create():
this.initSoundSystem(Object.values(SFX_KEYS), mapping, { namespace: 'my-game' });
```

## Adding SFX to a New Game

1. **Create audio files** in `public/assets/audio/<game>/`.
2. **Define SFX keys** in a constants file, using `sfx-` prefix:
```ts
import { COMMON_SFX_KEYS } from '@core-engine/SoundManager';

export const SFX_KEYS = {
UI_CLICK: COMMON_SFX_KEYS.UI_CLICK,
CARD_DRAW: 'sfx-card-draw',
} as const;
```
3. **Load audio** in `preload()` using namespace-prefixed keys and `audioPathWithFallback`.
4. **Register and connect** in `create()` via `initSoundSystem()` with the namespace option.
5. **Document** any new game-specific audio files in this document or the game's README.

## Main Street Synth SFX

Main Street uses ToneForge-generated synth audio in addition to WAV fallbacks. The synth key mapping is defined in `example-games/main-street/sfx-tf-mapping.ts` and uses the same `sfx-` prefix convention.

## Testing

- Run `npm test` to verify all SFX-related tests pass.
- The `SoundManager.test.ts` includes tests for `COMMON_SFX_KEYS`, namespace collision protection, and registration inspection.
- The `sfxTfMapping.test.ts` validates Main Street synth key mappings.
6 changes: 6 additions & 0 deletions docs/gym/GYM_INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ npx vitest run --project browser tests/gym/*.browser.test.ts
| Lighting Spike | `GymGraphicsLightingSpikeScene` | Point light, shadow evaluation, WebGL fallback | [`scenes/GymGraphicsLightingSpikeScene.ts`](../../example-games/gym/scenes/GymGraphicsLightingSpikeScene.ts) | [`GymSceneSmoke.browser.test.ts`](../../tests/gym/GymSceneSmoke.browser.test.ts) |
| Screen Layout Language (SLL) | `GymSllScene` | `validateScreenLayoutDocument`, `parseScreenLayoutDocument`, `normalizedToPixels`, `getZoneRect`, `anchorPoint` | [`scenes/GymSllScene.ts`](../../example-games/gym/scenes/GymSllScene.ts) | [`GymSllLayout.test.ts`](../../tests/gym/GymSllLayout.test.ts), [`GymSllScene.browser.test.ts`](../../tests/gym/GymSllScene.browser.test.ts) |

## Scene Navigation

All Gym demo scenes that extend `GymSceneBase` include `[ < Prev ]` and `[ Next > ]` buttons in the header bar, positioned to the right of the `[ Menu ]` button. These cycle through the `GYM_SCENE_CATALOGUE` with wrap-around navigation.

The `getAdjacentGymSceneKey()` helper in `GymRegistry.ts` provides the scene key for the previous or next scene. Unit tests in `GymSceneHeaderNavigation.test.ts` verify wrap-around behaviour and that the Router scene is excluded.

## Deterministic Headless Tests

All Gym scenes are validated by deterministic headless smoke tests in [`GymHeadlessDeterminism.test.ts`](../../tests/gym/GymHeadlessDeterminism.test.ts), which assert:
Expand Down
Loading
Loading