dreamboard 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-JUL27YLF.js → chunk-3I26YUTP.js} +11 -24
- package/dist/chunk-3I26YUTP.js.map +1 -0
- package/dist/{chunk-DD36YMMQ.js → chunk-MP7IBNWW.js} +1837 -107
- package/dist/chunk-MP7IBNWW.js.map +1 -0
- package/dist/{dist-W7KDKXWG.js → dist-B3R64F6G.js} +2 -2
- package/dist/{embedded-harness-DQOVBU6E.js → embedded-harness-TJZ5LB64.js} +2 -2
- package/dist/index.js +429 -266
- package/dist/index.js.map +1 -1
- package/dist/{prompt-XCUPD2NB.js → prompt-GMZABCJC.js} +3 -3
- package/dist/{prompt-XCUPD2NB.js.map → prompt-GMZABCJC.js.map} +1 -1
- package/package.json +1 -1
- package/skills/dreamboard/SKILL.md +4 -29
- package/skills/dreamboard/references/adversarial-testing.md +1 -1
- package/skills/dreamboard/references/api-reference.md +4 -0
- package/skills/dreamboard/references/app-best-practices.md +45 -33
- package/skills/dreamboard/references/building-your-first-game.md +49 -0
- package/skills/dreamboard/references/hands-vs-decks.md +84 -46
- package/skills/dreamboard/references/manifest-authoring.md +144 -406
- package/skills/dreamboard/references/rule-authoring.md +55 -57
- package/skills/dreamboard/references/ui-best-practices.md +97 -99
- package/dist/chunk-DD36YMMQ.js.map +0 -1
- package/dist/chunk-JUL27YLF.js.map +0 -1
- /package/dist/{dist-W7KDKXWG.js.map → dist-B3R64F6G.js.map} +0 -0
- /package/dist/{embedded-harness-DQOVBU6E.js.map → embedded-harness-TJZ5LB64.js.map} +0 -0
|
@@ -1082,7 +1082,7 @@ function createConsola2(options = {}) {
|
|
|
1082
1082
|
defaults: { level },
|
|
1083
1083
|
stdout: process.stdout,
|
|
1084
1084
|
stderr: process.stderr,
|
|
1085
|
-
prompt: (...args) => import("./prompt-
|
|
1085
|
+
prompt: (...args) => import("./prompt-GMZABCJC.js").then((m) => m.prompt(...args)),
|
|
1086
1086
|
reporters: options.reporters || [
|
|
1087
1087
|
options.fancy ?? !(T || R) ? new FancyReporter() : new BasicReporter()
|
|
1088
1088
|
],
|
|
@@ -1932,23 +1932,24 @@ import path from "path";
|
|
|
1932
1932
|
|
|
1933
1933
|
// src/generated/skill-content.generated.ts
|
|
1934
1934
|
var SKILL_ASSET_FILES = {
|
|
1935
|
-
"references/adversarial-testing.md": "# Adversarial Testing\n\nAdversarial testing uses scenario files to intentionally stress game rules, turn gating, and edge conditions.\n\nUse this guide with `dreamboard run --scenario <file>`.\n\nThis file is about adversarial coverage strategy, not JSON schema.\n\n- For the JSON step format itself, see [scenario-format.md](scenario-format.md).\n- For TypeScript regression tests, see [test-harness.md](test-harness.md).\n\n## Goals\n\n1. Validate that illegal or out-of-sequence actions are rejected.\n2. Validate that legal actions still work after edge-state pressure.\n3. Catch flaky behavior by running with deterministic seeds.\n\n## Core Workflow\n\n1. Start from a known state:\n\n```bash\ndreamboard run --new-session --seed 1337 --players <playerCount>\n```\n\n2. Inspect `.dreamboard/run/latest-your-turn.json` to identify:\n\n - Current controllable player(s)\n - Available actions and parameter shapes\n - Turn/phase context\n\n3. Write a focused scenario file that targets one adversarial behavior.\n4. Execute the scenario:\n\n```bash\ndreamboard run --players <playerCount> --scenario scenario-for-<case>.json\n```\n\n5. Validate outcomes using:\n - CLI exit result (`scenario_completed` vs `scenario_rejected`)\n - `.dreamboard/run/events.ndjson`\n - `.dreamboard/run/last-run-summary.json`\n\nFor deeper inspection, extract specific fields from events:\n\n```bash\nnode .agents/skills/dreamboard/scripts/events-extract.mjs --type ACTION_REJECTED --field message.reason\nnode .agents/skills/dreamboard/scripts/events-extract.mjs --type YOUR_TURN --field message.availableActions --limit 1\n```\n\n## Progression Strategy\n\n1. Start simple first:\n - Begin with 1-step or 2-step scenarios.\n - Use a single player (`player-1`) and one expected behavior per file.\n - Confirm your action names and parameter shapes are correct before scaling up.\n2. Then move to complex simulations:\n - Create multi-step scenarios that chain several turns.\n - Include multiple players across steps (for example `player-1`, `player-2`, `player-3`).\n - Combine normal and adversarial actions in the same flow to test resilience after state transitions.\n3. Create scenario that fully tests a round (e.g. all players played their hand, or all players placed their workers)\n4. Ensure AUTO phase transitions are correct.\n5. Keep deterministic inputs (`--new-session --seed 1337`) while increasing complexity so failures stay reproducible.\n\nBuild scenario files using the contract in [scenario-format.md](scenario-format.md), then use the patterns below to decide what to test.\n\n## Adversarial Patterns\n\nCreate one scenario per pattern so failures are easy to triage.\n\n1. Invalid parameters: missing required input, invalid enum, or out-of-range values.\n2. Wrong player: action submitted by a non-active/non-controllable player.\n3. Order violation: execute actions in a sequence that should be illegal.\n4. Resource exhaustion: repeat actions until resources/cards/slots are depleted.\n5. Duplicate or replay behavior: attempt the same step repeatedly in one turn.\n6. Boundary transitions: move through end-of-round/end-of-game boundaries and attempt one extra action.\n\n## Practical Conventions\n\n1. Keep each scenario small and single-purpose.\n2. Name files by expected behavior, for example:\n - `scenario-reject-invalid-params.json`\n - `scenario-reject-wrong-player.json`\n - `scenario-endgame-boundary.json`\n3. Prefer `--new-session --seed 1337` while authoring/debugging to avoid random drift.\n4. If turn context is missing, `dreamboard run --scenario ...` will wait for the first `YOUR_TURN` automatically.\n5. API scenarios are strict: if a step gets `ACTION_REJECTED`, the run stops with `scenario_rejected`.\n6. Use `events-extract.
|
|
1935
|
+
"references/adversarial-testing.md": "# Adversarial Testing\n\nAdversarial testing uses scenario files to intentionally stress game rules, turn gating, and edge conditions.\n\nUse this guide with `dreamboard run --scenario <file>`.\n\nThis file is about adversarial coverage strategy, not JSON schema.\n\n- For the JSON step format itself, see [scenario-format.md](scenario-format.md).\n- For TypeScript regression tests, see [test-harness.md](test-harness.md).\n\n## Goals\n\n1. Validate that illegal or out-of-sequence actions are rejected.\n2. Validate that legal actions still work after edge-state pressure.\n3. Catch flaky behavior by running with deterministic seeds.\n\n## Core Workflow\n\n1. Start from a known state:\n\n```bash\ndreamboard run --new-session --seed 1337 --players <playerCount>\n```\n\n2. Inspect `.dreamboard/run/latest-your-turn.json` to identify:\n\n - Current controllable player(s)\n - Available actions and parameter shapes\n - Turn/phase context\n\n3. Write a focused scenario file that targets one adversarial behavior.\n4. Execute the scenario:\n\n```bash\ndreamboard run --players <playerCount> --scenario scenario-for-<case>.json\n```\n\n5. Validate outcomes using:\n - CLI exit result (`scenario_completed` vs `scenario_rejected`)\n - `.dreamboard/run/events.ndjson`\n - `.dreamboard/run/last-run-summary.json`\n\nFor deeper inspection, extract specific fields from events:\n\n```bash\nnode .agents/skills/dreamboard/scripts/events-extract.mjs --type ACTION_REJECTED --field message.reason\nnode .agents/skills/dreamboard/scripts/events-extract.mjs --type YOUR_TURN --field message.availableActions --limit 1\n```\n\n## Progression Strategy\n\n1. Start simple first:\n - Begin with 1-step or 2-step scenarios.\n - Use a single player (`player-1`) and one expected behavior per file.\n - Confirm your action names and parameter shapes are correct before scaling up.\n2. Then move to complex simulations:\n - Create multi-step scenarios that chain several turns.\n - Include multiple players across steps (for example `player-1`, `player-2`, `player-3`).\n - Combine normal and adversarial actions in the same flow to test resilience after state transitions.\n3. Create scenario that fully tests a round (e.g. all players played their hand, or all players placed their workers)\n4. Ensure AUTO phase transitions are correct.\n5. Keep deterministic inputs (`--new-session --seed 1337`) while increasing complexity so failures stay reproducible.\n\nBuild scenario files using the contract in [scenario-format.md](scenario-format.md), then use the patterns below to decide what to test.\n\n## Adversarial Patterns\n\nCreate one scenario per pattern so failures are easy to triage.\n\n1. Invalid parameters: missing required input, invalid enum, or out-of-range values.\n2. Wrong player: action submitted by a non-active/non-controllable player.\n3. Order violation: execute actions in a sequence that should be illegal.\n4. Resource exhaustion: repeat actions until resources/cards/slots are depleted.\n5. Duplicate or replay behavior: attempt the same step repeatedly in one turn.\n6. Boundary transitions: move through end-of-round/end-of-game boundaries and attempt one extra action.\n\n## Practical Conventions\n\n1. Keep each scenario small and single-purpose.\n2. Name files by expected behavior, for example:\n - `scenario-reject-invalid-params.json`\n - `scenario-reject-wrong-player.json`\n - `scenario-endgame-boundary.json`\n3. Prefer `--new-session --seed 1337` while authoring/debugging to avoid random drift.\n4. If turn context is missing, `dreamboard run --scenario ...` will wait for the first `YOUR_TURN` automatically.\n5. API scenarios are strict: if a step gets `ACTION_REJECTED`, the run stops with `scenario_rejected`.\n6. Use `events-extract.mjs` to quickly inspect rejection reasons, available actions, and player transitions before editing scenarios.\n\n### Error: `Card is not in your hand`\n\nWhen this rejection appears, verify the target player's hand state across turns before and after each scenario step.\n\n1. Extract rejection events first:\n\n```bash\nnode .agents/skills/dreamboard/scripts/events-extract.mjs --type ACTION_REJECTED --field message.reason\n```\n\n2. Inspect hand snapshots from turn events for the target player (example: `player-2`):\n\n```bash\nnode .agents/skills/dreamboard/scripts/events-extract.mjs --type YOUR_TURN --player player-2 --field message.gameState.hands\n```\n\n3. Compare entries by `index` in output to see how the player's hand changes between turns.\n4. Confirm the `cardId` in your scenario step exists in the target player's hand at the turn where the action is submitted.\n5. If the card disappears earlier than expected, inspect preceding steps for draws/discards/transfers that changed ownership.\n\n## Optional UI Driver\n\nUse `--scenario-driver ui` only for browser input-path coverage (click targets, interaction wiring).\nKeep most adversarial rule tests on the API driver for reliability and speed.\n",
|
|
1936
1936
|
"references/all-players-tracking.md": '# ALL_PLAYERS Phase - Tracking Who Has Acted\n\nThe engine automatically tracks action progress in `ALL_PLAYERS` (`MultiplePlayer`) phases through `ctx.phase`. Do not implement manual KV tracking for this.\n\n## Built-in phase tracking API\n\nUse `ctx.phase` in `validateAction` and `checkCompletion`:\n\n- `ctx.phase.hasPlayerActed(playerId)` - has this player already submitted an action?\n- `ctx.phase.haveAllExpectedPlayersActed()` - are all currently expected players done?\n- `ctx.phase.getPlayersStillWaiting()` - which players are still pending?\n- `ctx.phase.getExpectedPlayers()` - who is expected to act this phase?\n- `ctx.phase.getPlayersWhoActed()` - who has already acted?\n\n## Recommended pattern\n\n```typescript\nimport type { MultiplePlayerPhaseDefinition } from "../sdk/phaseHandlers";\nimport { createMultiplePlayerPhase } from "../sdk/phaseHandlers";\nimport { validationSuccess, validationError } from "../sdk/validation.js";\n\ntype PhaseAction = "revealCard";\n\nconst phaseDefinition = {\n onEnter(ctx) {\n const { state, apis } = ctx;\n apis.gameApi.setActivePlayers(state.player.getOrder());\n // No manual tracking setup required.\n },\n\n validateAction(ctx, playerId, actionType, parameters) {\n if (ctx.phase.hasPlayerActed(playerId)) {\n return validationError("already-acted", "You already acted this phase");\n }\n return validationSuccess();\n },\n\n onPlayerAction(ctx, playerId, actionType, parameters) {\n const { apis } = ctx;\n // ... process action ...\n // Engine tracking updates automatically after accepted actions.\n },\n\n checkCompletion(ctx) {\n if (ctx.phase.haveAllExpectedPlayersActed()) {\n return "nextPhase";\n }\n return null;\n },\n\n onComplete(ctx) {\n // Optional cleanup only. No action-tracking cleanup required.\n },\n\n getUIArgs(ctx, playerId) {\n return {\n waitingOn: ctx.phase.getPlayersStillWaiting(),\n };\n },\n} satisfies MultiplePlayerPhaseDefinition<"revealCards", PhaseAction>;\n\nexport const phase = createMultiplePlayerPhase(phaseDefinition);\n```\n\n## If UI needs progress data\n\n`ctx.phase` is available in app logic. For UI, pass derived values via `getUIArgs()` (for example `waitingOn`), or write explicit UI-facing values via global/player state APIs.\n\n## Checklist for ALL_PLAYERS phases\n\n1. Call `apis.gameApi.setActivePlayers(state.player.getOrder())` in `onEnter`.\n2. Block duplicate actions with `ctx.phase.hasPlayerActed(playerId)` in `validateAction`.\n3. Process each action in `onPlayerAction` without manual tracking writes.\n4. Transition from `checkCompletion` when `ctx.phase.haveAllExpectedPlayersActed()` is true.\n5. Use `ctx.phase.getPlayersStillWaiting()` / `getPlayersWhoActed()` for logs or UI args as needed.\n',
|
|
1937
|
-
"references/api-reference.md": '# State & API Reference\n\n## Read APIs (`ctx.state.*`)\n\n### `state.player`\n\n| Method | Returns | Description |\n| -------------------------------------- | ------------- | ------------------------------------- |\n| `.getOrder()` | `PlayerId[]` | All players in turn order |\n| `.getCurrentIds()` | `PlayerId[]` | Currently active player IDs |\n| `.get(playerId)` | `Player` | Player data: `{ id, name, score }` |\n| `.getState(playerId)` | `PlayerState` | Player variables from manifest schema |\n| `.getHand(playerId, handId)` | `CardId[]` | Cards in a specific hand |\n| `.getAllHands(playerId)` | `CardId[]` | All cards across all hands |\n| `.isInHand(playerId, cardId, handId?)` | `boolean` | Check if card is in hand |\n| `.isActive(playerId)` | `boolean` | Is player currently active |\n\n### `state.deck`\n\n| Method | Returns | Description |\n| --------------------- | ---------------- | ---------------------- |\n| `.getCards(deckId)` | `CardId[]` | Cards in a shared deck |\n| `.getTopCard(deckId)` | `CardId \\| null` | Top card of deck |\n\n### `state.card`\n\n| Method | Returns | Description |\n| -------------------------------- | ------------------ | ---------------------------------------------------- |\n| `.get(cardId)` | `Card` | Full card data |\n| `.getProperties(cardId)` | Typed properties | Card-specific props (rank, suit, etc.) |\n| `.getLocation(cardId)` | `Location` | Where the card is (InHand, InDeck, InZone, Detached) |\n| `.getPlayedBy(cardId)` | `PlayerId \\| null` | Who played this card to a deck/zone |\n| `.getOwner(cardId)` | `PlayerId \\| null` | Card owner |\n| `.isVisibleTo(cardId, playerId)` | `boolean` | Visibility check |\n| `.getOwnedBy(playerId)` | `CardId[]` | All cards owned by player |\n\n### `state.game`\n\n| Method | Returns | Description |\n| -------------------- | ------------- | ------------------------------------- |\n| `.getGlobalState()` | `GlobalState` | Global variables from manifest schema |\n| `.getCurrentState()` | `StateName` | Current state machine state |\n\n## Mutation APIs (`ctx.apis.*`)\n\n### `apis.cardApi`\n\n| Method | Description |\n| --------------------------------------------------------------------------- | ------------------------------ |\n| `.moveCardsFromHandToDeck(playerId, handId, cardIds, deckId)` | Hand \u2192 Deck |\n| `.moveCardsFromHandToHand(fromPlayer, fromHand, toPlayer, toHand, cardIds)` | Hand \u2192 Hand |\n| `.moveCardsToPlayer(cardIds, toPlayerId, handId)` | Any \u2192 Hand |\n| `.flip(deckId, cardId)` | Flip card face up/down |\n| `.detachCard(cardId)` | Remove card from all locations |\n| `.transferOwnership(cardId, toPlayer)` | Change card owner |\n\n#### Card movement semantics\n\n- `moveCardsFromHandToHand(fromPlayer, fromHand, toPlayer, toHand, cardIds)` is **additive** at destination.\n- Destination cards are preserved; moved cards are added to the destination hand.\n- This API does **not** replace/overwrite the destination hand.\n- For pass/rotate mechanics where each player should receive exactly one other player\'s hand, avoid in-place cyclic moves. Snapshot source hands first, then move via a temporary location (or other two-phase transfer pattern).\n\n### `apis.deckApi`\n\n| Method | Description |\n| ------------------------------------------------------------- | ------------------------- |\n| `.moveCardsFromDeckToPlayer(deckId, playerId, handId, count)` | Deck \u2192 Hand (deal) |\n| `.moveCardsFromDeckToDeck(fromDeckId, toDeckId)` | Deck \u2192 Deck |\n| `.shuffle(deckId)` | Shuffle a deck |\n| `.addCards(deckId, cardIds)` | Add cards to a deck |\n| `.removeCard(deckId, cardId)` | Remove a card from a deck |\n\n### `apis.gameApi`\n\n| Method | Description |\n| ---------------------------------- | ---------------------------------------------- |\n| `.setActivePlayers(playerIds)` | Set who can act (use in ALL_PLAYERS `onEnter`) |\n| `.setNextPlayer(playerId)` | Set single active player |\n| `.advanceTurn()` | Move to next player in order |\n| `.declareWinner(playerId, reason)` | Declare game winner |\n| `.endGame()` | End the game |\n\n### `apis.globalStateApi`\n\n| Method | Description |\n| ------------------------------------- | ---------------------------- |\n| `.setGlobalState(newState)` | Replace all global variables |\n| `.setPlayerState(playerId, newState)` | Replace a player\'s variables |\n\n### `apis.kvApi`\n\nInternal-only key-value store. **UI cannot access these values.**\n\n| Method | Returns | Description |\n| ------------------ | ---------------- | ----------------------------------- |\n| `.set(key, value)` | `KvSetResult` | Store a JSON value |\n| `.get(key)` | `KvGetResult` | Read a value (`.success`, `.value`) |\n| `.delete(key)` | `KvDeleteResult` | Remove a key |\n| `.has(key)` | `boolean` | Check existence |\n| `.keys()` | `string[]` | List all keys |\n\n### `apis.resourceApi`\n\n| Method | Description |\n| ------------------------- | -------------------------- |\n| `.add(playerId, cost)` | Give resources to player |\n| `.deduct(playerId, cost)` | Take resources from player |\n\n### `apis.dieApi`\n\n| Method | Description |\n| -------------------------- | ------------------------- |\n| `.roll(dieId)` | Roll a die |\n| `.setValue(dieId, value?)` | Set die to specific value |\n\n## Typed KV Store\n\nUse `createTypedKv` from `sdk/stateApi.js` for type-safe KV access:\n\n```typescript\nimport { createTypedKv } from "../sdk/stateApi.js";\n\ninterface MyKv {\n playersActed: PlayerId[];\n roundData: { scores: number[] };\n}\n\nconst kv = createTypedKv<MyKv>(apis.kvApi);\nkv.set("playersActed", ["player-1"]); // Type-checked key and value\nconst acted = kv.get("playersActed"); // PlayerId[] | null\nkv.has("playersActed"); // boolean\nkv.delete("playersActed"); // boolean (existed)\n```\n\n## Card `playedBy` Tracking\n\nWhen cards move from a Hand to a Deck via `moveCardsFromHandToDeck()`, the engine automatically records `playedBy` on each card:\n\n```typescript\n// Move card from player\'s hand to a shared deck\napis.cardApi.moveCardsFromHandToDeck(\n playerId,\n "main-hand",\n [cardId],\n "play-area",\n);\n\n// Later, check who played a specific card\nconst whoPlayed = state.card.getPlayedBy(cardId); // PlayerId | null\n```\n\n## Location Type\n\nCards have a location discriminated union:\n\n```typescript\ntype Location =\n | { type: "Detached" }\n | {\n type: "InDeck";\n deckId: DeckId;\n playedBy: PlayerId | null;\n position: number | null;\n }\n | {\n type: "InHand";\n handId: HandId;\n playerId: PlayerId;\n position: number | null;\n }\n | {\n type: "InZone";\n zoneId: string;\n playedBy: PlayerId | null;\n position: number | null;\n };\n```\n\n## Validation Helpers\n\n```typescript\nimport { validationSuccess, validationError } from "../sdk/validation.js";\n\n// Valid action\nreturn validationSuccess();\n\n// Invalid action \u2014 errorCode should be kebab-case\nreturn validationError(\n "must-play-valid-combination",\n "You must play a valid card combination",\n);\n```\n',
|
|
1938
|
-
"references/app-best-practices.md": '# App Best Practices\n\
|
|
1939
|
-
"references/
|
|
1940
|
-
"references/manifest-authoring.md": '# Manifest Authoring Guide\n\n`manifest.json` is the source of truth for your game\'s structure \u2014 components, actions, state machine, and variables. After editing, run `dreamboard update` to regenerate scaffolded files (`app/phases/`, `shared/manifest.d.ts`, etc.).\n\n## Top-Level Structure\n\n```json\n{\n "version": "1.0.0",\n "playerConfig": { ... },\n "deckDefinitions": [ ... ],\n "playerHandDefinitions": [ ... ],\n "components": [ ... ],\n "resources": [ ... ],\n "boardDefinitions": [ ... ],\n "availableActions": [ ... ],\n "stateMachine": { ... },\n "variableSchema": { ... }\n}\n```\n\n| Field | Required | Description |\n| ----------------------- | -------- | -------------------------------------------------- |\n| `version` | \u2705 | Manifest version - increment manually when updated |\n| `playerConfig` | \u2705 | Min/max/optimal player counts |\n| `deckDefinitions` | \u2705 | Card deck blueprints (preset or manual) |\n| `playerHandDefinitions` | \u2705 | Per-player card containers |\n| `components` | \u2705 | Shared game components (decks, dice) |\n| `resources` | \u274C | Typed resource economy (gold, wood) |\n| `boardDefinitions` | \u274C | Spatial boards (hex, network, square, track) |\n| `availableActions` | \u2705 | Player submission buttons |\n| `stateMachine` | \u2705 | Game phases and transitions |\n| `variableSchema` | \u2705 | Minimal state for game logic |\n\n---\n\n## Authoring Sequence\n\nWork through sections in this order. Skip any step that doesn\'t apply to your game.\n\n### 1. `playerConfig`\n\n```json\n{\n "playerConfig": {\n "minPlayers": 2,\n "maxPlayers": 4,\n "optimalPlayers": 4\n }\n}\n```\n\n| Field | Type | Range | Description |\n| ---------------- | ------- | ----- | ------------------------------------ |\n| `minPlayers` | integer | 1\u201310 | Minimum players required |\n| `maxPlayers` | integer | 1\u201310 | Maximum players supported |\n| `optimalPlayers` | integer | 1\u201310 | Best player count for the experience |\n\n### 2. `deckDefinitions`\n\nDeck definitions are blueprints for card types. There are two kinds:\n\n#### Preset decks\n\nUse `"standard_52_deck"` for standard playing cards. **Do NOT define 52 cards manually.**\n\n```json\n{\n "deckDefinitions": [\n {\n "type": "preset",\n "id": "standard_52_deck",\n "name": "Standard 52-Card Deck"\n }\n ]\n}\n```\n\n#### Manual (custom) decks\n\nDefine your own cards with a `cardSchema` and a `cards` list.\n\n```json\n{\n "type": "manual",\n "id": "resource-deck",\n "name": "Resource Cards",\n "cardSchema": {\n "properties": {\n "value": { "type": "integer", "description": "Point value of the card" },\n "category": { "type": "string", "description": "Resource category" }\n }\n },\n "cards": [\n {\n "type": "lumber",\n "name": "Lumber",\n "count": 4,\n "properties": { "value": "1", "category": "wood" }\n },\n {\n "type": "brick",\n "name": "Brick",\n "count": 3,\n "properties": { "value": "2", "category": "stone" }\n }\n ]\n}\n```\n\n**Card fields:**\n\n| Field | Required | Description |\n| ------------ | -------- | ---------------------------------------------------------------------------------------- |\n| `type` | \u2705 | Card type ID. Runtime IDs are generated as `{type}-1`, `{type}-2`, etc. when `count > 1` |\n| `name` | \u2705 | Display name |\n| `count` | \u2705 | Number of copies (\u2265 1) |\n| `properties` | \u2705 | Values matching `cardSchema` (all values are strings) |\n| `imageUrl` | \u274C | Card image URL |\n| `text` | \u274C | Text content on the card |\n| `cardType` | \u274C | Optional category within the deck |\n\n**Property schema types:** `string`, `integer`, `number`, `boolean`, `array`, `object`, `enum`, `deckId`, `cardId`, `playerId`\n\n### 3. `playerHandDefinitions`\n\nPer-player card containers. Each player gets their own instance automatically.\n\n```json\n{\n "playerHandDefinitions": [\n {\n "id": "main-hand",\n "displayName": "Hand",\n "visibility": "ownerOnly",\n "maxCards": 7,\n "deckDefinitionIds": ["standard_52_deck"]\n },\n {\n "id": "score-pile",\n "displayName": "Scored Cards",\n "visibility": "public"\n }\n ]\n}\n```\n\n| Field | Required | Default | Description |\n| ------------------- | -------- | ------------- | ----------------------------------------------------------- |\n| `id` | \u2705 | \u2014 | Unique hand ID (becomes `HandId` type) |\n| `displayName` | \u2705 | \u2014 | UI label |\n| `visibility` | \u274C | `"ownerOnly"` | `"ownerOnly"`, `"public"`, or `"hidden"` |\n| `maxCards` | \u274C | \u2014 | Maximum cards allowed |\n| `minCards` | \u274C | \u2014 | Minimum cards required |\n| `deckDefinitionIds` | \u274C | \u2014 | Restrict to cards from these deck definitions (empty = any) |\n| `description` | \u274C | \u2014 | Purpose description |\n\n> **DECK vs HAND:** `DeckComponent` (in `components`) is **shared** \u2014 one instance per game (draw piles, discard piles, trick zones). `PlayerHandDefinition` is **per-player** \u2014 private hands, tableaus, collected cards.\n\n### 4. `components` \u2014 Shared Decks\n\nDeck components are shared game zones that reference a deck definition.\n\n```json\n{\n "components": [\n {\n "type": "deck",\n "id": "draw-pile",\n "name": "Draw Pile",\n "deckDefinitionId": "standard_52_deck",\n "layout": "stack"\n },\n {\n "type": "deck",\n "id": "discard-pile",\n "name": "Discard Pile",\n "deckDefinitionId": "standard_52_deck",\n "layout": "spread"\n }\n ]\n}\n```\n\n| Field | Required | Default | Description |\n| ------------------ | -------- | --------- | --------------------------------------- |\n| `type` | \u2705 | \u2014 | `"deck"` |\n| `id` | \u2705 | \u2014 | Unique component ID |\n| `name` | \u2705 | \u2014 | Display name |\n| `deckDefinitionId` | \u2705 | \u2014 | Which deck definition this sources from |\n| `layout` | \u274C | `"stack"` | `"stack"`, `"spread"`, or `"fan"` |\n\n### 5. `components` \u2014 Dice\n\nAdd dice as components with `type: "die"`.\n\n```json\n{\n "type": "die",\n "id": "d6-die",\n "name": "Six-Sided Die",\n "sides": 6\n}\n```\n\n| Field | Required | Description |\n| ------- | -------- | --------------------------------------------- |\n| `type` | \u2705 | `"die"` |\n| `id` | \u2705 | Unique die ID (e.g., `"d6-die"`, `"d20-die"`) |\n| `name` | \u2705 | Display name |\n| `sides` | \u2705 | Number of sides (\u2265 2) |\n\n### 6. `resources` (Optional)\n\nTyped resource economy for games with currencies or materials. Resources have a dedicated API (`canAfford`, `deduct`, `add`, `transfer`).\n\n```json\n{\n "resources": [\n { "id": "gold", "name": "Gold" },\n { "id": "wood", "name": "Wood" },\n { "id": "victoryPoints", "name": "Victory Points" }\n ]\n}\n```\n\n| Field | Required | Description |\n| ------ | -------- | ------------------------------------------------------------------ |\n| `id` | \u2705 | Unique resource ID (alphanumeric + underscore, starts with letter) |\n| `name` | \u2705 | Display name |\n\n> **Don\'t duplicate resources in `playerVariableSchema`.** Use the resource system instead.\n\n### 7. `boardDefinitions` (Optional)\n\nFor games with spatial structure. Each board type has its own shape:\n\n| Board Type | Use Case | Key Concepts |\n| ---------- | ------------------------------------- | ------------------------------------------------------------------------------------------------- |\n| `hex` | Hexagonal grids (Catan) | Tiles (pre-defined with IDs/types), Edges `[TileId, TileId]`, Vertices `[TileId, TileId, TileId]` |\n| `network` | Graph maps (Ticket to Ride, Pandemic) | Nodes (locations using TileId), Edges `[TileId, TileId]` |\n| `square` | Grid boards (Chess, Checkers) | Cells derived from row/col (e.g., `"a1"` to `"h8"`), Pieces placed on cells |\n| `track` | Path boards (Monopoly, VP track) | Sequential spaces with IDs, pieces on spaces |\n\n### 8. `availableActions`\n\nActions are **submissions** (buttons), **NOT selections**. Card/tile selection happens via UI clicks \u2014 the action definition declares the parameters that carry the selected items in the POST request.\n\n```json\n{\n "availableActions": [\n {\n "actionType": "playCard",\n "displayName": "Play Card",\n "description": "Play the selected card from your hand",\n "parameters": [\n {\n "name": "cardId",\n "type": "cardId",\n "required": true,\n "array": false,\n "deckDefinitionId": "standard_52_deck"\n }\n ],\n "errorCodes": ["NOT_YOUR_TURN", "INVALID_CARD", "MUST_FOLLOW_SUIT"]\n },\n {\n "actionType": "pass",\n "displayName": "Pass",\n "parameters": [],\n "errorCodes": ["CANNOT_PASS"]\n },\n {\n "actionType": "discardCards",\n "displayName": "Discard",\n "parameters": [\n {\n "name": "cardIds",\n "type": "cardId",\n "required": true,\n "array": true,\n "minLength": 1,\n "maxLength": 3,\n "deckDefinitionId": "standard_52_deck"\n }\n ],\n "errorCodes": ["WRONG_COUNT", "CARD_NOT_IN_HAND"]\n }\n ]\n}\n```\n\n**ActionDefinition fields:**\n\n| Field | Required | Description |\n| ------------- | -------- | ------------------------------------------------------------------------- |\n| `actionType` | \u2705 | Unique ID in camelCase (e.g., `"playCard"`, `"rollDice"`) |\n| `displayName` | \u2705 | Button label |\n| `description` | \u274C | Help text |\n| `parameters` | \u2705 | List of parameters (can be empty for parameterless actions like `"pass"`) |\n| `errorCodes` | \u274C | Possible validation error codes |\n\n**ActionParameterDefinition fields:**\n\n| Field | Required | Default | Description |\n| ------------------ | -------- | ------- | ----------------------------------------------------- |\n| `name` | \u2705 | \u2014 | Parameter name |\n| `type` | \u2705 | \u2014 | See parameter types below |\n| `required` | \u274C | `true` | Whether required |\n| `array` | \u274C | `false` | Set `true` when multiple values can be sent |\n| `minLength` | \u274C | \u2014 | Min items (only when `array: true`) |\n| `maxLength` | \u274C | \u2014 | Max items (only when `array: true`) |\n| `deckDefinitionId` | \u274C | \u2014 | Links `"cardId"` params to a specific deck definition |\n| `description` | \u274C | \u2014 | Help text |\n\n**Parameter types:**\n\n| Type | Description |\n| -------------- | ----------------------------------------------------------- |\n| `"cardId"` | Runtime card instance ID (e.g., `"lumber-1"`, `"lumber-2"`) |\n| `"cardType"` | Manifest-level card type identifier (e.g., `"lumber"`) |\n| `"deckId"` | Deck component ID |\n| `"playerId"` | Player ID |\n| `"string"` | Free-form string |\n| `"number"` | Numeric value |\n| `"boolean"` | Boolean value |\n| `"tileId"` | Hex tile or network node ID |\n| `"edgeId"` | Edge between tiles/nodes |\n| `"vertexId"` | Vertex between tiles |\n| `"spaceId"` | Track board space ID |\n| `"pieceId"` | Board piece ID |\n| `"zoneId"` | Zone ID |\n| `"tokenId"` | Token ID |\n| `"resourceId"` | Resource ID |\n\n**Key rules:**\n\n- **NO `"selectCard"` actions.** UI clicks are not actions.\n- If an action involves cards, **always include a `cardId` parameter** with the correct `deckDefinitionId`. Use `array: true` when multiple cards can be sent.\n- Never use placeholder string params or empty parameter lists when the action consumes card data.\n- Use `"cardId"` for specific card instances, `"cardType"` for card categories.\n- Use camelCase for `actionType` names.\n- Always use the specific type (e.g. deckId, cardId, playerId, etc) to narrow the parameter type instead of string where appropriate.\n\n### 9. `stateMachine`\n\nDefine game phases and transitions.\n\n```json\n{\n "stateMachine": {\n "initialState": "dealCards",\n "states": [\n {\n "name": "dealCards",\n "type": "AUTO",\n "description": "Shuffle the deck and deal 7 cards to each player.",\n "transitions": [{ "targetState": "playCard" }],\n "autoAdvance": true\n },\n {\n "name": "playCard",\n "type": "SINGLE_PLAYER",\n "description": "Active player must play a valid card or draw from the pile.",\n "availableActions": ["playCard", "drawCard"],\n "transitions": [\n { "targetState": "playCard", "description": "Next player\'s turn" },\n {\n "targetState": "gameOver",\n "description": "Player has no cards left"\n }\n ],\n "autoAdvance": true\n },\n {\n "name": "gameOver",\n "type": "AUTO",\n "description": "Calculate final scores and determine the winner.",\n "transitions": []\n }\n ]\n }\n}\n```\n\n**State types:**\n\n| Type | Description | Example |\n| --------------- | ----------------------------------------------------------- | ------------------------------------------- |\n| `SINGLE_PLAYER` | Engine waits for **one** player to act, then auto-advances | Chess turns, Poker betting |\n| `ALL_PLAYERS` | Engine waits for **all** players to submit before advancing | Rock-Paper-Scissors, 7 Wonders card passing |\n| `AUTO` | No player interaction \u2014 executes immediately | Dealing, scoring, state checks |\n\n**StateDefinition fields:**\n\n| Field | Required | Default | Description |\n| ------------------ | -------- | ------- | ------------------------------------------------------------------------------- |\n| `name` | \u2705 | \u2014 | Unique state name in camelCase (use verbNoun format: `dealCards`, `playTurn`) |\n| `type` | \u2705 | \u2014 | `AUTO`, `SINGLE_PLAYER`, or `ALL_PLAYERS` |\n| `description` | \u2705 | \u2014 | Full logic description \u2014 what happens, what players can do |\n| `availableActions` | \u274C | \u2014 | Action types available in this state (only for `SINGLE_PLAYER` / `ALL_PLAYERS`) |\n| `transitions` | \u2705 | \u2014 | List of possible next states |\n| `autoAdvance` | \u274C | `true` | Whether to auto-advance when complete |\n\n**StateTransition fields:**\n\n| Field | Required | Description |\n| ------------- | -------- | ------------------------------- |\n| `targetState` | \u2705 | Name of the next state |\n| `description` | \u274C | When/why this transition occurs |\n\n### 10. `variableSchema`\n\nMinimal state for game logic. Split into global (shared) and per-player variables.\n\n```json\n{\n "variableSchema": {\n "globalVariableSchema": {\n "properties": {\n "currentRound": {\n "type": "integer",\n "description": "Current round number"\n },\n "trumpSuit": { "type": "string", "description": "Current trump suit" }\n }\n },\n "playerVariableSchema": {\n "properties": {\n "score": { "type": "integer", "description": "Player\'s current score" },\n "hasPassed": {\n "type": "boolean",\n "description": "Whether player has passed this round"\n }\n }\n }\n }\n}\n```\n\n**Rules for variables:**\n\n- **MINIMIZE state.** Only include what\'s needed for rules and logic.\n- \u2705 **Include:** scores, flags (`hasPassed`), logic blockers (`lastPlayedCards`), trump suit, round counters\n- \u274C **Exclude:** derivable data (hand sizes, deck sizes, current player \u2014 the engine tracks these)\n- \u274C **Don\'t duplicate resources** \u2014 use the `resources` section instead of player variables for economies\n- Use `globalVariableSchema` for shared/global state (turn counter, current round)\n- Use `playerVariableSchema` for per-player state (scores, flags)\n\n**Property types:** `string`, `integer`, `number`, `boolean`, `array` (with `items`), `object` (with `properties`), `enum` (with `enums` list), `deckId`, `cardId`, `playerId`\n\n---\n\n## ID Naming Conventions\n\n- Use **human-readable, kebab-case IDs** for components and hands: `"draw-pile"`, `"main-hand"`, `"d6-die"`\n- Use **camelCase** for state names and action types: `"dealCards"`, `"playCard"`, `"rollDice"`\n- Use **camelCase** for variable names: `"currentRound"`, `"hasPassed"`, `"trumpSuit"`\n- Resource IDs: alphanumeric + underscore, starting with a letter: `"gold"`, `"victoryPoints"`\n\n---\n\n## Minimal Example\n\nA simple draw-and-play card game for 2\u20134 players:\n\n```json\n{\n "version": "1.0.0",\n "playerConfig": {\n "minPlayers": 2,\n "maxPlayers": 4,\n "optimalPlayers": 3\n },\n "deckDefinitions": [\n {\n "type": "preset",\n "id": "standard_52_deck",\n "name": "Standard 52-Card Deck"\n }\n ],\n "playerHandDefinitions": [\n {\n "id": "main-hand",\n "displayName": "Hand",\n "visibility": "ownerOnly",\n "maxCards": 7,\n "deckDefinitionIds": ["standard_52_deck"]\n }\n ],\n "components": [\n {\n "type": "deck",\n "id": "draw-pile",\n "name": "Draw Pile",\n "deckDefinitionId": "standard_52_deck",\n "layout": "stack"\n },\n {\n "type": "deck",\n "id": "discard-pile",\n "name": "Discard Pile",\n "deckDefinitionId": "standard_52_deck",\n "layout": "spread"\n }\n ],\n "availableActions": [\n {\n "actionType": "playCard",\n "displayName": "Play Card",\n "description": "Play a card from your hand to the discard pile",\n "parameters": [\n {\n "name": "cardId",\n "type": "cardId",\n "required": true,\n "deckDefinitionId": "standard_52_deck"\n }\n ],\n "errorCodes": ["NOT_YOUR_TURN", "INVALID_PLAY"]\n },\n {\n "actionType": "drawCard",\n "displayName": "Draw Card",\n "description": "Draw a card from the draw pile",\n "parameters": [],\n "errorCodes": ["HAND_FULL", "DECK_EMPTY"]\n }\n ],\n "stateMachine": {\n "initialState": "dealCards",\n "states": [\n {\n "name": "dealCards",\n "type": "AUTO",\n "description": "Shuffle the deck and deal 5 cards to each player. Place remaining cards face-down as the draw pile. Flip the top card to start the discard pile.",\n "transitions": [{ "targetState": "playTurn" }]\n },\n {\n "name": "playTurn",\n "type": "SINGLE_PLAYER",\n "description": "Active player must play a matching card from their hand or draw from the draw pile. A card matches if it shares the same suit or rank as the top discard.",\n "availableActions": ["playCard", "drawCard"],\n "transitions": [\n { "targetState": "playTurn", "description": "Next player\'s turn" },\n {\n "targetState": "endRound",\n "description": "Player empties their hand"\n }\n ]\n },\n {\n "name": "endRound",\n "type": "AUTO",\n "description": "The player who emptied their hand wins. Calculate scores based on cards remaining in other players\' hands.",\n "transitions": []\n }\n ]\n },\n "variableSchema": {\n "globalVariableSchema": {\n "properties": {}\n },\n "playerVariableSchema": {\n "properties": {\n "score": {\n "type": "integer",\n "description": "Accumulated score across rounds"\n }\n }\n }\n }\n}\n```\n\n---\n\n## After Editing\n\nRun `dreamboard update` to push the manifest and regenerate scaffolded files:\n\n```bash\ndreamboard update\n```\n\nThis regenerates:\n\n- `app/phases/` \u2014 One handler file per state in the state machine\n- `shared/manifest.d.ts` \u2014 TypeScript type definitions derived from the manifest\n- Action handler stubs and variable type definitions\n',
|
|
1937
|
+
"references/api-reference.md": '# State & API Reference\n\nUse this page as a lookup while writing `app/phases/*.ts`.\n\nThis is a reference-first document, not a modeling guide. For schema design, use [manifest-authoring.md](manifest-authoring.md). For hand-vs-deck decisions, use [hands-vs-decks.md](hands-vs-decks.md).\n\n## Read APIs (`ctx.state.*`)\n\n### `state.player`\n\n| Method | Returns | Description |\n| -------------------------------------- | ------------- | ------------------------------------- |\n| `.getOrder()` | `PlayerId[]` | All players in turn order |\n| `.getCurrentIds()` | `PlayerId[]` | Currently active player IDs |\n| `.get(playerId)` | `Player` | Player data: `{ id, name, score }` |\n| `.getState(playerId)` | `PlayerState` | Player variables from manifest schema |\n| `.getHand(playerId, handId)` | `CardId[]` | Cards in a specific hand |\n| `.getAllHands(playerId)` | `CardId[]` | All cards across all hands |\n| `.isInHand(playerId, cardId, handId?)` | `boolean` | Check if card is in hand |\n| `.isActive(playerId)` | `boolean` | Is player currently active |\n\n### `state.deck`\n\n| Method | Returns | Description |\n| --------------------- | ---------------- | ---------------------- |\n| `.getCards(deckId)` | `CardId[]` | Cards in a shared deck |\n| `.getTopCard(deckId)` | `CardId \\| null` | Top card of deck |\n\n### `state.card`\n\n| Method | Returns | Description |\n| -------------------------------- | ------------------ | ---------------------------------------------------- |\n| `.get(cardId)` | `Card` | Full card data |\n| `.getProperties(cardId)` | Typed properties | Card-specific props (rank, suit, etc.) |\n| `.getLocation(cardId)` | `Location` | Where the card is (InHand, InDeck, InZone, Detached) |\n| `.getPlayedBy(cardId)` | `PlayerId \\| null` | Who played this card to a deck/zone |\n| `.getOwner(cardId)` | `PlayerId \\| null` | Card owner |\n| `.isVisibleTo(cardId, playerId)` | `boolean` | Visibility check |\n| `.getOwnedBy(playerId)` | `CardId[]` | All cards owned by player |\n\n### `state.game`\n\n| Method | Returns | Description |\n| -------------------- | ------------- | ------------------------------------- |\n| `.getGlobalState()` | `GlobalState` | Global variables from manifest schema |\n| `.getCurrentState()` | `StateName` | Current state machine state |\n\n## Mutation APIs (`ctx.apis.*`)\n\n### `apis.cardApi`\n\n| Method | Description |\n| --------------------------------------------------------------------------- | ------------------------------ |\n| `.moveCardsFromHandToDeck(playerId, handId, cardIds, deckId)` | Hand \u2192 Deck |\n| `.moveCardsFromHandToHand(fromPlayer, fromHand, toPlayer, toHand, cardIds)` | Hand \u2192 Hand |\n| `.moveCardsToPlayer(cardIds, toPlayerId, handId)` | Any \u2192 Hand |\n| `.flip(deckId, cardId)` | Flip card face up/down |\n| `.detachCard(cardId)` | Remove card from all locations |\n| `.transferOwnership(cardId, toPlayer)` | Change card owner |\n\n#### Card movement semantics\n\n- `moveCardsFromHandToHand(fromPlayer, fromHand, toPlayer, toHand, cardIds)` is **additive** at destination.\n- Destination cards are preserved; moved cards are added to the destination hand.\n- This API does **not** replace/overwrite the destination hand.\n- For pass/rotate mechanics where each player should receive exactly one other player\'s hand, avoid in-place cyclic moves. Snapshot source hands first, then move via a temporary location (or other two-phase transfer pattern).\n\n### `apis.deckApi`\n\n| Method | Description |\n| ------------------------------------------------------------- | ------------------------- |\n| `.moveCardsFromDeckToPlayer(deckId, playerId, handId, count)` | Deck \u2192 Hand (deal) |\n| `.moveCardsFromDeckToDeck(fromDeckId, toDeckId)` | Deck \u2192 Deck |\n| `.shuffle(deckId)` | Shuffle a deck |\n| `.addCards(deckId, cardIds)` | Add cards to a deck |\n| `.removeCard(deckId, cardId)` | Remove a card from a deck |\n\n### `apis.gameApi`\n\n| Method | Description |\n| ---------------------------------- | ---------------------------------------------- |\n| `.setActivePlayers(playerIds)` | Set who can act (use in ALL_PLAYERS `onEnter`) |\n| `.setNextPlayer(playerId)` | Set single active player |\n| `.advanceTurn()` | Move to next player in order |\n| `.declareWinner(playerId, reason)` | Declare game winner |\n| `.endGame()` | End the game |\n\n### `apis.globalStateApi`\n\n| Method | Description |\n| ------------------------------------- | ---------------------------- |\n| `.setGlobalState(newState)` | Replace all global variables |\n| `.setPlayerState(playerId, newState)` | Replace a player\'s variables |\n\n### `apis.kvApi`\n\nInternal-only key-value store. **UI cannot access these values.**\n\n| Method | Returns | Description |\n| ------------------ | ---------------- | ----------------------------------- |\n| `.set(key, value)` | `KvSetResult` | Store a JSON value |\n| `.get(key)` | `KvGetResult` | Read a value (`.success`, `.value`) |\n| `.delete(key)` | `KvDeleteResult` | Remove a key |\n| `.has(key)` | `boolean` | Check existence |\n| `.keys()` | `string[]` | List all keys |\n\n### `apis.resourceApi`\n\n| Method | Description |\n| ------------------------- | -------------------------- |\n| `.add(playerId, cost)` | Give resources to player |\n| `.deduct(playerId, cost)` | Take resources from player |\n\n### `apis.dieApi`\n\n| Method | Description |\n| -------------------------- | ------------------------- |\n| `.roll(dieId)` | Roll a die |\n| `.setValue(dieId, value?)` | Set die to specific value |\n\n## Typed KV Store\n\nUse `createTypedKv` from `sdk/stateApi.js` for type-safe KV access:\n\n```typescript\nimport { createTypedKv } from "../sdk/stateApi.js";\n\ninterface MyKv {\n playersActed: PlayerId[];\n roundData: { scores: number[] };\n}\n\nconst kv = createTypedKv<MyKv>(apis.kvApi);\nkv.set("playersActed", ["player-1"]); // Type-checked key and value\nconst acted = kv.get("playersActed"); // PlayerId[] | null\nkv.has("playersActed"); // boolean\nkv.delete("playersActed"); // boolean (existed)\n```\n\n## Card `playedBy` Tracking\n\nWhen cards move from a Hand to a Deck via `moveCardsFromHandToDeck()`, the engine automatically records `playedBy` on each card:\n\n```typescript\n// Move card from player\'s hand to a shared deck\napis.cardApi.moveCardsFromHandToDeck(\n playerId,\n "main-hand",\n [cardId],\n "play-area",\n);\n\n// Later, check who played a specific card\nconst whoPlayed = state.card.getPlayedBy(cardId); // PlayerId | null\n```\n\n## Location Type\n\nCards have a location discriminated union:\n\n```typescript\ntype Location =\n | { type: "Detached" }\n | {\n type: "InDeck";\n deckId: DeckId;\n playedBy: PlayerId | null;\n position: number | null;\n }\n | {\n type: "InHand";\n handId: HandId;\n playerId: PlayerId;\n position: number | null;\n }\n | {\n type: "InZone";\n zoneId: string;\n playedBy: PlayerId | null;\n position: number | null;\n };\n```\n\n## Validation Helpers\n\n```typescript\nimport { validationSuccess, validationError } from "../sdk/validation.js";\n\n// Valid action\nreturn validationSuccess();\n\n// Invalid action \u2014 errorCode should be kebab-case\nreturn validationError(\n "must-play-valid-combination",\n "You must play a valid card combination",\n);\n```\n',
|
|
1938
|
+
"references/app-best-practices.md": '# App Best Practices\n\nUse this guide when implementing `app/phases/*.ts`.\n\nThis page is about phase logic and engine APIs. If you are still deciding the game model itself, fix `rule.md` or `manifest.json` first.\n\n## Before Writing Code\n\n1. Read `shared/manifest.ts` so you know the actual generated IDs and types.\n2. Read every file in `app/phases/` to understand the current flow.\n3. Review the generated SDK types in `app/sdk/`.\n\nIf the card model is still unclear, stop and revisit [hands-vs-decks.md](hands-vs-decks.md) and [manifest-authoring.md](manifest-authoring.md).\n\n## Core Principles\n\n1. **The app is authoritative**\n All gameplay mutations must go through `ctx.apis.*`. Do not assume the UI moved cards or tracked state correctly.\n2. **Prefer mutation results inside the current transaction**\n Values returned by mutation APIs are safer than immediately rereading `ctx.state.*`, which may still reflect the previous snapshot.\n3. **Use the right active-player API**\n Use `setActivePlayers()` for `ALL_PLAYERS` phases and `setNextPlayer()` for `SINGLE_PLAYER` phases.\n4. **All card movement goes through move APIs**\n Never hand-edit card locations in your own state.\n5. **Use generated IDs exactly**\n Deck IDs, hand IDs, resource IDs, board IDs, and action names should come from `shared/manifest.ts`.\n6. **KV is internal-only**\n `kvApi` is for app logic, not UI data. Pass UI-facing data through `getUIArgs()` or stored state.\n7. **Keep player-visible outcomes around long enough to render**\n If an `AUTO` phase resolves a trick, round, or reveal, preserve that result until the next player-facing phase can show it.\n\n## Validation\n\n- Implement `validateAction` for `SINGLE_PLAYER` and `ALL_PLAYERS` phases.\n- Return `validationSuccess()` for valid actions.\n- Return `validationError(code, message)` for invalid actions.\n- Keep error codes stable and kebab-case, for example `must-play-valid-combination`.\n\n## UI Args Contract\n\n- Define UI arg interfaces in `shared/ui-args.ts`.\n- Implement `getUIArgs(ctx, playerId)` for each player-facing phase.\n- Send only the data the UI actually needs.\n- Prefer computed values over forcing the UI to recompute them.\n\nExample:\n\n- better: `canPlayCard: boolean`\n- worse: raw state fragments that force the UI to rebuild game rules\n\n## Safety Habits\n\n- Check array length before indexing.\n- Use generated guards from `../generated/guards` for typed ID checks.\n- Avoid `any`.\n- Log compact payloads with `ctx.logger`, not giant arrays or full state dumps.\n\n## Common Patterns\n\n### Typed KV store\n\n```typescript\nimport { createTypedKv } from "../sdk/stateApi.js";\n\ninterface PhaseKv {\n playersActed: PlayerId[];\n roundNumber: number;\n}\n\nconst kv = createTypedKv<PhaseKv>(apis.kvApi);\nkv.set("playersActed", []);\nconst acted = kv.get("playersActed"); // PlayerId[] | null\n```\n\n### Card `playedBy` tracking\n\n```typescript\napis.cardApi.moveCardsFromHandToDeck(\n playerId,\n "main-hand",\n [cardId],\n "play-area",\n);\n\nconst whoPlayed = state.card.getPlayedBy(cardId); // PlayerId | null\n```\n\n### Turn advancement\n\n```typescript\nonAfterAction(ctx, playerId, actionType) {\n ctx.apis.gameApi.advanceTurn();\n},\n```\n\n### ALL_PLAYERS progress tracking\n\nUse the engine-managed helpers in [all-players-tracking.md](all-players-tracking.md) instead of building your own action-tracking store.\n',
|
|
1939
|
+
"references/building-your-first-game.md": '# Building Your First Game\n\nUse this as the default end-to-end workflow for a new Dreamboard game. The goal is not just "something renders"; the goal is a complete, playable game with rules, app logic, UI, and tests all aligned.\n\n## Core Workflow\n\n1. Create a game: `dreamboard new <slug> --description "<<short description>>"`\n2. Author `rule.md` and `manifest.json`\n Quality bar: define the full game loop up front, including setup, turn structure, win/loss conditions, scoring, tie behavior, hidden/public information, and every player action the app must support. `manifest.json` should reflect the real player counts, entities, zones, and controls needed by the rules, not a partial guess.\n Refer to\n - [references/rule-authoring.md](rule-authoring.md),\n - [references/manifest-authoring.md](manifest-authoring.md)\n3. Run `dreamboard update` after every manifest or rule change\n Quality bar: do this immediately after each rule or manifest edit so generated scaffold, types, and framework assumptions stay in sync. Do not keep coding against stale generated files.\n4. Implement rules in `app/phases/*.ts` and UI data in `shared/ui-args.ts`\n Quality bar: every gameplay phase must be implemented end to end. Cover all rule branches, all legal player actions, all transitions, all round-resolution logic, and all game-end paths. `shared/ui-args.ts` must provide enough data for the UI to render every playable state without guessing or reconstructing hidden game logic in React.\n Refer to\n - [references/phase-handlers.md](phase-handlers.md),\n - [references/api-reference.md](api-reference.md),\n - [references/hands-vs-decks.md](hands-vs-decks.md),\n - [references/all-players-tracking.md](all-players-tracking.md),\n - [references/app-best-practices.md](app-best-practices.md)\n5. Implement UI in `ui/App.tsx` and `ui/components/*`\n Quality bar: the entire game must be playable through the UI, including setup, core turns, confirmations, resolution states, errors and disabled states, and end-of-game feedback. Follow the styling requirements in [ui-style-guide.md](ui-style-guide.md). Avoid half-finished interfaces such as raw debug JSON, missing controls, placeholder labels, or flows that require reading logs to understand what to do next.\n Refer to\n - [references/ui-best-practices.md](ui-best-practices.md)\n6. Typecheck from the game root: `bun install && bun run typecheck`\n Quality bar: typecheck must pass cleanly from the game root with no ignored errors and no dead code left behind from iteration.\n7. Push compiled changes: `dreamboard push`\n Quality bar: the pushed build should reflect the same rules and UI you validated locally, with no pending local-only fixes or known desync between authored files and compiled output.\n8. Generate deterministic bases for testing: `dreamboard test generate`\n Quality bar: regenerate bases whenever initial setup, compiled behavior, or game identity changes. The generated bases should be stable enough that regressions are easy to spot.\n9. Run regression scenarios: `dreamboard test run`\n Quality bar: cover the happy path, invalid actions, boundary conditions, phase transitions, simultaneous or multi-player interactions where relevant, and every game-ending outcome. All scenario statuses should be green.\n Refer to\n - [references/test-harness.md](test-harness.md)\n10. Validate the runtime behavior and live UI with `dreamboard run --scenario <scenario_file>` and `dreamboard run --screenshot`\n Quality bar: if Playwright is installed locally, screenshot verification is required. Confirm the UI looks complete and usable in real runtime states, including the initial view, an active turn, resolution moments, and the end-game state. Fix any broken layout, missing affordances, or styling drift before considering the game done.\n Refer to\n - [references/adversarial-testing.md](adversarial-testing.md)\n\nDo not treat UI, rules, and tests as separate finish-later tracks. A game is only complete when the authored rules, app logic, UI flow, pushed build, and regression coverage all agree on the same behavior.\n\n## What "Done" Means\n\n- App and logic are complete: all phases are implemented, all rule branches are covered, and no required gameplay path is left as a TODO or inferred only in the UI.\n- UI is complete: the full game is playable without relying on logs, debug panels, or manual backend intervention.\n- Styling is complete: the game follows [ui-style-guide.md](ui-style-guide.md) rather than shipping a generic or half-finished interface.\n- Verification is complete: typecheck passes, regression scenarios are green, and runtime screenshot verification has been performed when Playwright is available.\n',
|
|
1940
|
+
"references/hands-vs-decks.md": '# Hands vs Decks\n\nUse this guide when you are deciding how to model a card container in `manifest.json`.\n\nDreamboard separates three ideas that are easy to blur together in tabletop rules:\n\n- `cardSets`: what cards exist\n- `decks`: shared card containers\n- `playerHandDefinitions`: per-player card containers\n\nFor the full manifest schema, see [manifest-authoring.md](manifest-authoring.md).\n\n## Quick Decision Rule\n\n1. If you are defining the cards themselves, use a `cardSet`.\n2. If the whole table shares one container, use a `deck`.\n3. If each player gets their own copy of the container, use a `playerHandDefinition`.\n\n## Canonical Meanings\n\n| Concept | Manifest key | Runtime ID | Use it for |\n| -------- | ----------------------- | ------------- | --------------------------------- |\n| Card set | `cardSets` | none directly | Card content and card schema |\n| Hand | `playerHandDefinitions` | `HandId` | Cards each player owns separately |\n| Deck | `decks` | `DeckId` | Shared piles or shared card areas |\n\n## Naming Rule\n\nUse the Dreamboard meaning, not the tabletop nickname.\n\nIf a real-world game says each player has a "deck", but each player owns a separate pile, model it as a hand.\n\nExamples:\n\n- a player\'s draw pile in War is a hand\n- a player\'s scored pile is a hand\n- the shared draw pile in Poker is a deck\n- the shared discard pile is a deck\n- a shared trick area is a deck\n\n## Common Mistakes\n\n- Do not use a deck just because cards are face-down.\n- Do not use a deck for something every player owns separately.\n- Do not use a hand for a communal area just because cards stay there for a while.\n\nVisibility is separate from ownership:\n\n- if each player has their own container, it is still a hand\n- set `visibility: "public"` if other players should be able to see that hand\n\n## Example\n\n```json\n{\n "cardSets": [\n {\n "type": "preset",\n "id": "standard_52_deck",\n "name": "Standard 52-Card Deck"\n }\n ],\n "decks": [\n {\n "id": "main-deck",\n "name": "Main Deck",\n "cardSetId": "standard_52_deck"\n },\n {\n "id": "battle-zone",\n "name": "Battle Zone",\n "cardSetId": "standard_52_deck"\n }\n ],\n "playerHandDefinitions": [\n {\n "id": "player-deck",\n "displayName": "Your Deck",\n "visibility": "ownerOnly",\n "cardSetIds": ["standard_52_deck"]\n },\n {\n "id": "won-pile",\n "displayName": "Won Cards",\n "visibility": "public",\n "cardSetIds": ["standard_52_deck"]\n }\n ]\n}\n```\n\nIn that example:\n\n- `standard_52_deck` defines the cards\n- `main-deck` and `battle-zone` are shared decks\n- `player-deck` and `won-pile` are per-player hands\n\n## Related Move APIs\n\n```typescript\n// Hand -> Deck\napis.cardApi.moveCardsFromHandToDeck(playerId, handId, cardIds, deckId);\n\n// Deck -> Hand\napis.deckApi.moveCardsFromDeckToPlayer(deckId, playerId, handId, count);\n\n// Hand -> Hand\napis.cardApi.moveCardsFromHandToHand(\n fromPlayer,\n fromHand,\n toPlayer,\n toHand,\n cardIds,\n);\n\n// Deck -> Deck\napis.deckApi.moveCardsFromDeckToDeck(fromDeckId, toDeckId);\n```\n\n## Rule Of Thumb\n\n- shared container: `deck`\n- per-player container: `playerHandDefinition`\n- card blueprint: `cardSet`\n',
|
|
1941
|
+
"references/manifest-authoring.md": '# Manifest Authoring Guide\n\nUse this guide when editing `manifest.json`.\n\n`manifest.json` describes the runtime shape of the game:\n\n- who can play\n- what cards, resources, dice, and boards exist\n- what actions players can submit\n- what phases and stored state the engine needs\n\nKeep the full game explanation in `rule.md`. Use the manifest to turn that explanation into typed runtime data.\n\nAfter editing the manifest, run `dreamboard update` to regenerate scaffolded files such as `shared/manifest.ts` and `app/phases/`.\n\nIf you are still deciding whether something is a hand, a deck, or a card set, read [hands-vs-decks.md](hands-vs-decks.md) alongside this guide.\n\n## Supported Top-Level Shape\n\n```json\n{\n "playerConfig": { ... },\n "cardSets": [ ... ],\n "playerHandDefinitions": [ ... ],\n "decks": [ ... ],\n "dice": [ ... ],\n "resources": [ ... ],\n "boardDefinitions": [ ... ],\n "availableActions": [ ... ],\n "stateMachine": { ... },\n "variableSchema": { ... }\n}\n```\n\n| Field | Required | Purpose |\n| ----------------------- | -------- | -------------------------------------------- |\n| `playerConfig` | \u2705 | Supported player counts |\n| `cardSets` | \u2705 | Card blueprints and card property schemas |\n| `playerHandDefinitions` | \u2705 | Per-player card containers |\n| `decks` | \u274C | Shared card containers |\n| `dice` | \u274C | Shared dice |\n| `resources` | \u274C | Typed resource economy |\n| `boardDefinitions` | \u274C | Structured boards such as hex, square, track |\n| `availableActions` | \u2705 | Player-submittable actions |\n| `stateMachine` | \u2705 | Game phases and transitions |\n| `variableSchema` | \u2705 | Global and per-player stored state |\n\n## Authoring Rules\n\n- Only use documented schema fields. If a key is not described in this guide, do not invent it in `manifest.json`.\n- Optional list fields should be omitted or set to `[]`. Do not use `null`.\n- Keep the manifest about runtime structure. Put presentation choices in UI code or `shared/ui-args.ts`.\n- There is no top-level `version` field in the manifest schema.\n- A deck entry is structural data only. Use `id`, `name`, and `cardSetId`.\n\n## Recommended Authoring Order\n\n1. Start from `rule.md`.\n2. Define the game\'s containers and content: players, card sets, hands, decks, resources, dice, and boards.\n3. Define the actions players can submit.\n4. Define the phase flow in `stateMachine`.\n5. Add only the stored state that cannot be derived from the rest of the game state.\n\n## 1. `playerConfig`\n\nUse `playerConfig` to declare the supported player counts.\n\n```json\n{\n "playerConfig": {\n "minPlayers": 2,\n "maxPlayers": 4,\n "optimalPlayers": 4\n }\n}\n```\n\n## 2. `cardSets`\n\n`cardSets` define the cards that exist in the game. They do not describe where those cards live during play.\n\nThere are two card set styles:\n\n- `preset` for built-in sets such as a standard 52-card deck\n- `manual` for authored card content and custom card schemas\n\n### Preset card set\n\n```json\n{\n "cardSets": [\n {\n "type": "preset",\n "id": "standard_52_deck",\n "name": "Standard 52-Card Deck"\n }\n ]\n}\n```\n\n### Manual card set\n\n```json\n{\n "type": "manual",\n "id": "resource-cards",\n "name": "Resource Cards",\n "cardSchema": {\n "properties": {\n "value": { "type": "integer", "description": "Point value" },\n "category": { "type": "string", "description": "Resource category" }\n }\n },\n "cards": [\n {\n "type": "lumber",\n "name": "Lumber",\n "count": 4,\n "properties": { "value": "1", "category": "wood" }\n }\n ]\n}\n```\n\nCard authoring reminders:\n\n- `type` is the manifest-level card type.\n- `count` is the number of copies to create.\n- runtime card IDs are derived from `type` and `count` (`lumber`, `lumber-2`, `lumber-3`, ...)\n- `properties` must match `cardSchema`\n- `name` and `text` are worth keeping because they help scaffolding, debugging, and agent reasoning\n\n## 3. `playerHandDefinitions`\n\nEach `playerHandDefinition` creates one container per player.\n\nUse a hand for anything each player owns separately, even if the tabletop rules call it a "deck", "reserve", or "scored pile".\n\n```json\n{\n "playerHandDefinitions": [\n {\n "id": "main-hand",\n "displayName": "Hand",\n "visibility": "ownerOnly",\n "maxCards": 7,\n "cardSetIds": ["standard_52_deck"]\n }\n ]\n}\n```\n\n| Field | Required | Purpose |\n| ------------- | -------- | ----------------------------------------------- |\n| `id` | \u2705 | Stable hand ID (`HandId`) |\n| `displayName` | \u2705 | Human-readable name |\n| `visibility` | \u274C | `ownerOnly`, `public`, or `hidden` |\n| `maxCards` | \u274C | Maximum allowed card count |\n| `minCards` | \u274C | Minimum required card count |\n| `cardSetIds` | \u274C | Allowed card sets; omit for any compatible card |\n| `description` | \u274C | Explanation for authors and tools |\n\n## 4. `decks`\n\n`decks` are shared card containers.\n\nUse a deck for any card location the whole table shares: a draw pile, discard pile, market row, trick area, or communal play area.\n\n```json\n{\n "decks": [\n {\n "id": "draw-pile",\n "name": "Draw Pile",\n "cardSetId": "standard_52_deck"\n },\n {\n "id": "discard-pile",\n "name": "Discard Pile",\n "cardSetId": "standard_52_deck"\n }\n ]\n}\n```\n\n| Field | Required | Purpose |\n| ----------- | -------- | -------------------------------------- |\n| `id` | \u2705 | Stable deck ID (`DeckId`) |\n| `name` | \u2705 | Human-readable name |\n| `cardSetId` | \u2705 | Card set used by this shared container |\n\n## 5. `dice`, `resources`, and `boardDefinitions`\n\nAdd these sections only if the game needs them.\n\n- `dice` define shared dice such as `d6` or custom dice.\n- `resources` define typed player economies such as coins, food, or energy.\n- `boardDefinitions` define structured boards such as hex maps, square grids, tracks, or networks.\n\nExample:\n\n```json\n{\n "dice": [{ "id": "d6", "name": "Six-Sided Die", "sides": 6 }],\n "resources": [{ "id": "coins", "displayName": "Coins" }]\n}\n```\n\n## 6. `availableActions`\n\nActions describe what players may submit to the engine.\n\nThink of actions as structured intent, not UI clicks. The UI gathers selections, then submits those values through action parameters.\n\n```json\n{\n "availableActions": [\n {\n "actionType": "playCard",\n "displayName": "Play Card",\n "description": "Play the selected card",\n "parameters": [\n {\n "name": "cardId",\n "type": "cardId",\n "required": true,\n "cardSetId": "standard_52_deck"\n }\n ],\n "errorCodes": ["NOT_YOUR_TURN", "INVALID_CARD"]\n }\n ]\n}\n```\n\nAction authoring reminders:\n\n- use stable, readable IDs\n- use `cardSetId` when a `cardId` or `cardType` parameter should be limited to one card set\n- use `array: true` with `minLength` and `maxLength` when an action takes multiple values\n\n## 7. `stateMachine`\n\n`stateMachine` defines the phase flow of the game.\n\nUse it to name phases, describe who can act, and define how the game moves from one phase to the next.\n\nState types:\n\n- `AUTO`: engine-driven phase\n- `SINGLE_PLAYER`: one active player acts\n- `ALL_PLAYERS`: multiple players may act in parallel\n\n```json\n{\n "stateMachine": {\n "initialState": "dealCards",\n "states": [\n {\n "name": "dealCards",\n "type": "AUTO",\n "description": "Shuffle and deal cards.",\n "transitions": [{ "targetState": "playTurn" }]\n },\n {\n "name": "playTurn",\n "type": "SINGLE_PLAYER",\n "description": "Active player plays a card.",\n "availableActions": ["playCard"],\n "transitions": [{ "targetState": "playTurn" }]\n }\n ]\n }\n}\n```\n\n## 8. `variableSchema`\n\n`variableSchema` is for state that must be stored explicitly.\n\nKeep it small. If something can be derived from card locations, resources, board state, or the current phase, it usually does not belong here.\n\nGood candidates:\n\n- running scores across rounds\n- round number, trump suit, current bid\n- flags that affect future legal moves\n\nAvoid storing:\n\n- counts that can be derived from hands or decks\n- phase flow already represented by `stateMachine`\n- duplicated resource totals\n\n```json\n{\n "variableSchema": {\n "globalVariableSchema": {\n "properties": {\n "roundNumber": { "type": "integer", "description": "Current round" }\n }\n },\n "playerVariableSchema": {\n "properties": {\n "score": {\n "type": "integer",\n "description": "Accumulated score"\n }\n }\n }\n }\n}\n```\n\n## Final Checklist\n\nBefore running `dreamboard update`, check that:\n\n- every referenced `cardSetId`, `handId`, `deckId`, resource ID, board ID, and action name exists\n- the manifest only uses documented schema fields\n- `variableSchema` stores only non-derivable state\n- optional list fields are omitted or `[]`, not `null`\n\nThen run:\n\n```bash\ndreamboard update\n```\n',
|
|
1941
1942
|
"references/phase-handlers.md": '# Phase Handlers\n\nPhase handlers are the core abstraction for implementing game logic. Each state in the manifest\'s `stateMachine` maps to a phase handler file in `app/phases/`.\n\n## Phase Types\n\n| Type | File Import | When to use |\n| --------------- | --------------------------- | ------------------------------------------------------------ |\n| `AUTO` | `createAutoPhase` | No player input needed (deal, resolve, score) |\n| `SINGLE_PLAYER` | `createSinglePlayerPhase` | One player acts at a time (play a card, roll dice) |\n| `ALL_PLAYERS` | `createMultiplePlayerPhase` | All players act simultaneously (reveal, pass cards, discard) |\n\n## Phase Lifecycle\n\n**AUTO**: `execute(ctx)` \u2192 returns next state name\n\n**SINGLE_PLAYER / ALL_PLAYERS**:\n\n1. `onEnter(ctx)` \u2014 initialize phase state, set active players\n2. `validateAction(ctx, playerId, actionType, parameters)` \u2014 validate before execution\n3. `onPlayerAction(ctx, playerId, actionType, parameters)` \u2014 process the action (mutate state)\n4. `onAfterAction(ctx, playerId, actionType)` \u2014 per-action side effects (e.g., advance turn)\n5. `checkCompletion(ctx)` \u2014 return `null` if not done, or the next state name if done\n6. `onComplete(ctx)` \u2014 cleanup after transition decision\n\n## PhaseContext (`ctx`)\n\n```typescript\ninterface PhaseContext {\n readonly state: StateApi; // Read game state\n readonly apis: GameApis; // Mutate game state\n readonly logger: Logger; // Log messages\n}\n```\n\n## AUTO Phase Example\n\n```typescript\nimport type { AutoPhaseDefinition } from "../sdk/phaseHandlers";\nimport { createAutoPhase } from "../sdk/phaseHandlers";\n\nconst phaseDefinition = {\n execute(ctx) {\n const { state, apis, logger } = ctx;\n apis.deckApi.shuffle("main-deck");\n for (const playerId of state.player.getOrder()) {\n apis.deckApi.moveCardsFromDeckToPlayer(\n "main-deck",\n playerId,\n "main-hand",\n 5,\n );\n }\n return "playPhase";\n },\n} satisfies AutoPhaseDefinition<"dealCards">;\n\nexport const phase = createAutoPhase(phaseDefinition);\n```\n\n## SINGLE_PLAYER Phase Example\n\n```typescript\nimport type { SinglePlayerPhaseDefinition } from "../sdk/phaseHandlers";\nimport { createSinglePlayerPhase } from "../sdk/phaseHandlers";\nimport { validationSuccess, validationError } from "../sdk/validation.js";\n\nconst phaseDefinition = {\n onEnter(ctx) {\n const { state, apis } = ctx;\n const firstPlayer = state.player.getOrder()[0];\n apis.gameApi.setNextPlayer(firstPlayer);\n },\n\n validateAction(ctx, playerId, actionType, parameters) {\n return validationSuccess();\n },\n\n onPlayerAction(ctx, playerId, actionType, parameters) {\n const { apis } = ctx;\n // Process action...\n },\n\n onAfterAction(ctx, playerId, actionType) {\n const { apis } = ctx;\n apis.gameApi.advanceTurn(); // Move to next player\n },\n\n checkCompletion(ctx) {\n // Return null to keep playing, or next state name to transition\n return null;\n },\n\n getUIArgs(ctx, playerId) {\n return {};\n },\n} satisfies SinglePlayerPhaseDefinition<"playPhase", "playCard">;\n\nexport const phase = createSinglePlayerPhase(phaseDefinition);\n```\n\n## ALL_PLAYERS Phase Example\n\nSee [all-players-tracking.md](all-players-tracking.md) for the recommended tracking pattern.\n\n## Terminal Phase (Game End)\n\nFor states with no transitions (game end), use `TerminalPhaseDefinition`:\n\n```typescript\nimport type { TerminalPhaseDefinition } from "../sdk/phaseHandlers";\nimport { createTerminalPhase } from "../sdk/phaseHandlers";\n\nconst phaseDefinition = {\n execute(ctx) {\n const { state, apis } = ctx;\n // Optionally declare winner\n apis.gameApi.declareWinner("player-1", "Most points");\n apis.gameApi.endGame();\n return "endGame";\n },\n} satisfies TerminalPhaseDefinition<"endGame">;\n\nexport const phase = createTerminalPhase(phaseDefinition);\n```\n\n## Type Safety\n\nPhase definitions use `satisfies` for type checking:\n\n- `AutoPhaseDefinition<\'stateName\'>` \u2014 state name from manifest\n- `SinglePlayerPhaseDefinition<\'stateName\', \'actionName\'>` \u2014 with action type union\n- `MultiplePlayerPhaseDefinition<\'stateName\', \'actionName\'>` \u2014 for ALL_PLAYERS\n- Union multiple actions: `\'playCard\' | \'drawCard\'`\n',
|
|
1942
|
-
"references/rule-authoring.md": '# Rule Authoring Guide\n\n`rule.md` is the source of
|
|
1943
|
+
"references/rule-authoring.md": '# Rule Authoring Guide\n\nUse this guide when creating or rewriting `rule.md`.\n\n`rule.md` should explain how the game works in plain language. It is the source of intent for the game. `manifest.json` comes later and turns that intent into runtime structure.\n\nKeep the jobs separate:\n\n- `rule.md` explains the rules\n- `manifest.json` defines the runtime model\n- `app/phases/*.ts` implements the rules\n\nIf you need schema details, use [manifest-authoring.md](manifest-authoring.md). If you need help deciding between hands and decks, use [hands-vs-decks.md](hands-vs-decks.md).\n\n## Recommended Document Structure\n\nUse this section order in `rule.md`.\n\n1. **Overview**\n\n - Game premise and objective\n - Player count\n - Match length target\n\n2. **Components**\n\n - Cards, hands, decks, tokens, dice, boards, resources\n - Public vs hidden information\n - Limits such as hand size or token caps\n\n3. **Setup**\n\n - Starting layout\n - Starting player and turn order\n - Initial cards, resources, and other player state\n\n4. **Gameplay**\n\n - Turn or phase loop in order\n - Available actions in each phase\n - Validation constraints\n - Transition conditions between phases\n\n5. **Scoring and Progression**\n\n - How points or progress are gained or lost\n - When scoring happens\n - Tie-breakers\n\n6. **Winning Conditions**\n\n - Exact end-game triggers\n - Winner resolution rules\n\n7. **Special Rules and Edge Cases**\n\n - Simultaneous actions\n - Empty deck behavior\n - Invalid or no-op actions\n - What happens when a player cannot act\n\n## Writing Rules That Map Cleanly To The Engine\n\n- **Phase model**\n State whether a phase is automatic, single-player, or all-players, and say how it ends.\n- **Action model**\n Name each action, say what it does, and list required inputs and constraints.\n- **State model**\n Separate stored state from derived state. If something can be computed from cards, resources, or the current phase, call that out.\n- **Visibility model**\n Say who can see what, when hidden information becomes public, and when reveals happen.\n- **Determinism**\n Resolve ambiguous cases with explicit priority order. Define random events precisely.\n\n## Writing Style\n\n- Use `must` for mandatory behavior.\n- Use `may` only for optional player choices.\n- Define terms once, then reuse the same words consistently.\n- Avoid UI-specific verbs such as "click" or "drag".\n\n## High-Signal Template\n\nUse this template when creating or rewriting `rule.md`:\n\n```markdown\n# <Game Name>\n\n## Overview\n\n- Players: <min-max> (optimal: <n>)\n- Objective: <how to win>\n- Duration: <target minutes>\n\n## Components\n\n- Card sets:\n- Shared decks:\n- Player hands:\n- Boards:\n- Resources, tokens, or dice:\n- Public vs hidden information:\n\n## Setup\n\n1. ...\n2. ...\n3. ...\n\n## Gameplay\n\n### Phase 1: <name> (<AUTO|SINGLE_PLAYER|ALL_PLAYERS>)\n\n- Entry:\n- Allowed actions:\n- Validation:\n- Completion -> Next phase:\n\n### Phase 2: <name>\n\n- ...\n\n## Scoring and Progression\n\n- ...\n\n## Winning Conditions\n\n- End trigger:\n- Winner determination:\n- Tie-breaker:\n```\n\n## Iteration Workflow\n\n1. Edit `rule.md` first.\n2. Align `manifest.json` to the updated rules.\n3. Run `dreamboard update`.\n4. Implement or refine `app/phases/*.ts`.\n5. Validate the flow with `dreamboard run`, `dreamboard test generate`, and `dreamboard test run` as needed.\n',
|
|
1943
1944
|
"references/scenario-format.md": '# Scenario Format\n\nScenarios are JSON files used with `dreamboard run --scenario <file>` to automate gameplay actions.\n\nUse this file for the JSON runtime-scenario contract.\n\n- For TypeScript regression tests with `dreamboard test generate` / `dreamboard test run`, see [test-harness.md](test-harness.md).\n- For what adversarial cases to write with these JSON scenarios, see [adversarial-testing.md](adversarial-testing.md).\n\n`dreamboard run` is now a single command flow:\n\n1. Without `--scenario`, it observes SSE until a stop condition (default `YOUR_TURN`).\n2. With `--scenario`, it executes scenario steps (default `--max-steps 1`).\n3. Scenario driver defaults to `api` (no Playwright required).\n4. Playwright is used only for `--scenario-driver ui` or `--screenshot`.\n\n## Default Structure (API Driver)\n\n```json\n{\n "steps": [\n {\n "playerId": "player-1",\n "actionType": "playCard",\n "parameters": { "cardId": "hearts-7" },\n "turns": 1\n },\n {\n "playerId": "player-1",\n "actionType": "endTurn",\n "parameters": {},\n "turns": 1\n }\n ]\n}\n```\n\n## API Step Fields (Default)\n\n| Field | Type | Description |\n| ------------ | ------------------------- | ----------------------------------------------------------------- |\n| `playerId` | `string` | Player ID this step applies to (required) |\n| `actionType` | `string` | Action type submitted to runtime API (required) |\n| `parameters` | `Record<string, unknown>` | Action parameters object (optional, default `{}`) |\n| `turns` | `number` | Number of turns to wait after this action (optional, default `1`) |\n\n## UI Step Fields (`--scenario-driver ui`)\n\n| Field | Type | Description |\n| ---------- | ---------- | --------------------------------------------------------------------------- |\n| `playerId` | `string` | Player ID this step applies to (required) |\n| `buttons` | `string[]` | Mouse/keyboard buttons to simulate (`"left_mouse_button"`, `"right"`, etc.) |\n| `turns` | `number` | Number of turns to apply this step |\n| `mouse_x` | `number` | X coordinate of the simulated click (optional) |\n| `mouse_y` | `number` | Y coordinate of the simulated click (optional) |\n\n## Conventions\n\n1. `playerId` is required on every step.\n2. Run one acting step per invocation for deterministic agent loops:\n - `dreamboard run` (observe turn context)\n - `dreamboard run --scenario ... --max-steps 1` (act once)\n3. All steps executed in one invocation must target the same `playerId`.\n4. The step `playerId` must be eligible for the latest `YOUR_TURN` context.\n\n## Observe Artifacts\n\nWhen running without `--scenario`, the CLI writes:\n\n1. `.dreamboard/run/session.json`\n2. `.dreamboard/run/events.ndjson`\n3. `.dreamboard/run/latest-your-turn.json`\n4. `.dreamboard/run/last-run-summary.json`\n\nBy default, `events.ndjson` stores only `YOUR_TURN` messages.\n\n## Usage\n\n```bash\n# Observe until actionable state (default: until YOUR_TURN)\ndreamboard run\n\n# Observe stream behavior\ndreamboard run --until GAME_ENDED\ndreamboard run --observe-events all\ndreamboard run --timeout-ms 15000\ndreamboard run --screenshot\n\n# Execute API scenario steps (default driver, no Playwright required)\ndreamboard run --scenario path/to/scenario.json\ndreamboard run --scenario path/to/scenario.json --max-steps 1\n\n# Execute UI scenario steps (Playwright driver)\ndreamboard run --scenario path/to/scenario.json --scenario-driver ui --max-steps 1\ndreamboard run --scenario path/to/scenario.json --scenario-driver ui --max-steps 1 --screenshot\n\n# Start a fresh session instead of resuming\ndreamboard run --new-session --players 4\n```\n',
|
|
1944
1945
|
"references/test-harness.md": '# Test Harness\n\nTypeScript scenario tests run against the Dreamboard backend API using deterministic base snapshots.\n\nUse this file for the TypeScript regression harness:\n\n- `dreamboard test generate`\n- `dreamboard test run`\n\nThis is not the same system as `dreamboard run --scenario <file>.json`.\n\n- For JSON runtime scenarios, see [scenario-format.md](scenario-format.md).\n- For rejection-focused runtime coverage, see [adversarial-testing.md](adversarial-testing.md).\n\n## Workspace Layout\n\nThe harness expects this structure in the game project:\n\n- `test/base-scenarios.json`\n- `test/scenarios/*.scenario.ts`\n- `test/generated/*` (generated by `dreamboard test generate`)\n\nCurrent canonical example workspace (`examples/things-in-rings`) includes bases like `initial-turn` and `after-first-placement`, plus scenarios under `test/scenarios/`.\n\n## Commands\n\nGenerate deterministic bases:\n\n```bash\ndreamboard test generate\n```\n\nRun all scenarios:\n\n```bash\ndreamboard test run\n```\n\nRun one scenario file:\n\n```bash\ndreamboard test run --scenario test/scenarios/reject-card-not-in-hand.scenario.ts\n```\n\nSource-checkout builds may expose internal environment overrides for harness work, but those flags are not part of the published CLI contract.\n\nRegenerate whenever any fingerprint input changes: base definition (`seed`, `players`, `steps`), game manifest hash, compiled result ID, or game ID.\n\n## Base Definitions\n\n`test/base-scenarios.json` is required and must explicitly include `initial-turn`.\n\nEach base entry has:\n\n- `seed: number`\n- `players: number`\n- `steps: ApiScenarioStep[]`\n\n`steps` may be empty. Use an empty-step base plus an empty-step scenario to assert opening-state invariants without inventing fake actions.\n\nExample:\n\n```json\n{\n "initial-turn": {\n "seed": 1337,\n "players": 4,\n "steps": []\n },\n "after-first-placement": {\n "seed": 1337,\n "players": 4,\n "steps": [\n {\n "playerId": "player-2",\n "actionType": "placeThing",\n "parameters": { "cardId": "a-shadow", "ringId": "ring-1" }\n }\n ]\n }\n}\n```\n\nGeneration flow for each used base:\n\n1. Create session with deterministic seed/player count.\n2. Start game.\n3. Wait for first `YOUR_TURN`.\n4. Execute base setup steps.\n5. Save snapshot + fingerprint metadata.\n\nBase setup events are not counted in test assertions. `events` in `then.assert` only reflects actions from `when.steps`, so zero-step scenarios start with an empty event history.\n\n## Generated Artifacts\n\n`dreamboard test generate` writes:\n\n- `test/generated/base-states.generated.ts`\n- `test/generated/base-states.generated.d.ts`\n- `test/generated/scenario-manifest.generated.ts`\n- `test/generated/.generation-meta.json`\n\nThese files are generated and should not be edited manually.\n\n## Scenario Contract\n\nScenarios are loaded from `test/scenarios/**/*.scenario.ts` and validated strictly (unknown fields are rejected).\n\nIn current projects, scenario files typically import from `../testing-types`:\n\n```ts\nimport { AssertContext, defineScenario } from "../testing-types";\n\nexport default defineScenario({\n meta: {\n id: "reject-card-not-in-hand",\n description: "Player tries to place a card they don\'t hold",\n },\n given: {\n base: "initial-turn",\n },\n when: {\n steps: [\n {\n playerId: "player-2",\n actionType: "placeThing",\n parameters: { cardId: "a-dog", ringId: "ring-1" },\n },\n ],\n },\n then: {\n assert: ({ gameState, events, expect }: AssertContext) => {\n expect(events.count("ACTION_REJECTED")).toBe(1);\n expect(events.last("ACTION_REJECTED")?.errorCode).toBe(\n "CARD_NOT_IN_HAND",\n );\n expect(gameState.currentState).toBe("placeThing");\n },\n },\n});\n```\n\n`given.base` must exist in `test/base-scenarios.json`, or the run fails with an available-base list.\n\nFor invariant coverage, add at least one zero-step scenario:\n\n```ts\nimport {\n AssertContext,\n defineScenario,\n getNormalizedHands,\n} from "../testing-types";\n\nexport default defineScenario({\n meta: {\n id: "initial-state",\n description: "Opening hands are dealt to the correct players",\n },\n given: {\n base: "initial-turn",\n },\n when: {\n steps: [],\n },\n then: {\n assert: ({ gameState, expect }: AssertContext) => {\n const hands = getNormalizedHands(gameState);\n expect(hands["player-1"]?.hand).toEqual([\n "seat-1-flower-1",\n "seat-1-flower-2",\n "seat-1-flower-3",\n "seat-1-skull",\n ]);\n },\n },\n});\n```\n\n## Runtime Assertion Surface\n\n`then.assert` receives `{ gameState, events, expect }`.\n\n- `events.count(type)` returns number of matching events.\n- `events.last(type)` returns the latest matching event.\n- `events.all(type)` returns all matching events.\n- `expect(actual)` supports `toBe`, `toEqual`, `toBeDefined`, `toContain`, and `toBeGreaterThanOrEqual`.\n\nDuring scenario assertions, event history includes only scenario steps (base-step events are cleared before scenario execution).\n\n`gameState.globalVariables` and `gameState.playerVariables` are JSON-parsed for assertions.\n\n`testing-types` also exports:\n\n- `getNormalizedHands(gameState)` to read each player hand with sorted card IDs.\n- `getNormalizedDecks(gameState)` to read deck contents with sorted card IDs.\n\n## Failure And Staleness Signals\n\nCommon failures indicate stale generated bases:\n\n- `Missing test/generated/base-states.generated.ts. Run \'dreamboard test generate\' first.`\n- `Fingerprint mismatch: base setup changed (...). Run \'dreamboard test generate\' to refresh deterministic bases.`\n- `Fingerprint mismatch: compiled result changed (...). Run \'dreamboard test generate\' to refresh deterministic bases.`\n- `Fingerprint mismatch: base setup changed (...); compiled result changed (...). Run \'dreamboard test generate\' to refresh deterministic bases.`\n- Unknown `given.base` values.\n\n## Common edge cases\n\nWhen testing out-of-turn behavior in any turn-based game, include these checks:\n\n- Wrong actor attempts a valid action: choose a base where the active player is known, send a normal action from a different player, and assert a turn-violation error (for example `NOT_YOUR_TURN`).\n- Unauthorized role attempts an action: if your game has role-restricted actions, attempt the action from an actor without permission and assert the appropriate authorization error.\n- Correct actor, wrong phase/state: attempt an action that is valid in one phase/state while the game is in another, and assert a deterministic phase/state rejection.\n- Consecutive out-of-turn attempts: send multiple invalid actions from different non-active players and verify each is rejected independently.\n- No state mutation on rejection: assert no successful action event was emitted, the game phase/state is unchanged, and the active player pointer is unchanged.\n\nRecommended assertion template:\n\n```ts\nexpect(events.count("ACTION_REJECTED")).toBe(1);\nexpect(events.count("ACTION_EXECUTED")).toBe(0);\nexpect(events.last("ACTION_REJECTED")?.errorCode).toBe("NOT_YOUR_TURN");\nexpect(gameState.currentState).toBe(expectedState);\nexpect(gameState.currentPlayerIds[0]).toBe(expectedActivePlayerId);\n```\n',
|
|
1945
|
-
"references/ui-best-practices.md": '# UI Best Practices\n\nBest practices for working on `ui/App.tsx` and `ui/components/*` \u2014 the React frontend.\n\n## General principles\n\n1. **Use SDK components** \u2014 import from `./sdk/components/`. Prefer library components over rolling your own. Customize them via render props if needed.\n2. **Mobile-first** \u2014 mobile is the primary target, desktop is an enhancement. Start with mobile-optimized layouts, use large touch targets, and relative units (rem, em).\n3. **No state duplication** \u2014 avoid `React.useState()` for state that needs to sync across players. Use `useGameState`, `useUIArgs`, and action submitters instead.\n4. **Error handling on actions** \u2014 always wrap action submissions in try/catch and show feedback via `useToast`.\n5. **Do not use `any`** \u2014 always work with typed hooks and props.\n6. **Persistent feedback matters** \u2014 include a stable feedback region that survives auto-phase transitions so players can always see the current phase, latest status message, last round result, and game-over summary.\n\n## SDK hooks (from `./sdk/hooks/`)\n\n| Hook | Returns | When to use |\n| ---------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |\n| `useGameState` | Full game state (currentPlayerIds, decks, currentState, isMyTurn) | Core game state. All fields are non-nullable \u2014 do not use optional chaining. |\n| `useCard(cardId)` | `CardItem \\| undefined` | Get a single card\'s data |\n| `useCards(cardIds)` | `CardItem[]` | Get multiple cards |\n| `useMyHand(handId)` | Hand data for current player | Player\'s cards |\n| `useMyResources()` | Current player\'s resources | Resource display |\n| `useMe` | `{ playerId, name, isHost }` | Current player identity |\n| `usePlayerInfo` | Map of all players | Player list/display |\n| `useAction` | Phase-specific action submitters | Submitting actions. Keyed by phase with "Actions" suffix (e.g., `playCardsActions`). Only current phase returns submitters; others return null. |\n| `useUIArgs` | Phase-specific UI args | Data from `getUIArgs()`. Keyed by phase name. Only current phase has data. |\n| `useGameNotifications` | Event listeners | `onYourTurn`, `onActionRejected`, `onGameEnded` |\n| `useLobby` | Lobby state | seats, canStart, hostUserId |\n| `useDice(dieIds)` | Dice values and roll function | Dice mechanics |\n\n## SDK components (from `./sdk/components/`)\n\n### Core display\n\n| Component | Purpose |\n| --------------- | -------------------------------------------------------------------------------------------- |\n| `Card` | Displays a game card with animations. Supports `renderContent` for custom faces. |\n| `ConnectedCard` | Auto-fetches card data from context. |\n| `Hand` | Container for player\'s hand. Uses render props: `renderCard`, `renderDrawer`, `renderEmpty`. |\n| `PlayArea` | Central board area for active cards. Supports grid/row layouts. |\n| `PlayerInfo` | Player avatar with status, score, and turn indicators. |\n\n### Action UI\n\n| Component | Purpose |\n| -------------- | ------------------------------------------------------------ |\n| `ActionButton` | Button with integrated cost display and affordability check. |\n| `ActionPanel` | Collapsible container for action groups. |\n| `ActionGroup` | Groups related actions with title and variant styling. |\n\n### Game state display\n\n| Component | Purpose |\n| ----------------- | ------------------------------------------------------------------- |\n| `PhaseIndicator` | Shows current phase and "Your Turn" indicator. |\n| `ResourceCounter` | Displays resource counts with icons and animations. |\n| `CostDisplay` | Shows resource costs with green/red affordability indication. |\n| `DiceRoller` | Display component for dice values with render prop. |\n| `GameEndDisplay` | End-of-game overlay with trophy, scoreboard, and return-to-lobby. |\n| `Drawer` | Mobile-friendly drawer for overflow content (e.g., too many cards). |\n\n### Board components\n\n| Component | Use case | Hook |\n| -------------- | --------------------------------------- | --------------------------------- |\n| `SquareGrid` | Chess, Checkers, Go, Tic-Tac-Toe | `useSquareBoard(boardId)` |\n| `HexGrid` | Catan, wargames, Hive | `useHexBoard(boardId)` |\n| `TrackBoard` | Monopoly, racing, Snakes & Ladders | `useTrackBoard(boardId)` |\n| `NetworkGraph` | Ticket to Ride, Pandemic, Power Grid | `useNetworkBoard(boardId)` |\n| `ZoneMap` | Risk, Small World, area control | `useZoneMap(zones, pieces)` |\n| `SlotSystem` | Agricola, Viticulture, worker placement | `useSlotSystem(slots, occupants)` |\n\n## Layout guidelines\n\n- Use Tailwind CSS for all styling.\n- Use `clsx` for conditional class composition.\n- Use `framer-motion` for animations (already included via SDK components).\n- Keep spacing tight on mobile, generous on desktop.\n- Limit simultaneous animations for performance.\n- Use `useIsMobile()` hook when you need device-specific behavior.\n\n## Persistent feedback region\n\nReserve a visible area near the top of the layout for game status. This should outlive individual action panels and phase-specific subviews.\n\n- Show the current phase and whether it is the local player\'s turn.\n- Show a human-readable status message for the latest meaningful change.\n- Keep the last round result visible until the next player-facing state can explain it.\n- Show a clear game-over summary when the game ends.\n\nIf your game uses AUTO phases, do not let those transitions wipe the only copy of round-resolution feedback before the UI can render it.\n\n## Common patterns\n\n### Phase-based rendering\n\n```tsx\nconst { currentState, isMyTurn } = useGameState();\nconst uiArgs = useUIArgs();\nconst actions = useAction();\n\n// Render based on current phase\nif (currentState === "playCards") {\n const phaseArgs = uiArgs.playCards;\n const phaseActions = actions.playCardsActions;\n return <PlayCardsView args={phaseArgs} actions={phaseActions} />;\n}\n```\n\n### Action submission with error handling\n\n```tsx\nconst { toast } = useToast();\n\nconst handlePlay = async () => {\n try {\n await actions.playCardsActions.playCard({ cardIds: selectedCards });\n } catch (err) {\n toast.error(err instanceof Error ? err.message : "Action failed");\n }\n};\n```\n\n### Game end detection\n\n```tsx\nconst { currentState } = useGameState();\n\nif (currentState === "endGame") {\n return <GameEndDisplay isGameOver scores={scores} />;\n}\n```\n\n## Public hands in UI\n\nFor public card zones (for example a scored area), prefer the built-in hook instead of custom `getUIArgs()` plumbing.\n\n```ts\nimport { useMyHand, usePublicHands } from "@dreamboard/ui-sdk";\n\nconst myHand = useMyHand("hand");\nconst scoredByPlayer = usePublicHands("scored-area");\nconst player2Scored = scoredByPlayer["player-2"] ?? [];\n```\n\nNotes:\n\n- `useMyHand(handId)` returns cards for the currently controlling player only.\n- `usePublicHands(handId)` returns all players\' cards for that hand, keyed by `playerId`.\n- `usePublicHands()` is only populated for hands marked `"visibility": "public"`.\n\n## Genre-specific guidance\n\nFor detailed UI patterns and recommended components per game genre, see:\n\n- [ui-genre-trick-taking.md](ui-genre-trick-taking.md) \u2014 Hearts, Spades, Bridge, Big Two\n- [ui-genre-worker-placement.md](ui-genre-worker-placement.md) \u2014 Agricola, Viticulture, Lords of Waterdeep\n- [ui-genre-resource-management.md](ui-genre-resource-management.md) \u2014 Catan, Splendor, engine builders\n',
|
|
1946
|
+
"references/ui-best-practices.md": '# UI Best Practices\n\nUse this guide when implementing `ui/App.tsx` and `ui/components/*`.\n\nThis page is about rendering, interaction, and UI data flow. Keep rules in `rule.md`, runtime structure in `manifest.json`, and phase-specific UI data in `getUIArgs()`.\n\n## Core Principles\n\n1. **Start with SDK components**\n Prefer `./sdk/components/` and customize them with render props before building custom replacements.\n2. **Design mobile-first**\n Mobile is the baseline. Desktop can add polish, but the primary layout should work on smaller screens first.\n3. **Do not duplicate authoritative game state**\n Use hooks such as `useGameState`, `useUIArgs`, and `useAction` instead of mirroring shared state in local React state.\n4. **Wrap actions in error handling**\n Show feedback through `useToast` or another clear error surface.\n5. **Keep types intact**\n Use the generated hook and action types. Avoid `any`.\n6. **Reserve a persistent feedback area**\n Players should always be able to see the current phase, turn ownership, latest meaningful result, and end-of-game summary.\n\n## SDK Hooks\n\nUse these from `./sdk/hooks/`.\n\n| Hook | Returns | When to use |\n| ------------------------ | ------------------------------------------------------------------------- | ---------------------------------- |\n| `useGameState` | Full game state (`currentPlayerIds`, `decks`, `currentState`, `isMyTurn`) | Core session state |\n| `useCard(cardId)` | `CardItem \\| undefined` | Resolve one card |\n| `useCards(cardIds)` | `CardItem[]` | Resolve multiple cards |\n| `useMyHand(handId)` | Current player\'s cards for one hand | Private or public per-player cards |\n| `useMyResources()` | Current player\'s resources | Resource display |\n| `useMe()` | `{ playerId, name, isHost }` | Current player identity |\n| `usePlayerInfo()` | All players keyed by ID | Player labels and scoreboards |\n| `useAction()` | Phase-specific action submitters | Submitting actions |\n| `useUIArgs()` | Phase-specific UI args | Data returned from `getUIArgs()` |\n| `useGameNotifications()` | Event listeners | Turn prompts, rejections, game end |\n| `useLobby()` | Lobby state | Seating and pre-game UI |\n| `useDice(dieIds)` | Dice values and roll function | Dice mechanics |\n\n## SDK Components\n\nUse these from `./sdk/components/`.\n\n### Core display\n\n| Component | Purpose |\n| --------------- | ---------------------------------------------- |\n| `Card` | Display a card with animation support |\n| `ConnectedCard` | Resolve card data from context automatically |\n| `Hand` | Render a player\'s hand with custom card layout |\n| `PlayArea` | Render a shared card area |\n| `PlayerInfo` | Show player name, score, and turn state |\n\n### Actions and status\n\n| Component | Purpose |\n| ----------------- | ---------------------------------------- |\n| `ActionButton` | Action button with optional cost display |\n| `ActionPanel` | Container for related actions |\n| `ActionGroup` | Group actions inside a panel |\n| `PhaseIndicator` | Show current phase and turn state |\n| `ResourceCounter` | Display typed resources |\n| `CostDisplay` | Show affordability |\n| `DiceRoller` | Show die values |\n| `GameEndDisplay` | Show final results |\n| `Drawer` | Mobile overflow surface |\n\n### Board components\n\n| Component | Use case | Hook |\n| -------------- | ----------------------------- | --------------------------------- |\n| `SquareGrid` | Chess, checkers, grid tactics | `useSquareBoard(boardId)` |\n| `HexGrid` | Hex maps and wargames | `useHexBoard(boardId)` |\n| `TrackBoard` | Race tracks and score tracks | `useTrackBoard(boardId)` |\n| `NetworkGraph` | Connected-node boards | `useNetworkBoard(boardId)` |\n| `ZoneMap` | Area-control boards | `useZoneMap(zones, pieces)` |\n| `SlotSystem` | Worker placement | `useSlotSystem(slots, occupants)` |\n\n## Layout Guidelines\n\n- Use Tailwind CSS for styling.\n- Use `clsx` for conditional class composition.\n- Use `framer-motion` for meaningful motion, not motion everywhere.\n- Keep touch targets large and spacing readable on mobile.\n- Use `useIsMobile()` when behavior must differ by device.\n\n## Persistent Feedback Region\n\nReserve a visible area near the top of the layout for status that should survive subview changes.\n\nThat region should be able to show:\n\n- the current phase\n- whether it is the local player\'s turn\n- the latest meaningful status message\n- round or trick resolution results\n- game-over state and winner summary\n\nIf your game uses `AUTO` phases, do not let those transitions erase the only copy of round-resolution feedback before the next interactive view appears.\n\n## Common Patterns\n\n### Phase-based rendering\n\n```tsx\nconst { currentState } = useGameState();\nconst uiArgs = useUIArgs();\nconst actions = useAction();\n\nif (currentState === "playCards") {\n const phaseArgs = uiArgs.playCards;\n const phaseActions = actions.playCardsActions;\n return <PlayCardsView args={phaseArgs} actions={phaseActions} />;\n}\n```\n\n### Action submission with error handling\n\n```tsx\nconst { toast } = useToast();\n\nconst handlePlay = async () => {\n try {\n await actions.playCardsActions.playCard({ cardIds: selectedCards });\n } catch (err) {\n toast.error(err instanceof Error ? err.message : "Action failed");\n }\n};\n```\n\n### Public hands in UI\n\nFor per-player containers that should be visible to everyone, prefer the built-in public-hand hook instead of duplicating that data in `getUIArgs()`.\n\n```ts\nimport { useMyHand, usePublicHands } from "@dreamboard/ui-sdk";\n\nconst myHand = useMyHand("hand");\nconst scoredByPlayer = usePublicHands("scored-area");\nconst player2Scored = scoredByPlayer["player-2"] ?? [];\n```\n\nNotes:\n\n- `useMyHand(handId)` reads the current player\'s copy of that hand\n- `usePublicHands(handId)` reads all players\' copies, keyed by `playerId`\n- `usePublicHands()` only works for hands marked `visibility: "public"`\n\n## Genre-Specific Guides\n\nUse these when the game matches one of the built-in patterns:\n\n- [ui-genre-trick-taking.md](ui-genre-trick-taking.md)\n- [ui-genre-worker-placement.md](ui-genre-worker-placement.md)\n- [ui-genre-resource-management.md](ui-genre-resource-management.md)\n',
|
|
1946
1947
|
"references/ui-genre-resource-management.md": '# UI Guide: Resource Management Games\n\nPatterns for Catan, Splendor, engine builders, trading games, and similar resource-centric games.\n\n## Game characteristics\n\n- Players **collect, spend, and trade resources** to build structures, buy cards, or score points.\n- May involve a shared board (hex grid, network, market row) plus personal tableaux.\n- Key UI signals: resource pools, affordability, available purchases, trade offers, build options.\n\n## Recommended SDK components\n\n### Primary\n\n| Component | Usage |\n| ----------------------------- | -------------------------------------------------------------------------- |\n| `ResourceCounter` | Display each player\'s resource pool with icons and animated count changes. |\n| `CostDisplay` | Show build/purchase costs with green/red affordability feedback. |\n| `ActionButton` | "Build", "Buy", "Trade" buttons with integrated cost display. |\n| `ActionPanel` / `ActionGroup` | Organize actions by type (Build, Trade, End Turn). |\n| `Card` / `Hand` | Display purchasable cards or player tableau cards. |\n\n### Board components (pick based on game)\n\n| Component | Use case | Hook |\n| -------------- | ----------------------------------------- | -------------------------- |\n| `HexGrid` | Catan-style resource hex boards | `useHexBoard(boardId)` |\n| `NetworkGraph` | Trade route networks | `useNetworkBoard(boardId)` |\n| `SquareGrid` | Grid-based markets or tableaux | `useSquareBoard(boardId)` |\n| `PlayArea` | Market row / available cards for purchase | \u2014 |\n\n### Supporting\n\n| Component | Usage |\n| ---------------- | -------------------------------------------------------- |\n| `PlayerInfo` | Player name, score, and victory point tracker. |\n| `PhaseIndicator` | Current phase (roll, trade, build, etc.). |\n| `DiceRoller` | For dice-based resource generation (e.g., Catan). |\n| `GameEndDisplay` | Final scores with score breakdown. |\n| `Drawer` | Mobile drawer for trade interface or detailed card view. |\n\n### Hooks\n\n| Hook | Usage |\n| -------------------------- | ----------------------------------------------------- |\n| `useMyResources()` | Current player\'s resource counts. |\n| `useHexBoard(boardId)` | Hex board state for Catan-style games. |\n| `useNetworkBoard(boardId)` | Network/route state. |\n| `useSquareBoard(boardId)` | Grid board state. |\n| `useDice(dieIds)` | Dice rolling mechanics. |\n| `useGameState` | Turn order, current phase, active players. |\n| `useAction` | Submit build, trade, end-turn actions. |\n| `useUIArgs` | Phase-specific data (available builds, trade offers). |\n| `usePlayerInfo` | All player info for scoreboards. |\n\n## Key patterns\n\n### Resource display with affordability\n\n```tsx\nconst resources = useMyResources();\nconst resourceDefs: ResourceDefinition[] = [\n { type: "brick", label: "Brick", icon: Blocks, color: "text-red-600" },\n { type: "lumber", label: "Lumber", icon: TreePine, color: "text-green-700" },\n { type: "ore", label: "Ore", icon: Mountain, color: "text-slate-500" },\n { type: "grain", label: "Grain", icon: Wheat, color: "text-yellow-500" },\n { type: "wool", label: "Wool", icon: Cloud, color: "text-green-400" },\n];\n\n<ResourceCounter\n resources={resourceDefs.map((d) => ({\n ...d,\n iconColor: d.color,\n bgColor: "bg-slate-800",\n }))}\n counts={resources}\n layout="row"\n/>;\n```\n\n### Build actions with costs\n\n```tsx\nconst { buildPhase } = useUIArgs();\nconst resources = useMyResources();\n\n<ActionPanel title="Build" state="buildPhase">\n <ActionGroup title="Structures">\n <ActionButton\n label="Build Road"\n cost={{ brick: 1, lumber: 1 }}\n currentResources={resources}\n resourceDefs={resourceDefs}\n available={buildPhase?.canBuildRoad}\n onClick={() => actions.buildActions.build({ type: "road" })}\n />\n <ActionButton\n label="Build Settlement"\n cost={{ brick: 1, lumber: 1, grain: 1, wool: 1 }}\n currentResources={resources}\n resourceDefs={resourceDefs}\n available={buildPhase?.canBuildSettlement}\n onClick={() => actions.buildActions.build({ type: "settlement" })}\n />\n </ActionGroup>\n</ActionPanel>;\n```\n\n### Hex board for Catan-style games\n\n```tsx\nconst { tiles, edges, vertices } = useHexBoard("catan-board");\n\n<HexGrid\n tiles={tiles}\n edges={edges}\n vertices={vertices}\n hexSize={50}\n renderTile={(tile) => (\n <DefaultHexTile\n size={50}\n fill={terrainColors[tile.typeId]}\n label={tile.data?.diceNumber}\n />\n )}\n renderEdge={(edge, pos) => (\n <DefaultHexEdge position={pos} color={playerColors[edge.owner]} />\n )}\n renderVertex={(vertex, pos) => (\n <DefaultHexVertex position={pos} color={playerColors[vertex.owner]} />\n )}\n interactiveVertices={placementMode === "settlement"}\n interactiveEdges={placementMode === "road"}\n onInteractiveVertexClick={(v) => handlePlacement("settlement", v.id)}\n onInteractiveEdgeClick={(e) => handlePlacement("road", e.id)}\n/>;\n```\n\n### Market row (purchasable cards)\n\n```tsx\n<PlayArea\n cards={marketCards}\n layout="row"\n interactive\n onCardClick={(cardId) => handlePurchase(cardId)}\n renderCard={(card) => (\n <div className="relative">\n <DefaultCardContent card={card} />\n <CostDisplay\n cost={card.properties.cost}\n currentResources={resources}\n resourceDefs={resourceDefs}\n layout="inline"\n />\n </div>\n )}\n/>\n```\n\n## UIArgs recommendations\n\n```typescript\n// shared/ui-args.ts\nexport interface BuildPhaseUIArgs {\n canBuildRoad: boolean;\n canBuildSettlement: boolean;\n canBuildCity: boolean;\n canBuyCard: boolean;\n availableBuildLocations?: string[]; // Board positions where building is allowed\n}\n\nexport interface TradePhaseUIArgs {\n canTrade: boolean;\n tradeOffers?: {\n from: string;\n offering: Record<string, number>;\n requesting: Record<string, number>;\n }[];\n bankTradeRatios?: Record<string, number>; // e.g., { brick: 4, lumber: 3 } for ports\n}\n\nexport interface DicePhaseUIArgs {\n diceValues?: number[];\n resourcesGained?: Record<string, number>;\n}\n```\n',
|
|
1947
1948
|
"references/ui-genre-trick-taking.md": '# UI Guide: Trick-Taking Games\n\nPatterns for Hearts, Spades, Bridge, Big Two, and similar card-based trick-taking games.\n\n## Game characteristics\n\n- Players hold a **hand of cards** and play them to a shared **trick area**.\n- One trick is resolved at a time; the winner collects cards.\n- Turn order matters \u2014 typically clockwise from the trick leader.\n- Key UI signals: whose turn it is, which cards are legal to play, current trick, scores.\n\n## Recommended SDK components\n\n### Primary\n\n| Component | Usage |\n| ------------ | --------------------------------------------------------------------------------------------------- |\n| `Hand` | Display the current player\'s hand. Use `renderCard` to highlight playable cards. |\n| `Card` | Render individual cards in the hand and trick area. Use `renderContent` for custom suit/rank faces. |\n| `PlayArea` | Display the current trick \u2014 cards played by each player. Use `layout="row"`. |\n| `PlayerInfo` | Show each player\'s name, score, and turn indicator around the table. |\n\n### Supporting\n\n| Component | Usage |\n| ---------------- | ----------------------------------------------------------- |\n| `PhaseIndicator` | Show current phase (dealing, passing, playing, scoring). |\n| `GameEndDisplay` | Final scoreboard at game end. |\n| `useToast` | Feedback for invalid plays (e.g., "Must follow suit"). |\n| `Drawer` | Overflow drawer when the hand has too many cards on mobile. |\n\n### Hooks\n\n| Hook | Usage |\n| -------------------- | -------------------------------------------------------------------------- |\n| `useMyHand(handId)` | Get the current player\'s cards. |\n| `useCard / useCards` | Resolve card data for display. |\n| `useGameState` | Get `currentPlayerIds`, `isMyTurn`, `currentState`. |\n| `useAction` | Submit play-card action. |\n| `useUIArgs` | Get phase-specific data (e.g., legal card IDs, trick cards, trick winner). |\n| `usePlayerInfo` | Map player IDs to names for trick display. |\n\n## Key patterns\n\n### Card selection for play\n\n```tsx\nconst [selectedCards, setSelectedCards] = useState<string[]>([]);\nconst { playCards } = useUIArgs();\nconst legalCardIds = new Set(playCards?.legalCardIds ?? []);\n\n<Hand\n cards={myCards}\n selectedIds={selectedCards}\n renderCard={({ card, isSelected, ...pos }) => (\n <Card\n card={card}\n selected={isSelected}\n disabled={!legalCardIds.has(card.id)}\n onCardClick={(id) => toggleSelection(id)}\n style={{\n position: "absolute",\n left: pos.x,\n transform: `translateY(${pos.y}px)`,\n zIndex: pos.zIndex,\n }}\n />\n )}\n renderDrawer={({ cards }) => <DrawerContent cards={cards} />}\n renderEmpty={() => <p>No cards</p>}\n/>;\n```\n\n### Trick display with player labels\n\n```tsx\nconst playerInfo = usePlayerInfo();\nconst { playCards } = useUIArgs();\nconst trickCards = playCards?.trickCards ?? []; // { cardId, playerId }[]\n\n<PlayArea\n cards={resolvedCards}\n layout="row"\n renderCard={(card) => (\n <div className="flex flex-col items-center gap-1">\n <span className="text-xs text-slate-400">\n {playerInfo[card.properties?.playedBy]?.name}\n </span>\n <SuitRankCard card={card} />\n </div>\n )}\n/>;\n```\n\n## UIArgs recommendations\n\n```typescript\n// shared/ui-args.ts\nexport interface PlayCardsUIArgs {\n legalCardIds: string[]; // Cards the player can legally play\n trickCards: { cardId: string; playerId: string }[]; // Cards in the current trick\n trickLeader: string | null; // Who led the trick\n trumpSuit?: string; // Trump suit if applicable\n}\n\nexport interface ScoringUIArgs {\n roundScores: Record<string, number>;\n totalScores: Record<string, number>;\n}\n```\n',
|
|
1948
1949
|
"references/ui-genre-worker-placement.md": '# UI Guide: Worker Placement Games\n\nPatterns for Agricola, Viticulture, Lords of Waterdeep, Caverna, and similar euro-style worker placement games.\n\n## Game characteristics\n\n- Players place **worker tokens** on shared **action slots** to gain resources or take actions.\n- Slots may be exclusive (one player per round) or shared.\n- Phases typically include: placement, resolution/harvest, and cleanup.\n- Key UI signals: available slots, slot costs/rewards, number of workers remaining, resource counts.\n\n## Recommended SDK components\n\n### Primary\n\n| Component | Usage |\n| --------------------- | ----------------------------------------------------------------------------------------------------- |\n| `SlotSystem` | Core board \u2014 renders all action slots with occupants. Use `layout="grouped"` to organize by category. |\n| `DefaultSlotItem` | Pre-built slot renderer showing name, description, capacity, cost/reward labels. |\n| `DefaultSlotOccupant` | Pre-built worker token display with player color. |\n| `ResourceCounter` | Display each player\'s resource pool (wood, stone, food, etc.). |\n| `ActionButton` | "Place Worker" button with integrated cost display and affordability check. |\n\n### Supporting\n\n| Component | Usage |\n| ----------------------------- | ------------------------------------------------------------ |\n| `ActionPanel` / `ActionGroup` | Group available actions (e.g., "Place Worker", "End Turn"). |\n| `CostDisplay` | Show resource costs on slot details. |\n| `PlayerInfo` | Show each player\'s name, workers remaining, and turn status. |\n| `PhaseIndicator` | Distinguish placement phase from harvest/scoring phases. |\n| `GameEndDisplay` | Final scoring with detailed breakdown. |\n| `Drawer` | Mobile drawer for detailed slot inspection. |\n\n### Hooks\n\n| Hook | Usage |\n| --------------------------------- | ---------------------------------------------------------------------------------------------- |\n| `useSlotSystem(slots, occupants)` | Utility for slot lookups: `getSlot`, `isFull`, `getRemainingCapacity`, `getOccupantsByPlayer`. |\n| `useMyResources()` | Current player\'s resource pool. |\n| `useGameState` | Get `isMyTurn`, `currentState`, `currentPlayerIds`. |\n| `useAction` | Submit place-worker or end-turn actions. |\n| `useUIArgs` | Get phase-specific data (available slots, worker count, etc.). |\n| `usePlayerInfo` | Map player IDs to names and colors. |\n\n## Key patterns\n\n### Slot board with placement\n\n```tsx\nconst { placementPhase } = useUIArgs();\nconst slots = placementPhase?.slots ?? [];\nconst occupants = placementPhase?.occupants ?? [];\nconst availableSlotIds = new Set(placementPhase?.availableSlotIds ?? []);\nconst slotApi = useSlotSystem(slots, occupants);\n\n<SlotSystem\n slots={slots}\n occupants={occupants}\n layout="grouped"\n renderSlot={(slot, slotOccupants) => (\n <DefaultSlotItem\n name={slot.name}\n description={slot.description}\n capacity={slot.capacity}\n occupantCount={slotOccupants.length}\n isExclusive={slot.exclusive}\n isAvailable={availableSlotIds.has(slot.id)}\n costLabel={formatCost(slot.cost)}\n rewardLabel={formatReward(slot.reward)}\n onClick={() => handlePlaceWorker(slot.id)}\n renderOccupants={() => (\n <div className="flex gap-1">\n {slotOccupants.map((o) => (\n <DefaultSlotOccupant\n key={o.pieceId}\n color={playerColors[o.playerId]}\n label={playerInfo[o.playerId]?.name}\n />\n ))}\n </div>\n )}\n />\n )}\n/>;\n```\n\n### Resource display\n\n```tsx\nconst resources = useMyResources();\n\n<ResourceCounter\n resources={[\n {\n type: "wood",\n label: "Wood",\n icon: TreePine,\n iconColor: "text-amber-700",\n },\n {\n type: "stone",\n label: "Stone",\n icon: Mountain,\n iconColor: "text-slate-400",\n },\n { type: "food", label: "Food", icon: Apple, iconColor: "text-red-400" },\n { type: "gold", label: "Gold", icon: Coins, iconColor: "text-yellow-400" },\n ]}\n counts={resources}\n layout="row"\n/>;\n```\n\n### Action buttons with costs\n\n```tsx\n<ActionButton\n label="Build Farm"\n cost={{ wood: 2, stone: 1 }}\n currentResources={resources}\n resourceDefs={resourceDefinitions}\n icon={Home}\n onClick={() => actions.buildActions.build({ buildingType: "farm" })}\n/>\n```\n\n## UIArgs recommendations\n\n```typescript\n// shared/ui-args.ts\nexport interface PlacementPhaseUIArgs {\n slots: SlotDefinition[];\n occupants: SlotOccupant[];\n availableSlotIds: string[]; // Slots the current player can legally place on\n workersRemaining: number;\n}\n\nexport interface HarvestPhaseUIArgs {\n feedingRequired: number; // Food needed\n currentFood: number;\n}\n```\n',
|
|
1949
1950
|
"references/ui-style-guide.md": '# UI Style Guide\n\nThis style guide details the design philosophy, visual tokens, and core components for the Dreamboard.games UI SDK. The framework uses a **Hand-Drawn** aesthetic that emphasizes human creativity over corporate polish.\n\n## Design Philosophy\n\nThe Hand-Drawn design style celebrates authentic imperfection and human touch in a digital world. It rejects clinical precision in favor of organic, playful irregularity that evokes sketches on paper, sticky notes on a wall, and napkin diagrams from a brainstorming session.\n\n### Core Principles\n\n- **No Straight Lines**: Every border, shape, and container uses irregular border-radius values to create wobbly, hand-drawn edges that reject geometric perfection.\n- **Authentic Texture**: The design layer paper grain, dot patterns, and subtle background textures to simulate physical media (notebook paper, post-its, sketch pads).\n- **Playful Rotation**: Elements are deliberately tilted using small rotation transforms (`-rotate-1`, `rotate-2`) to break rigid grid alignment and create casual energy.\n- **Hard Offset Shadows**: Reject soft blur shadows entirely. Use solid, offset box-shadows (like `4px 4px 0px`) to create a cut-paper, layered collage aesthetic.\n- **Handwritten Typography**: Use exclusively handwritten or marker-style fonts (like Kalam for headings and Patrick Hand for body text) that feel human and approachable.\n- **Limited Color Palette**: Stick to pencil blacks (`#2d2d2d`), paper whites (`#fdfbf7`), correction marker red (`#ff4d4d`), post-it yellow (`#fff9c4`), and ballpoint blue (`#2d5da1`).\n- **Intentional Messiness**: Embrace overlap, asymmetry, and visual "mistakes" that make the design feel spontaneous.\n\n## CSS Tokens & Utilities\n\nThe platform provides several global CSS utilities designed for this aesthetic:\n\n- **Borders**: `.wobbly-border`, `.wobbly-border-md`, `.wobbly-border-lg` apply the irregular `border-radius` shapes. Used with `border-2`, `border-[3px]`, or `border-[4px]`.\n- **Shadows**: `.hard-shadow`, `.hard-shadow-md`, `.hard-shadow-lg` apply the solid offset shadows. Use `.hard-shadow-sm` for active states (buttons pressing flat).\n\n## Implementing in Components\n\nWhen writing or modifying UI components in `ui/` or `ui-sdk/`:\n\n### Containers & Cards\n\n- Add `wobbly-border-md` or `wobbly-border-lg`.\n- Use `border-[3px] border-border` to give it a thick pencil-like stroke.\n- Apply `bg-[#fdfbf7]` (warm paper) or `bg-white`.\n- Add a solid shadow with `hard-shadow` or `hard-shadow-lg`.\n- Slightly rotate the element with `rotate-1` or `-rotate-1`.\n\n### Buttons\n\n- Use `wobbly-border`.\n- Normal state: `border-[3px] border-border bg-[#fdfbf7] text-foreground hard-shadow`.\n- Hover state: `hover:bg-primary hover:text-white hover:hard-shadow-sm hover:-translate-y-1`.\n- Active state: `active:shadow-none active:translate-y-1 active:translate-x-1`.\n- Secondary variants can use `bg-[#e5e0d8]` or `bg-[#fff9c4]`.\n\n### Typography\n\n- Add `font-sans` for standard handwritten text or `font-display` for bold marker headings.\n- Make things bold! `font-bold` works well with handwritten fonts to make them legible.\n\n### Decoration\n\n- For emphasis, wrap text in a post-it styling: `bg-[#fff9c4] px-2 py-1 border-2 border-border wobbly-border rotate-2 inline-block`.\n- Add "tape" to tops of containers: `<div className="absolute top-2 left-1/2 -translate-x-1/2 w-24 h-6 bg-[#e5e0d8] border border-border opacity-80 backdrop-blur-sm -rotate-2 z-10" />`.\n',
|
|
1950
1951
|
"scripts/events-extract.mjs": '#!/usr/bin/env node\n\nimport { access, readFile } from "node:fs/promises";\n\nconst DEFAULT_EVENTS_PATH = ".dreamboard/run/events.ndjson";\n\nfunction printHelp() {\n console.log(`Extract fields from dreamboard run artifacts.\n\nUsage:\n node .agents/skills/dreamboard/scripts/events-extract.mjs [options]\n\nOptions:\n --file, -f <path> NDJSON file path (default: ${DEFAULT_EVENTS_PATH})\n --type <eventType> Filter by event type (repeatable)\n --field <path> Dot path to extract (e.g. message.reason)\n --player <playerId> Filter by player ID across common message fields\n --limit <number> Limit output records\n --no-index Omit line index in output\n --help, -h Show this help\n\nExamples:\n node .agents/skills/dreamboard/scripts/events-extract.mjs --type YOUR_TURN\n node .agents/skills/dreamboard/scripts/events-extract.mjs --type ACTION_REJECTED --field message.reason\n node .agents/skills/dreamboard/scripts/events-extract.mjs --player player-2 --field message.availableActions\n`);\n}\n\nfunction parseCliArgs(argv) {\n const options = {\n filePath: DEFAULT_EVENTS_PATH,\n types: new Set(),\n includeIndex: true,\n };\n\n for (let index = 0; index < argv.length; index += 1) {\n const token = argv[index];\n const nextToken = argv[index + 1];\n\n if (token === "--help" || token === "-h") {\n printHelp();\n process.exit(0);\n }\n if (token === "--no-index") {\n options.includeIndex = false;\n continue;\n }\n if (token === "--file" || token === "-f") {\n if (!nextToken) {\n throw new Error("--file requires a value");\n }\n options.filePath = nextToken;\n index += 1;\n continue;\n }\n if (token === "--type") {\n if (!nextToken) {\n throw new Error("--type requires a value");\n }\n options.types.add(nextToken);\n index += 1;\n continue;\n }\n if (token === "--field") {\n if (!nextToken) {\n throw new Error("--field requires a value");\n }\n options.fieldPath = nextToken;\n index += 1;\n continue;\n }\n if (token === "--player") {\n if (!nextToken) {\n throw new Error("--player requires a value");\n }\n options.playerId = nextToken;\n index += 1;\n continue;\n }\n if (token === "--limit") {\n if (!nextToken) {\n throw new Error("--limit requires a value");\n }\n const parsed = Number.parseInt(nextToken, 10);\n if (!Number.isFinite(parsed) || parsed <= 0) {\n throw new Error("--limit must be a positive integer");\n }\n options.limit = parsed;\n index += 1;\n continue;\n }\n\n throw new Error(`Unknown argument: ${token}`);\n }\n\n return options;\n}\n\nfunction getByPath(value, path) {\n const segments = path.split(".").filter(Boolean);\n let current = value;\n\n for (const segment of segments) {\n if (current === null || current === undefined) {\n return undefined;\n }\n if (typeof current !== "object") {\n return undefined;\n }\n current = current[segment];\n }\n\n return current;\n}\n\nfunction extractPlayerCandidates(record) {\n const message = record.message ?? {};\n const players = new Set();\n\n const singleCandidates = [\n message.playerId,\n message.targetPlayer,\n message.toUser,\n ];\n for (const candidate of singleCandidates) {\n if (typeof candidate === "string") {\n players.add(candidate);\n }\n }\n\n const listCandidates = [\n message.activePlayers,\n message.previousPlayers,\n message.currentPlayers,\n message.controllablePlayerIds,\n message.eligiblePlayerIds,\n ];\n for (const candidate of listCandidates) {\n if (!Array.isArray(candidate)) {\n continue;\n }\n for (const item of candidate) {\n if (typeof item === "string") {\n players.add(item);\n }\n }\n }\n\n return [...players];\n}\n\nfunction toOutputRecord(record, lineIndex, options) {\n const output = {};\n\n if (options.includeIndex) {\n output.index = lineIndex;\n }\n output.observedAt = record.observedAt ?? null;\n output.sessionId = record.sessionId ?? null;\n output.eventId = record.eventId ?? null;\n output.type = record.type ?? null;\n\n if (options.fieldPath) {\n output.field = options.fieldPath;\n output.value = getByPath(record, options.fieldPath);\n } else {\n output.message = record.message ?? null;\n }\n\n return output;\n}\n\nasync function main() {\n const options = parseCliArgs(process.argv.slice(2));\n await access(options.filePath);\n\n const text = await readFile(options.filePath, "utf8");\n const lines = text\n .split(/\\r?\\n/u)\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n\n let emitted = 0;\n for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {\n const line = lines[lineIndex];\n let record;\n try {\n record = JSON.parse(line);\n } catch {\n continue;\n }\n\n if (options.types.size > 0 && !options.types.has(record.type ?? "")) {\n continue;\n }\n\n if (options.playerId) {\n const players = extractPlayerCandidates(record);\n if (!players.includes(options.playerId)) {\n continue;\n }\n }\n\n const output = toOutputRecord(record, lineIndex + 1, options);\n console.log(JSON.stringify(output));\n emitted += 1;\n\n if (options.limit !== undefined && emitted >= options.limit) {\n break;\n }\n }\n}\n\nmain().catch((error) => {\n const message = error instanceof Error ? error.message : String(error);\n console.error(`events-extract failed: ${message}`);\n process.exit(1);\n});\n',
|
|
1951
|
-
"SKILL.md":
|
|
1952
|
+
"SKILL.md": "---\nname: dreamboard\ndescription: Create multiplayer, rule-enforced, turn-based board game on Dreamboard.games platform.\nmetadata:\n short-description: Dreamboard Game Development Workflow\n tags: [dreamboard, cli, game-dev, board-game, turn-based, multiplayer]\n---\n\n# Dreamboard\n\n## Goal\n\nCreate and iterate on a Dreamboard game locally, then sync rules, scaffold, UI, and tests back to the platform.\n\n## Why Dreamboard\n\nDreamboard exists to turn a board-game idea into a playable digital prototype quickly.\n\n- Describe the game instead of hand-building every primitive first.\n- Generate rules scaffolding, components, and UI structure faster.\n- Playtest with real players without repeated print-and-play friction.\n- Iterate live while Dreamboard handles turns, hands, and multiplayer plumbing.\n\n## Prereqs\n\n- Dreamboard CLI installed and available as `dreamboard`\n Install with `npm install -g dreamboard`\n- Authenticated via `dreamboard login`\n\n## Building Your First Game\n\nRead [references/building-your-first-game.md](references/building-your-first-game.md)\n\n## References\n\n- Rules and manifest:\n [references/rule-authoring.md](references/rule-authoring.md),\n [references/manifest-authoring.md](references/manifest-authoring.md)\n- App logic and engine concepts:\n [references/phase-handlers.md](references/phase-handlers.md),\n [references/api-reference.md](references/api-reference.md),\n [references/hands-vs-decks.md](references/hands-vs-decks.md),\n [references/all-players-tracking.md](references/all-players-tracking.md),\n [references/app-best-practices.md](references/app-best-practices.md)\n- UI and UX:\n [references/ui-best-practices.md](references/ui-best-practices.md),\n [references/ui-style-guide.md](references/ui-style-guide.md),\n genre references under `references/ui-genre-*.md`\n- TypeScript regression harness (`dreamboard test generate/run`):\n [references/test-harness.md](references/test-harness.md)\n- JSON runtime scenarios (`dreamboard run --scenario ...`):\n [references/scenario-format.md](references/scenario-format.md)\n- Rejection and edge-case pressure using JSON runtime scenarios:\n [references/adversarial-testing.md](references/adversarial-testing.md)\n\n## Testing Modes\n\nThere are two different scenario systems:\n\n- `dreamboard test generate` / `dreamboard test run`\n TypeScript scenario files in `test/scenarios/*.scenario.ts` against deterministic generated base snapshots. Use this for regression coverage.\n- `dreamboard run --scenario path/to/file.json`\n JSON action scripts that drive a live session. Use this for debugging flows, reproductions, and adversarial experiments.\n\n## Guardrails\n\n- `manifest.json` and `rule.md` are the source of truth for scaffolding.\n- Run `dreamboard update` after manifest changes to keep generated files in sync.\n- `dreamboard update` is local-first and fails when remote drift is detected.\n- Use `dreamboard update --pull` only when you explicitly need to reconcile unexpected remote changes into the workspace.\n- `dreamboard test generate` should be re-run after base changes, compiled-result changes, or game identity changes.\n- Add at least one zero-step `initial-state` scenario so setup invariants fail before action tests begin.\n- Keep `scripts/events-extract.mjs` as a debugging helper for `.dreamboard/run/events.ndjson`; assertions belong in test scenarios.\n\n## Editable Surface\n\nEdit:\n\n- `app/phases/*.ts`\n- `shared/ui-args.ts`\n- `ui/App.tsx`\n- `ui/components/*`\n- `ui/sdk/components/*`\n\nDo not edit framework-owned files such as:\n\n- `app/index.ts`\n- `app/sdk/*`\n- `app/generated/*`\n- `ui/index.tsx`\n- `ui/sdk/context/*`\n- `ui/sdk/hooks/*`\n- `ui/sdk/types/*`\n\n## Framework Feedback\n\nUse `feedback.md` in the game project root to record framework issues, missing features, or workflow friction. Include reproduction steps, expected behavior, and actual behavior when possible.\n"
|
|
1952
1953
|
};
|
|
1953
1954
|
var SKILL_MD_CONTENT = SKILL_ASSET_FILES["SKILL.md"];
|
|
1954
1955
|
|
|
@@ -2000,15 +2001,6 @@ import crypto from "crypto";
|
|
|
2000
2001
|
function hashContent(content) {
|
|
2001
2002
|
return crypto.createHash("sha256").update(content, "utf8").digest("hex");
|
|
2002
2003
|
}
|
|
2003
|
-
function getUserIdFromToken(token) {
|
|
2004
|
-
const [, payload] = token.split(".");
|
|
2005
|
-
if (!payload) throw new Error("Invalid auth token.");
|
|
2006
|
-
const decoded = JSON.parse(
|
|
2007
|
-
Buffer.from(payload, "base64").toString("utf8")
|
|
2008
|
-
);
|
|
2009
|
-
if (!decoded.sub) throw new Error("Auth token missing user id.");
|
|
2010
|
-
return decoded.sub;
|
|
2011
|
-
}
|
|
2012
2004
|
|
|
2013
2005
|
// src/services/project/local-files.ts
|
|
2014
2006
|
import { readdir, unlink } from "fs/promises";
|
|
@@ -2017,7 +2009,6 @@ import path2 from "path";
|
|
|
2017
2009
|
// src/constants.ts
|
|
2018
2010
|
var DEFAULT_API_BASE_URL = "https://api.dreamboard.games";
|
|
2019
2011
|
var DEFAULT_WEB_BASE_URL = "https://dreamboard.games";
|
|
2020
|
-
var CODE_EDITS_BUCKET = "code-edits";
|
|
2021
2012
|
var PROD_SUPABASE_URL = "https://upsjrgzihskmqporuaye.supabase.co";
|
|
2022
2013
|
var PROD_SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InVwc2pyZ3ppaHNrbXFwb3J1YXllIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjAwOTM1MjQsImV4cCI6MjA3NTY2OTUyNH0.9IZZ9Jmtqk4OzhQnkU7pApVx-IfdNjAt55ZKjOBjBB4";
|
|
2023
2014
|
var PROJECT_DIR_NAME = ".dreamboard";
|
|
@@ -2261,7 +2252,6 @@ export {
|
|
|
2261
2252
|
client,
|
|
2262
2253
|
DEFAULT_API_BASE_URL,
|
|
2263
2254
|
DEFAULT_WEB_BASE_URL,
|
|
2264
|
-
CODE_EDITS_BUCKET,
|
|
2265
2255
|
PROJECT_DIR_NAME,
|
|
2266
2256
|
ENVIRONMENT_CONFIGS,
|
|
2267
2257
|
PROJECT_CONFIG_FILE,
|
|
@@ -2278,14 +2268,11 @@ export {
|
|
|
2278
2268
|
readJsonFile,
|
|
2279
2269
|
writeJsonFile,
|
|
2280
2270
|
hashContent,
|
|
2281
|
-
getUserIdFromToken,
|
|
2282
2271
|
isAllowedGamePath,
|
|
2283
2272
|
isDynamicGeneratedPath,
|
|
2284
2273
|
isDynamicSeedPath,
|
|
2285
2274
|
isLibraryPath,
|
|
2286
|
-
PRESERVED_USER_FILES,
|
|
2287
2275
|
isAllowedGamePath2,
|
|
2288
|
-
isLibraryPath2,
|
|
2289
2276
|
writeSourceFiles,
|
|
2290
2277
|
writeScaffoldFiles,
|
|
2291
2278
|
removeExtraneousFiles,
|
|
@@ -2298,4 +2285,4 @@ export {
|
|
|
2298
2285
|
writeSnapshotFromFiles,
|
|
2299
2286
|
getLocalDiff
|
|
2300
2287
|
};
|
|
2301
|
-
//# sourceMappingURL=chunk-
|
|
2288
|
+
//# sourceMappingURL=chunk-3I26YUTP.js.map
|