@dreamboard-games/agent-skills 0.1.0-alpha.0 → 0.1.0-alpha.1

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/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "@dreamboard-games/agent-skills",
3
- "version": "0.1.0-alpha.0",
3
+ "version": "0.1.0-alpha.1",
4
4
  "description": "Dreamboard agent skills for game development workflows.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
7
7
  "files": [
8
8
  "skills"
9
9
  ],
10
+ "scripts": {
11
+ "prepack": "rm -rf skills && mkdir -p skills && cp -R ../../skills/dreamboard skills/dreamboard",
12
+ "pack:publish": "npm pack --dry-run ."
13
+ },
10
14
  "keywords": [
11
15
  "dreamboard",
12
16
  "agent-skills",
@@ -6,18 +6,18 @@ metadata:
6
6
  tags: [dreamboard, cli, game-dev, board-game, turn-based, multiplayer]
7
7
  ---
8
8
 
9
- # Dreamboard CLI
9
+ # Dreamboard
10
10
 
11
11
  ## Goal
12
12
 
13
- Create and iterate on a Dreamboard game locally, then sync authored changes,
14
- compile, run, and test against the current reducer-native scaffold.
13
+ Create and iterate on a Dreamboard game locally with the Git-native Dreamboard
14
+ command, then verify exact commits, run tests, and use the local dev host.
15
15
 
16
16
  ## Prereqs
17
17
 
18
- - Dreamboard CLI installed and available as `dreamboard`
18
+ - Dreamboard installed and available as `dreamboard`
19
19
  Install with `npm install -g dreamboard`
20
- - Authenticated via `dreamboard login`
20
+ - Authenticated via `dreamboard auth login`
21
21
 
22
22
  ## Buliding Your First Game
23
23
  See [tutorials/building-your-first-game.md](references/building-your-first-game.md)
@@ -60,18 +60,25 @@ The current scaffold centers on these files:
60
60
 
61
61
  Use the commands for different kinds of state:
62
62
 
63
- - `dreamboard sync`
64
- Upload local authored changes and advance the remote authored head. Run this after editing `rule.md`, `manifest.json`, or authored source files that should be part of the next remote build.
65
- - `dreamboard compile`
66
- Compile the current remote authored head. Run this after `sync`, or re-run it after a failed compile when you have not made new authored edits.
67
- - `dreamboard pull`
68
- Reconcile remote authored changes into the current workspace. Run this when someone else advanced the remote authored head or `dreamboard status` shows authored state `behind` or `diverged`.
63
+ - `dreamboard project create <slug> --description <text>`
64
+ Create a project workspace and configure its Git remote.
65
+ - `dreamboard project clone <project>`
66
+ Clone an existing project repository.
67
+ - `dreamboard project status --commit <rev> [--wait]`
68
+ Read server state for one exact commit.
69
+ - `dreamboard verify --commit <rev>`
70
+ Verify one exact commit from a detached worktree.
71
+ - `dreamboard test`
72
+ Regenerate derived test artifacts as needed and run offline reducer tests.
73
+ - `dreamboard dev [--from-scenario <id>]`
74
+ Start the local dev host for browser validation.
69
75
 
70
76
  Quick rule:
71
77
 
72
- - edited files locally: `dreamboard sync` then `dreamboard compile`
73
- - compile failed but you have not edited files since: `dreamboard compile` again
74
- - remote authored head moved: `dreamboard pull` first
78
+ - edited files locally: commit them with Git and push the branch
79
+ - need server readback: `dreamboard project status --commit <rev> --wait`
80
+ - need exact local proof: `dreamboard verify --commit <rev>`
81
+ - need scenario proof: `dreamboard test`
75
82
 
76
83
  ## Workflow
77
84
 
@@ -79,23 +86,44 @@ Use this order by default:
79
86
 
80
87
  1. Write or revise `rule.md`.
81
88
  2. Align `manifest.json` to the rules.
82
- 3. Run `dreamboard sync`.
83
- 4. Implement reducer state, phases, actions, and views in `app/`.
84
- 5. Implement the playable UI in `ui/App.tsx`.
85
- 6. Run `dreamboard compile`.
86
- 7. Generate test artifacts with `dreamboard test generate`.
87
- 8. Run scenarios with `dreamboard test run`.
88
- 9. Validate the local runtime with `dreamboard run`.
89
+ 3. Implement reducer state, phases, actions, and views in `app/`.
90
+ 4. Implement the playable UI in `ui/App.tsx`.
91
+ 5. Commit and push authored changes with Git.
92
+ 6. Run `dreamboard project status --commit HEAD --wait`.
93
+ 7. Run `dreamboard verify --commit HEAD`.
94
+ 8. Run scenarios with `dreamboard test`.
95
+ 9. Validate the local runtime with `dreamboard dev`.
89
96
  10. For agent-built games, run `dreamboard dev` and verify the browser UI before handoff. Use Playwright to open the dev host, check that the plugin iframe renders without console errors, and click a primary interaction such as selecting a playable hand card. Reducer scenarios alone are not enough when the game has an interactive UI.
90
97
 
98
+ ## Orchestrated Cursor Cloud Jobs
99
+
100
+ When `AGENTS.md` says the workspace is an orchestrated Dreamboard build job,
101
+ follow that local contract instead of the human-authenticated server-readback
102
+ workflow:
103
+
104
+ 1. Implement the requested game changes.
105
+ 2. Create a branch whose name includes the Dreamboard job id, and open one pull request targeting the default branch.
106
+ 3. Run local `dreamboard test --json` and `dreamboard verify --commit HEAD --json` only as advisory checks when available.
107
+ 4. Commit the finished changes.
108
+ 5. Push only the pull-request branch and keep the pull request open.
109
+
110
+ If the runner continues the same thread with verifier diagnostics, make a
111
+ focused correction on the same pull request, create a new commit, and push the
112
+ updated branch again.
113
+
114
+ Do not run authenticated project status, preview, release, or accept commands
115
+ from an orchestrated build job. Do not edit `.github/**` or
116
+ `.dreamboard/control/**`. Authoritative verification runs after the Cursor job
117
+ finishes, and the runner and backend report final build and preview state after
118
+ the pull request passes independent verification.
119
+
91
120
  ## Guardrails
92
121
 
93
122
  - `manifest.json` and `rule.md` are the source of truth for scaffolding.
94
- - Run `dreamboard sync` after authored changes to keep generated files and the remote authored head in sync.
95
- - `dreamboard pull` reconciles authored divergence into the current workspace.
96
- - `dreamboard compile` is separate from authored sync; failed compiles do not mean the workspace needs a pull or another sync unless you changed authored files again.
97
- - Use `dreamboard status` to distinguish authored sync from compile health before deciding whether the next command should be `sync`, `compile`, or `pull`.
98
- - Re-run `dreamboard test generate` after runtime-shape changes in `manifest.json` or `app/`.
123
+ - Use Git for source-control state transitions; Dreamboard does not stage,
124
+ commit, pull, merge, branch, or push for you.
125
+ - Use `dreamboard project status --commit <rev>` for server state, not local Git status.
126
+ - Run `dreamboard test` after runtime-shape changes in `manifest.json` or `app/`; it regenerates derived test artifacts automatically.
99
127
  - Keep reducer-owned UI data in views; do not reintroduce the old `shared/ui-args.ts` pattern in new scaffolds.
100
128
  - When a game exposes clickable hands, markets, boards, or prompts, prove the same interaction works through `dreamboard dev` in a browser. A direct scenario submission can pass even when the rendered surface does not collect the input.
101
129
  - For interactive card hands, render generated surfaces such as `handSurface.Hand` and `handSurface.Card` consistently. Do not swap a surface card for a raw `Card` or custom tile based on `me.canAct`; the surface primitive is responsible for disabling unavailable interactions.
@@ -124,7 +152,3 @@ Do not edit generated or framework-owned files such as:
124
152
 
125
153
  ## Offical Documentation
126
154
  Visit https://dreamboard.games/docs
127
-
128
- ## Framework Feedback
129
-
130
- Use `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.
@@ -0,0 +1,510 @@
1
+ <!-- Generated by apps/dreamboard-cli/scripts/sync-skill-docs.ts. -->
2
+ <!-- Source: docs/tutorials/building-your-first-game.mdx -->
3
+
4
+ # Building your first game
5
+
6
+ Tutorial: build a complete Dreamboard game from rule to tests.
7
+
8
+ This tutorial walks through a deliberately small Dreamboard game so you can see
9
+ the whole authoring loop once: rules, manifest, reducer, UI, and tests.
10
+
11
+ The example game is `Race to Ten`.
12
+
13
+ - 2 players
14
+ - one shared d6
15
+ - each turn, the active player rolls the die and adds the result to their score
16
+ - the first player to reach 10 points wins
17
+
18
+ For tutorial stability, the main walkthrough uses a deterministic die sequence
19
+ `1, 2, 3, 4, 5, 6, ...` instead of true randomness. The important part is how a
20
+ die component, a `rollDie` action, reducer state, UI, and tests fit together.
21
+ This is also consistent with the reducer model: reducers stay pure, and
22
+ runtime-owned effects handle live randomness.
23
+
24
+ ## What you will build
25
+
26
+ By the end of the tutorial you will have:
27
+
28
+ - a `rule.md` that explains the game
29
+ - a `manifest.json` with a shared die
30
+ - reducer code in `app/`
31
+ - a playable `ui/App.tsx`
32
+ - a base and scenario under `test/`
33
+
34
+ ## Prerequisites
35
+
36
+ - Dreamboard installed: `npm install -g dreamboard`
37
+ - authenticated with `dreamboard auth login`
38
+
39
+ ## 1. Create the workspace
40
+
41
+ ```bash
42
+ dreamboard project create race-to-ten --description "A tiny scoring game with one shared die"
43
+ cd race-to-ten
44
+ ```
45
+
46
+ The scaffold gives you the files you will edit next:
47
+
48
+ - `rule.md`
49
+ - `manifest.json`
50
+ - `app/game-contract.ts`
51
+ - `app/game.ts`
52
+ - `app/phases/*`
53
+ - `ui/App.tsx`
54
+ - `test/bases/*`
55
+ - `test/scenarios/*`
56
+
57
+ ## 2. Write `rule.md`
58
+
59
+ ```md
60
+ # Race to Ten
61
+
62
+ ## Overview
63
+
64
+ - Players: 2
65
+ - Objective: be the first player to reach 10 points
66
+ - Duration: 3 to 5 minutes
67
+
68
+ ## Components
69
+
70
+ - 1 shared six-sided die
71
+ - visible score totals for each player
72
+ - no hidden information
73
+
74
+ ## Setup
75
+
76
+ 1. Seat two players.
77
+ 2. Set both scores to 0.
78
+ 3. Clear the die value.
79
+ 4. Player 1 takes the first turn.
80
+
81
+ ## Gameplay
82
+
83
+ ### Phase 1: takeTurn
84
+
85
+ - Acting player: the current player only
86
+ - Allowed actions: `rollDie`
87
+ - Validation: only the active player may act, and no actions are legal after a winner exists
88
+ - Completion:
89
+ - `rollDie` sets the shared die to a new value
90
+ - the acting player adds that value to their score
91
+ - if the acting player reaches 10 points, the game ends immediately
92
+ - otherwise the turn passes to the other player
93
+
94
+ ## Scoring and progression
95
+
96
+ - `rollDie` increases the acting player's score by the rolled value
97
+
98
+ ## Winning conditions
99
+
100
+ - End trigger: a player reaches 10 points
101
+ - Winner determination: the player who reached 10 points wins
102
+ - Tie-breaker: not applicable because turns resolve one at a time
103
+
104
+ ## Special rules and edge cases
105
+
106
+ - Actions after game end are illegal
107
+ - Out-of-turn actions are illegal
108
+ - For this tutorial implementation, the die value cycles deterministically from 1 to 6 so the example stays reproducible
109
+ ```
110
+
111
+ For a fuller reference, see [Rule authoring](./rule-authoring.md).
112
+
113
+ ## 3. Write `manifest.json`
114
+
115
+ This game needs player-count metadata and one shared die.
116
+
117
+ ```json
118
+ {
119
+ "players": {
120
+ "minPlayers": 2,
121
+ "maxPlayers": 2,
122
+ "optimalPlayers": 2
123
+ },
124
+ "dieTypes": [
125
+ {
126
+ "id": "standard-d6",
127
+ "name": "Standard d6",
128
+ "sides": 6
129
+ }
130
+ ],
131
+ "dieSeeds": [
132
+ {
133
+ "id": "turn-die",
134
+ "name": "Turn die",
135
+ "typeId": "standard-d6"
136
+ }
137
+ ]
138
+ }
139
+ ```
140
+
141
+ Commit and push the authored source with Git, then wait for the server to
142
+ observe and verify the exact commit:
143
+
144
+ ```bash
145
+ git add .
146
+ git commit -m "Build Race to Ten"
147
+ git push -u origin main
148
+ dreamboard project status --commit HEAD --wait
149
+ dreamboard verify --commit HEAD
150
+ ```
151
+
152
+ ## 4. Define the reducer contract
153
+
154
+ Open `app/game-contract.ts` and replace the empty schemas with the state the
155
+ rules require.
156
+
157
+ ```ts
158
+ import { z } from "zod";
159
+ import * as manifestContract from "../shared/manifest-contract";
160
+ import { defineGameContract, type GameStateOf } from "@dreamboard/app-sdk/reducer";
161
+
162
+ const playerId = manifestContract.ids.playerId;
163
+
164
+ const publicStateSchema = z.object({
165
+ currentPlayerId: playerId,
166
+ winnerPlayerId: playerId.nullable(),
167
+ lastRoll: z.number().int().min(1).max(6).nullable(),
168
+ scores: z.record(playerId, z.number().int().nonnegative()),
169
+ });
170
+
171
+ const privateStateSchema = z.object({});
172
+
173
+ const hiddenStateSchema = z.object({
174
+ rollCount: z.number().int().nonnegative(),
175
+ });
176
+
177
+ export const gameContract = defineGameContract({
178
+ manifest: manifestContract.manifestContract,
179
+ state: {
180
+ public: publicStateSchema,
181
+ private: privateStateSchema,
182
+ hidden: hiddenStateSchema,
183
+ initial: {
184
+ public: ({ playerIds }) => ({
185
+ currentPlayerId: playerIds[0],
186
+ winnerPlayerId: null,
187
+ lastRoll: null,
188
+ scores: Object.fromEntries(playerIds.map((playerId) => [playerId, 0])),
189
+ }),
190
+ private: () => ({}),
191
+ hidden: () => ({
192
+ rollCount: 0,
193
+ }),
194
+ },
195
+ },
196
+ });
197
+
198
+ export type GameContract = typeof gameContract;
199
+ export type GameState = GameStateOf<GameContract>;
200
+ ```
201
+
202
+ The public state holds player-visible game progress. The hidden state tracks the
203
+ deterministic roll index so the test can stay reproducible.
204
+
205
+ ## 5. Add a player view
206
+
207
+ Create `app/player-view.ts`:
208
+
209
+ ```ts
210
+ import { z } from "zod";
211
+ import { defineView } from "@dreamboard/app-sdk/reducer";
212
+ import type { GameContract } from "./game-contract";
213
+
214
+ export const playerView = defineView<GameContract>()({
215
+ schema: z.object({
216
+ currentPlayerId: z.string(),
217
+ winnerPlayerId: z.string().nullable(),
218
+ lastRoll: z.number().nullable(),
219
+ scores: z.record(z.string(), z.number()),
220
+ isMyTurn: z.boolean(),
221
+ targetScore: z.number(),
222
+ turnDieId: z.string(),
223
+ }),
224
+ project({ state, playerId }) {
225
+ return {
226
+ currentPlayerId: state.publicState.currentPlayerId,
227
+ winnerPlayerId: state.publicState.winnerPlayerId,
228
+ lastRoll: state.publicState.lastRoll,
229
+ scores: state.publicState.scores,
230
+ isMyTurn: state.publicState.currentPlayerId === playerId,
231
+ targetScore: 10,
232
+ turnDieId: "turn-die",
233
+ };
234
+ },
235
+ });
236
+ ```
237
+
238
+ The UI will read this view instead of reconstructing game logic in React.
239
+
240
+ ## 6. Implement the phase and action
241
+
242
+ Create `app/phases/take-turn.ts`:
243
+
244
+ ```ts
245
+ import { z } from "zod";
246
+ import { defineAction, definePhase, setActivePlayers } from "@dreamboard/app-sdk/reducer";
247
+ import type { GameContract } from "../game-contract";
248
+
249
+ const TARGET_SCORE = 10;
250
+ const TURN_DIE_ID = "turn-die" as const;
251
+
252
+ const rollDie = defineAction<GameContract>()({
253
+ params: z.object({}),
254
+ validate({ state, input }) {
255
+ if (state.publicState.winnerPlayerId) {
256
+ return {
257
+ errorCode: "GAME_ALREADY_ENDED",
258
+ message: "The game has already ended.",
259
+ };
260
+ }
261
+
262
+ if (input.playerId !== state.publicState.currentPlayerId) {
263
+ return {
264
+ errorCode: "NOT_YOUR_TURN",
265
+ message: "It is not your turn.",
266
+ };
267
+ }
268
+
269
+ return null;
270
+ },
271
+ reduce({ state, input, accept }) {
272
+ const nextRoll = (state.hiddenState.rollCount % 6) + 1;
273
+ const nextScore = state.publicState.scores[input.playerId] + nextRoll;
274
+ const nextScores = {
275
+ ...state.publicState.scores,
276
+ [input.playerId]: nextScore,
277
+ };
278
+
279
+ const nextPlayerId =
280
+ input.playerId === "player-1" ? "player-2" : "player-1";
281
+
282
+ const nextTable = {
283
+ ...state.table,
284
+ dice: {
285
+ ...state.table.dice,
286
+ [TURN_DIE_ID]: {
287
+ ...state.table.dice[TURN_DIE_ID],
288
+ value: nextRoll,
289
+ },
290
+ },
291
+ };
292
+
293
+ return accept({
294
+ ...state,
295
+ table: nextTable,
296
+ publicState: {
297
+ ...state.publicState,
298
+ currentPlayerId: nextScore >= TARGET_SCORE ? input.playerId : nextPlayerId,
299
+ winnerPlayerId: nextScore >= TARGET_SCORE ? input.playerId : null,
300
+ lastRoll: nextRoll,
301
+ scores: nextScores,
302
+ },
303
+ hiddenState: {
304
+ ...state.hiddenState,
305
+ rollCount: state.hiddenState.rollCount + 1,
306
+ },
307
+ });
308
+ },
309
+ });
310
+
311
+ export const takeTurn = definePhase<GameContract>()({
312
+ kind: "player",
313
+ state: z.object({}),
314
+ initialState: () => ({}),
315
+ enter({ state, accept }) {
316
+ if (state.publicState.winnerPlayerId) {
317
+ return accept(state);
318
+ }
319
+
320
+ return accept(
321
+ setActivePlayers(state, [state.publicState.currentPlayerId]),
322
+ );
323
+ },
324
+ actions: {
325
+ rollDie,
326
+ },
327
+ });
328
+ ```
329
+
330
+ Then update `app/phases/index.ts`:
331
+
332
+ ```ts
333
+ import { takeTurn } from "./take-turn";
334
+
335
+ export const phases = {
336
+ takeTurn,
337
+ };
338
+ ```
339
+
340
+ ## 7. Register the game
341
+
342
+ Update `app/game.ts`:
343
+
344
+ ```ts
345
+ import { defineGame } from "@dreamboard/app-sdk/reducer";
346
+ import { gameContract } from "./game-contract";
347
+ import { phases } from "./phases";
348
+ import { playerView } from "./player-view";
349
+
350
+ export default defineGame({
351
+ contract: gameContract,
352
+ initialPhase: "takeTurn",
353
+ phases,
354
+ views: {
355
+ player: playerView,
356
+ },
357
+ });
358
+ ```
359
+
360
+ At this point the rules exist, but the UI is still the scaffold placeholder.
361
+
362
+ ## 8. Build the UI
363
+
364
+ Update `ui/App.tsx`:
365
+
366
+ ```tsx
367
+ import { DiceRoller, useActions, useDice, useGameView } from "@dreamboard/ui-sdk";
368
+
369
+ export default function App() {
370
+ const view = useGameView();
371
+ const phase = useActions();
372
+ const dice = useDice(["turn-die"]);
373
+
374
+ const rollDie = async () => {
375
+ if (phase.phase !== "takeTurn") {
376
+ return;
377
+ }
378
+
379
+ await phase.dispatch(phase.commands.rollDie());
380
+ };
381
+
382
+ return (
383
+ <main style={{ padding: 24, fontFamily: "system-ui, sans-serif" }}>
384
+ <h1>Race to Ten</h1>
385
+ <p>First to {view.targetScore} points wins.</p>
386
+
387
+ <DiceRoller
388
+ values={dice.values}
389
+ diceCount={1}
390
+ render={({ values }) => (
391
+ <div style={{ fontSize: 32, marginBottom: 16 }}>
392
+ Die: {values?.[0] ?? "?"}
393
+ </div>
394
+ )}
395
+ />
396
+
397
+ <ul>
398
+ {Object.entries(view.scores).map(([playerId, score]) => (
399
+ <li key={playerId}>
400
+ {playerId}: {score}
401
+ {view.currentPlayerId === playerId ? " <- current player" : ""}
402
+ </li>
403
+ ))}
404
+ </ul>
405
+
406
+ <p>Last roll: {view.lastRoll ?? "not rolled yet"}</p>
407
+
408
+ {view.winnerPlayerId ? (
409
+ <p>Winner: {view.winnerPlayerId}</p>
410
+ ) : (
411
+ <button onClick={() => void rollDie()} disabled={!view.isMyTurn}>
412
+ Roll die
413
+ </button>
414
+ )}
415
+ </main>
416
+ );
417
+ }
418
+ ```
419
+
420
+ The important part is that:
421
+
422
+ - the die exists in authored game structure
423
+ - the reducer updates the die value and the score together
424
+ - the UI reads projected view data and the die state from the runtime
425
+ - actions still go through `useActions()`
426
+
427
+ ## 9. Add a base and a scenario
428
+
429
+ Replace `test/bases/initial-turn.base.ts`:
430
+
431
+ ```ts
432
+ import { defineBase } from "../testing-types";
433
+
434
+ export default defineBase({
435
+ id: "initial-turn",
436
+ seed: 1337,
437
+ players: 2,
438
+ setup: async ({ game }) => {
439
+ await game.start();
440
+ },
441
+ });
442
+ ```
443
+
444
+ Create `test/scenarios/player-two-wins.scenario.ts`:
445
+
446
+ ```ts
447
+ import { defineScenario } from "../testing-types";
448
+
449
+ export default defineScenario({
450
+ id: "player-two-wins",
451
+ description: "The deterministic roll sequence lets player 2 reach ten first",
452
+ from: "initial-turn",
453
+ when: async ({ game }) => {
454
+ await game.action("player-1", "rollDie", {});
455
+ await game.action("player-2", "rollDie", {});
456
+ await game.action("player-1", "rollDie", {});
457
+ await game.action("player-2", "rollDie", {});
458
+ await game.action("player-1", "rollDie", {});
459
+ await game.action("player-2", "rollDie", {});
460
+ },
461
+ then: ({ publicState, view, expect, history, phase }) => {
462
+ const state = publicState();
463
+
464
+ expect(phase()).toBe("takeTurn");
465
+ expect(state.lastRoll).toBe(6);
466
+ expect(state.scores["player-1"]).toBe(9);
467
+ expect(state.scores["player-2"]).toBe(12);
468
+ expect(state.winnerPlayerId).toBe("player-2");
469
+ expect(view("player-2").winnerPlayerId).toBe("player-2");
470
+ expect(history().accepted().length).toBe(6);
471
+ },
472
+ });
473
+ ```
474
+
475
+ Run the test suite:
476
+
477
+ ```bash
478
+ dreamboard test
479
+ ```
480
+
481
+ ## 10. Run the game locally
482
+
483
+ Use the local dev host to verify the same flow manually:
484
+
485
+ ```bash
486
+ dreamboard dev
487
+ ```
488
+
489
+ If you edit `rule.md` or `manifest.json`, commit and push the changes before
490
+ running commit-scoped build, preview, release, or remote test commands.
491
+
492
+ ## Where to go next
493
+
494
+ This tutorial uses the smallest die-based loop that still touches authored
495
+ components, reducer state, UI, and tests. The next layer of depth is:
496
+
497
+ - add setup profiles or setup options
498
+ - add richer reducer state and more than one phase
499
+ - replace the plain button UI with grouped action panels and richer views
500
+ - add rejection-path tests such as out-of-turn actions
501
+
502
+ If you need live randomness in authored reducer logic, do not call
503
+ `Math.random()` inside reducers. Use runtime-owned effects instead:
504
+
505
+ - `effects.rollDie(...)` when the runtime only needs to update an authored die
506
+ - `effects.randomInt(...)` when reducer logic needs the sampled value back
507
+
508
+ The main walkthrough stays deterministic so the rule, reducer, UI, and test
509
+ snippets all line up exactly. See [Reducer](./reducer.md) for the
510
+ runtime-random versions.