@dreamboard-games/cli 0.1.30-alpha.1 → 0.1.30-alpha.2

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 (114) hide show
  1. package/README.md +179 -22
  2. package/dist/{chunk-C6UAT6EH.js → chunk-N7XPNNUI.js} +9 -12
  3. package/dist/chunk-N7XPNNUI.js.map +1 -0
  4. package/dist/chunk-SEGVTWSK.js +44 -0
  5. package/dist/{chunk-RS7UXJZV.js → chunk-TAQKH67O.js} +21300 -35881
  6. package/dist/chunk-TAQKH67O.js.map +1 -0
  7. package/dist/{global-config-AGFBDFYD.js → global-config-S4ZIPECE.js} +3 -3
  8. package/dist/index.js +415 -37
  9. package/dist/index.js.map +1 -1
  10. package/dist/internal.js +3 -4
  11. package/dist/{agent-verifier/keychain-backend-TNOPQV3Z.mjs → keychain-backend-HDF4TZDL.js} +2 -1
  12. package/dist/{agent-verifier/prompt-3BAINGAQ.mjs → prompt-NDV3AE5L.js} +2 -1
  13. package/package.json +6 -6
  14. package/skills/dreamboard/references/building-your-first-game.md +510 -0
  15. package/skills/dreamboard/references/cli.md +104 -0
  16. package/skills/dreamboard/references/game-interface.md +548 -0
  17. package/skills/dreamboard/references/manifest-authoring.md +597 -0
  18. package/skills/dreamboard/references/quickstart.md +66 -0
  19. package/skills/dreamboard/references/reducer.md +864 -0
  20. package/skills/dreamboard/references/rule-authoring.md +147 -0
  21. package/skills/dreamboard/references/testing.md +249 -0
  22. package/skills/dreamboard/scripts/events-extract.mjs +218 -0
  23. package/dist/agent-verifier/agent-workspace-verifier.mjs +0 -227
  24. package/dist/agent-verifier/chunk-2E5P5NWG.mjs +0 -835
  25. package/dist/agent-verifier/chunk-2GBBP27W.mjs +0 -301
  26. package/dist/agent-verifier/chunk-2QMNAVV4.mjs +0 -14522
  27. package/dist/agent-verifier/chunk-2SZHMP6F.mjs +0 -264
  28. package/dist/agent-verifier/chunk-4WD3YU2E.mjs +0 -166
  29. package/dist/agent-verifier/chunk-54TAYXUD.mjs +0 -12
  30. package/dist/agent-verifier/chunk-6A5HRJMQ.mjs +0 -3174
  31. package/dist/agent-verifier/chunk-6UUJEYDV.mjs +0 -213
  32. package/dist/agent-verifier/chunk-7653FPGJ.mjs +0 -381
  33. package/dist/agent-verifier/chunk-7E65UQLY.mjs +0 -38
  34. package/dist/agent-verifier/chunk-BVVNBJM4.mjs +0 -221
  35. package/dist/agent-verifier/chunk-CEDUHGNH.mjs +0 -74
  36. package/dist/agent-verifier/chunk-CEQ2VJWN.mjs +0 -149
  37. package/dist/agent-verifier/chunk-CFU5EWIC.mjs +0 -69
  38. package/dist/agent-verifier/chunk-CJEEA6NJ.mjs +0 -730
  39. package/dist/agent-verifier/chunk-EIQWDQWJ.mjs +0 -186
  40. package/dist/agent-verifier/chunk-EOQIV6PS.mjs +0 -649
  41. package/dist/agent-verifier/chunk-HBNDKQT5.mjs +0 -8381
  42. package/dist/agent-verifier/chunk-HJFQDSTU.mjs +0 -225
  43. package/dist/agent-verifier/chunk-JH22JNYD.mjs +0 -1681
  44. package/dist/agent-verifier/chunk-LI3ZR3BI.mjs +0 -41
  45. package/dist/agent-verifier/chunk-LM3OZLZG.mjs +0 -48
  46. package/dist/agent-verifier/chunk-MINCYHXN.mjs +0 -106
  47. package/dist/agent-verifier/chunk-MRCUP5SW.mjs +0 -128
  48. package/dist/agent-verifier/chunk-RBDDIIPM.mjs +0 -19
  49. package/dist/agent-verifier/chunk-SHUMAVAP.mjs +0 -59
  50. package/dist/agent-verifier/chunk-SYPLYRGB.mjs +0 -2812
  51. package/dist/agent-verifier/chunk-U6OJN7XS.mjs +0 -8092
  52. package/dist/agent-verifier/chunk-VYJTHSYR.mjs +0 -44
  53. package/dist/agent-verifier/chunk-XYDL7GY6.mjs +0 -10
  54. package/dist/agent-verifier/compile-5QSPIOUT.mjs +0 -313
  55. package/dist/agent-verifier/global-config-WX3ZZIVU.mjs +0 -17
  56. package/dist/agent-verifier/local-files-MTPLP62S.mjs +0 -46
  57. package/dist/agent-verifier/local-typecheck-QFYYAZOK.mjs +0 -9
  58. package/dist/agent-verifier/materialize-workspace-FKALAE2T.mjs +0 -90
  59. package/dist/agent-verifier/project-state-7GR6BQTQ.mjs +0 -32
  60. package/dist/agent-verifier/reducer-bundle-preflight-C73LEXI2.mjs +0 -23
  61. package/dist/agent-verifier/reducer-contract-preflight-22X7DSZW.mjs +0 -10
  62. package/dist/agent-verifier/reducer-native-test-harness-GMWBUISX.mjs +0 -53
  63. package/dist/agent-verifier/static-scaffold-AJMZZQWS.mjs +0 -28
  64. package/dist/agent-verifier/sync-3DUQH32H.mjs +0 -594
  65. package/dist/agent-verifier/test-P4U5INTD.mjs +0 -356
  66. package/dist/agent-verifier/testing-5K2BJYF2.mjs +0 -674
  67. package/dist/agent-verifier/workspace-codegen-JDZJRSDV.mjs +0 -11
  68. package/dist/agent-verifier/workspace-dependencies-HZ6VVS4G.mjs +0 -14
  69. package/dist/chunk-2H7UOFLK.js +0 -11
  70. package/dist/chunk-7FOO4AJI.js +0 -50
  71. package/dist/chunk-7FOO4AJI.js.map +0 -1
  72. package/dist/chunk-C6UAT6EH.js.map +0 -1
  73. package/dist/chunk-RS7UXJZV.js.map +0 -1
  74. package/dist/internal.d.ts +0 -311
  75. package/dist/keychain-backend-JHTXAKWC.js +0 -135
  76. package/dist/prompt-GMZABCJC.js +0 -756
  77. package/dist/runtime-packages/ui-host-runtime/src/actor-principal.ts +0 -71
  78. package/dist/runtime-packages/ui-host-runtime/src/browser-interaction.ts +0 -139
  79. package/dist/runtime-packages/ui-host-runtime/src/components/host-controls.tsx +0 -374
  80. package/dist/runtime-packages/ui-host-runtime/src/components/host-feedback-toaster.tsx +0 -266
  81. package/dist/runtime-packages/ui-host-runtime/src/components/host-feedback.tsx +0 -212
  82. package/dist/runtime-packages/ui-host-runtime/src/components/host-primitives.tsx +0 -271
  83. package/dist/runtime-packages/ui-host-runtime/src/components/host-session-metadata.tsx +0 -135
  84. package/dist/runtime-packages/ui-host-runtime/src/components/index.ts +0 -5
  85. package/dist/runtime-packages/ui-host-runtime/src/components/perf-overlay.tsx +0 -194
  86. package/dist/runtime-packages/ui-host-runtime/src/gameplay-authority-transport.ts +0 -626
  87. package/dist/runtime-packages/ui-host-runtime/src/host-controls.tsx +0 -1
  88. package/dist/runtime-packages/ui-host-runtime/src/host-feedback.tsx +0 -1
  89. package/dist/runtime-packages/ui-host-runtime/src/host-session-transport.ts +0 -294
  90. package/dist/runtime-packages/ui-host-runtime/src/index.ts +0 -3
  91. package/dist/runtime-packages/ui-host-runtime/src/logger.ts +0 -11
  92. package/dist/runtime-packages/ui-host-runtime/src/perf.ts +0 -324
  93. package/dist/runtime-packages/ui-host-runtime/src/plugin-bridge.ts +0 -195
  94. package/dist/runtime-packages/ui-host-runtime/src/plugin-health-check.ts +0 -138
  95. package/dist/runtime-packages/ui-host-runtime/src/plugin-messages.ts +0 -159
  96. package/dist/runtime-packages/ui-host-runtime/src/plugin-session-gateway.ts +0 -551
  97. package/dist/runtime-packages/ui-host-runtime/src/runtime/index.ts +0 -13
  98. package/dist/runtime-packages/ui-host-runtime/src/screenshot/projection-to-snapshot.ts +0 -122
  99. package/dist/runtime-packages/ui-host-runtime/src/screenshot/static-store-api.ts +0 -26
  100. package/dist/runtime-packages/ui-host-runtime/src/session-ingress-controller.ts +0 -583
  101. package/dist/runtime-packages/ui-host-runtime/src/session-ingress.ts +0 -219
  102. package/dist/runtime-packages/ui-host-runtime/src/session-live-runtime.ts +0 -117
  103. package/dist/runtime-packages/ui-host-runtime/src/session-model.ts +0 -431
  104. package/dist/runtime-packages/ui-host-runtime/src/session-projection.ts +0 -211
  105. package/dist/runtime-packages/ui-host-runtime/src/session-recovery.ts +0 -80
  106. package/dist/runtime-packages/ui-host-runtime/src/session-state-reducer.ts +0 -1034
  107. package/dist/runtime-packages/ui-host-runtime/src/sse-manager.ts +0 -416
  108. package/dist/runtime-packages/ui-host-runtime/src/unified-session-store.ts +0 -184
  109. package/dist/testing-KLSV6CPJ.js +0 -674
  110. package/dist/testing-KLSV6CPJ.js.map +0 -1
  111. /package/dist/{chunk-2H7UOFLK.js.map → chunk-SEGVTWSK.js.map} +0 -0
  112. /package/dist/{global-config-AGFBDFYD.js.map → global-config-S4ZIPECE.js.map} +0 -0
  113. /package/dist/{keychain-backend-JHTXAKWC.js.map → keychain-backend-HDF4TZDL.js.map} +0 -0
  114. /package/dist/{prompt-GMZABCJC.js.map → prompt-NDV3AE5L.js.map} +0 -0
@@ -1,1034 +0,0 @@
1
- import type {
2
- GameplayPerspective,
3
- GameplayViewport,
4
- SessionContext,
5
- UnifiedSessionModel,
6
- } from "./session-model.js";
7
- import {
8
- gameplayViewportForPlayer,
9
- getGameplayViewport,
10
- getSessionContext,
11
- resolveControllablePlayerIds,
12
- withContext,
13
- } from "./session-model.js";
14
- import type {
15
- NormalizedSessionEvent,
16
- NormalizedSessionSnapshot,
17
- } from "./session-ingress.js";
18
-
19
- interface ActivityItemBase<
20
- TType extends string,
21
- TPayload extends { type: TType },
22
- > {
23
- id: string;
24
- type: TType;
25
- payload: TPayload;
26
- timestamp: number;
27
- }
28
-
29
- export type Notification =
30
- | (ActivityItemBase<
31
- "YOUR_TURN",
32
- { type: "YOUR_TURN"; activePlayers: string[] }
33
- > & {
34
- read: boolean;
35
- })
36
- | (ActivityItemBase<
37
- "PROMPT_OPENED",
38
- {
39
- type: "PROMPT_OPENED";
40
- promptId: string;
41
- promptInstanceId: string;
42
- targetPlayer: string;
43
- title?: string;
44
- }
45
- > & { read: boolean })
46
- | (ActivityItemBase<
47
- "ACTION_EXECUTED",
48
- { type: "ACTION_EXECUTED"; playerId: string; actionType: string }
49
- > & { read: boolean })
50
- | (ActivityItemBase<
51
- "ACTION_REJECTED",
52
- { type: "ACTION_REJECTED"; reason: string; targetPlayer?: string }
53
- > & { read: boolean })
54
- | (ActivityItemBase<
55
- "TURN_CHANGED",
56
- {
57
- type: "TURN_CHANGED";
58
- previousPlayers: string[];
59
- currentPlayers: string[];
60
- }
61
- > & { read: boolean })
62
- | (ActivityItemBase<
63
- "STATE_CHANGED",
64
- { type: "STATE_CHANGED"; newState: string }
65
- > & {
66
- read: boolean;
67
- })
68
- | (ActivityItemBase<
69
- "GAME_ENDED",
70
- {
71
- type: "GAME_ENDED";
72
- winner?: string;
73
- finalScores: Record<string, number>;
74
- reason: string;
75
- }
76
- > & { read: boolean })
77
- | (ActivityItemBase<
78
- "ERROR",
79
- { type: "ERROR"; message: string; code?: string }
80
- > & {
81
- read: boolean;
82
- });
83
-
84
- export type NotificationType = Notification["type"];
85
- export type NotificationPayload = Notification["payload"];
86
-
87
- export type HostFeedback =
88
- | ActivityItemBase<
89
- "ACTION_REJECTED",
90
- { type: "ACTION_REJECTED"; reason: string; targetPlayer?: string }
91
- >
92
- | ActivityItemBase<
93
- "PROMPT_OPENED",
94
- {
95
- type: "PROMPT_OPENED";
96
- promptId: string;
97
- promptInstanceId: string;
98
- targetPlayer: string;
99
- title?: string;
100
- }
101
- >
102
- | ActivityItemBase<
103
- "YOUR_TURN",
104
- { type: "YOUR_TURN"; activePlayers: string[] }
105
- >;
106
-
107
- export type HostFeedbackType = HostFeedback["type"];
108
- export type HostFeedbackPayload = HostFeedback["payload"];
109
-
110
- export interface SSEEventEntry {
111
- id: number;
112
- eventType: string;
113
- data: unknown;
114
- timestamp: string;
115
- }
116
-
117
- export interface ConnectionState {
118
- lobby: boolean;
119
- gameplay: boolean;
120
- error: string | null;
121
- isConnected: boolean;
122
- recovery: ConnectionRecoveryState;
123
- }
124
-
125
- export interface ConnectionRecoveryState {
126
- active: boolean;
127
- message: string | null;
128
- retryAfterMs: number | null;
129
- attempt: number | null;
130
- since: number | null;
131
- }
132
-
133
- export interface ActivityState {
134
- notifications: Notification[];
135
- hostFeedback: HostFeedback[];
136
- syncId: number;
137
- lastSyncTimestamp: number | null;
138
- lastAckTimestamp: number | null;
139
- }
140
-
141
- export interface DebugState {
142
- sseEvents: SSEEventEntry[];
143
- maxEvents: number;
144
- }
145
-
146
- export interface UnifiedSessionState {
147
- session: UnifiedSessionModel;
148
- connection: ConnectionState;
149
- activity: ActivityState;
150
- debug: DebugState;
151
- }
152
-
153
- export type SnapshotIngressSource =
154
- | "short-code"
155
- | "snapshot"
156
- | "start"
157
- | "dev-new";
158
-
159
- export type EventIngressSource = "sse" | "submit-response";
160
-
161
- export type SessionStateIngress =
162
- | {
163
- type: "command.loading";
164
- target?: { sessionId?: string; shortCode?: string };
165
- }
166
- | { type: "command.failed"; message: string }
167
- | {
168
- type: "snapshot.loaded";
169
- source: SnapshotIngressSource;
170
- snapshot: NormalizedSessionSnapshot;
171
- expectedPerspectivePlayerId?: string | null;
172
- }
173
- | {
174
- type: "event.received";
175
- source: EventIngressSource;
176
- event: NormalizedSessionEvent;
177
- debugEvent?: { eventType: string; data: unknown };
178
- clientActionId?: string | null;
179
- }
180
- | {
181
- type: "connection.prepared";
182
- userId: string | null;
183
- switchablePlayerIds: string[];
184
- }
185
- | {
186
- type: "connection.changed";
187
- channel: "lobby" | "gameplay";
188
- connected: boolean;
189
- }
190
- | { type: "connection.failed"; message: string }
191
- | {
192
- type: "connection.recovering";
193
- message: string;
194
- retryAfterMs: number;
195
- attempt: number;
196
- }
197
- | { type: "connection.errorCleared" }
198
- | {
199
- type: "feedback.actionRejected";
200
- reason: string;
201
- targetPlayer?: string;
202
- }
203
- | { type: "local.playerSelected"; playerId: string; sessionId: string }
204
- | { type: "activity.stateAcked" }
205
- | { type: "activity.notificationRead"; id: string }
206
- | { type: "activity.notificationsCleared" }
207
- | { type: "activity.hostFeedbackDismissed"; id: string }
208
- | { type: "activity.hostFeedbackCleared" }
209
- | { type: "debug.sseEventsCleared" }
210
- | { type: "streams.closed" }
211
- | { type: "session.reset" };
212
-
213
- export type SessionStateEffect =
214
- | {
215
- type: "requestGameplayResync";
216
- sessionId: string;
217
- playerId: string;
218
- reason: string;
219
- }
220
- | {
221
- type: "reconnectGameplay";
222
- sessionId: string;
223
- playerId: string;
224
- source: "player-switch";
225
- }
226
- | {
227
- type: "perf.storeApplied";
228
- gameplayVersion: number;
229
- syncId: number;
230
- }
231
- | {
232
- type: "log.warn";
233
- message: string;
234
- };
235
-
236
- export interface SessionStateReducerEnvironment {
237
- fallbackToAllSeatsWhenUserIdMissing: boolean;
238
- nextEventId: () => number;
239
- nextNotificationId: () => string;
240
- nowMs: () => number;
241
- nowIso: () => string;
242
- }
243
-
244
- export interface SessionStateReducerResult {
245
- state: UnifiedSessionState;
246
- effects: SessionStateEffect[];
247
- }
248
-
249
- export function createInitialUnifiedSessionState(): UnifiedSessionState {
250
- return {
251
- session: { type: "idle" },
252
- connection: {
253
- lobby: false,
254
- gameplay: false,
255
- error: null,
256
- isConnected: false,
257
- recovery: {
258
- active: false,
259
- message: null,
260
- retryAfterMs: null,
261
- attempt: null,
262
- since: null,
263
- },
264
- },
265
- activity: {
266
- notifications: [],
267
- hostFeedback: [],
268
- syncId: 0,
269
- lastSyncTimestamp: null,
270
- lastAckTimestamp: null,
271
- },
272
- debug: { sseEvents: [], maxEvents: 500 },
273
- };
274
- }
275
-
276
- function appendSseEvent(
277
- state: UnifiedSessionState,
278
- debugEvent: { eventType: string; data: unknown },
279
- env: SessionStateReducerEnvironment,
280
- ) {
281
- const newEvent: SSEEventEntry = {
282
- id: env.nextEventId(),
283
- eventType: debugEvent.eventType,
284
- data: debugEvent.data,
285
- timestamp: env.nowIso(),
286
- };
287
- return [...state.debug.sseEvents, newEvent].slice(-state.debug.maxEvents);
288
- }
289
-
290
- function createActionRejectedArtifacts(
291
- reason: string,
292
- targetPlayer: string | undefined,
293
- env: SessionStateReducerEnvironment,
294
- ) {
295
- return {
296
- notification: {
297
- id: env.nextNotificationId(),
298
- type: "ACTION_REJECTED" as const,
299
- payload: { type: "ACTION_REJECTED" as const, reason, targetPlayer },
300
- timestamp: env.nowMs(),
301
- read: false,
302
- },
303
- hostFeedback: {
304
- id: env.nextNotificationId(),
305
- type: "ACTION_REJECTED" as const,
306
- payload: { type: "ACTION_REJECTED" as const, reason, targetPlayer },
307
- timestamp: env.nowMs(),
308
- },
309
- };
310
- }
311
-
312
- function createPromptOpenedArtifacts(
313
- prompt: {
314
- interactionId: string;
315
- context?: { to?: string; title?: string };
316
- },
317
- env: SessionStateReducerEnvironment,
318
- ) {
319
- const payload = {
320
- type: "PROMPT_OPENED" as const,
321
- promptId: prompt.interactionId,
322
- promptInstanceId: prompt.interactionId,
323
- targetPlayer: prompt.context?.to ?? "",
324
- title: prompt.context?.title ?? undefined,
325
- };
326
- return {
327
- notification: {
328
- id: env.nextNotificationId(),
329
- type: "PROMPT_OPENED" as const,
330
- payload,
331
- timestamp: env.nowMs(),
332
- read: false,
333
- },
334
- hostFeedback: {
335
- id: env.nextNotificationId(),
336
- type: "PROMPT_OPENED" as const,
337
- payload,
338
- timestamp: env.nowMs(),
339
- },
340
- };
341
- }
342
-
343
- function getOpenedPrompts(
344
- previous: ReadonlyArray<{ kind: string; interactionId: string }>,
345
- next: ReadonlyArray<{
346
- kind: string;
347
- interactionId: string;
348
- context?: { to?: string; title?: string };
349
- }>,
350
- ) {
351
- const previousPromptIds = new Set(
352
- previous
353
- .filter((descriptor) => descriptor.kind === "prompt")
354
- .map((prompt) => prompt.interactionId),
355
- );
356
- return next.filter(
357
- (descriptor) =>
358
- descriptor.kind === "prompt" &&
359
- !previousPromptIds.has(descriptor.interactionId),
360
- );
361
- }
362
-
363
- function hasBoardStaticHashMismatch(
364
- gameplay: GameplayViewport | null,
365
- incomingGameplay: GameplayViewport,
366
- ): boolean {
367
- const incomingHash = incomingGameplay.boardStaticHash ?? null;
368
- const cachedHash = gameplay?.boardStaticHash ?? null;
369
- return (
370
- cachedHash !== null && incomingHash !== null && incomingHash !== cachedHash
371
- );
372
- }
373
-
374
- function hasRenderableGameplayView(gameplay: GameplayViewport): boolean {
375
- return gameplay.view !== null || gameplay.boardStatic !== null;
376
- }
377
-
378
- function shouldIgnoreGameplayUpdate(
379
- currentGameplay: GameplayViewport | null,
380
- incomingGameplay: GameplayViewport,
381
- ): boolean {
382
- if (!currentGameplay) return false;
383
- if (incomingGameplay.version < currentGameplay.version) return true;
384
- if (incomingGameplay.version > currentGameplay.version) return false;
385
- if (
386
- currentGameplay.actionSetVersion.endsWith(":history") &&
387
- !incomingGameplay.actionSetVersion.endsWith(":history")
388
- ) {
389
- return false;
390
- }
391
- if (
392
- !hasRenderableGameplayView(currentGameplay) &&
393
- hasRenderableGameplayView(incomingGameplay)
394
- ) {
395
- return false;
396
- }
397
- return true;
398
- }
399
-
400
- function applyGameplaySnapshotToState(
401
- state: UnifiedSessionState,
402
- gameplay: GameplayViewport,
403
- context: SessionContext,
404
- options: {
405
- nextSyncId: number;
406
- perspective: GameplayPerspective;
407
- debugEvent?: { eventType: string; data: unknown };
408
- env: SessionStateReducerEnvironment;
409
- },
410
- ): UnifiedSessionState {
411
- const previousGameplay = getGameplayViewport(state.session);
412
- const promptArtifacts = getOpenedPrompts(
413
- previousGameplay?.availableInteractions ?? [],
414
- gameplay.availableInteractions,
415
- ).map((prompt) => createPromptOpenedArtifacts(prompt, options.env));
416
- return {
417
- ...state,
418
- session: {
419
- type: "gameplay",
420
- context,
421
- perspective: options.perspective,
422
- gameplay,
423
- },
424
- activity: {
425
- ...state.activity,
426
- notifications: [
427
- ...state.activity.notifications,
428
- ...promptArtifacts.map((artifact) => artifact.notification),
429
- ],
430
- hostFeedback: [
431
- ...state.activity.hostFeedback,
432
- ...promptArtifacts.map((artifact) => artifact.hostFeedback),
433
- ],
434
- syncId: options.nextSyncId,
435
- lastSyncTimestamp: options.env.nowMs(),
436
- },
437
- debug: {
438
- ...state.debug,
439
- sseEvents: options.debugEvent
440
- ? appendSseEvent(state, options.debugEvent, options.env)
441
- : state.debug.sseEvents,
442
- },
443
- connection: {
444
- ...state.connection,
445
- error: null,
446
- recovery: {
447
- active: false,
448
- message: null,
449
- retryAfterMs: null,
450
- attempt: null,
451
- since: null,
452
- },
453
- },
454
- };
455
- }
456
-
457
- function assertExpectedPerspectivePlayer(
458
- snapshot: NormalizedSessionSnapshot,
459
- expectedPlayerId: string | null | undefined,
460
- ) {
461
- if (!expectedPlayerId || snapshot.type !== "gameplay") {
462
- return;
463
- }
464
- if (snapshot.perspective.playerId !== expectedPlayerId) {
465
- throw new Error(
466
- `Switch snapshot resolved ${snapshot.perspective.playerId} instead of ${expectedPlayerId}.`,
467
- );
468
- }
469
- }
470
-
471
- function reduceSnapshotLoaded(
472
- state: UnifiedSessionState,
473
- ingress: Extract<SessionStateIngress, { type: "snapshot.loaded" }>,
474
- env: SessionStateReducerEnvironment,
475
- ): UnifiedSessionState {
476
- assertExpectedPerspectivePlayer(
477
- ingress.snapshot,
478
- ingress.expectedPerspectivePlayerId,
479
- );
480
- const context = ingress.snapshot.context;
481
- const nextSyncId = state.activity.syncId + 1;
482
-
483
- if (ingress.snapshot.type !== "gameplay") {
484
- const session: UnifiedSessionModel =
485
- ingress.snapshot.type === "ended"
486
- ? { type: "ended", context }
487
- : {
488
- type: "lobby",
489
- context,
490
- preferredPlayerId: ingress.snapshot.preferredPlayerId,
491
- };
492
- return {
493
- ...state,
494
- session,
495
- activity: {
496
- ...state.activity,
497
- syncId: nextSyncId,
498
- lastSyncTimestamp: env.nowMs(),
499
- },
500
- connection: {
501
- ...state.connection,
502
- error: null,
503
- recovery: {
504
- active: false,
505
- message: null,
506
- retryAfterMs: null,
507
- attempt: null,
508
- since: null,
509
- },
510
- },
511
- };
512
- }
513
-
514
- return {
515
- ...applyGameplaySnapshotToState(state, ingress.snapshot.gameplay, context, {
516
- nextSyncId,
517
- perspective: ingress.snapshot.perspective,
518
- env,
519
- }),
520
- connection: {
521
- ...state.connection,
522
- error: null,
523
- recovery: {
524
- active: false,
525
- message: null,
526
- retryAfterMs: null,
527
- attempt: null,
528
- since: null,
529
- },
530
- },
531
- };
532
- }
533
-
534
- function reduceEventReceived(
535
- state: UnifiedSessionState,
536
- ingress: Extract<SessionStateIngress, { type: "event.received" }>,
537
- env: SessionStateReducerEnvironment,
538
- ): SessionStateReducerResult {
539
- const context = getSessionContext(state.session);
540
- const currentGameplay = getGameplayViewport(state.session);
541
- const nextSyncId = state.activity.syncId + 1;
542
- const debugEvent = ingress.source === "sse" ? ingress.debugEvent : undefined;
543
-
544
- switch (ingress.event.type) {
545
- case "session.snapshot": {
546
- const snapshotState = reduceSnapshotLoaded(
547
- state,
548
- {
549
- type: "snapshot.loaded",
550
- source: "snapshot",
551
- snapshot: ingress.event.snapshot,
552
- },
553
- env,
554
- );
555
- return {
556
- state: debugEvent
557
- ? {
558
- ...snapshotState,
559
- debug: {
560
- ...snapshotState.debug,
561
- sseEvents: appendSseEvent(state, debugEvent, env),
562
- },
563
- }
564
- : snapshotState,
565
- effects: [],
566
- };
567
- }
568
- case "session.lobbyUpdated": {
569
- const nextContext = ingress.event.context;
570
- if (!nextContext) return { state, effects: [] };
571
- return {
572
- state: {
573
- ...state,
574
- session: withContext(state.session, nextContext),
575
- activity: {
576
- ...state.activity,
577
- syncId: nextSyncId,
578
- lastSyncTimestamp: env.nowMs(),
579
- },
580
- debug: {
581
- ...state.debug,
582
- sseEvents: debugEvent
583
- ? appendSseEvent(state, debugEvent, env)
584
- : state.debug.sseEvents,
585
- },
586
- connection: {
587
- ...state.connection,
588
- error: null,
589
- recovery: {
590
- active: false,
591
- message: null,
592
- retryAfterMs: null,
593
- attempt: null,
594
- since: null,
595
- },
596
- },
597
- },
598
- effects: [],
599
- };
600
- }
601
- case "session.gameplayUpdated": {
602
- if (!context || !ingress.event.context) return { state, effects: [] };
603
- if (
604
- ingress.source === "sse" &&
605
- ((state.session.type === "gameplay" &&
606
- ingress.event.perspective.playerId !==
607
- state.session.perspective.playerId) ||
608
- (state.session.type === "gameplayLoading" &&
609
- ingress.event.perspective.playerId !==
610
- state.session.requestedPlayerId))
611
- ) {
612
- return { state, effects: [] };
613
- }
614
- if (shouldIgnoreGameplayUpdate(currentGameplay, ingress.event.gameplay)) {
615
- return { state, effects: [] };
616
- }
617
- const updatedState = applyGameplaySnapshotToState(
618
- state,
619
- ingress.event.gameplay,
620
- ingress.event.context,
621
- {
622
- nextSyncId,
623
- perspective: ingress.event.perspective,
624
- debugEvent,
625
- env,
626
- },
627
- );
628
- const effects: SessionStateEffect[] = [
629
- {
630
- type: "perf.storeApplied",
631
- gameplayVersion: ingress.event.gameplay.version,
632
- syncId: nextSyncId,
633
- },
634
- ];
635
- if (hasBoardStaticHashMismatch(currentGameplay, ingress.event.gameplay)) {
636
- effects.push({
637
- type: "requestGameplayResync",
638
- sessionId: context.identity.sessionId,
639
- playerId: ingress.event.perspective.playerId,
640
- reason: `boardStaticHash mismatch cached=${currentGameplay?.boardStaticHash} incoming=${ingress.event.gameplay.boardStaticHash}`,
641
- });
642
- }
643
- return { state: updatedState, effects };
644
- }
645
- case "session.historyUpdated": {
646
- const nextContext = ingress.event.context;
647
- if (!nextContext) return { state, effects: [] };
648
- return {
649
- state: {
650
- ...state,
651
- session: withContext(state.session, nextContext),
652
- activity: {
653
- ...state.activity,
654
- syncId: nextSyncId,
655
- lastSyncTimestamp: env.nowMs(),
656
- },
657
- debug: {
658
- ...state.debug,
659
- sseEvents: debugEvent
660
- ? appendSseEvent(state, debugEvent, env)
661
- : state.debug.sseEvents,
662
- },
663
- connection: {
664
- ...state.connection,
665
- error: null,
666
- recovery: {
667
- active: false,
668
- message: null,
669
- retryAfterMs: null,
670
- attempt: null,
671
- since: null,
672
- },
673
- },
674
- },
675
- effects: [],
676
- };
677
- }
678
- case "session.ended": {
679
- const nextContext = ingress.event.context;
680
- if (!nextContext) return { state, effects: [] };
681
- return {
682
- state: {
683
- ...state,
684
- session: { type: "ended", context: nextContext },
685
- activity: {
686
- ...state.activity,
687
- syncId: nextSyncId,
688
- lastSyncTimestamp: env.nowMs(),
689
- },
690
- debug: {
691
- ...state.debug,
692
- sseEvents: debugEvent
693
- ? appendSseEvent(state, debugEvent, env)
694
- : state.debug.sseEvents,
695
- },
696
- connection: {
697
- ...state.connection,
698
- error: null,
699
- recovery: {
700
- active: false,
701
- message: null,
702
- retryAfterMs: null,
703
- attempt: null,
704
- since: null,
705
- },
706
- },
707
- },
708
- effects: [],
709
- };
710
- }
711
- case "session.error":
712
- return {
713
- state: {
714
- ...state,
715
- session: {
716
- type: "error",
717
- message: ingress.event.message,
718
- context: context ?? undefined,
719
- },
720
- activity: {
721
- ...state.activity,
722
- syncId: nextSyncId,
723
- lastSyncTimestamp: env.nowMs(),
724
- },
725
- debug: {
726
- ...state.debug,
727
- sseEvents: debugEvent
728
- ? appendSseEvent(state, debugEvent, env)
729
- : state.debug.sseEvents,
730
- },
731
- },
732
- effects: [],
733
- };
734
- }
735
- }
736
-
737
- export function reduceSessionState(
738
- state: UnifiedSessionState,
739
- ingress: SessionStateIngress,
740
- env: SessionStateReducerEnvironment,
741
- ): SessionStateReducerResult {
742
- switch (ingress.type) {
743
- case "command.loading":
744
- return {
745
- state: {
746
- ...state,
747
- session: { type: "loading", target: ingress.target },
748
- connection: {
749
- ...state.connection,
750
- error: null,
751
- recovery: {
752
- active: false,
753
- message: null,
754
- retryAfterMs: null,
755
- attempt: null,
756
- since: null,
757
- },
758
- },
759
- },
760
- effects: [],
761
- };
762
- case "command.failed":
763
- return {
764
- state: {
765
- ...state,
766
- session: {
767
- type: "error",
768
- message: ingress.message,
769
- context: getSessionContext(state.session) ?? undefined,
770
- },
771
- },
772
- effects: [],
773
- };
774
- case "snapshot.loaded":
775
- return {
776
- state: reduceSnapshotLoaded(state, ingress, env),
777
- effects: [],
778
- };
779
- case "event.received":
780
- return reduceEventReceived(state, ingress, env);
781
- case "connection.prepared": {
782
- const context = getSessionContext(state.session);
783
- if (!context) return { state, effects: [] };
784
- return {
785
- state: {
786
- ...state,
787
- session: withContext(state.session, {
788
- ...context,
789
- userId: ingress.userId,
790
- switchablePlayerIds: ingress.switchablePlayerIds,
791
- }),
792
- connection: {
793
- ...state.connection,
794
- error: null,
795
- recovery: {
796
- active: false,
797
- message: null,
798
- retryAfterMs: null,
799
- attempt: null,
800
- since: null,
801
- },
802
- },
803
- },
804
- effects: [],
805
- };
806
- }
807
- case "connection.changed": {
808
- const connection = {
809
- ...state.connection,
810
- [ingress.channel]: ingress.connected,
811
- };
812
- return {
813
- state: {
814
- ...state,
815
- connection: {
816
- ...connection,
817
- isConnected: connection.lobby || connection.gameplay,
818
- recovery: ingress.connected
819
- ? {
820
- active: false,
821
- message: null,
822
- retryAfterMs: null,
823
- attempt: null,
824
- since: null,
825
- }
826
- : connection.recovery,
827
- },
828
- },
829
- effects: [],
830
- };
831
- }
832
- case "connection.failed":
833
- return {
834
- state: {
835
- ...state,
836
- connection: {
837
- ...state.connection,
838
- error: ingress.message,
839
- recovery: {
840
- active: false,
841
- message: null,
842
- retryAfterMs: null,
843
- attempt: null,
844
- since: null,
845
- },
846
- },
847
- },
848
- effects: [],
849
- };
850
- case "connection.recovering":
851
- return {
852
- state: {
853
- ...state,
854
- connection: {
855
- ...state.connection,
856
- error: null,
857
- recovery: {
858
- active: true,
859
- message: ingress.message,
860
- retryAfterMs: ingress.retryAfterMs,
861
- attempt: ingress.attempt,
862
- since: state.connection.recovery.since ?? env.nowMs(),
863
- },
864
- },
865
- },
866
- effects: [],
867
- };
868
- case "connection.errorCleared":
869
- return {
870
- state: {
871
- ...state,
872
- connection: { ...state.connection, error: null },
873
- },
874
- effects: [],
875
- };
876
- case "feedback.actionRejected": {
877
- const { notification, hostFeedback } = createActionRejectedArtifacts(
878
- ingress.reason,
879
- ingress.targetPlayer,
880
- env,
881
- );
882
- return {
883
- state: {
884
- ...state,
885
- activity: {
886
- ...state.activity,
887
- notifications: [...state.activity.notifications, notification],
888
- hostFeedback: [...state.activity.hostFeedback, hostFeedback],
889
- },
890
- },
891
- effects: [],
892
- };
893
- }
894
- case "local.playerSelected": {
895
- const context = getSessionContext(state.session);
896
- if (!context) return { state, effects: [] };
897
- const controllablePlayerIds = resolveControllablePlayerIds(
898
- context.switchablePlayerIds,
899
- context.seats,
900
- context.userId,
901
- env.fallbackToAllSeatsWhenUserIdMissing,
902
- );
903
- if (!controllablePlayerIds.includes(ingress.playerId)) {
904
- return {
905
- state,
906
- effects: [
907
- {
908
- type: "log.warn",
909
- message: `[UnifiedSession] Cannot switch to ${ingress.playerId} - not controllable`,
910
- },
911
- ],
912
- };
913
- }
914
- const currentGameplay = getGameplayViewport(state.session);
915
- const localGameplay = currentGameplay
916
- ? gameplayViewportForPlayer(currentGameplay, ingress.playerId)
917
- : null;
918
- if (localGameplay) {
919
- return {
920
- state: applyGameplaySnapshotToState(state, localGameplay, context, {
921
- nextSyncId: state.activity.syncId + 1,
922
- perspective: { playerId: ingress.playerId },
923
- env,
924
- }),
925
- effects: [
926
- {
927
- type: "reconnectGameplay",
928
- sessionId: ingress.sessionId,
929
- playerId: ingress.playerId,
930
- source: "player-switch",
931
- },
932
- ],
933
- };
934
- }
935
- return {
936
- state: {
937
- ...state,
938
- session: {
939
- type: "gameplayLoading",
940
- context,
941
- requestedPlayerId: ingress.playerId,
942
- },
943
- },
944
- effects: [
945
- {
946
- type: "reconnectGameplay",
947
- sessionId: ingress.sessionId,
948
- playerId: ingress.playerId,
949
- source: "player-switch",
950
- },
951
- ],
952
- };
953
- }
954
- case "activity.stateAcked":
955
- return {
956
- state: {
957
- ...state,
958
- activity: { ...state.activity, lastAckTimestamp: env.nowMs() },
959
- },
960
- effects: [],
961
- };
962
- case "activity.notificationRead":
963
- return {
964
- state: {
965
- ...state,
966
- activity: {
967
- ...state.activity,
968
- notifications: state.activity.notifications.map((notification) =>
969
- notification.id === ingress.id
970
- ? { ...notification, read: true }
971
- : notification,
972
- ),
973
- },
974
- },
975
- effects: [],
976
- };
977
- case "activity.notificationsCleared":
978
- return {
979
- state: {
980
- ...state,
981
- activity: { ...state.activity, notifications: [] },
982
- },
983
- effects: [],
984
- };
985
- case "activity.hostFeedbackDismissed":
986
- return {
987
- state: {
988
- ...state,
989
- activity: {
990
- ...state.activity,
991
- hostFeedback: state.activity.hostFeedback.filter(
992
- (item) => item.id !== ingress.id,
993
- ),
994
- },
995
- },
996
- effects: [],
997
- };
998
- case "activity.hostFeedbackCleared":
999
- return {
1000
- state: {
1001
- ...state,
1002
- activity: { ...state.activity, hostFeedback: [] },
1003
- },
1004
- effects: [],
1005
- };
1006
- case "debug.sseEventsCleared":
1007
- return {
1008
- state: { ...state, debug: { ...state.debug, sseEvents: [] } },
1009
- effects: [],
1010
- };
1011
- case "streams.closed":
1012
- return {
1013
- state: {
1014
- ...state,
1015
- connection: {
1016
- ...state.connection,
1017
- lobby: false,
1018
- gameplay: false,
1019
- isConnected: false,
1020
- recovery: {
1021
- active: false,
1022
- message: null,
1023
- retryAfterMs: null,
1024
- attempt: null,
1025
- since: null,
1026
- },
1027
- },
1028
- },
1029
- effects: [],
1030
- };
1031
- case "session.reset":
1032
- return { state: createInitialUnifiedSessionState(), effects: [] };
1033
- }
1034
- }