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