@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 +5 -1
- package/skills/dreamboard/SKILL.md +54 -30
- package/skills/dreamboard/references/building-your-first-game.md +510 -0
- package/skills/dreamboard/references/cli.md +104 -0
- package/skills/dreamboard/references/game-interface.md +548 -0
- package/skills/dreamboard/references/manifest-authoring.md +605 -0
- package/skills/dreamboard/references/quickstart.md +69 -0
- package/skills/dreamboard/references/reducer.md +864 -0
- package/skills/dreamboard/references/rule-authoring.md +147 -0
- package/skills/dreamboard/references/testing.md +242 -0
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dreamboard-games/agent-skills",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
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
|
|
9
|
+
# Dreamboard
|
|
10
10
|
|
|
11
11
|
## Goal
|
|
12
12
|
|
|
13
|
-
Create and iterate on a Dreamboard game locally
|
|
14
|
-
|
|
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
|
|
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
|
|
64
|
-
|
|
65
|
-
- `dreamboard
|
|
66
|
-
|
|
67
|
-
- `dreamboard
|
|
68
|
-
|
|
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:
|
|
73
|
-
-
|
|
74
|
-
-
|
|
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.
|
|
83
|
-
4. Implement
|
|
84
|
-
5.
|
|
85
|
-
6. Run `dreamboard
|
|
86
|
-
7.
|
|
87
|
-
8. Run scenarios with `dreamboard test
|
|
88
|
-
9. Validate the local runtime with `dreamboard
|
|
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
|
-
-
|
|
95
|
-
|
|
96
|
-
- `dreamboard
|
|
97
|
-
-
|
|
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.
|