Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
81d2397
CG-0MPPL0OXC005USUT: Migrate FeudalismRenderer to use shared createAc…
May 28, 2026
2d67484
CG-0MPPL0OXC005USUT: Fix action button and instruction text overlap
May 28, 2026
5c4d770
CG-0MPPL0P7V005JFGZ: Migrate Sushi Go to use shared Renderer API via …
May 28, 2026
9d4ffbc
CG-0MPPL0PI5002GP3Q: Migrate Beleaguered Castle to use shared Rendere…
May 28, 2026
2dbc2fb
CG-0MPQ522AH002WHD0: Fix missing Next Round button in Sushi Go round …
May 28, 2026
4228465
CG-0MPPL0HC5008UWUG: Increase Deck RNG gym card grid scale by 15% to …
Jun 1, 2026
c033ae3
CG-0MPDQRNWX00701UE: Add GymSceneUtils utilities (createEventLog, cre…
Jun 1, 2026
3804c1b
CG-0MPDQRNWX00701UE: Add DeckRng and EventLog smoke tests
Jun 1, 2026
1c97a33
CG-0MPDQRNWX00701UE: Refactor 7 gym scenes to use createEventLog utility
Jun 1, 2026
e99a407
CG-0MPDQRNWX00701UE: Refactor GymDeckRngScene to use createDeckGrid u…
Jun 1, 2026
2660c23
CG-0MPDQRNWX00701UE: Refactor GymHandPileScene to use createSlider ut…
Jun 1, 2026
89fcdd7
CG-0MPDQRNWX00701UE: Add Shared Gym Utilities documentation and compl…
Jun 1, 2026
4a771d4
CG-0MPDQRNWX00701UE: Guard event log rendering in GymGraphicsLighting…
Jun 1, 2026
1806f13
Merge feature/CG-0MPPL0P7V005JFGZ-sushi-go-renderer-adapter into dev:…
Jun 2, 2026
d6de4c2
Merge branch 'feature/CG-0MPPL0PI5002GP3Q-beleaguered-castle-renderer…
Jun 2, 2026
8964e87
Merge branch 'release/dev-to-main-20260601192035' into dev
Jun 2, 2026
d037627
Merge wl-CG-0MPPL0OXC005USUT-feudalism-renderer-adapter into dev: res…
Jun 2, 2026
669e76e
CG-0MPWZ5R1M001MZ3B: Add MarketOfferEngine extraction parity tests
Jun 2, 2026
225d24a
CG-0MPWZ5R1M001MZ3B: Update documentation for MarketOfferEngine extra…
Jun 2, 2026
b553df8
CG-0MPWZ5R1M001MZ3B: Add positive-path purchase result tests and fix …
Jun 2, 2026
587220d
CG-0MPWZ5R1M001MZ3B: Add missing negative-path and reshuffle tests fo…
Jun 2, 2026
f280588
CG-0MPWZ5RFI001DJUA: Implement EconomyLedger shared module and compre…
Jun 2, 2026
4fda53c
CG-0MPWZ5RFI001DJUA: Update PRD milestone-6 with EconomyLedger test c…
Jun 2, 2026
ade1d1c
CG-0MPWZ5RFI001DJUA: Address audit gaps — add EconomyLedger documenta…
Jun 2, 2026
b334f4d
CG-0MPWZ5RFI001DJUA: Fix audit gaps in EconomyLedger tests
Jun 2, 2026
d2bc021
CG-0MPWZ5RFI001DJUA: Add missing audit gap fixes — computeScore equiv…
Jun 2, 2026
9a6f632
CG-0MPWZ5RFI001DJUA: Fix test count in PRD milestone-6 (47 → 51)
Jun 2, 2026
8463243
CG-0MPWZ5RFI001DJUA: Merge dev into feature branch, resolve conflicts
Jun 2, 2026
78caa41
CG-0MPWZ5RYR005VX1F: Implement MarketOfferEngine Extraction
Jun 3, 2026
0d8a1a2
CG-0MPWZ5S95004ZFTO: Implement EconomyLedger Extraction - Main Street…
Jun 3, 2026
8892ba4
Remove unrelated file from commit
Jun 3, 2026
e6548e4
CG-0MPWZ5RPC000OB02 + CG-0MPWZ5SIS00553H8: Test + Implement ActionCom…
Jun 3, 2026
8c3f485
CG-0MPWZ5SIS00553H8: Implement ActionCommands Extraction - Main Stree…
Jun 3, 2026
9a92281
Remove unrelated file from commit
Jun 3, 2026
4e16d65
CG-0MPXU9ZO7003WM8P: Fix Save Snapshot - display thumbnail and persis…
Jun 3, 2026
aac0f49
CG-0MPXU9ZO7003WM8P: Fix snapshot not displaying - use RenderTexture …
Jun 3, 2026
ec09386
CG-0MPXU9ZO7003WM8P: Rework scene - hand of cards, score, screenshot …
Jun 3, 2026
c2d1883
CG-0MPXU9ZO7003WM8P: Use HandView for hand display with arc layout, c…
Jun 3, 2026
55f2f49
CG-0MPXU9ZO7003WM8P: Fix card graphics, hand centering, position, ful…
Jun 3, 2026
73018db
Merge origin/dev into main (automated)
Jun 3, 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: 23 additions & 0 deletions .ralph.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"model_source": "remote",
"model": {
"remote": {
"intake": "opencode/claude-opus-4.7",
"planning": "opencode/gpt-5.5",
"implementation": "opencode-go/qwen3.6-plus",
"audit": "opencode-go/glm-5.1"
},
"local": {
"intake": "qwen3",
"planning": "qwen3",
"implementation": "qwen3",
"audit": "qwen3"
}
},
"timeout": {
"pi_stream": {
"remote": 900,
"local": 60
}
}
}
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ tableau-card-engine/
├── tests/ Vitest test files
├── docs/ Developer documentation
│ ├── DEVELOPER.md Detailed developer guide
│ └── core-engine/ Engine API notes (including spatial rules)
│ ├── core-engine/ Engine API notes (including spatial rules)
│ └── rule-engine/ Rule engine API docs (including economy ledger)
├── dist/ Production build output (gitignored)
├── AGENTS.md Project guidance and Worklog rules
├── package.json
Expand Down
2 changes: 1 addition & 1 deletion docs/gym/GYM_INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ npx vitest run --project browser tests/gym/*.browser.test.ts
| Overlay & UI Config | `GymOverlayUiScene` | `createOverlayBackground`, `dismissOverlay` | [`scenes/GymOverlayUiScene.ts`](../../example-games/gym/scenes/GymOverlayUiScene.ts) | [`GymSceneSmoke.browser.test.ts`](../../tests/gym/GymSceneSmoke.browser.test.ts) |
| Undo / Redo | `GymUndoRedoScene` | `UndoRedoManager`, `CompoundCommand` | [`scenes/GymUndoRedoScene.ts`](../../example-games/gym/scenes/GymUndoRedoScene.ts) | [`GymUndoRedo.test.ts`](../../tests/gym/GymUndoRedo.test.ts) |
| Transcript Recording | `GymTranscriptScene` | `TranscriptRecorderBase`, `createSeededRng` | [`scenes/GymTranscriptScene.ts`](../../example-games/gym/scenes/GymTranscriptScene.ts) | [`GymTranscript.test.ts`](../../tests/gym/GymTranscript.test.ts) |
| Save / Load State | `GymSaveLoadScene` | `SaveLoadStore`, `serializeWithVersion`, `deserializeWithVersion` | [`scenes/GymSaveLoadScene.ts`](../../example-games/gym/scenes/GymSaveLoadScene.ts) | [`GymSaveLoad.test.ts`](../../tests/gym/GymSaveLoad.test.ts) |
| Save / Load State | `GymSaveLoadScene` | `SaveLoadStore`, `serializeWithVersion`, `deserializeWithVersion`, `RenderTexture.saveTexture()`, `RenderTexture.snapshot()`, snapshot persistence via base64 data URL | [`scenes/GymSaveLoadScene.ts`](../../example-games/gym/scenes/GymSaveLoadScene.ts) | [`GymSaveLoad.test.ts`](../../tests/gym/GymSaveLoad.test.ts) |
| Audio & Feedback Config | `GymAudioFeedbackScene` | `SoundManager`, `GameEventEmitter`, `EventSoundMapping` | [`scenes/GymAudioFeedbackScene.ts`](../../example-games/gym/scenes/GymAudioFeedbackScene.ts) | [`GymAudioFeedback.test.ts`](../../tests/gym/GymAudioFeedback.test.ts) |
| Shader & Blend Spike | `GymGraphicsShaderSpikeScene` | Sprite tinting, blend modes, shader feasibility | [`scenes/GymGraphicsShaderSpikeScene.ts`](../../example-games/gym/scenes/GymGraphicsShaderSpikeScene.ts) | [`GymSceneSmoke.browser.test.ts`](../../tests/gym/GymSceneSmoke.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) |
Expand Down
11 changes: 11 additions & 0 deletions docs/main-street/prd-milestone-6.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ Approach:
- `tests/main-street/**`
- `tests/e2e/replay-main-street.e2e.test.ts`
- `tests/main-street/monte-carlo-balance.test.ts`
- `tests/main-street/market-extraction-parity.test.ts` — extraction parity oracle for MarketOfferEngine (57 tests, CG-0MPWZ5R1M001MZ3B)

### Manual checks

Expand Down Expand Up @@ -298,6 +299,16 @@ Suggested implementation sequence:

---

## 9.1 Implementation progress

| Component | Status | Work Item | Notes |
|---|---|---|---|
| MarketOfferEngine — extraction parity tests | ✅ Done | CG-0MPWZ5R1M001MZ3B | 57 tests in `tests/main-street/market-extraction-parity.test.ts` (positive + negative paths, reshuffle behavior, integration, refill) |
| MarketOfferEngine — shared module extraction | ⏳ Pending | — | Awaiting follow-up implementation work |
| Economy Ledger — extraction parity tests | ✅ Done | CG-0MPWZ5RFI001DJUA | 51 unit + integration tests in `tests/rule-engine/EconomyLedger.test.ts`; shared module at `src/rule-engine/EconomyLedger.ts` |
| Economy Ledger — shared module extraction | ✅ Done | CG-0MPWZ5RFI001DJUA | `EconomyLedger` interface and `createEconomyLedger` factory at `src/rule-engine/EconomyLedger.ts` with barrel export; 51 unit + integration tests; API documented in `docs/rule-engine/economy-ledger.md`; Main Street migration to consume it is a follow-up task |
| Action Commands | ⏳ Pending | — | — |

## 10. Open questions

1. Should HintEngine live under `src/ai/` (strategy-centric) or `src/rule-engine/` (rule-eval-centric)?
Expand Down
164 changes: 164 additions & 0 deletions docs/rule-engine/economy-ledger.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Rule Engine: Economy Ledger API

Work item: CG-0MPWZ5RFI001DJUA

The rule engine provides a generic resource-tracking component for managing
mutable game economy values (coins, reputation, score). Extracted from the
Main Street game to provide reusable economy mutation semantics for any
tableau card game.

## Exports

```ts
import {
createEconomyLedger,
type EconomyLedger,
type EconomyLedgerConfig,
type EconomyConstraints,
type ResourceDelta,
type ResourceSnapshot,
} from '@rule-engine';
```

## Quick Start

```ts
const ledger = createEconomyLedger({ coins: 10, reputation: 3 });

// Check if a purchase is affordable (with constraints)
const purchaseLedger = createEconomyLedger({
coins: 10,
constraints: { minCoins: 0 },
});

if (purchaseLedger.canApply({ coins: -8 })) {
purchaseLedger.apply({ coins: -8 }, 'buy-business');
}

// Earn income
ledger.apply({ coins: 5 }, 'income');

// Event resolution with multiple resources
ledger.apply({ coins: 3, reputation: 1 }, 'event-resolve');

// Set score directly (for games where score is independent)
ledger.setScore(150);

// Snapshot for undo/redo or save/load
const snapshot = ledger.snapshot();
// { coins: 15, reputation: 4, score: 150 }
```

## Design Principles

### No Built-in Loss Conditions

The EconomyLedger does **not** enforce bankruptcy, reputation collapse, or
other loss conditions. Coins and reputation are allowed to go negative. The
game engine is responsible for win/loss detection after mutations. This
matches the Main Street baseline where `checkImmediateLoss()` reads from
the resource bank after all mutations are applied.

### Purely Additive Semantics

All `apply` operations are purely additive — no multiplicative or clamping
behavior. Negative deltas subtract, positive deltas add. Unspecified fields
in a `ResourceDelta` are left unchanged.

### Constraints are Optional

By default, `canApply` always returns `true`. Constraints (`minCoins`,
`minReputation`) are opt-in and only affect `canApply` — they do **not**
prevent `apply` from executing. The caller is responsible for checking
`canApply` before calling `apply`.

### Score is Independent

Score is treated as an independent resource that can be set directly via
`setScore()`. Unlike coins and reputation (which use additive deltas via
`apply`), score can be set to any absolute value. Games that derive score
from other resources should compute it externally and call `setScore()`.

## API Reference

### `createEconomyLedger(config?)`

Factory function that creates a new `EconomyLedger` instance.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `config.coins` | `number` | `0` | Initial coin balance |
| `config.reputation` | `number` | `0` | Initial reputation |
| `config.score` | `number` | `0` | Initial score |
| `config.constraints` | `EconomyConstraints` | `{}` | Optional `canApply` constraints |

### `EconomyLedger` Interface

| Method | Description |
|--------|-------------|
| `get(resource)` | Returns current value of `coins`, `reputation`, or `score` |
| `snapshot()` | Returns a `ResourceSnapshot` copy of all current values |
| `canApply(delta)` | Checks if delta can be applied given constraints (always `true` without constraints) |
| `apply(delta, reason?)` | Applies additive deltas to specified resources |
| `setScore(value)` | Sets score to an absolute value |

### Types

```ts
interface ResourceDelta {
coins?: number;
reputation?: number;
score?: number;
}

interface ResourceSnapshot {
coins: number;
reputation: number;
score: number;
}

interface EconomyConstraints {
minCoins?: number;
minReputation?: number;
}
```

## Integration Notes

### Main Street Migration

Main Street currently manages economy directly on `state.resourceBank`.
To migrate, create an `EconomyLedger` during game setup and delegate all
resource mutations through it:

```ts
// Before (MainStreetEngine.ts):
state.resourceBank.coins -= card.cost;
state.resourceBank.coins += income;

// After:
ledger.apply({ coins: -card.cost }, 'purchase');
ledger.apply({ coins: income }, 'income');
```

The ledger's `snapshot()` method can replace manual resource bank cloning
for undo/redo, and `get()` provides a clean read API for UI display.

### Reputation Multiplier

The EconomyLedger does **not** include reputation-based coin scaling.
Games should apply the reputation multiplier upstream (before calling
`apply`) using their own `applyReputationMultiplier` function. This keeps
the ledger generic and avoids coupling it to Main Street's specific
multiplier formula.

## Tests

51 unit and integration tests are available in
`tests/rule-engine/EconomyLedger.test.ts`, covering:

- `get` / `snapshot` / `canApply` / `apply` / `setScore` semantics
- Invariant checks (no underflow guards, deterministic ordering, additive behavior)
- Integration parity with Main Street economy outcomes
- Direct `computeScore()` function equivalence
- Negative economy integration (bankruptcy, reputation collapse, combined)
4 changes: 2 additions & 2 deletions example-games/feudalism/scenes/FeudalismConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ export const AI_AREA_Y = LOWER_TOP;
export const DIVIDER_X = 640;

// Action buttons
export const ACTION_Y = 660;
export const INSTRUCTION_Y = 696;
export const ACTION_Y = 652;
export const INSTRUCTION_Y = 708;

// ── Audio asset keys ──────────────────────────────────────
export const SFX_KEYS = {
Expand Down
2 changes: 1 addition & 1 deletion example-games/feudalism/scenes/FeudalismRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ import {
ACTION_Y, INSTRUCTION_Y,
RESOURCE_FILL, RESOURCE_TEXT_COLOR, RESOURCE_ICON_COLOR, RESOURCE_LABEL_COLOR,
} from './FeudalismConstants';
import { createFeudalismActionButton } from '../../../src/ui/Renderer/adapters/FeudalismAdapter';
import {
buildTokenEntries,
getBonusRenderOrder,
getTokenRenderOrder,
} from './FeudalismRenderHelpers';
import { createFeudalismActionButton } from '../../../src/ui/Renderer/adapters/FeudalismAdapter';

export interface MarketCallbacks {
onMarketCardClick: (card: DevelopmentCard) => void;
Expand Down
104 changes: 103 additions & 1 deletion example-games/gym/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,106 @@ deckView.onClick(() => { /* draw logic */ deckView.update(); });
deckView.destroy();
```

**API**: `setPile(pile)`, `peek()`, `update()`, `onClick(cb)`, `getCountText()`, `getSprite()`, `getPile()`, `destroy()`.
**API**: `setPile(pile)`, `peek()`, `update()`, `onClick(cb)`, `getCountText()`, `getSprite()`, `getPile()`, `destroy()`.

## Shared Gym Utilities

The Gym provides shared utility functions (in [`src/ui/GymSceneUtils.ts`](../../src/ui/GymSceneUtils.ts)) that extract common rendering patterns from demo scenes. These are used internally by Gym scenes but can also be imported directly for custom scenes or testing.

### `createEventLog(scene, baseY, options?)`

Renders a centered header and scrollable log line area. Used by 7 Gym scenes to display event logs.

```ts
import { createEventLog } from '@ui/GymSceneUtils';

const eventLog = createEventLog(scene, 200, {
headerText: '── Event Log ──', // default
maxLines: 14, // default
lineHeight: 17, // default
textColor: '#aaddaa', // default
fontSize: '11px', // default
lineX: 40, // default
});

// Later, update the display:
eventLog.render(myLogLines);

// Cleanup:
eventLog.destroy();
```

**Options**: `headerText`, `lineHeight`, `textColor`, `maxLines`, `fontSize`, `headerX`, `lineX`, `headerFontSize`, `headerColor`.

**Returns**: `{ header, lines, baseY, render(lines), destroy() }`.

### `createDeckGrid(scene, deck, options?)`

Renders a deck of cards as a compact face-up grid. Used by GymDeckRngScene.

```ts
import { createDeckGrid } from '@ui/GymSceneUtils';

const grid = createDeckGrid(scene, myDeck, {
cols: 8, // default
gapX: 4, // default
gapY: 4, // default
centerX: 640, // default: GAME_W / 2
centerY: 370, // default gym position
});

// Replace with a new shuffled deck:
grid.destroy();
const newGrid = createDeckGrid(scene, shuffledDeck);
```

**Options**: `gapX`, `gapY`, `cols`, `centerX`, `centerY`, `cardScale`.

**Returns**: `{ sprites[], destroy() }`.

### `createSlider(scene, x, y, options?)`

Creates a horizontal slider with track, fill bar, handle, and value text. Encapsulates drag logic. Used by GymHandPileScene's three live-control sliders.

```ts
import { createSlider } from '@ui/GymSceneUtils';

const slider = createSlider(scene, 100, 680, {
initialValue: 0.5,
minValue: 0,
maxValue: 1,
label: 'Volume',
width: 150,
textColor: '#88ff88',
});

// Wire value changes:
slider.onValueChange = (value) => {
console.log('Slider value:', value);
};

// Wire scene input to slider drag:
scene.input.on('pointermove', (pointer) => {
slider.handlePointerMove(pointer.x);
});
scene.input.on('pointerup', () => {
slider.handlePointerUp();
});

// Programmatic set (does not fire onValueChange):
slider.setValue(0.75);
```

**Options**: `initialValue`, `minValue`, `maxValue`, `label`, `width`, `trackHeight`, `trackColor`, `fillColor`, `handleColor`, `fontSize`, `textColor`.

**Returns**: `{ value, track, fill, handle, valueText, hitArea, onValueChange, setValue(v), destroy(), handlePointerMove(px), handlePointerUp() }`.

### Migration Notes

The following Gym scenes were migrated to use these shared utilities:

- **Event log** (`createEventLog`): GymAudioFeedbackScene, GymGraphicsLightingSpikeScene, GymGraphicsShaderSpikeScene, GymOverlayUiScene, GymSaveLoadScene, GymTranscriptScene, GymUndoRedoScene
- **Deck grid** (`createDeckGrid`): GymDeckRngScene
- **Slider** (`createSlider`): GymHandPileScene (3 sliders: arc, spacing, rotation)

Each migration preserved the original visual parameters (header text, colors, line spacing, slider ranges) via options, so player-facing behavior is unchanged.
Loading
Loading