@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.
- package/LICENSE +89 -0
- package/NOTICE +1 -0
- package/dist/hex-geometry.d.ts +2 -0
- package/dist/hex-geometry.js +49 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +22 -0
- package/dist/manifest-contract.d.ts +14 -0
- package/dist/manifest-contract.js +4897 -0
- package/dist/manifest-validation.d.ts +6 -0
- package/dist/manifest-validation.js +506 -0
- package/dist/ownership.d.ts +31 -0
- package/dist/ownership.js +86 -0
- package/dist/preset-card-sets.d.ts +5 -0
- package/dist/preset-card-sets.js +135 -0
- package/dist/seeds.d.ts +6 -0
- package/dist/seeds.js +766 -0
- package/ownership.json +51 -0
- package/package.json +46 -0
- package/src/__fixtures__/sdk-types/invalid-card-properties-extra-key.ts +62 -0
- package/src/__fixtures__/sdk-types/invalid-card-properties-missing-required.ts +60 -0
- package/src/__fixtures__/sdk-types/invalid-card-properties-wrong-enum.ts +61 -0
- package/src/__fixtures__/sdk-types/invalid-card-properties-wrong-nested.ts +61 -0
- package/src/__fixtures__/sdk-types/invalid-card-properties-wrong-scalar.ts +61 -0
- package/src/__fixtures__/sdk-types/invalid-card-visibility.ts +40 -0
- package/src/__fixtures__/sdk-types/invalid-container-card-set-manifest.ts +62 -0
- package/src/__fixtures__/sdk-types/invalid-die-fields-wrong-array-item.ts +43 -0
- package/src/__fixtures__/sdk-types/invalid-die-fields-wrong-player-id.ts +43 -0
- package/src/__fixtures__/sdk-types/invalid-die-fields-wrong-resource-id.ts +43 -0
- package/src/__fixtures__/sdk-types/invalid-die-home-per-player-zone-no-owner.ts +35 -0
- package/src/__fixtures__/sdk-types/invalid-die-seed-type-id.ts +31 -0
- package/src/__fixtures__/sdk-types/invalid-die-visibility.ts +28 -0
- package/src/__fixtures__/sdk-types/invalid-generic-board-edge-id.ts +38 -0
- package/src/__fixtures__/sdk-types/invalid-generic-board-nested-field.ts +45 -0
- package/src/__fixtures__/sdk-types/invalid-hex-edge-field-edge-id.ts +47 -0
- package/src/__fixtures__/sdk-types/invalid-hex-vertex-field-vertex-id.ts +47 -0
- package/src/__fixtures__/sdk-types/invalid-manifest.ts +143 -0
- package/src/__fixtures__/sdk-types/invalid-piece-fields-extra-key.ts +62 -0
- package/src/__fixtures__/sdk-types/invalid-piece-fields-wrong-card-id.ts +61 -0
- package/src/__fixtures__/sdk-types/invalid-piece-fields-wrong-enum.ts +61 -0
- package/src/__fixtures__/sdk-types/invalid-piece-fields-wrong-scalar.ts +61 -0
- package/src/__fixtures__/sdk-types/invalid-piece-fields-wrong-zone-id.ts +61 -0
- package/src/__fixtures__/sdk-types/invalid-piece-home-per-player-container-no-owner.ts +48 -0
- package/src/__fixtures__/sdk-types/invalid-piece-home-per-player-edge-no-owner.ts +47 -0
- package/src/__fixtures__/sdk-types/invalid-piece-home-per-player-space-no-owner.ts +42 -0
- package/src/__fixtures__/sdk-types/invalid-piece-home-per-player-vertex-no-owner.ts +49 -0
- package/src/__fixtures__/sdk-types/invalid-piece-seed-type-id.ts +30 -0
- package/src/__fixtures__/sdk-types/invalid-piece-visibility.ts +28 -0
- package/src/__fixtures__/sdk-types/invalid-slot-host-manifest.ts +47 -0
- package/src/__fixtures__/sdk-types/invalid-slot-id-manifest.ts +49 -0
- package/src/__fixtures__/sdk-types/invalid-square-board-edge-id.ts +47 -0
- package/src/__fixtures__/sdk-types/invalid-square-board-space-id.ts +47 -0
- package/src/__fixtures__/sdk-types/invalid-square-board-vertex-id.ts +49 -0
- package/src/__fixtures__/sdk-types/invalid-square-container-field-space-id.ts +59 -0
- package/src/__fixtures__/sdk-types/invalid-square-container-host-space-id.ts +49 -0
- package/src/__fixtures__/sdk-types/invalid-square-relation-field-scalar.ts +48 -0
- package/src/__fixtures__/sdk-types/invalid-square-relation-space-id.ts +48 -0
- package/src/__fixtures__/sdk-types/invalid-square-space-fields-enum.ts +44 -0
- package/src/__fixtures__/sdk-types/invalid-square-space-fields-extra-key.ts +45 -0
- package/src/__fixtures__/sdk-types/valid-die-type-omits-sides.ts +29 -0
- package/src/__fixtures__/sdk-types/valid-manifest-omits-board-templates.ts +19 -0
- package/src/__fixtures__/sdk-types/valid-manifest.ts +612 -0
- package/src/__fixtures__/sdk-types/valid-player-scoped-seed-homes.ts +59 -0
- package/src/authoring-benchmark.test.ts +362 -0
- package/src/hex-geometry.ts +69 -0
- package/src/index.ts +64 -0
- package/src/manifest-contract.test.ts +1764 -0
- package/src/manifest-contract.ts +6581 -0
- package/src/manifest-validation.test.ts +393 -0
- package/src/manifest-validation.ts +795 -0
- package/src/ownership.ts +127 -0
- package/src/preset-card-sets.ts +169 -0
- package/src/sdk-types-authoring.test.ts +361 -0
- 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
|
+
}
|