dreamboard 0.1.7 → 0.1.8
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/LICENSE +89 -0
- package/NOTICE +1 -0
- package/README.md +22 -178
- package/dist/{chunk-MP7IBNWW.js → chunk-BMYC6772.js} +577 -38
- package/dist/chunk-BMYC6772.js.map +1 -0
- package/dist/chunk-D4HDZEJT.js +37 -0
- package/dist/chunk-EM7NOFFC.js +19072 -0
- package/dist/chunk-EM7NOFFC.js.map +1 -0
- package/dist/chunk-SET56RVA.js +435 -0
- package/dist/chunk-SET56RVA.js.map +1 -0
- package/dist/{dist-B3R64F6G.js → dist-WJRJNFLI.js} +3 -3
- package/dist/global-config-VQJZ3JOA.js +15 -0
- package/dist/global-config-VQJZ3JOA.js.map +1 -0
- package/dist/index.js +23993 -24387
- package/dist/index.js.map +1 -1
- package/dist/keychain-backend-2DL3C6BS.js +125 -0
- package/dist/keychain-backend-2DL3C6BS.js.map +1 -0
- package/dist/{prompt-GMZABCJC.js → prompt-IV67HSPU.js} +2 -2
- package/dist/scaffold/assets/static/app/tsconfig.framework.json +24 -0
- package/dist/scaffold/assets/static/app/tsconfig.json +3 -0
- package/dist/scaffold/assets/static/ui/App.tsx +22 -0
- package/dist/scaffold/assets/static/ui/index.tsx +12 -0
- package/dist/scaffold/assets/static/ui/style.css +4 -0
- package/dist/scaffold/assets/static/ui/tsconfig.framework.json +31 -0
- package/dist/scaffold/assets/static/ui/tsconfig.json +5 -0
- package/dist/src-K2NK3A4P.js +626 -0
- package/dist/src-K2NK3A4P.js.map +1 -0
- package/package.json +41 -4
- package/skills/dreamboard/SKILL.md +126 -59
- package/skills/dreamboard/references/authoring-lifecycle.md +102 -0
- package/skills/dreamboard/references/board-surfaces.md +36 -0
- package/skills/dreamboard/references/board-topology.md +443 -0
- package/skills/dreamboard/references/boards-and-topology.md +100 -0
- package/skills/dreamboard/references/card-actions.md +107 -0
- package/skills/dreamboard/references/cli.md +120 -0
- package/skills/dreamboard/references/core-concepts.md +514 -0
- package/skills/dreamboard/references/custom-renderers.md +26 -0
- package/skills/dreamboard/references/derived-values.md +55 -0
- package/skills/dreamboard/references/effects.md +111 -0
- package/skills/dreamboard/references/game-contract.md +89 -0
- package/skills/dreamboard/references/game-definition.md +89 -0
- package/skills/dreamboard/references/game-shell.md +80 -0
- package/skills/dreamboard/references/hand-surfaces.md +33 -0
- package/skills/dreamboard/references/index.md +112 -0
- package/skills/dreamboard/references/inputs-and-targets.md +160 -0
- package/skills/dreamboard/references/interactions.md +158 -0
- package/skills/dreamboard/references/manifest-fields.md +137 -0
- package/skills/dreamboard/references/manifest.md +81 -0
- package/skills/dreamboard/references/package-surfaces.md +69 -0
- package/skills/dreamboard/references/phases.md +145 -0
- package/skills/dreamboard/references/prompts-and-choices.md +55 -0
- package/skills/dreamboard/references/quickstart.md +71 -0
- package/skills/dreamboard/references/rule-authoring.md +81 -77
- package/skills/dreamboard/references/setup-bootstrap.md +78 -0
- package/skills/dreamboard/references/stages-and-zones.md +59 -0
- package/skills/dreamboard/references/static-views.md +67 -0
- package/skills/dreamboard/references/table-queries-and-ops.md +103 -0
- package/skills/dreamboard/references/testing-bases.md +147 -0
- package/skills/dreamboard/references/testing-generated-contracts.md +142 -0
- package/skills/dreamboard/references/testing-runtime-assertions.md +124 -0
- package/skills/dreamboard/references/testing-scenarios.md +148 -0
- package/skills/dreamboard/references/testing-ui-tests.md +174 -0
- package/skills/dreamboard/references/testing.md +161 -0
- package/skills/dreamboard/references/ui-architecture.md +137 -0
- package/skills/dreamboard/references/ui-components.md +34 -0
- package/skills/dreamboard/references/views.md +72 -0
- package/skills/dreamboard/references/workspace-layout.md +136 -0
- package/dist/chunk-2H7UOFLK.js +0 -11
- package/dist/chunk-MP7IBNWW.js.map +0 -1
- package/dist/chunk-WM7TY7DV.js +0 -2328
- package/dist/chunk-WM7TY7DV.js.map +0 -1
- package/dist/embedded-harness-IHP7A2ZX.js +0 -344
- package/dist/embedded-harness-IHP7A2ZX.js.map +0 -1
- package/skills/dreamboard/references/adversarial-testing.md +0 -113
- package/skills/dreamboard/references/all-players-tracking.md +0 -75
- package/skills/dreamboard/references/api-reference.md +0 -197
- package/skills/dreamboard/references/app-best-practices.md +0 -98
- package/skills/dreamboard/references/building-your-first-game.md +0 -49
- package/skills/dreamboard/references/hands-vs-decks.md +0 -124
- package/skills/dreamboard/references/manifest-authoring.md +0 -328
- package/skills/dreamboard/references/phase-handlers.md +0 -134
- package/skills/dreamboard/references/scenario-format.md +0 -99
- package/skills/dreamboard/references/test-harness.md +0 -225
- package/skills/dreamboard/references/ui-best-practices.md +0 -156
- package/skills/dreamboard/references/ui-genre-resource-management.md +0 -187
- package/skills/dreamboard/references/ui-genre-trick-taking.md +0 -110
- package/skills/dreamboard/references/ui-genre-worker-placement.md +0 -143
- package/skills/dreamboard/references/ui-style-guide.md +0 -54
- package/skills/dreamboard/scripts/events-extract.mjs +0 -218
- /package/dist/{chunk-2H7UOFLK.js.map → chunk-D4HDZEJT.js.map} +0 -0
- /package/dist/{dist-B3R64F6G.js.map → dist-WJRJNFLI.js.map} +0 -0
- /package/dist/{prompt-GMZABCJC.js.map → prompt-IV67HSPU.js.map} +0 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
# Board topology
|
|
2
|
+
|
|
3
|
+
Author hex and square boards with shared reducer and UI topology primitives.
|
|
4
|
+
|
|
5
|
+
Use tiled boards when your game has stable spaces plus meaningful borders or
|
|
6
|
+
corners.
|
|
7
|
+
|
|
8
|
+
Dreamboard currently supports two fixed-topology tiled layouts:
|
|
9
|
+
|
|
10
|
+
- `layout: "hex"` for axial hex maps
|
|
11
|
+
- `layout: "square"` for row and column grids
|
|
12
|
+
|
|
13
|
+
Keep `layout: "generic"` for authored graphs that are not tile-based, such as
|
|
14
|
+
tracks, networks, or map nodes.
|
|
15
|
+
|
|
16
|
+
It is normal for one manifest to mix these layouts. For example, a game can use
|
|
17
|
+
one shared generic market board, one shared hex frontier map, and one
|
|
18
|
+
per-player square mat without splitting that coverage across multiple
|
|
19
|
+
workspaces.
|
|
20
|
+
|
|
21
|
+
The generated manifest contract now preserves that mixed topology directly with
|
|
22
|
+
template-level literals plus board-scoped edge and vertex helpers:
|
|
23
|
+
|
|
24
|
+
{/* Generated from examples/board-contract-lab/app/authoring-benchmark-typing-smoke.ts (tiled-board-helpers) */}
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
const playerMatId = boardHelpers.boardIdForPlayer("player-mat", "player-1");
|
|
28
|
+
const hexBoardIds = boardHelpers.boardIdsForLayout("hex");
|
|
29
|
+
const frontierRouteId = boardHelpers.edgeIds("frontier-map", "ferry-route")[0]!;
|
|
30
|
+
const frontierCheckpointId = boardHelpers.vertexIds(
|
|
31
|
+
"frontier-map",
|
|
32
|
+
"checkpoint",
|
|
33
|
+
)[0]!;
|
|
34
|
+
const marketTemplateBoards =
|
|
35
|
+
boardHelpers.boardBaseIdsForTemplate("market-template");
|
|
36
|
+
const playerMatTemplateLayout = boardHelpers.boardTemplateLayout(
|
|
37
|
+
"player-mat-template",
|
|
38
|
+
);
|
|
39
|
+
//
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## What lives where
|
|
43
|
+
|
|
44
|
+
Put immutable topology in the manifest:
|
|
45
|
+
|
|
46
|
+
- spaces and their coordinates
|
|
47
|
+
- optional authored edge and vertex metadata
|
|
48
|
+
- authored containers and extra relations
|
|
49
|
+
|
|
50
|
+
Put mutable gameplay state in reducer state:
|
|
51
|
+
|
|
52
|
+
- ownership keyed by `spaceId`, `edgeId`, or `vertexId`
|
|
53
|
+
- placed tiles, markers, units, or hazards
|
|
54
|
+
- highlights, legal targets, and temporary UI state
|
|
55
|
+
|
|
56
|
+
That split keeps the board reusable across different games and avoids treating
|
|
57
|
+
manifest topology as a mutable gameplay store.
|
|
58
|
+
|
|
59
|
+
## Fixed-topology constraint
|
|
60
|
+
|
|
61
|
+
Hex and square boards in this release are fixed once the session starts.
|
|
62
|
+
|
|
63
|
+
- Terraforming Mars fits this model: spaces stay fixed while ownership and tile
|
|
64
|
+
state change.
|
|
65
|
+
- Gloomhaven fits this model: spaces stay fixed while units move and range is
|
|
66
|
+
recalculated.
|
|
67
|
+
- Cascadia fits this model when the habitat map is pre-authored and scoring
|
|
68
|
+
reads adjacency from the fixed board.
|
|
69
|
+
|
|
70
|
+
Carcassonne-style board growth is not part of this release. Treat that as a
|
|
71
|
+
future topology adapter rather than part of the current `square` layout.
|
|
72
|
+
|
|
73
|
+
## Shared topology vocabulary
|
|
74
|
+
|
|
75
|
+
Reducer helpers and UI hooks use the same terms:
|
|
76
|
+
|
|
77
|
+
| Term | Meaning |
|
|
78
|
+
| ----------- | ----------------------------------------------------- |
|
|
79
|
+
| `space` | One tile or cell on the board |
|
|
80
|
+
| `edge` | One border shared by one or two spaces |
|
|
81
|
+
| `vertex` | One corner touched by one to four spaces |
|
|
82
|
+
| `adjacent` | The default movement or scoring graph for a layout |
|
|
83
|
+
| `incident` | Edges touching a vertex, or vertices touching an edge |
|
|
84
|
+
| `occupancy` | Components currently on a space, edge, or vertex |
|
|
85
|
+
|
|
86
|
+
Reducer-side board helpers are covered in [Table queries and ops](./table-queries-and-ops.md).
|
|
87
|
+
UI-side board rendering is covered in [Board targets](./board-surfaces.md)
|
|
88
|
+
and [UI components](./ui-components.md).
|
|
89
|
+
|
|
90
|
+
## Choosing a layout
|
|
91
|
+
|
|
92
|
+
| Layout | Use it for | Default adjacency |
|
|
93
|
+
| --------- | ----------------------------------------------------- | ---------------------------------- |
|
|
94
|
+
| `generic` | Tracks, networks, arbitrary relation graphs | Whatever you author in `relations` |
|
|
95
|
+
| `hex` | Terraforming Mars, Gloomhaven, Cascadia, wargame maps | Derived hex adjacency |
|
|
96
|
+
| `square` | Chess, Checkers, Onitama, fixed tile grids | Orthogonal 4-way adjacency |
|
|
97
|
+
|
|
98
|
+
Square boards can still use diagonal logic. The reducer and UI helpers expose
|
|
99
|
+
square-specific variants for diagonal neighbors and Chebyshev distance.
|
|
100
|
+
|
|
101
|
+
## Reducer state pattern
|
|
102
|
+
|
|
103
|
+
Store dynamic tiled state in plain overlay maps keyed by stable location IDs.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { z } from "zod";
|
|
107
|
+
import type {
|
|
108
|
+
TiledEdgeMap,
|
|
109
|
+
TiledSpaceMap,
|
|
110
|
+
TiledVertexMap,
|
|
111
|
+
} from "@dreamboard/app-sdk/reducer";
|
|
112
|
+
import { ids } from "../shared/manifest-contract";
|
|
113
|
+
|
|
114
|
+
const publicState = z.object({
|
|
115
|
+
ownershipBySpaceId: z.partialRecord(ids.spaceId, ids.playerId).default({}),
|
|
116
|
+
effectByEdgeId: z.partialRecord(ids.edgeId, z.string()).default({}),
|
|
117
|
+
markerByVertexId: z
|
|
118
|
+
.partialRecord(
|
|
119
|
+
ids.vertexId,
|
|
120
|
+
z.object({
|
|
121
|
+
ownerId: ids.playerId,
|
|
122
|
+
kind: z.string(),
|
|
123
|
+
}),
|
|
124
|
+
)
|
|
125
|
+
.default({}),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
type OwnershipBySpaceId = TiledSpaceMap<
|
|
129
|
+
typeof manifestContract.table,
|
|
130
|
+
"mars",
|
|
131
|
+
string
|
|
132
|
+
>;
|
|
133
|
+
type EffectByEdgeId = TiledEdgeMap<
|
|
134
|
+
typeof manifestContract.table,
|
|
135
|
+
"arena",
|
|
136
|
+
string
|
|
137
|
+
>;
|
|
138
|
+
type MarkerByVertexId = TiledVertexMap<
|
|
139
|
+
typeof manifestContract.table,
|
|
140
|
+
"arena",
|
|
141
|
+
{ ownerId: string; kind: string }
|
|
142
|
+
>;
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Use `z.partialRecord(...)` for mutable tiled overlays. Runtime gameplay state is
|
|
146
|
+
usually sparse, so empty-object initialization should compile and missing keys
|
|
147
|
+
should mean "nothing placed here yet".
|
|
148
|
+
|
|
149
|
+
Use this pattern for:
|
|
150
|
+
|
|
151
|
+
- Terraforming Mars: `ownershipBySpaceId`, placed tile type, adjacency-bonus flags
|
|
152
|
+
- Gloomhaven: movement blockers by edge, trap markers by vertex, units keyed by
|
|
153
|
+
space
|
|
154
|
+
- Cascadia: adjacency-driven scoring keyed by placed habitat or wildlife tiles
|
|
155
|
+
|
|
156
|
+
## Projecting UI-ready boards
|
|
157
|
+
|
|
158
|
+
Reducer views should join immutable topology with mutable overlay state before
|
|
159
|
+
rendering. Destructure the injected `q` inside `project({ state, playerId, q })`
|
|
160
|
+
instead of constructing a fresh query from state.
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { defineView } from "@dreamboard/app-sdk/reducer";
|
|
164
|
+
import type { GameContract } from "./game-contract";
|
|
165
|
+
|
|
166
|
+
export const playerView = defineView<GameContract>()({
|
|
167
|
+
project({ state, q }) {
|
|
168
|
+
const board = q.board.tiled("arena");
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
boardId: board.id,
|
|
172
|
+
layout: board.layout,
|
|
173
|
+
spaces: Object.values(board.spaces).map((space) => ({
|
|
174
|
+
...space,
|
|
175
|
+
ownerId: state.publicState.ownershipBySpaceId[space.id] ?? null,
|
|
176
|
+
edgeIds: q.board.spaceEdges(board.id, space.id),
|
|
177
|
+
vertexIds: q.board.spaceVertices(board.id, space.id),
|
|
178
|
+
})),
|
|
179
|
+
legalEdgeIds: state.publicState.legalEdgeIds,
|
|
180
|
+
legalVertexIds: state.publicState.legalVertexIds,
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Keep canonical legality and scoring in reducer code. The UI should consume a
|
|
187
|
+
projected board view plus simple highlight or legal-target sets.
|
|
188
|
+
|
|
189
|
+
## Board shape and traversal
|
|
190
|
+
|
|
191
|
+
- `board.spaces` is an ID-keyed record.
|
|
192
|
+
- `board.edges` is an array of edge records.
|
|
193
|
+
- `board.vertices` is an array of vertex records.
|
|
194
|
+
|
|
195
|
+
<Warning>
|
|
196
|
+
Do not use `Object.keys(board.edges)` or `Object.keys(board.vertices)` to
|
|
197
|
+
discover runtime IDs. Those expressions return array indexes, not edge or
|
|
198
|
+
vertex IDs.
|
|
199
|
+
</Warning>
|
|
200
|
+
|
|
201
|
+
Use reducer-native query helpers when you need topology traversal:
|
|
202
|
+
|
|
203
|
+
- `q.board.spaceEdges(boardId, spaceId)`
|
|
204
|
+
- `q.board.spaceVertices(boardId, spaceId)`
|
|
205
|
+
- `q.board.incidentEdges(boardId, vertexId)`
|
|
206
|
+
- `q.board.incidentVertices(boardId, edgeId)`
|
|
207
|
+
|
|
208
|
+
`incidentEdges(...)` and `incidentVertices(...)` are geometry helpers. They
|
|
209
|
+
return all tiled edges or vertices that share the same incident spaces as the
|
|
210
|
+
input location. They do not apply gameplay-specific filters such as road
|
|
211
|
+
ownership or blocked-path rules.
|
|
212
|
+
|
|
213
|
+
## Wiring the UI
|
|
214
|
+
|
|
215
|
+
Use one of the layout-specific renderers:
|
|
216
|
+
|
|
217
|
+
- `HexGrid` for hex spaces, edges, and vertices
|
|
218
|
+
- `SquareGrid` for square cells, edges, and vertices
|
|
219
|
+
|
|
220
|
+
Use the headless topology hooks when the UI needs geometry-aware logic:
|
|
221
|
+
|
|
222
|
+
- `useBoardTopology(...)` for shared tiled queries
|
|
223
|
+
- `useHexBoard(...)` for hex-specific coordinate helpers
|
|
224
|
+
- `useSquareBoard(...)` for square-specific neighbor and distance variants
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
import { HexGrid, SquareGrid, useBoardTopology, useSquareBoard } from "@dreamboard/ui-sdk";
|
|
228
|
+
|
|
229
|
+
function ArenaBoard({ board, onEdgeClick, onVertexClick }) {
|
|
230
|
+
const topology = useSquareBoard(board);
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<SquareGrid
|
|
234
|
+
board={board}
|
|
235
|
+
interactiveEdges={{ selectTargetId: onEdgeClick }}
|
|
236
|
+
interactiveVertices={{ selectTargetId: onVertexClick }}
|
|
237
|
+
renderCell={(row, col) => {
|
|
238
|
+
const cell = topology.getCellAt(row, col);
|
|
239
|
+
return <Cell ownerId={cell?.owner ?? null} />;
|
|
240
|
+
}}
|
|
241
|
+
renderPiece={(piece) => <Piece piece={piece} />}
|
|
242
|
+
/>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
This keeps interactivity consistent across layouts. Authors do not need to
|
|
248
|
+
re-derive borders, corners, or adjacency in UI code.
|
|
249
|
+
|
|
250
|
+
Project the whole tiled board record into the reducer view. Then keep
|
|
251
|
+
game-specific legality beside it:
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
project({ state, q }) {
|
|
255
|
+
return {
|
|
256
|
+
board: q.board.tiled("arena"),
|
|
257
|
+
legalEdgeIds: state.publicState.legalEdgeIds,
|
|
258
|
+
legalVertexIds: state.publicState.legalVertexIds,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Use this rule when deciding what to expose:
|
|
264
|
+
|
|
265
|
+
- `spaces` are enough for tile or cell rendering only
|
|
266
|
+
- `edges` are required when the UI should render or interact with borders
|
|
267
|
+
- `vertices` are required when the UI should render or interact with corners
|
|
268
|
+
|
|
269
|
+
For example, a route-building board view should project all three so the UI can
|
|
270
|
+
render paths and markers and also expose placement targets.
|
|
271
|
+
|
|
272
|
+
The important reducer-native detail is that target-layer IDs keep the board's
|
|
273
|
+
exact runtime types. If the reducer view projects typed `edge.id` and
|
|
274
|
+
`vertex.id` values, `board={board}` preserves those same ids through
|
|
275
|
+
`selectTargetId` instead of forcing the UI to remap topology ids by hand.
|
|
276
|
+
|
|
277
|
+
## Runtime IDs
|
|
278
|
+
|
|
279
|
+
Hex and square edge and vertex IDs are derived runtime IDs based on authored
|
|
280
|
+
space topology.
|
|
281
|
+
|
|
282
|
+
- author spaces in the manifest
|
|
283
|
+
- optionally attach authored edge and vertex metadata by `ref.spaces`
|
|
284
|
+
- read the runtime `edgeId` and `vertexId` values from reducer helpers,
|
|
285
|
+
`boardHelpers.edgeIds(...)`, `boardHelpers.vertexIds(...)`,
|
|
286
|
+
`boardHelpers.boardIdForPlayer(...)`, or the projected board view
|
|
287
|
+
- use those IDs as keys in reducer state and as click targets in the UI
|
|
288
|
+
|
|
289
|
+
<Warning>
|
|
290
|
+
Do not hardcode the derived string format in app code. Read the runtime
|
|
291
|
+
`edgeId` and `vertexId` values from the helpers and projected board view
|
|
292
|
+
above instead.
|
|
293
|
+
</Warning>
|
|
294
|
+
|
|
295
|
+
## Valid and invalid tiled refs
|
|
296
|
+
|
|
297
|
+
Author edge and vertex refs with real shared geometry, not just “nearby”
|
|
298
|
+
spaces. These examples show the difference.
|
|
299
|
+
|
|
300
|
+
### Square edge refs
|
|
301
|
+
|
|
302
|
+
Valid square edge ref: the two cells share one border.
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
{
|
|
306
|
+
layout: "square",
|
|
307
|
+
spaces: [
|
|
308
|
+
{ id: "a1", row: 0, col: 0 },
|
|
309
|
+
{ id: "a2", row: 0, col: 1 },
|
|
310
|
+
],
|
|
311
|
+
edges: [
|
|
312
|
+
{ ref: { spaces: ["a1", "a2"] }, typeId: "road" },
|
|
313
|
+
],
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Invalid square edge ref: diagonal cells do not share an edge.
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
{
|
|
321
|
+
layout: "square",
|
|
322
|
+
spaces: [
|
|
323
|
+
{ id: "a1", row: 0, col: 0 },
|
|
324
|
+
{ id: "b2", row: 1, col: 1 },
|
|
325
|
+
],
|
|
326
|
+
edges: [
|
|
327
|
+
{ ref: { spaces: ["a1", "b2"] }, typeId: "road" },
|
|
328
|
+
],
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Why invalid: `a1` and `b2` only meet at a corner, so this must be a vertex ref
|
|
333
|
+
instead of an edge ref.
|
|
334
|
+
|
|
335
|
+
### Square vertex refs
|
|
336
|
+
|
|
337
|
+
Valid square vertex ref: the cells resolve to one shared corner.
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
{
|
|
341
|
+
layout: "square",
|
|
342
|
+
spaces: [
|
|
343
|
+
{ id: "a1", row: 0, col: 0 },
|
|
344
|
+
{ id: "a2", row: 0, col: 1 },
|
|
345
|
+
{ id: "b1", row: 1, col: 0 },
|
|
346
|
+
{ id: "b2", row: 1, col: 1 },
|
|
347
|
+
],
|
|
348
|
+
vertices: [
|
|
349
|
+
{ ref: { spaces: ["a1", "a2", "b1", "b2"] }, typeId: "tower" },
|
|
350
|
+
],
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Invalid square vertex ref: the cells do not collapse to exactly one corner.
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
{
|
|
358
|
+
layout: "square",
|
|
359
|
+
spaces: [
|
|
360
|
+
{ id: "a1", row: 0, col: 0 },
|
|
361
|
+
{ id: "a2", row: 0, col: 1 },
|
|
362
|
+
{ id: "c1", row: 2, col: 0 },
|
|
363
|
+
],
|
|
364
|
+
vertices: [
|
|
365
|
+
{ ref: { spaces: ["a1", "a2", "c1"] }, typeId: "tower" },
|
|
366
|
+
],
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Why invalid: those three cells do not touch the same corner, so the ref cannot
|
|
371
|
+
resolve to one runtime vertex ID.
|
|
372
|
+
|
|
373
|
+
### Hex edge refs
|
|
374
|
+
|
|
375
|
+
Valid hex edge ref: the two hexes share one border.
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
{
|
|
379
|
+
layout: "hex",
|
|
380
|
+
spaces: [
|
|
381
|
+
{ id: "hex-a", q: 0, r: 0 },
|
|
382
|
+
{ id: "hex-b", q: 1, r: 0 },
|
|
383
|
+
],
|
|
384
|
+
edges: [
|
|
385
|
+
{ ref: { spaces: ["hex-a", "hex-b"] }, typeId: "road" },
|
|
386
|
+
],
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Invalid hex edge ref: non-adjacent hexes do not form an edge.
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
{
|
|
394
|
+
layout: "hex",
|
|
395
|
+
spaces: [
|
|
396
|
+
{ id: "hex-a", q: 0, r: 0 },
|
|
397
|
+
{ id: "hex-c", q: 2, r: 0 },
|
|
398
|
+
],
|
|
399
|
+
edges: [
|
|
400
|
+
{ ref: { spaces: ["hex-a", "hex-c"] }, typeId: "road" },
|
|
401
|
+
],
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Why invalid: `hex-a` and `hex-c` are separated by another hex, so there is no
|
|
406
|
+
shared border to attach an edge to.
|
|
407
|
+
|
|
408
|
+
### Hex vertex refs
|
|
409
|
+
|
|
410
|
+
Valid hex vertex ref: the three hexes meet at one corner.
|
|
411
|
+
|
|
412
|
+
```ts
|
|
413
|
+
{
|
|
414
|
+
layout: "hex",
|
|
415
|
+
spaces: [
|
|
416
|
+
{ id: "hex-a", q: 0, r: 0 },
|
|
417
|
+
{ id: "hex-b", q: 1, r: 0 },
|
|
418
|
+
{ id: "hex-c", q: 0, r: 1 },
|
|
419
|
+
],
|
|
420
|
+
vertices: [
|
|
421
|
+
{ ref: { spaces: ["hex-a", "hex-b", "hex-c"] }, typeId: "settlement" },
|
|
422
|
+
],
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Invalid hex vertex ref: collinear hexes do not share a single corner.
|
|
427
|
+
|
|
428
|
+
```ts
|
|
429
|
+
{
|
|
430
|
+
layout: "hex",
|
|
431
|
+
spaces: [
|
|
432
|
+
{ id: "hex-a", q: 0, r: 0 },
|
|
433
|
+
{ id: "hex-b", q: 1, r: 0 },
|
|
434
|
+
{ id: "hex-c", q: 2, r: 0 },
|
|
435
|
+
],
|
|
436
|
+
vertices: [
|
|
437
|
+
{ ref: { spaces: ["hex-a", "hex-b", "hex-c"] }, typeId: "settlement" },
|
|
438
|
+
],
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Why invalid: those three hexes never meet at one point, so the ref fails
|
|
443
|
+
validation instead of producing an ambiguous runtime vertex.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Boards and topology
|
|
2
|
+
|
|
3
|
+
Model generic, hex, and square boards with typed spaces, edges, vertices, containers, and board refs.
|
|
4
|
+
|
|
5
|
+
Boards are manifest-defined topology. They can be generic, hex, or square, shared or per-player, inline or based on a template.
|
|
6
|
+
|
|
7
|
+
## Board layouts
|
|
8
|
+
|
|
9
|
+
| Layout | Use for |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| `generic` | Free-form spaces and relations, tracks, maps, abstract zones. |
|
|
12
|
+
| `hex` | Hex grids with spaces, edges, and vertices. |
|
|
13
|
+
| `square` | Square grids with rows, columns, spaces, edges, and vertices. |
|
|
14
|
+
|
|
15
|
+
All layouts can have spaces and containers. Tiled layouts also expose typed edge and vertex ids.
|
|
16
|
+
|
|
17
|
+
## Inline board
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
boards: [
|
|
21
|
+
{
|
|
22
|
+
id: "action-board",
|
|
23
|
+
name: "Action Board",
|
|
24
|
+
layout: "square",
|
|
25
|
+
scope: "shared",
|
|
26
|
+
spaces: [
|
|
27
|
+
{ id: "plaza", row: 0, col: 0, typeId: "action" },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Use inline boards when a board appears once. Use templates when several board instances share topology.
|
|
34
|
+
|
|
35
|
+
## Templates
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
boardTemplates: [
|
|
39
|
+
{
|
|
40
|
+
id: "market-template",
|
|
41
|
+
name: "Market Template",
|
|
42
|
+
layout: "generic",
|
|
43
|
+
spaces: [
|
|
44
|
+
{ id: "auction", typeId: "action" },
|
|
45
|
+
{ id: "harvest", typeId: "action" },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
boards: [
|
|
50
|
+
{
|
|
51
|
+
id: "market-board",
|
|
52
|
+
name: "Market Board",
|
|
53
|
+
templateId: "market-template",
|
|
54
|
+
layout: "generic",
|
|
55
|
+
scope: "shared",
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Template-backed boards still get their own board id. Reducer and UI code should address runtime board data by board id, not by template id.
|
|
61
|
+
|
|
62
|
+
## Shared and per-player boards
|
|
63
|
+
|
|
64
|
+
`scope: "shared"` creates one board instance for the session. `scope: "perPlayer"` creates one board per player.
|
|
65
|
+
|
|
66
|
+
Use shared boards for central maps and markets. Use per-player boards for player mats, personal tracks, and private layouts.
|
|
67
|
+
|
|
68
|
+
## Runtime board queries
|
|
69
|
+
|
|
70
|
+
Inside reducer callbacks, views, derived values, and target rules, read board data through injected `q`:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const board = q.board.hex("island");
|
|
74
|
+
const edge = q.board.edge("island", edgeId);
|
|
75
|
+
const adjacent = q.board.incidentVertices("island", edgeId);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Use `q.board.get` for layout-agnostic access, `q.board.hex` / `q.board.square` for layout-specific topology, and traversal helpers such as `incidentEdges`, `incidentVertices`, `spaceEdges`, `spaceVertices`, and `squareNeighbors` for rules.
|
|
79
|
+
|
|
80
|
+
## Static board projection
|
|
81
|
+
|
|
82
|
+
Static board topology belongs in [Static views](./static-views.md), not in dynamic player views.
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
export const boardStatic = defineStaticView<GameContract>()({
|
|
86
|
+
project: ({ q }) => {
|
|
87
|
+
const board = q.board.hex("island");
|
|
88
|
+
return {
|
|
89
|
+
...board,
|
|
90
|
+
portsByVertexId: computePorts(board),
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Keep game-specific decoration local, but use SDK static board queries for reusable topology access.
|
|
97
|
+
|
|
98
|
+
## Deep topology reference
|
|
99
|
+
|
|
100
|
+
For exact square/hex edge and vertex id rules, see [Board topology](./board-topology.md).
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Card actions
|
|
2
|
+
|
|
3
|
+
Use defineCardAction for first-class actions that play a card from a hand zone.
|
|
4
|
+
|
|
5
|
+
Use `defineCardAction` when the player action starts from a concrete card in a hand or card zone: play an action card, reveal a tactic, discard a reaction, trigger a card ability.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { cardTypes, zones } from "../shared/manifest-contract";
|
|
9
|
+
|
|
10
|
+
const playBoost = defineCardAction<
|
|
11
|
+
GameContract,
|
|
12
|
+
typeof playerTurnPhaseStateSchema
|
|
13
|
+
>()({
|
|
14
|
+
cardType: cardTypes.boost,
|
|
15
|
+
playFrom: zones.actionHand,
|
|
16
|
+
label: "Play boost",
|
|
17
|
+
step: "main",
|
|
18
|
+
inputs: {
|
|
19
|
+
selectedSpaceId: boardInput.space<GameState, SpaceId>({
|
|
20
|
+
target: selectableSpaceTarget,
|
|
21
|
+
}),
|
|
22
|
+
stealFromPlayerId: formInput(ids.playerId.nullable()),
|
|
23
|
+
},
|
|
24
|
+
rules: [
|
|
25
|
+
{
|
|
26
|
+
id: "can-play-action-card",
|
|
27
|
+
errorCode: "CARD_NOT_PLAYABLE",
|
|
28
|
+
validate: ({ state }) => validateCanPlayActionCard(state),
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
reduce({ state, input, accept, ops }) {
|
|
32
|
+
return accept(
|
|
33
|
+
pipe(
|
|
34
|
+
state,
|
|
35
|
+
ops.moveCardFromPlayerZoneToSharedZone({
|
|
36
|
+
playerId: input.playerId,
|
|
37
|
+
fromZoneId: zones.actionHand,
|
|
38
|
+
toZoneId: zones.playedCards,
|
|
39
|
+
cardId: input.params.cardId,
|
|
40
|
+
playedBy: input.playerId,
|
|
41
|
+
}),
|
|
42
|
+
ops.patchPublicState({ selectedSpaceId: input.params.selectedSpaceId }),
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## How it differs from defineInteraction
|
|
50
|
+
|
|
51
|
+
`defineCardAction` supplies the card-specific contract:
|
|
52
|
+
|
|
53
|
+
| Field | Meaning |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| `cardType` | Manifest card type that can trigger this action. |
|
|
56
|
+
| `playFrom` | Manifest player zone id the card must be playable from. |
|
|
57
|
+
| `inputs` | Additional params besides the selected card. |
|
|
58
|
+
| `step` / `steps` | Optional phase-step gates, typed from `state.phase.step` when your phase state schema declares a string literal union. |
|
|
59
|
+
| `rules` | Card-specific availability and validation, often shared across a family of cards. |
|
|
60
|
+
| `cost` / `paramsSchema` / `commit` | Same interaction metadata as `defineInteraction`, when the card action needs affordability, cross-field params, or manual draft control. |
|
|
61
|
+
| `reduce` | Receives `input.params.cardId` automatically. |
|
|
62
|
+
|
|
63
|
+
The generated interaction contract treats card actions as card-anchored
|
|
64
|
+
interactions. `Zone` plus `Interaction.CardInput` can show a playable card and
|
|
65
|
+
collect additional inputs without each workspace inventing local card-action
|
|
66
|
+
plumbing.
|
|
67
|
+
|
|
68
|
+
Card actions are always card-anchored interactions. Use `defineInteraction` for
|
|
69
|
+
non-card commands and group them visually in React.
|
|
70
|
+
|
|
71
|
+
## Zone-backed rendering
|
|
72
|
+
|
|
73
|
+
Declare the hand zone once in the phase:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
zones: [zones.actionHand],
|
|
77
|
+
cardActions: {
|
|
78
|
+
playBoost,
|
|
79
|
+
playDrawCards,
|
|
80
|
+
playBuildPath,
|
|
81
|
+
},
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The reducer projection is keyed by the manifest zone id, such as `action-hand`.
|
|
85
|
+
Import `zones` and `cardTypes` from the generated manifest contract so authoring
|
|
86
|
+
code uses typed handles instead of raw string ids. Cards come from the listed
|
|
87
|
+
manifest player zone; playable descriptors are derived from `cardActions` whose
|
|
88
|
+
`playFrom` matches that zone.
|
|
89
|
+
|
|
90
|
+
## Validation
|
|
91
|
+
|
|
92
|
+
Target validation still runs before authored rules. Use `rules` for
|
|
93
|
+
card-specific business checks: once per turn, bought this turn, enough
|
|
94
|
+
resources, correct phase step, or no pending trade.
|
|
95
|
+
|
|
96
|
+
Move the card in `reduce`. `defineCardAction` makes the card id available; it does not decide whether the card should be discarded, trashed, returned, tucked, or left in play.
|
|
97
|
+
|
|
98
|
+
## When to use defineInteraction instead
|
|
99
|
+
|
|
100
|
+
Use `defineInteraction` when:
|
|
101
|
+
|
|
102
|
+
- the action is not tied to a card instance
|
|
103
|
+
- the player chooses from a board target, form, or panel button first
|
|
104
|
+
- a prompt asks for a response
|
|
105
|
+
- the card is only one optional input among several unrelated choices
|
|
106
|
+
|
|
107
|
+
Use `defineCardAction` when the card is the object being played.
|