Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
652e3ae
CG-0MQ1GGG7M007CJAX: Migrate all Renderer classes to import from @ui/…
Jun 6, 2026
e705aab
CG-0MQ1GGG7M007CJAX: Fix GolfRenderer to import createSceneTitle/crea…
Jun 6, 2026
08a5b1b
CG-0MQ5TP9R6001L256: Add zone container metadata and z-order tests
Jun 9, 2026
230c236
CG-0MQ5TQWLO007RRUZ: Migrate Main Street zone containers to use creat…
Jun 9, 2026
56843bc
CG-0MQ5TRDNE004QG7Z: Fix zone dimension expectations to 1280x720 (GAM…
Jun 9, 2026
b12c0cd
CG-0MQ5TRTH9002UZGF: Migrate FeudalismRenderer zone containers to cre…
Jun 9, 2026
9f8505c
CG-0MQ5TSATY000X4W0: Add Zone & Container Best Practices documentation
Jun 9, 2026
39d5a31
Merge branch 'feature/CG-0MQ1GGG7M007CJAX-common-ui-migration' into dev
Jun 9, 2026
772477a
CG-0MQ6H7FPW000WV67: Extend SLL schema with optional w/h dimensions
Jun 9, 2026
79397f2
CG-0MQ6H7FPW000WV67: Add JSDoc to normalizedToPixels() addressing aud…
Jun 9, 2026
fdb3433
CG-0MQ6H7FPX0060BMO: Add tutorial zone resolution tests and tutorial …
Jun 9, 2026
b47e2fd
CG-0MQ6H7FPY0021RER: Add compose dimension tests and tutorial fixture…
Jun 9, 2026
2e4628f
CG-0MQ6H7FPY0021RER: Add maximum:1 bound to w/h schema and out-of-ran…
Jun 9, 2026
e3b2847
CG-0MQ6H7NHF003VZ0D: Update documentation for tutorial layout composi…
Jun 9, 2026
cec6c86
CG-0MQ6H7NHF003VZ0D: Add @deprecated JSDoc to zoneToAnchor and Tutori…
Jun 9, 2026
b4ee636
CG-0MQ6H7NHF003VZ0D: Add tutorial layout mention to main README.md
Jun 9, 2026
ce533c1
CG-0MQ6H7NHM0006XYR: Refactor zoneToAnchor to use SLL composition and…
Jun 9, 2026
783819d
CG-0MQ6H7NHM0006XYR: Update tutorial-layout-resolution.test.ts null-z…
Jun 9, 2026
37da3cf
Merge origin/dev into main (automated)
Jun 10, 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
23 changes: 0 additions & 23 deletions .ralph.json

This file was deleted.

8 changes: 0 additions & 8 deletions .ralph/event.pending

This file was deleted.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ tableau-card-engine/
| Sushi Go! | `example-games/sushi-go/` | Card drafting game (human vs. AI). Pick and pass hands over 3 rounds, collect sets of sushi dishes, and score the most points |
| Feudalism | `example-games/feudalism/` | Engine-building card game (human vs. AI). Collect gem tokens, purchase development cards for bonuses, attract nobles, and reach 15 prestige to win |
| Lost Cities | `example-games/lost-cities/` | Two-player expedition card game (human vs. AI). Bet on up to 5 colored expeditions across a 3-round match with investment multipliers, ascending-play rules, and cumulative scoring |
| Main Street | `example-games/main-street/` | Single-player tableau builder. Buy businesses/upgrades/events, place businesses on a 10-slot street rendered as a responsive 2x5 grid, and optimize score over 20 turns |
| Main Street | `example-games/main-street/` | Single-player tableau builder. Buy businesses/upgrades/events, place businesses on a 10-slot street rendered as a responsive 2x5 grid, and optimize score over 20 turns. Tutorial overlay zones are defined in a separate SLL layout file (`main-street-tutorial.layout.json`) composed with the base layout. |
| Scenario: Tutorial | `example-games/main-street/scenes/MainStreetTutorialScene.ts` | Guided introduction to Main Street. Non-interactive tutorial overlays walk through the market, street placement, synergies, events, and scoring. Easy difficulty, 25 turns. Accessible from the Game Selector. |

More games are planned: Coloretto.
Expand Down
99 changes: 99 additions & 0 deletions docs/DEVELOPER.md
Original file line number Diff line number Diff line change
Expand Up @@ -1345,6 +1345,105 @@ When adding a new example game, follow this pattern:
- If zones/anchors are missing at runtime, look for `UNKNOWN_ZONE` / `UNKNOWN_ANCHOR` issues.
- If scene behavior unexpectedly matches legacy coordinates, verify that the adapter sees a valid layout document and that the relevant zone names exist.

### Tutorial layout composition pattern

Main Street uses a **tutorial-specific layout file** that complements the base layout with
bounding-box zones for tutorial highlight areas. This pattern allows the tutorial to define
zones that don't exist in the base scene layout (HUD strip, help button, investments row) while
reusing base layout zones through composition.

#### File layout

| File | Purpose |
|------|--------|
| `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 |

#### How composition works

The tutorial layout is composed with the base layout using `composeResolvedLayouts()`:

```typescript
import { composeResolvedLayouts } from '@ui';
import type { ScreenLayoutDocument } from '@ui';

// Load both layout documents
const baseDoc = parseScreenLayoutDocument(baseLayoutJson) as ScreenLayoutDocument;
const tutorialDoc = parseScreenLayoutDocument(tutorialLayoutJson) as ScreenLayoutDocument;

// Compose with sceneWins policy (tutorial zones override base zones on collision)
const resolved = composeResolvedLayouts(
baseDoc,
tutorialDoc,
{ width: 1280, height: 720 }, // viewport
1, // DPR
{ policy: 'sceneWins' },
);

// Access tutorial-specific zones
const hudRect = resolved.zones.hud.rect; // { x, y, width, height }
const streetRect = resolved.zones.streetGrid.rect;

// Access base zones alongside tutorial zones
const marketRect = resolved.zones.market.rect; // still available from base
```

#### Tutorial zone names

The tutorial layout defines these zones (all use normalized coordinates with optional `w`/`h` dimensions):

| Zone ID | Description | Uses dimensions |
|---------|-------------|-----------------|
| `hud` | HUD strip (top bar with coins, reputation, score) | Yes (full-width bounding box) |
| `marketBusinessRow` | Business card row in the market area | Yes |
| `streetGrid` | The 2×5 street grid for placing businesses | Yes (full-width) |
| `endTurnButton` | End Turn action button area | Yes |
| `incidentQueue` | Scrollable incident cards queue | Yes |
| `investmentsRow` | Investment/upgrade card row | Yes |
| `helpButton` | Help/settings button area | Yes |

Zones that return `null` for highlighting (no bounding box needed):
- `center-modal` — centered overlay
- `completion-modal` — centered completion dialog

#### Schema extension for dimensions

The `NormalizedRect` type and JSON Schema were extended with optional `w` (width) and `h` (height)
fields. These are **fully backward-compatible** — existing position-only zones continue to work
without modification. When `w` and `h` are present, `getZoneRect()` returns a `PixelRect` with
`width` and `height` set.

```typescript
// Position-only (existing pattern)
interface PositionOnlyRect {
x: number; // 0-1 normalized
y: number; // 0-1 normalized
}

// Dimensioned (new pattern for bounding boxes)
interface DimensionedRect {
x: number;
y: number;
w?: number; // optional width (0-1 normalized)
h?: number; // optional height (0-1 normalized)
}
```

#### Authoring a tutorial layout

When creating a new tutorial layout file:

1. **Copy the base layout** structure (`version`, `id`, `baseViewport`, `requiredZones`)
2. **Define only the zones needed** for tutorial highlights (you don't need all base zones)
3. **Include `w` and `h`** for all zones that need bounding-box dimensions
4. **Use normalized coordinates** (0-1) — resolution is handled at runtime by `normalizedToPixels()`
5. **Add anchors** for each zone (used for tooltip positioning relative to the zone)
6. **Validate** with `validateScreenLayoutDocument()` and `composeResolvedLayouts()` before committing

See `example-games/main-street/layouts/main-street-tutorial.layout.json` for a complete example.

### Related follow-up scope

- Tutorial-specific layout migration remains tracked separately in work item **Adapt tutorial system to use layout description (CG-0MP7IZ4RK008065O)**.
Expand Down
19 changes: 10 additions & 9 deletions example-games/feudalism/scenes/FeudalismRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import {
import type { FeudalismSession } from '../FeudalismGame';
import { getInfluence, getBonuses } from '../FeudalismGame';
import { addCropIcon, cssColorToNumber } from './CropIconRenderer';
import { FONT_FAMILY, GAME_W, createOverlayBackground } from '../../../src/ui';
import { FONT_FAMILY, GAME_W, GAME_H, createOverlayBackground } from '../../../src/ui';
import type { SingleSelectionManager, SelectionController } from '../../../src/ui';
import { attachSelection, createSingleSelectionManager } from '../../../src/ui';
import { createGameZone } from '../../../src/ui/Renderer';
import {
PATRON_W, PATRON_H, PATRON_X,
SUPPLY_TOKEN_R, SUPPLY_GAP, SUPPLY_TOTAL_H, SUPPLY_X, SUPPLY_Y,
Expand Down Expand Up @@ -101,14 +102,14 @@ export class FeudalismRenderer {

// ── Init ────────────────────────────────────────────────
createContainers(): void {
this.sectionBoxContainer = this.scene.add.container(0, 0);
this.marketContainer = this.scene.add.container(0, 0);
this.patronContainer = this.scene.add.container(0, 0);
this.supplyContainer = this.scene.add.container(0, 0);
this.playerContainer = this.scene.add.container(0, 0);
this.aiContainer = this.scene.add.container(0, 0);
this.actionContainer = this.scene.add.container(0, 0);
this.discardContainer = this.scene.add.container(0, 0);
this.sectionBoxContainer = createGameZone(this.scene, 0, 0, GAME_W, GAME_H, 'sectionBoxContainer');
this.marketContainer = createGameZone(this.scene, 0, 0, GAME_W, GAME_H, 'marketContainer');
this.patronContainer = createGameZone(this.scene, 0, 0, GAME_W, GAME_H, 'patronContainer');
this.supplyContainer = createGameZone(this.scene, 0, 0, GAME_W, GAME_H, 'supplyContainer');
this.playerContainer = createGameZone(this.scene, 0, 0, GAME_W, GAME_H, 'playerContainer');
this.aiContainer = createGameZone(this.scene, 0, 0, GAME_W, GAME_H, 'aiContainer');
this.actionContainer = createGameZone(this.scene, 0, 0, GAME_W, GAME_H, 'actionContainer');
this.discardContainer = createGameZone(this.scene, 0, 0, GAME_W, GAME_H, 'discardContainer');
this.marketSelectionManager = createSingleSelectionManager(this.scene);
}

Expand Down
11 changes: 10 additions & 1 deletion example-games/main-street/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ Main Street now uses the shared **Screen Layout Language (SLL)** as its canonica
## Layout files and adapter

- Canonical layout JSON: `example-games/main-street/layouts/main-street.layout.json`
- Tutorial layout JSON: `example-games/main-street/layouts/main-street-tutorial.layout.json`
- Defines 7 bounding-box zones for tutorial highlight areas (HUD, market, street, etc.)
- Uses optional `w`/`h` dimensions on `NormalizedRect` for zone extents
- Composed with the base layout via `composeResolvedLayouts()` in the tutorial system
- Scene adapter: `example-games/main-street/scenes/MainStreetLayoutAdapter.ts`
- Renderer entrypoint: `example-games/main-street/scenes/MainStreetRenderer.ts`

Expand Down Expand Up @@ -35,9 +39,14 @@ npx vitest run tests/e2e/replay-main-street.e2e.test.ts --project unit

## Follow-up work

Tutorial-specific layout migration is tracked separately in:
The tutorial overlay system (`MainStreetTutorialHints.ts`) currently uses `zoneToAnchor()` with
per-zone pixel-math to compute highlight bounding boxes. A follow-up work item tracks migrating
this to resolve zones directly through the composed SLL layout:

- **Adapt tutorial system to use layout description (CG-0MP7IZ4RK008065O)**
- Will refactor `zoneToAnchor()` to use `composeResolvedLayouts(baseLayout, tutorialLayout)`
- Replaces hardcoded pixel-math with SLL-resolved bounding boxes
- Zone names align with those in `main-street-tutorial.layout.json`

## Milestone 5: Tutorial, Onboarding, and Game Selector Integration

Expand Down
55 changes: 38 additions & 17 deletions example-games/main-street/TutorialFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,38 @@

/**
* The zone of the screen that should be highlighted for a given step.
*
* @deprecated These zone names are transitional. They are currently used by
* `MainStreetTutorialHints.zoneToAnchor()` to compute bounding-box coordinates
* for tutorial highlight overlays. During the SLL migration (CG-0MP7IZ4RK008065O)
* these kebab-case values will be replaced by camelCase SLL zone IDs from
* `main-street-tutorial.layout.json` and resolution will switch to direct SLL
* lookups via `composeResolvedLayouts()` + `getZoneRect()`.
*
* Zone name mapping (transitional kebab-case → SLL camelCase):
*
* | TutorialHighlightZone | SLL tutorial zone ID |
* |----------------------|---------------------|
* | `hud` | `hud` |
* | `market-business-row` | `marketBusinessRow` |
* | `street-grid` | `streetGrid` |
* | `end-turn-button` | `endTurnButton` |
* | `incident-queue` | `incidentQueue` |
* | `investments-row` | `investmentsRow` |
* | `help-button` | `helpButton` |
* | `center-modal` | _(null — no highlight)_ |
* | `completion-modal` | _(null — no highlight)_ |
*/
export type TutorialHighlightZone =
| 'center-modal'
| 'centerModal'
| 'hud'
| 'market-business-row'
| 'street-grid'
| 'end-turn-button'
| 'incident-queue'
| 'investments-row'
| 'help-button'
| 'completion-modal';
| 'marketBusinessRow'
| 'streetGrid'
| 'endTurnButton'
| 'incidentQueue'
| 'investmentsRow'
| 'helpButton'
| 'completionModal';

/**
* The type of player action expected to complete a step.
Expand Down Expand Up @@ -66,7 +87,7 @@ export const TUTORIAL_STEP_DEFS: readonly TutorialStepDef[] = [
title: 'Welcome to Main Street',
body:
'Build the best Main Street in 20 turns. I\'ll guide your first few actions.',
highlightZone: 'center-modal',
highlightZone: 'centerModal',
requiredAction: 'confirm',
},
{
Expand All @@ -82,63 +103,63 @@ export const TUTORIAL_STEP_DEFS: readonly TutorialStepDef[] = [
title: 'Market Rows',
body:
'Businesses go on your street. Investments are upgrades and events that shape your strategy.',
highlightZone: 'market-business-row',
highlightZone: 'marketBusinessRow',
requiredAction: 'select-business',
},
{
id: 'T4',
title: 'Place a Business',
body:
'Place this business in a highlighted slot. Adjacent matching types create synergy bonuses.',
highlightZone: 'street-grid',
highlightZone: 'streetGrid',
requiredAction: 'place-business',
},
{
id: 'T5',
title: 'End Turn',
body:
'End Turn resolves income and incidents, then starts a new market day.',
highlightZone: 'end-turn-button',
highlightZone: 'endTurnButton',
requiredAction: 'end-turn',
},
{
id: 'T6',
title: 'Incident Queue',
body:
'Incidents are upcoming events. Watch this queue to plan ahead.',
highlightZone: 'incident-queue',
highlightZone: 'incidentQueue',
requiredAction: 'acknowledge-queue',
},
{
id: 'T7',
title: 'Held Event Card',
body:
'You can hold one event card and play it when timing is best.',
highlightZone: 'investments-row',
highlightZone: 'investmentsRow',
requiredAction: 'buy-event',
},
{
id: 'T8',
title: 'Upgrade Concept',
body:
'Upgrades improve an existing business. Strong upgrades compound over remaining turns.',
highlightZone: 'investments-row',
highlightZone: 'investmentsRow',
requiredAction: 'apply-upgrade',
},
{
id: 'T9',
title: 'Help + Hint Tools',
body:
'Need a refresher? Open Help anytime. Hint suggests one strong move per turn.',
highlightZone: 'help-button',
highlightZone: 'helpButton',
requiredAction: 'open-help',
},
{
id: 'T10',
title: 'Tutorial Complete',
body:
'Great job! You\'re ready for a full run. Tutorial can be replayed from menu/settings.',
highlightZone: 'completion-modal',
highlightZone: 'completionModal',
requiredAction: 'confirm-complete',
},
] as const;
Expand Down
Loading
Loading