@dreamboard-games/workspace-codegen 0.1.0

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.
Files changed (73) hide show
  1. package/LICENSE +89 -0
  2. package/NOTICE +1 -0
  3. package/dist/hex-geometry.d.ts +2 -0
  4. package/dist/hex-geometry.js +49 -0
  5. package/dist/index.d.ts +13 -0
  6. package/dist/index.js +22 -0
  7. package/dist/manifest-contract.d.ts +14 -0
  8. package/dist/manifest-contract.js +4897 -0
  9. package/dist/manifest-validation.d.ts +6 -0
  10. package/dist/manifest-validation.js +506 -0
  11. package/dist/ownership.d.ts +31 -0
  12. package/dist/ownership.js +86 -0
  13. package/dist/preset-card-sets.d.ts +5 -0
  14. package/dist/preset-card-sets.js +135 -0
  15. package/dist/seeds.d.ts +6 -0
  16. package/dist/seeds.js +766 -0
  17. package/ownership.json +51 -0
  18. package/package.json +46 -0
  19. package/src/__fixtures__/sdk-types/invalid-card-properties-extra-key.ts +62 -0
  20. package/src/__fixtures__/sdk-types/invalid-card-properties-missing-required.ts +60 -0
  21. package/src/__fixtures__/sdk-types/invalid-card-properties-wrong-enum.ts +61 -0
  22. package/src/__fixtures__/sdk-types/invalid-card-properties-wrong-nested.ts +61 -0
  23. package/src/__fixtures__/sdk-types/invalid-card-properties-wrong-scalar.ts +61 -0
  24. package/src/__fixtures__/sdk-types/invalid-card-visibility.ts +40 -0
  25. package/src/__fixtures__/sdk-types/invalid-container-card-set-manifest.ts +62 -0
  26. package/src/__fixtures__/sdk-types/invalid-die-fields-wrong-array-item.ts +43 -0
  27. package/src/__fixtures__/sdk-types/invalid-die-fields-wrong-player-id.ts +43 -0
  28. package/src/__fixtures__/sdk-types/invalid-die-fields-wrong-resource-id.ts +43 -0
  29. package/src/__fixtures__/sdk-types/invalid-die-home-per-player-zone-no-owner.ts +35 -0
  30. package/src/__fixtures__/sdk-types/invalid-die-seed-type-id.ts +31 -0
  31. package/src/__fixtures__/sdk-types/invalid-die-visibility.ts +28 -0
  32. package/src/__fixtures__/sdk-types/invalid-generic-board-edge-id.ts +38 -0
  33. package/src/__fixtures__/sdk-types/invalid-generic-board-nested-field.ts +45 -0
  34. package/src/__fixtures__/sdk-types/invalid-hex-edge-field-edge-id.ts +47 -0
  35. package/src/__fixtures__/sdk-types/invalid-hex-vertex-field-vertex-id.ts +47 -0
  36. package/src/__fixtures__/sdk-types/invalid-manifest.ts +143 -0
  37. package/src/__fixtures__/sdk-types/invalid-piece-fields-extra-key.ts +62 -0
  38. package/src/__fixtures__/sdk-types/invalid-piece-fields-wrong-card-id.ts +61 -0
  39. package/src/__fixtures__/sdk-types/invalid-piece-fields-wrong-enum.ts +61 -0
  40. package/src/__fixtures__/sdk-types/invalid-piece-fields-wrong-scalar.ts +61 -0
  41. package/src/__fixtures__/sdk-types/invalid-piece-fields-wrong-zone-id.ts +61 -0
  42. package/src/__fixtures__/sdk-types/invalid-piece-home-per-player-container-no-owner.ts +48 -0
  43. package/src/__fixtures__/sdk-types/invalid-piece-home-per-player-edge-no-owner.ts +47 -0
  44. package/src/__fixtures__/sdk-types/invalid-piece-home-per-player-space-no-owner.ts +42 -0
  45. package/src/__fixtures__/sdk-types/invalid-piece-home-per-player-vertex-no-owner.ts +49 -0
  46. package/src/__fixtures__/sdk-types/invalid-piece-seed-type-id.ts +30 -0
  47. package/src/__fixtures__/sdk-types/invalid-piece-visibility.ts +28 -0
  48. package/src/__fixtures__/sdk-types/invalid-slot-host-manifest.ts +47 -0
  49. package/src/__fixtures__/sdk-types/invalid-slot-id-manifest.ts +49 -0
  50. package/src/__fixtures__/sdk-types/invalid-square-board-edge-id.ts +47 -0
  51. package/src/__fixtures__/sdk-types/invalid-square-board-space-id.ts +47 -0
  52. package/src/__fixtures__/sdk-types/invalid-square-board-vertex-id.ts +49 -0
  53. package/src/__fixtures__/sdk-types/invalid-square-container-field-space-id.ts +59 -0
  54. package/src/__fixtures__/sdk-types/invalid-square-container-host-space-id.ts +49 -0
  55. package/src/__fixtures__/sdk-types/invalid-square-relation-field-scalar.ts +48 -0
  56. package/src/__fixtures__/sdk-types/invalid-square-relation-space-id.ts +48 -0
  57. package/src/__fixtures__/sdk-types/invalid-square-space-fields-enum.ts +44 -0
  58. package/src/__fixtures__/sdk-types/invalid-square-space-fields-extra-key.ts +45 -0
  59. package/src/__fixtures__/sdk-types/valid-die-type-omits-sides.ts +29 -0
  60. package/src/__fixtures__/sdk-types/valid-manifest-omits-board-templates.ts +19 -0
  61. package/src/__fixtures__/sdk-types/valid-manifest.ts +612 -0
  62. package/src/__fixtures__/sdk-types/valid-player-scoped-seed-homes.ts +59 -0
  63. package/src/authoring-benchmark.test.ts +362 -0
  64. package/src/hex-geometry.ts +69 -0
  65. package/src/index.ts +64 -0
  66. package/src/manifest-contract.test.ts +1764 -0
  67. package/src/manifest-contract.ts +6581 -0
  68. package/src/manifest-validation.test.ts +393 -0
  69. package/src/manifest-validation.ts +795 -0
  70. package/src/ownership.ts +127 -0
  71. package/src/preset-card-sets.ts +169 -0
  72. package/src/sdk-types-authoring.test.ts +361 -0
  73. package/src/seeds.ts +800 -0
package/src/seeds.ts ADDED
@@ -0,0 +1,800 @@
1
+ import type { GameTopologyManifest } from "@dreamboard/sdk-types";
2
+
3
+ export const SETUP_PROFILES_SEED_MARKER =
4
+ "Generated by dreamboard setup-profiles scaffold.";
5
+
6
+ function generateUiContractContent(): string {
7
+ return `/**
8
+ * Generated file.
9
+ * Do not edit directly.
10
+ */
11
+
12
+ import game from "../../app/game";
13
+ import {
14
+ createClientParamSchemasByPhase,
15
+ } from "@dreamboard/app-sdk/reducer";
16
+ import type {
17
+ ClientParamsOfInteractionOfDefinition,
18
+ DefaultedClientParamKeysOfInteractionOfDefinition,
19
+ InteractionIdOfDefinition,
20
+ InteractionIdOfDefinitionPhase,
21
+ PhaseNamesOfDefinition,
22
+ StageNamesOfDefinitionPhase,
23
+ ViewNamesOfDefinition,
24
+ ViewOfDefinition,
25
+ } from "@dreamboard/app-sdk/reducer";
26
+ import {
27
+ createDreamboardUI,
28
+ createHexBoardView,
29
+ InteractionField as InteractionFieldGeneric,
30
+ InteractionForm as InteractionFormGeneric,
31
+ useActivePlayers as useActivePlayersGeneric,
32
+ useBoardInteractions as useBoardInteractionsGeneric,
33
+ useGameView as useGameViewGeneric,
34
+ useInteractionByKey as useInteractionByKeyGeneric,
35
+ usePlayerTurnOrder as usePlayerTurnOrderGeneric,
36
+ type BoardInteractionsContext,
37
+ type BoardInteractionsOptions,
38
+ type BoardSpaceIdOf,
39
+ type ClientParamSchemaMap,
40
+ type HexBoardView,
41
+ type InteractionDescriptor,
42
+ type InteractionFieldProps as InteractionFieldPropsGeneric,
43
+ type InteractionFieldRenderMap,
44
+ type InteractionFormProps as InteractionFormPropsGeneric,
45
+ type InteractionHandle,
46
+ type DreamboardUI,
47
+ type UIContract,
48
+ } from "@dreamboard/ui-sdk";
49
+ import { createElement, useMemo, type ReactElement } from "react";
50
+ import {
51
+ literals,
52
+ staticBoards,
53
+ type CardId,
54
+ type EdgeId,
55
+ type PlayerId,
56
+ type SpaceId,
57
+ type VertexId,
58
+ type ZoneId as ManifestZoneId,
59
+ } from "../manifest-contract";
60
+
61
+ type GameDefinition = typeof game;
62
+
63
+ export type ViewName = ViewNamesOfDefinition<GameDefinition>;
64
+ export type InferView<Name extends ViewName> = ViewOfDefinition<
65
+ GameDefinition,
66
+ Name
67
+ >;
68
+ export type GameView = Extract<"player", ViewName> extends never
69
+ ? never
70
+ : InferView<Extract<"player", ViewName>>;
71
+
72
+ export type PhaseName = PhaseNamesOfDefinition<GameDefinition>;
73
+
74
+ // -------------------------------------------------------------------------
75
+ // Interaction / Stage / Zone types (authored via defineInteraction/Stage/zones)
76
+ // -------------------------------------------------------------------------
77
+
78
+ /** Union of all interaction ids across phases. */
79
+ export type InteractionId = InteractionIdOfDefinition<GameDefinition>;
80
+
81
+ /** Interactions declared in a specific phase. */
82
+ export type InteractionIdForPhase<Phase extends PhaseName> =
83
+ InteractionIdOfDefinitionPhase<GameDefinition, Phase>;
84
+
85
+ /**
86
+ * Client-facing params type for an interaction, inferred from its input
87
+ * collectors. Engine-sampled collectors (e.g. \`rngInput.*\`) are omitted
88
+ * — the trusted reducer bundle fills those fields during submit, so the
89
+ * client never supplies them.
90
+ */
91
+ export type InteractionParams<
92
+ Phase extends PhaseName,
93
+ Id extends InteractionIdForPhase<Phase>,
94
+ > = ClientParamsOfInteractionOfDefinition<GameDefinition, Phase, Id>;
95
+
96
+ /** Client-facing params with authored input defaults. */
97
+ export type InteractionDefaultedKeys<
98
+ Phase extends PhaseName,
99
+ Id extends InteractionIdForPhase<Phase>,
100
+ > = DefaultedClientParamKeysOfInteractionOfDefinition<GameDefinition, Phase, Id>;
101
+
102
+ /** Phase-qualified interaction key for a specific phase. */
103
+ export type InteractionKeyForPhase<Phase extends PhaseName> =
104
+ \`\${Phase}.\${InteractionIdForPhase<Phase>}\`;
105
+
106
+ /** Phase-qualified union of every client/UI interaction key. */
107
+ export type InteractionKey = {
108
+ [P in PhaseName]: InteractionKeyForPhase<P>;
109
+ }[PhaseName];
110
+
111
+ type PhaseOfInteractionKey<Key extends InteractionKey> =
112
+ Key extends \`\${infer P}.\${string}\` ? Extract<P, PhaseName> : never;
113
+
114
+ type IdOfInteractionKey<Key extends InteractionKey> =
115
+ Key extends \`\${infer P}.\${infer I}\`
116
+ ? P extends PhaseName
117
+ ? Extract<I, InteractionIdForPhase<P>>
118
+ : never
119
+ : never;
120
+
121
+ export type BoardInteractions = InteractionKey;
122
+
123
+ /** Stage names declared in a phase. */
124
+ export type StageName<Phase extends PhaseName> = StageNamesOfDefinitionPhase<
125
+ GameDefinition,
126
+ Phase
127
+ >;
128
+
129
+ /** Union of zone ids authored in the workspace manifest. */
130
+ export type ZoneId = ManifestZoneId;
131
+
132
+ type CamelCase<S extends string> = S extends \`\${infer Head}-\${infer Tail}\`
133
+ ? \`\${Head}\${Capitalize<CamelCase<Tail>>}\`
134
+ : S;
135
+
136
+ /** JS-friendly keys for authored zones, e.g. "dev-hand" -> "devHand". */
137
+ export type WorkspaceZoneKey = CamelCase<ZoneId>;
138
+
139
+ type PlayerCardZoneId = {
140
+ [Z in (typeof literals.playerZoneIds)[number]]: (typeof literals.cardSetIdsByPlayerZoneId)[Z] extends readonly []
141
+ ? never
142
+ : Z;
143
+ }[(typeof literals.playerZoneIds)[number]];
144
+
145
+ /** JS-friendly keys for per-player zones that can contain cards. */
146
+ export type WorkspacePlayerCardZoneKey = CamelCase<PlayerCardZoneId>;
147
+
148
+ /** Interaction descriptor specialised to a concrete phase-qualified key. */
149
+ export type InteractionDescriptorFor<Key extends InteractionKey> =
150
+ InteractionDescriptor<Key>;
151
+
152
+ /**
153
+ * Params shape for a phase-qualified interaction key. Drives strong typing
154
+ * for \`useInteractionByKey\`'s draft,
155
+ * \`submit\`, and \`setInput\`.
156
+ */
157
+ export type InteractionParamsOf<Key extends InteractionKey> =
158
+ InteractionParams<PhaseOfInteractionKey<Key>, IdOfInteractionKey<Key>>;
159
+
160
+ export type InteractionDefaultedKeysOf<Key extends InteractionKey> =
161
+ InteractionDefaultedKeys<PhaseOfInteractionKey<Key>, IdOfInteractionKey<Key>>;
162
+
163
+ type InteractionParamsShape<Key extends InteractionKey> =
164
+ InteractionParamsOf<Key> extends Record<string, unknown>
165
+ ? InteractionParamsOf<Key>
166
+ : Record<string, unknown>;
167
+
168
+ type InteractionInputKeysOf<Key extends InteractionKey> =
169
+ string extends keyof InteractionParamsOf<Key>
170
+ ? never
171
+ : keyof InteractionParamsOf<Key> & string;
172
+
173
+ type InteractionHandleDefaultedKeys<Key extends InteractionKey> = Extract<
174
+ InteractionDefaultedKeysOf<Key>,
175
+ keyof InteractionParamsShape<Key> & string
176
+ >;
177
+
178
+ type InteractionInputKeyOf<Key extends InteractionKey> =
179
+ Key extends InteractionKey ? InteractionInputKeysOf<Key> : never;
180
+
181
+ export type InteractionInputKey = InteractionInputKeyOf<InteractionKey>;
182
+
183
+ export type PromptKey = InteractionKey;
184
+
185
+ export type PromptOptionValue = string;
186
+
187
+ export type BoardTargetId = SpaceId | EdgeId | VertexId;
188
+
189
+ type UIInteractionRegistry = {
190
+ [K in InteractionKey]: {
191
+ interaction: K;
192
+ phase: PhaseOfInteractionKey<K>;
193
+ id: IdOfInteractionKey<K>;
194
+ };
195
+ };
196
+
197
+ type UIInputRegistry = {
198
+ [K in InteractionInputKey]: { input: K };
199
+ };
200
+
201
+ type UIPromptRegistry = {
202
+ [K in PromptKey]: { interaction: K };
203
+ };
204
+
205
+ type UIPromptOptionRegistry = {
206
+ [K in PromptOptionValue]: { value: K };
207
+ };
208
+
209
+ type UIZoneRegistry = {
210
+ [K in ZoneId & string]: { zone: K };
211
+ };
212
+
213
+ type UICardRegistry = {
214
+ [K in CardId & string]: { card: K };
215
+ };
216
+
217
+ type UIPhaseRegistry = {
218
+ [K in PhaseName & string]: { phase: K };
219
+ };
220
+
221
+ type UIBoardTargetRegistry = {
222
+ [K in BoardTargetId & string]: { target: K };
223
+ };
224
+
225
+ export const uiContract = {
226
+ interactions: {} as UIInteractionRegistry,
227
+ inputs: {} as UIInputRegistry,
228
+ prompts: {} as UIPromptRegistry,
229
+ promptOptions: {} as UIPromptOptionRegistry,
230
+ zones: {} as UIZoneRegistry,
231
+ cards: {} as UICardRegistry,
232
+ phases: {} as UIPhaseRegistry,
233
+ boardTargets: {} as UIBoardTargetRegistry,
234
+ } satisfies UIContract;
235
+
236
+ declare module "@dreamboard/ui-sdk" {
237
+ interface DreamboardUIRegister {
238
+ contract: GameDefinition;
239
+ ui: typeof uiContract;
240
+ }
241
+ }
242
+
243
+ type WorkspaceUI = DreamboardUI<typeof uiContract>;
244
+
245
+ export const UI: WorkspaceUI = createDreamboardUI(uiContract);
246
+ export const Interaction = UI.Interaction;
247
+ export const Prompt = UI.Prompt;
248
+ export const PromptInbox = UI.PromptInbox;
249
+ export const PromptDialogHost = UI.PromptDialogHost;
250
+ export const PlayerRoster: WorkspaceUI["PlayerRoster"] = UI.PlayerRoster;
251
+ export const Phase = UI.Phase;
252
+ export const Zone = UI.Zone;
253
+ export const Board = UI.Board;
254
+
255
+ export type InteractionFieldRenderers<Key extends InteractionKey> =
256
+ InteractionFieldRenderMap<InteractionParamsShape<Key>>;
257
+
258
+ export type InteractionFormProps<Key extends InteractionKey> = Omit<
259
+ InteractionFormPropsGeneric<
260
+ InteractionParamsShape<Key>,
261
+ InteractionHandleDefaultedKeys<Key>
262
+ >,
263
+ "descriptor" | "handle"
264
+ > & {
265
+ descriptor: InteractionDescriptorFor<Key>;
266
+ handle: InteractionHandle<
267
+ InteractionParamsShape<Key>,
268
+ InteractionHandleDefaultedKeys<Key>
269
+ >;
270
+ renderFields?: InteractionFieldRenderers<Key>;
271
+ };
272
+
273
+ export type InteractionFieldProps<
274
+ Key extends InteractionKey,
275
+ InputKey extends keyof InteractionParamsShape<Key> & string,
276
+ > = Omit<
277
+ InteractionFieldPropsGeneric<InteractionParamsShape<Key>, InputKey>,
278
+ "descriptor" | "handle"
279
+ > & {
280
+ descriptor: InteractionDescriptorFor<Key>;
281
+ handle: InteractionHandle<
282
+ InteractionParamsShape<Key>,
283
+ InteractionHandleDefaultedKeys<Key>
284
+ >;
285
+ };
286
+
287
+ export const clientParamSchemasByPhase = createClientParamSchemasByPhase(
288
+ game,
289
+ ) as ClientParamSchemaMap;
290
+
291
+ /**
292
+ * Workspace-typed wrapper over \`@dreamboard/ui-sdk\`'s generic
293
+ * \`useInteractionByKey\`. The \`key\` argument is constrained to the
294
+ * generated {@link InteractionKey} union (typos are compile errors), and
295
+ * the returned {@link InteractionHandle} is parameterised on
296
+ * {@link InteractionParamsOf} so \`handle.draft\`, \`handle.submit\`, and
297
+ * \`handle.setInput\` are statically typed against the interaction's
298
+ * declared inputs.
299
+ *
300
+ * \`\`\`ts
301
+ * const handle = useInteractionByKey("play.placeThingCard");
302
+ * if (!handle) return null;
303
+ * handle.setInput("cardId", card.id); // inferred as ThingsDeckCardId
304
+ * await handle.submit(); // params typed from the reducer contract
305
+ * \`\`\`
306
+ */
307
+ export function useInteractionByKey<Key extends InteractionKey>(
308
+ key: Key | null | undefined,
309
+ ): InteractionHandle<
310
+ InteractionParamsShape<Key>,
311
+ InteractionHandleDefaultedKeys<Key>
312
+ > | null {
313
+ return useInteractionByKeyGeneric<
314
+ Key,
315
+ InteractionParamsShape<Key>,
316
+ InteractionHandleDefaultedKeys<Key>
317
+ >(key);
318
+ }
319
+
320
+ export function InteractionForm<Key extends InteractionKey>(
321
+ props: InteractionFormProps<Key>,
322
+ ): ReactElement {
323
+ return createElement(
324
+ InteractionFormGeneric<
325
+ InteractionParamsShape<Key>,
326
+ InteractionHandleDefaultedKeys<Key>
327
+ >,
328
+ props,
329
+ );
330
+ }
331
+
332
+ export function InteractionField<
333
+ Key extends InteractionKey,
334
+ InputKey extends keyof InteractionParamsShape<Key> & string,
335
+ >(props: InteractionFieldProps<Key, InputKey>): ReactElement | null {
336
+ return createElement(
337
+ InteractionFieldGeneric<InteractionParamsShape<Key>, InputKey>,
338
+ props,
339
+ );
340
+ }
341
+
342
+ /**
343
+ * Workspace-typed wrapper over \`@dreamboard/ui-sdk\`'s
344
+ * \`useBoardInteractions\`. Returns a {@link BoardInteractionsContext}
345
+ * narrowed to this workspace's board-interaction union for exhaustive
346
+ * downstream logic.
347
+ *
348
+ * \`\`\`tsx
349
+ * const board = useBoardInteractions();
350
+ * <HexGrid interactiveVertices={board.targetLayers.vertex()} />
351
+ * \`\`\`
352
+ *
353
+ * Reach for this whenever a screen keeps more than one board target
354
+ * interaction live simultaneously and dispatch is target-driven instead of
355
+ * armed-then-clicked. Ambiguous unarmed overlaps should be resolved in the UI
356
+ * by arming an explicit interaction/input route.
357
+ */
358
+ export function useBoardInteractions(
359
+ options?: BoardInteractionsOptions,
360
+ ): BoardInteractionsContext<BoardInteractions> {
361
+ return useBoardInteractionsGeneric<BoardInteractions>(options);
362
+ }
363
+
364
+ /** Workspace-typed active-player hook. */
365
+ export function useActivePlayers(): readonly PlayerId[] {
366
+ return useActivePlayersGeneric() as readonly PlayerId[];
367
+ }
368
+
369
+ /** Workspace-typed reducer-native player view hook. */
370
+ export function useGameView(): GameView {
371
+ return useGameViewGeneric() as GameView;
372
+ }
373
+
374
+ /** Workspace-typed player turn order hook. */
375
+ export function usePlayerTurnOrder(): readonly PlayerId[] {
376
+ return usePlayerTurnOrderGeneric() as readonly PlayerId[];
377
+ }
378
+
379
+ // -------------------------------------------------------------------------
380
+ // Typed hex-board view adapter
381
+ // -------------------------------------------------------------------------
382
+
383
+ /** Generated hex-board topology source, keyed by hex-board id. */
384
+ const hexStaticBoards = staticBoards.hex;
385
+
386
+ /** Union of authored hex-board ids in this workspace's manifest. */
387
+ export type HexBoardId = keyof typeof hexStaticBoards & string;
388
+
389
+ /** Topology object for the named hex board, drawn from \`staticBoards.hex\`. */
390
+ export type HexBoardTopology<Id extends HexBoardId> = (typeof hexStaticBoards)[Id];
391
+
392
+ /** Space id type for the named hex board. */
393
+ export type HexBoardSpaceId<Id extends HexBoardId> = BoardSpaceIdOf<
394
+ HexBoardTopology<Id>
395
+ >;
396
+
397
+ /**
398
+ * Workspace-typed wrapper over \`@dreamboard/ui-sdk\`'s
399
+ * \`createHexBoardView\`. Joins the static topology of the named hex
400
+ * board with a per-space view overlay and returns a value ready for
401
+ * \`<HexGrid board={...} />\`. Each rendered tile carries a \`view\`
402
+ * field typed as the matched overlay row.
403
+ *
404
+ * Strict by construction: every static space must have exactly one
405
+ * overlay; missing, duplicate, or unknown overlay ids throw.
406
+ *
407
+ * \`\`\`tsx
408
+ * const island = useHexBoardView("island", { spaces: view.spaces });
409
+ *
410
+ * <HexGrid
411
+ * board={island}
412
+ * renderTile={(tile) => {
413
+ * const terrain = tile.view.terrain;
414
+ * // ...
415
+ * }}
416
+ * />
417
+ * \`\`\`
418
+ */
419
+ export function useHexBoardView<
420
+ const Id extends HexBoardId,
421
+ const TSpaceView extends { id: HexBoardSpaceId<Id> },
422
+ >(
423
+ boardId: Id,
424
+ options: { spaces: ReadonlyArray<TSpaceView> },
425
+ ): HexBoardView<HexBoardTopology<Id>, TSpaceView> {
426
+ const board = hexStaticBoards[boardId];
427
+ const { spaces } = options;
428
+ return useMemo(
429
+ () => createHexBoardView<HexBoardTopology<Id>, TSpaceView>(board, { spaces }),
430
+ [board, spaces],
431
+ );
432
+ }
433
+ `;
434
+ }
435
+
436
+ function generateReducerSupportSeed(): string {
437
+ return `import {
438
+ createReducerOps,
439
+ createStateQueries,
440
+ pipe,
441
+ } from "@dreamboard/app-sdk/reducer";
442
+ import type { GameState } from "./game-contract";
443
+
444
+ /**
445
+ * Small, SDK-shaped reducer helpers belong here.
446
+ *
447
+ * Keep schema and contract declarations in app/game-contract.ts.
448
+ * Keep initial state callbacks and defineGame wiring in app/game.ts.
449
+ * Keep memoized aggregates (winner checks, VP totals, longest-road,
450
+ * largest-army) in app/derived.ts via \`defineDerived\`.
451
+ * Keep phase files focused on one game-flow state.
452
+ *
453
+ * When this file starts collecting real game rules, split them by domain
454
+ * under app/rules/ instead, for example app/rules/board.ts,
455
+ * app/rules/resources.ts, or app/rules/scoring.ts.
456
+ *
457
+ * Recommended authoring pattern inside a phase reducer:
458
+ *
459
+ * const next = pipe(
460
+ * state,
461
+ * ops.setActivePlayers([q.players.order()[0]]),
462
+ * );
463
+ * return accept(next);
464
+ */
465
+
466
+ export const ops = createReducerOps<GameState>();
467
+
468
+ export { pipe };
469
+
470
+ export function stateQueries(state: GameState) {
471
+ return createStateQueries(state);
472
+ }
473
+ `;
474
+ }
475
+
476
+ function generateAppReadmeSeed(): string {
477
+ return `# App organization
478
+
479
+ This directory owns the reducer: game state schemas, phases, interactions,
480
+ derived values, player views, and reducer helpers.
481
+
482
+ Use this layout while the game is small:
483
+
484
+ \`\`\`txt
485
+ app/
486
+ game-contract.ts
487
+ game.ts
488
+ derived.ts
489
+ reducer-support.ts
490
+ phases/
491
+ setup.ts
492
+ \`\`\`
493
+
494
+ When a phase grows beyond a single idea, make the phase directory the table of
495
+ contents and split the implementation by game concept:
496
+
497
+ \`\`\`txt
498
+ app/phases/player-turn/
499
+ index.ts # definePhase assembly only
500
+ state.ts # phase-local constants and types
501
+ inputs.ts # shared input and presentation helpers
502
+ build.ts # build interactions
503
+ trade.ts # trade interactions
504
+ end-turn.ts # end-turn interaction
505
+ \`\`\`
506
+
507
+ Split a phase when it has multiple action families, shared input helpers, card
508
+ actions, or is roughly 250-300 lines. Keep \`index.ts\` as the assembly point
509
+ that imports interactions and registers them with \`definePhase\`.
510
+
511
+ Do not turn \`reducer-support.ts\` into a catch-all rule module. Keep it small
512
+ for shared reducer plumbing; put real game rules under \`app/rules/*\` once
513
+ there is more than one domain.
514
+ `;
515
+ }
516
+
517
+ function generateReducerDerivedSeed(): string {
518
+ return `// Memoized, pure projections of reducer state. Read them from reducer
519
+ // callbacks and view projections via the injected \`derived\` helper:
520
+ //
521
+ // reduce({ state, derived, accept }) {
522
+ // const winner = derived(winnerOf);
523
+ // return accept({ ...state, publicState: { ...state.publicState, winnerPlayerId: winner } });
524
+ // }
525
+ //
526
+ // Do NOT mirror derived values back into \`publicState\`. Keep the raw
527
+ // inputs (component locations, zone contents, counters) in state and
528
+ // express aggregates here.
529
+ //
530
+ // Uncomment the example below once you have something to derive.
531
+
532
+ // import { defineDerived } from "@dreamboard/app-sdk/reducer";
533
+ // import type { GameContract } from "./game-contract";
534
+ //
535
+ // export const winnerOf = defineDerived<GameContract>()({
536
+ // name: "winnerOf",
537
+ // compute: ({ state }) => {
538
+ // // Example: return the first player at or above the VP target.
539
+ // return state.publicState.winnerPlayerId ?? null;
540
+ // },
541
+ // });
542
+
543
+ export {};
544
+ `;
545
+ }
546
+
547
+ function generateReducerGameContractSeed(): string {
548
+ return `import { z } from "zod";
549
+ import { ids, manifestContract } from "../shared/manifest-contract";
550
+ import { defineGameContract, type GameStateOf } from "@dreamboard/app-sdk/reducer";
551
+
552
+ // Contract files should stay focused on schemas, phase names, and exported
553
+ // state types. Put reducers in app/phases/* and pure computations in
554
+ // app/derived.ts or app/rules/*.
555
+ const publicStateSchema = z.object({
556
+ currentPlayerId: ids.playerId.nullable(),
557
+ notesByPlayerId: z.partialRecord(ids.playerId, z.string()).default({}),
558
+ });
559
+ const privateStateSchema = z.object({});
560
+ const hiddenStateSchema = z.object({});
561
+
562
+ export const gameContract = defineGameContract({
563
+ manifest: manifestContract,
564
+ // Keep this list in sync with the keys of the \`phases\` record in ./phases.
565
+ // Declaring phase names here narrows \`fx.transition(...)\`, \`phase.dispatch\`,
566
+ // and the flow state's \`currentPhase\` to a literal union.
567
+ phaseNames: ["setup"] as const,
568
+ state: {
569
+ public: publicStateSchema,
570
+ private: privateStateSchema,
571
+ hidden: hiddenStateSchema,
572
+ },
573
+ });
574
+
575
+ export type GameContract = typeof gameContract;
576
+ export type GameState = GameStateOf<GameContract>;
577
+ `;
578
+ }
579
+
580
+ function generateReducerGameSeed(): string {
581
+ return `import { defineGame } from "@dreamboard/app-sdk/reducer";
582
+ import { gameContract } from "./game-contract";
583
+ import { phases } from "./phases";
584
+ import setupProfiles from "./setup-profiles";
585
+
586
+ // Keep this file as the defineGame assembly point. Put rules in phases,
587
+ // projections in views/derived modules, and reusable domain helpers in
588
+ // app/rules/* once reducer-support.ts is no longer small.
589
+ export default defineGame({
590
+ contract: gameContract,
591
+ initial: {
592
+ public: ({ playerIds }) => ({
593
+ currentPlayerId: playerIds[0] ?? null,
594
+ notesByPlayerId: {},
595
+ }),
596
+ private: () => ({}),
597
+ hidden: () => ({}),
598
+ },
599
+ initialPhase: "setup",
600
+ setupProfiles,
601
+ phases,
602
+ views: {},
603
+ });
604
+ `;
605
+ }
606
+
607
+ function generateSetupProfilesSeed(manifest: GameTopologyManifest): string {
608
+ const setupProfiles = manifest.setupProfiles ?? [];
609
+ const setupProfileEntries =
610
+ setupProfiles.length === 0
611
+ ? ""
612
+ : `{\n${setupProfiles
613
+ .map((profile) => ` ${JSON.stringify(profile.id)}: {},`)
614
+ .join("\n")}\n}`;
615
+
616
+ return `// ${SETUP_PROFILES_SEED_MARKER}
617
+ import { setupProfiles } from "../shared/manifest-contract";
618
+
619
+ export default setupProfiles(${setupProfileEntries || "{}"});
620
+ `;
621
+ }
622
+
623
+ export function isFrameworkOwnedSetupProfilesSeed(
624
+ content: string | null | undefined,
625
+ ): boolean {
626
+ if (content === null || content === undefined) {
627
+ return false;
628
+ }
629
+ const trimmed = content.trim();
630
+ if (trimmed.length === 0) {
631
+ return true;
632
+ }
633
+ if (trimmed.includes(SETUP_PROFILES_SEED_MARKER)) {
634
+ return true;
635
+ }
636
+ return false;
637
+ }
638
+
639
+ function generateSetupPhaseSeed(): string {
640
+ return `import { z } from "zod";
641
+ import type { GameContract } from "../game-contract";
642
+ import { definePhase } from "@dreamboard/app-sdk/reducer";
643
+
644
+ // A single phase file is fine while the phase is small. When a phase grows
645
+ // multiple action families, move it to app/phases/<phase>/index.ts and split
646
+ // interactions into neighboring concept files.
647
+ export const setup = definePhase<GameContract>()({
648
+ kind: "auto",
649
+ state: z.object({}),
650
+ initialState: () => ({}),
651
+ enter({ state, accept }) {
652
+ return accept(state);
653
+ },
654
+ });
655
+ `;
656
+ }
657
+
658
+ function generatePhaseIndexSeed(): string {
659
+ return `import { setup } from "./setup";
660
+ import type { GameContract } from "../game-contract";
661
+ import type { PhaseMapOf } from "@dreamboard/app-sdk/reducer";
662
+
663
+ export const phases = {
664
+ setup,
665
+ } satisfies PhaseMapOf<GameContract>;
666
+ `;
667
+ }
668
+
669
+ function generateReducerAppIndex(): string {
670
+ return `import game from "./game";
671
+ import { createReducerBundle } from "@dreamboard/app-sdk/reducer";
672
+
673
+ export default createReducerBundle(game);
674
+ `;
675
+ }
676
+
677
+ function generateAppFrameworkTsConfig(): string {
678
+ return `${JSON.stringify(
679
+ {
680
+ compilerOptions: {
681
+ target: "ES2020",
682
+ module: "ESNext",
683
+ moduleResolution: "bundler",
684
+ strict: true,
685
+ esModuleInterop: true,
686
+ skipLibCheck: true,
687
+ declaration: false,
688
+ outDir: "./dist",
689
+ paths: {
690
+ "@dreamboard/manifest-contract": ["../shared/manifest-contract.ts"],
691
+ "@dreamboard/ui-contract": ["../shared/generated/ui-contract.ts"],
692
+ "@shared/*": ["../shared/*"],
693
+ },
694
+ },
695
+ include: [
696
+ "./**/*.ts",
697
+ "./**/*.d.ts",
698
+ "../shared/manifest-*.ts",
699
+ "../shared/manifest-*.d.ts",
700
+ ],
701
+ exclude: ["node_modules", "dist", "../shared/generated"],
702
+ },
703
+ null,
704
+ 2,
705
+ )}\n`;
706
+ }
707
+
708
+ function generateUiFrameworkTsConfig(): string {
709
+ return `{
710
+ "compilerOptions": {
711
+ "target": "ES2020",
712
+ "module": "ESNext",
713
+ "moduleResolution": "bundler",
714
+ "jsx": "react-jsx",
715
+ "strict": true,
716
+ "esModuleInterop": true,
717
+ "skipLibCheck": true,
718
+ "paths": {
719
+ "@dreamboard/manifest-contract": [
720
+ "../shared/manifest-contract.ts"
721
+ ],
722
+ "@dreamboard/ui-contract": [
723
+ "../shared/generated/ui-contract.ts"
724
+ ],
725
+ "@shared/*": [
726
+ "../shared/*"
727
+ ]
728
+ }
729
+ },
730
+ "include": [
731
+ "./**/*.ts",
732
+ "./**/*.tsx",
733
+ "../shared/**/*.ts"
734
+ ],
735
+ "exclude": [
736
+ "node_modules",
737
+ "dist"
738
+ ]
739
+ }
740
+ `;
741
+ }
742
+
743
+ function generateReducerUiAppContent(): string {
744
+ return `import { Phase, PromptInbox } from "@dreamboard/ui-contract";
745
+
746
+ function SetupPhase() {
747
+ return (
748
+ <main>
749
+ <PromptInbox.Root>
750
+ <PromptInbox.Empty>No available prompts.</PromptInbox.Empty>
751
+ <PromptInbox.Items />
752
+ </PromptInbox.Root>
753
+ </main>
754
+ );
755
+ }
756
+
757
+ export default function App() {
758
+ return (
759
+ <Phase.Switch
760
+ routes={{
761
+ setup: () => <SetupPhase />,
762
+ }}
763
+ />
764
+ );
765
+ }
766
+ `;
767
+ }
768
+
769
+ export function generateSeedFiles(
770
+ manifest: GameTopologyManifest,
771
+ ): Record<string, string> {
772
+ return {
773
+ "app/README.md": generateAppReadmeSeed(),
774
+ "ui/App.tsx": generateReducerUiAppContent(),
775
+ "app/game-contract.ts": generateReducerGameContractSeed(),
776
+ "app/game.ts": generateReducerGameSeed(),
777
+ "app/setup-profiles.ts": generateSetupProfilesSeed(manifest),
778
+ "app/reducer-support.ts": generateReducerSupportSeed(),
779
+ "app/derived.ts": generateReducerDerivedSeed(),
780
+ "app/phases/setup.ts": generateSetupPhaseSeed(),
781
+ "app/phases/index.ts": generatePhaseIndexSeed(),
782
+ };
783
+ }
784
+
785
+ export function generateAuthoritativeIndexFile(
786
+ _manifest: GameTopologyManifest,
787
+ ): string {
788
+ return generateReducerAppIndex();
789
+ }
790
+
791
+ export function generateFrameworkFiles(
792
+ manifest: GameTopologyManifest,
793
+ ): Record<string, string> {
794
+ return {
795
+ "app/tsconfig.framework.json": generateAppFrameworkTsConfig(),
796
+ "ui/tsconfig.framework.json": generateUiFrameworkTsConfig(),
797
+ "shared/generated/ui-contract.ts": generateUiContractContent(),
798
+ "app/index.ts": generateAuthoritativeIndexFile(manifest),
799
+ };
800
+ }