@joystyk/session 0.0.0-stage → 0.2.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 (55) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +36 -0
  4. package/README.md +266 -2
  5. package/dist/assistance.d.ts +3 -0
  6. package/dist/assistance.js +20 -0
  7. package/dist/assistance.js.map +1 -0
  8. package/dist/browser/audio-sounds.d.ts +19 -0
  9. package/dist/browser/audio-sounds.js +413 -0
  10. package/dist/browser/audio-sounds.js.map +1 -0
  11. package/dist/browser/audio.d.ts +16 -0
  12. package/dist/browser/audio.js +182 -0
  13. package/dist/browser/audio.js.map +1 -0
  14. package/dist/browser/index.d.ts +48 -0
  15. package/dist/browser/index.js +140 -0
  16. package/dist/browser/index.js.map +1 -0
  17. package/dist/browser/preparer.d.ts +3 -0
  18. package/dist/browser/preparer.js +73 -0
  19. package/dist/browser/preparer.js.map +1 -0
  20. package/dist/browser/rooms.d.ts +35 -0
  21. package/dist/browser/rooms.js +140 -0
  22. package/dist/browser/rooms.js.map +1 -0
  23. package/dist/fixed-step.d.ts +66 -0
  24. package/dist/fixed-step.js +280 -0
  25. package/dist/fixed-step.js.map +1 -0
  26. package/dist/index.d.ts +6 -0
  27. package/dist/index.js +7 -0
  28. package/dist/local.d.ts +130 -0
  29. package/dist/local.js +723 -0
  30. package/dist/local.js.map +1 -0
  31. package/dist/ports.d.ts +70 -0
  32. package/dist/ports.js +10 -0
  33. package/dist/ports.js.map +1 -0
  34. package/dist/publisher.d.ts +11 -0
  35. package/dist/publisher.js +44 -0
  36. package/dist/publisher.js.map +1 -0
  37. package/dist/remote.d.ts +128 -0
  38. package/dist/remote.js +474 -0
  39. package/dist/remote.js.map +1 -0
  40. package/dist/replay.d.ts +65 -0
  41. package/dist/replay.js +199 -0
  42. package/dist/replay.js.map +1 -0
  43. package/dist/rngs.d.ts +22 -0
  44. package/dist/rngs.js +34 -0
  45. package/dist/rngs.js.map +1 -0
  46. package/dist/saves.d.ts +20 -0
  47. package/dist/saves.js +59 -0
  48. package/dist/saves.js.map +1 -0
  49. package/dist/stopwatch.d.ts +22 -0
  50. package/dist/stopwatch.js +51 -0
  51. package/dist/stopwatch.js.map +1 -0
  52. package/dist/values.d.ts +19 -0
  53. package/dist/values.js +66 -0
  54. package/dist/values.js.map +1 -0
  55. package/package.json +56 -3
@@ -0,0 +1,280 @@
1
+ import { compact, decode, isRecord, jsonEqual } from "./values.js";
2
+ import { alwaysVisible } from "./ports.js";
3
+ import { createPublisher } from "./publisher.js";
4
+ import { defaultRestoreRng } from "./rngs.js";
5
+ import { replaySimulation } from "./replay.js";
6
+ import { isSeatInfo, loadSave } from "./saves.js";
7
+ import { err, gameError, ok } from "@joystyk/contracts";
8
+ //#region src/fixed-step.ts
9
+ /** Starts a new real-time game. Fails when the config is invalid or `init` refuses it. */
10
+ var createFixedStepRunner = (simulation, options) => {
11
+ if (options.seats.length === 0 || !options.seats.every((seat, i) => isSeatInfo(seat) && seat.id === i)) return err(gameError("InvalidSeats", "The seats must be numbered 0, 1, 2 and so on."));
12
+ const config = decode(simulation.schemas.config, options.config, "game setup");
13
+ if (!config.ok) return config;
14
+ const rng = (options.restoreRng ?? defaultRestoreRng)(options.seed);
15
+ const initialized = simulation.init(config.value, rng);
16
+ if (!initialized.ok) return initialized;
17
+ return ok(run(simulation, options, {
18
+ state: initialized.value,
19
+ rng,
20
+ tick: 0,
21
+ seats: options.seats,
22
+ log: {
23
+ config: config.value,
24
+ seed: options.seed,
25
+ inputs: []
26
+ }
27
+ }));
28
+ };
29
+ /**
30
+ * Resumes a checkpoint. When the checkpoint carries a replay that still reproduces it, the
31
+ * replay log continues; otherwise the game resumes without one.
32
+ */
33
+ var restoreFixedStepRunner = (simulation, save, options) => {
34
+ const loaded = loadSave(save, options.manifest, simulation.rulesVersion, options.migrations, simulation.schemas.state);
35
+ if (!loaded.ok) return loaded;
36
+ const { envelope, state } = loaded.value;
37
+ const restoreRng = options.restoreRng ?? defaultRestoreRng;
38
+ if (envelope.tick === void 0) return err(gameError("SaveUnreadable", "This saved game could not be read.", { why: "tick" }));
39
+ let log = null;
40
+ const replay = envelope.replay;
41
+ if (isRecord(replay) && replay["layer"] === 3 && replay["endTick"] === envelope.tick && Array.isArray(replay["inputs"])) {
42
+ const config = decode(simulation.schemas.config, replay["config"], "saved setup");
43
+ const inputs = [];
44
+ let valid = config.ok;
45
+ for (const entry of replay["inputs"]) {
46
+ if (!isRecord(entry) || !Number.isInteger(entry["tick"])) {
47
+ valid = false;
48
+ break;
49
+ }
50
+ const input = decode(simulation.schemas.input, entry["input"], "saved input");
51
+ if (!input.ok) {
52
+ valid = false;
53
+ break;
54
+ }
55
+ inputs.push({
56
+ tick: entry["tick"],
57
+ input: input.value
58
+ });
59
+ }
60
+ if (valid && config.ok) {
61
+ const candidate = {
62
+ ...replay,
63
+ config: config.value,
64
+ inputs
65
+ };
66
+ const replayed = replaySimulation(simulation, candidate, restoreRng);
67
+ if (replayed.ok && jsonEqual(replayed.value.state, state) && jsonEqual(replayed.value.rng, envelope.rng)) log = {
68
+ config: config.value,
69
+ seed: candidate.seed,
70
+ inputs
71
+ };
72
+ }
73
+ }
74
+ if (log === null) options.report?.(/* @__PURE__ */ new Error("The checkpoint has no usable replay."), "Restored a checkpoint without its replay.");
75
+ return ok(run(simulation, options, {
76
+ state,
77
+ rng: restoreRng(envelope.rng),
78
+ tick: envelope.tick,
79
+ seats: envelope.seats,
80
+ log
81
+ }));
82
+ };
83
+ var run = (simulation, options, start) => {
84
+ const { manifest, frames } = options;
85
+ const restoreRng = options.restoreRng ?? defaultRestoreRng;
86
+ const report = options.report ?? (() => {});
87
+ const visibility = options.visibility ?? alwaysVisible;
88
+ const stepMs = 1e3 / simulation.stepHz;
89
+ const maxSteps = Math.max(1, options.maxStepsPerFrame ?? Math.ceil(simulation.stepHz / 10));
90
+ let state = start.state;
91
+ let rng = start.rng;
92
+ let tick = start.tick;
93
+ const log = start.log;
94
+ let revision = 0;
95
+ let notice = null;
96
+ let hidden = visibility.hidden();
97
+ let userPaused = options.startPaused ?? false;
98
+ let broken = false;
99
+ let disposed = false;
100
+ let accumulator = 0;
101
+ let lastFrame = null;
102
+ let cancelFrame = null;
103
+ /** Inputs waiting for the next tick, in arrival order. */
104
+ let queued = [];
105
+ const finished = () => simulation.outcome(state) !== null;
106
+ const paused = () => hidden || userPaused;
107
+ const elapsedMs = () => Math.round(tick * stepMs);
108
+ const snapshot = (events) => {
109
+ const outcome = simulation.outcome(state);
110
+ return {
111
+ meta: {
112
+ connection: "local",
113
+ seat: options.seat === void 0 ? 0 : options.seat,
114
+ revision,
115
+ seats: start.seats,
116
+ outcome: outcome === null ? null : {
117
+ ...outcome,
118
+ elapsedMs: elapsedMs()
119
+ },
120
+ safePoint: simulation.safePoint?.(state) ?? (outcome !== null || paused()),
121
+ notice
122
+ },
123
+ view: simulation.view(state),
124
+ tick,
125
+ paused: paused(),
126
+ events,
127
+ elapsedMs: elapsedMs()
128
+ };
129
+ };
130
+ const replay = () => log === null ? null : {
131
+ layer: 3,
132
+ gameId: manifest.id,
133
+ rulesVersion: simulation.rulesVersion,
134
+ config: log.config,
135
+ seed: log.seed,
136
+ inputs: log.inputs,
137
+ endTick: tick
138
+ };
139
+ const envelope = () => compact({
140
+ gameId: manifest.id,
141
+ build: manifest.versions.build,
142
+ rulesVersion: simulation.rulesVersion,
143
+ saveVersion: manifest.versions.save,
144
+ rng: rng.save(),
145
+ seats: start.seats,
146
+ state,
147
+ tick,
148
+ elapsedMs: elapsedMs(),
149
+ replay: replay() ?? void 0
150
+ });
151
+ let saving = Promise.resolve();
152
+ const persist = () => {
153
+ const store = options.storage;
154
+ if (store === void 0) return;
155
+ const save = envelope();
156
+ saving = saving.then(() => store.write(save)).catch((error) => report(error, "Saving the checkpoint failed."));
157
+ };
158
+ const publisher = createPublisher(snapshot([]), {
159
+ freeze: options.freeze ?? true,
160
+ report
161
+ });
162
+ const publish = (events) => {
163
+ revision += 1;
164
+ publisher.publish(snapshot(events));
165
+ };
166
+ const running = () => !disposed && !broken && !paused() && !finished();
167
+ const requestFrame = () => {
168
+ if (cancelFrame === null && running()) cancelFrame = frames.request(onFrame);
169
+ };
170
+ const stopFrames = () => {
171
+ cancelFrame?.();
172
+ cancelFrame = null;
173
+ lastFrame = null;
174
+ accumulator = 0;
175
+ };
176
+ const step = () => {
177
+ const inputs = queued;
178
+ queued = [];
179
+ for (const input of inputs) log?.inputs.push({
180
+ tick,
181
+ input
182
+ });
183
+ const result = simulation.step(state, inputs, rng);
184
+ state = result.state;
185
+ tick += 1;
186
+ return [...result.events ?? []];
187
+ };
188
+ function onFrame(now) {
189
+ cancelFrame = null;
190
+ if (!running()) return;
191
+ if (lastFrame === null) {
192
+ lastFrame = now;
193
+ return requestFrame();
194
+ }
195
+ accumulator += Math.max(0, now - lastFrame);
196
+ lastFrame = now;
197
+ let steps = Math.floor((accumulator + 1e-6) / stepMs);
198
+ if (steps > maxSteps) {
199
+ accumulator %= stepMs;
200
+ steps = maxSteps;
201
+ } else accumulator = Math.max(0, accumulator - steps * stepMs);
202
+ const events = [];
203
+ for (let i = 0; i < steps && !finished(); i += 1) {
204
+ const savedRng = rng.save();
205
+ const savedQueue = queued;
206
+ try {
207
+ events.push(...step());
208
+ } catch (error) {
209
+ rng = restoreRng(savedRng);
210
+ queued = savedQueue;
211
+ if (log !== null) log.inputs.splice(log.inputs.length - savedQueue.length);
212
+ broken = true;
213
+ report(error, `The simulation threw at tick ${tick}.`);
214
+ notice = {
215
+ code: "InternalError",
216
+ message: "Something went wrong. The game has stopped."
217
+ };
218
+ break;
219
+ }
220
+ }
221
+ if (steps > 0 || broken) {
222
+ if (!broken) notice = null;
223
+ if (finished() || broken || simulation.checkpoint?.(state, events) === true) persist();
224
+ publish(events);
225
+ }
226
+ requestFrame();
227
+ }
228
+ const setPaused = (next) => {
229
+ const was = paused();
230
+ if (next.hidden !== void 0) hidden = next.hidden;
231
+ if (next.user !== void 0) userPaused = next.user;
232
+ if (was === paused()) return;
233
+ if (paused()) {
234
+ stopFrames();
235
+ persist();
236
+ } else requestFrame();
237
+ publish([]);
238
+ };
239
+ const stopVisibility = visibility.subscribe((value) => {
240
+ if (!disposed) setPaused({ hidden: value });
241
+ });
242
+ requestFrame();
243
+ return {
244
+ get: () => publisher.get(),
245
+ subscribe: (listener) => publisher.subscribe(listener),
246
+ dispatch: (raw) => {
247
+ if (disposed) return;
248
+ if (!isRecord(raw)) return;
249
+ if (raw["kind"] === "pause" || raw["kind"] === "resume") return setPaused({ user: raw["kind"] === "pause" });
250
+ if (raw["kind"] !== "input") return;
251
+ const reject = (code, message) => {
252
+ notice = {
253
+ code,
254
+ message
255
+ };
256
+ publish([]);
257
+ };
258
+ if (finished() || broken) return reject("GameOver", "The game is over.");
259
+ if (paused()) return reject("Paused", "The game is paused.");
260
+ const input = decode(simulation.schemas.input, raw["input"], "input");
261
+ if (!input.ok) return reject(input.error.code, input.error.message);
262
+ queued.push(input.value);
263
+ },
264
+ dispose: () => {
265
+ if (disposed) return;
266
+ persist();
267
+ disposed = true;
268
+ stopFrames();
269
+ stopVisibility();
270
+ publisher.close();
271
+ },
272
+ alpha: () => Math.min(1, accumulator / stepMs),
273
+ save: envelope,
274
+ replay
275
+ };
276
+ };
277
+ //#endregion
278
+ export { createFixedStepRunner, restoreFixedStepRunner };
279
+
280
+ //# sourceMappingURL=fixed-step.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fixed-step.js","names":[],"sources":["../src/fixed-step.ts"],"sourcesContent":["// The Layer 3 scheduler: runs a fixed-step simulation against real time. It accumulates\n// frame time, runs as many ticks as that covers (with a cap on catch-up), assigns inputs to\n// the next tick in arrival order, pauses when the page is hidden, and publishes a snapshot\n// at frame cadence. Game time is ticks, so it is deterministic and replayable.\nimport {\n err,\n gameError,\n ok,\n type FixedStepSimulation,\n type GameError,\n type GameSession,\n type Json,\n type Migration,\n type Notice,\n type Result,\n type Rng,\n type RngState,\n type SaveEnvelope,\n type SeatId,\n type SeatInfo,\n type SimulationReplay,\n type SimulationSnapshot,\n type TickInput,\n} from '@joystyk/contracts';\nimport {\n alwaysVisible,\n type FrameScheduler,\n type Reporter,\n type RngFactory,\n type SaveStore,\n type Visibility,\n} from './ports.ts';\nimport { createPublisher } from './publisher.ts';\nimport { replaySimulation } from './replay.ts';\nimport { defaultRestoreRng } from './rngs.ts';\nimport { isSeatInfo, loadSave, type RunnerManifest } from './saves.ts';\nimport { compact, decode, isRecord, jsonEqual } from './values.ts';\n\n/** What the game's UI dispatches to a fixed-step runner. */\nexport type FixedStepInput<Input> =\n { readonly kind: 'input'; readonly input: Input } | { readonly kind: 'pause' } | { readonly kind: 'resume' };\n\n/** The contract's `SimulationSnapshot` plus the tick events since the last snapshot. */\nexport type FixedStepSnapshot<View, Event> = SimulationSnapshot<View> & {\n /** One-off events from the ticks since the previous snapshot, for sound and effects. */\n readonly events: readonly Event[];\n /** Active play time: ticks × step length, so a pause stops it. */\n readonly elapsedMs: number;\n};\n\nexport interface FixedStepRunner<Input, View, Event> extends GameSession<\n FixedStepSnapshot<View, Event>,\n FixedStepInput<Input>\n> {\n /**\n * How far real time has run into the next tick, from 0 up to 1. A renderer may interpolate\n * between the last two states with it; it never changes the simulation.\n */\n alpha(): number;\n /** A checkpoint of the current tick. */\n save(): SaveEnvelope;\n /** The replay log so far, or null after restoring a checkpoint that had none. */\n replay(): SimulationReplay | null;\n}\n\nexport type FixedStepPorts = {\n readonly manifest: RunnerManifest;\n /** Restores the random state. Default: the kit's `restoreRng`. */\n readonly restoreRng?: RngFactory;\n readonly frames: FrameScheduler;\n readonly visibility?: Visibility;\n /** Checkpoints go here on the simulation's request, pause, hide, outcome and dispose. */\n readonly storage?: SaveStore;\n /** The viewer. Default: seat 0. */\n readonly seat?: SeatId | null;\n /**\n * The most ticks one frame may run. After a long stall the rest of the backlog is dropped\n * instead of freezing the page. Default: a tenth of a second's worth (6 at 60 Hz).\n */\n readonly maxStepsPerFrame?: number;\n /** Start paused, e.g. behind a \"Press to start\" screen. Default false. */\n readonly startPaused?: boolean;\n readonly freeze?: boolean;\n readonly report?: Reporter;\n};\n\nexport type FixedStepOptions = FixedStepPorts & {\n readonly seats: readonly SeatInfo[];\n /** Decoded with the simulation's config schema. */\n readonly config: unknown;\n readonly seed: RngState;\n};\n\nexport type FixedStepRestoreOptions = FixedStepPorts & {\n readonly migrations: readonly Migration[];\n};\n\ntype Start<State, Input, Config> = {\n readonly state: State;\n readonly rng: Rng;\n readonly tick: number;\n readonly seats: readonly SeatInfo[];\n readonly log: { readonly config: Config; readonly seed: RngState; readonly inputs: TickInput<Input>[] } | null;\n};\n\n/** Starts a new real-time game. Fails when the config is invalid or `init` refuses it. */\nexport const createFixedStepRunner = <\n State extends Json,\n Input extends Json,\n View,\n Event extends Json,\n Config extends Json,\n>(\n simulation: FixedStepSimulation<State, Input, View, Event, Config>,\n options: FixedStepOptions,\n): Result<FixedStepRunner<Input, View, Event>, GameError> => {\n if (options.seats.length === 0 || !options.seats.every((seat, i) => isSeatInfo(seat) && seat.id === i)) {\n return err(gameError('InvalidSeats', 'The seats must be numbered 0, 1, 2 and so on.'));\n }\n const config = decode(simulation.schemas.config, options.config, 'game setup');\n if (!config.ok) return config;\n const rng = (options.restoreRng ?? defaultRestoreRng)(options.seed);\n const initialized = simulation.init(config.value, rng);\n if (!initialized.ok) return initialized;\n return ok(\n run(simulation, options, {\n state: initialized.value,\n rng,\n tick: 0,\n seats: options.seats,\n log: { config: config.value, seed: options.seed, inputs: [] },\n }),\n );\n};\n\n/**\n * Resumes a checkpoint. When the checkpoint carries a replay that still reproduces it, the\n * replay log continues; otherwise the game resumes without one.\n */\nexport const restoreFixedStepRunner = <\n State extends Json,\n Input extends Json,\n View,\n Event extends Json,\n Config extends Json,\n>(\n simulation: FixedStepSimulation<State, Input, View, Event, Config>,\n save: unknown,\n options: FixedStepRestoreOptions,\n): Result<FixedStepRunner<Input, View, Event>, GameError> => {\n const loaded = loadSave(\n save,\n options.manifest,\n simulation.rulesVersion,\n options.migrations,\n simulation.schemas.state,\n );\n if (!loaded.ok) return loaded;\n const { envelope, state } = loaded.value;\n const restoreRng = options.restoreRng ?? defaultRestoreRng;\n if (envelope.tick === undefined)\n return err(gameError('SaveUnreadable', 'This saved game could not be read.', { why: 'tick' }));\n let log: Start<State, Input, Config>['log'] = null;\n const replay: unknown = envelope.replay;\n if (\n isRecord(replay) &&\n replay['layer'] === 3 &&\n replay['endTick'] === envelope.tick &&\n Array.isArray(replay['inputs'])\n ) {\n const config = decode(simulation.schemas.config, replay['config'], 'saved setup');\n const inputs: TickInput<Input>[] = [];\n let valid = config.ok;\n for (const entry of replay['inputs'] as unknown[]) {\n if (!isRecord(entry) || !Number.isInteger(entry['tick'])) {\n valid = false;\n break;\n }\n const input = decode(simulation.schemas.input, entry['input'], 'saved input');\n if (!input.ok) {\n valid = false;\n break;\n }\n inputs.push({ tick: entry['tick'] as number, input: input.value });\n }\n if (valid && config.ok) {\n const candidate = { ...(replay as unknown as SimulationReplay<Input, Config>), config: config.value, inputs };\n const replayed = replaySimulation(simulation, candidate, restoreRng);\n if (replayed.ok && jsonEqual(replayed.value.state, state) && jsonEqual(replayed.value.rng, envelope.rng)) {\n log = { config: config.value, seed: candidate.seed, inputs };\n }\n }\n }\n if (log === null)\n options.report?.(new Error('The checkpoint has no usable replay.'), 'Restored a checkpoint without its replay.');\n return ok(\n run(simulation, options, { state, rng: restoreRng(envelope.rng), tick: envelope.tick, seats: envelope.seats, log }),\n );\n};\n\nconst run = <State extends Json, Input extends Json, View, Event extends Json, Config extends Json>(\n simulation: FixedStepSimulation<State, Input, View, Event, Config>,\n options: FixedStepPorts,\n start: Start<State, Input, Config>,\n): FixedStepRunner<Input, View, Event> => {\n const { manifest, frames } = options;\n const restoreRng = options.restoreRng ?? defaultRestoreRng;\n const report: Reporter = options.report ?? (() => {});\n const visibility = options.visibility ?? alwaysVisible;\n const stepMs = 1000 / simulation.stepHz;\n const maxSteps = Math.max(1, options.maxStepsPerFrame ?? Math.ceil(simulation.stepHz / 10));\n\n let state = start.state;\n let rng = start.rng;\n let tick = start.tick;\n const log = start.log;\n let revision = 0;\n let notice: Notice | null = null;\n let hidden = visibility.hidden();\n let userPaused = options.startPaused ?? false;\n let broken = false;\n let disposed = false;\n let accumulator = 0;\n let lastFrame: number | null = null;\n let cancelFrame: (() => void) | null = null;\n /** Inputs waiting for the next tick, in arrival order. */\n let queued: Input[] = [];\n\n const finished = (): boolean => simulation.outcome(state) !== null;\n const paused = (): boolean => hidden || userPaused;\n const elapsedMs = (): number => Math.round(tick * stepMs);\n\n const snapshot = (events: readonly Event[]): FixedStepSnapshot<View, Event> => {\n const outcome = simulation.outcome(state);\n return {\n meta: {\n connection: 'local',\n seat: options.seat === undefined ? 0 : options.seat,\n revision,\n seats: start.seats,\n outcome: outcome === null ? null : { ...outcome, elapsedMs: elapsedMs() },\n safePoint: simulation.safePoint?.(state) ?? (outcome !== null || paused()),\n notice,\n },\n view: simulation.view(state),\n tick,\n paused: paused(),\n events,\n elapsedMs: elapsedMs(),\n };\n };\n\n const replay = (): SimulationReplay | null =>\n log === null\n ? null\n : {\n layer: 3,\n gameId: manifest.id,\n rulesVersion: simulation.rulesVersion,\n config: log.config,\n seed: log.seed,\n inputs: log.inputs,\n endTick: tick,\n };\n\n const envelope = (): SaveEnvelope =>\n compact({\n gameId: manifest.id,\n build: manifest.versions.build,\n rulesVersion: simulation.rulesVersion,\n saveVersion: manifest.versions.save,\n rng: rng.save(),\n seats: start.seats,\n state,\n tick,\n elapsedMs: elapsedMs(),\n replay: replay() ?? undefined,\n });\n\n let saving: Promise<void> = Promise.resolve();\n const persist = (): void => {\n const store = options.storage;\n if (store === undefined) return;\n const save = envelope();\n saving = saving\n .then(() => store.write(save))\n .catch((error: unknown) => report(error, 'Saving the checkpoint failed.'));\n };\n\n const publisher = createPublisher(snapshot([]), { freeze: options.freeze ?? true, report });\n const publish = (events: readonly Event[]): void => {\n revision += 1;\n publisher.publish(snapshot(events));\n };\n\n const running = (): boolean => !disposed && !broken && !paused() && !finished();\n\n const requestFrame = (): void => {\n if (cancelFrame === null && running()) cancelFrame = frames.request(onFrame);\n };\n\n const stopFrames = (): void => {\n cancelFrame?.();\n cancelFrame = null;\n // Resume starts from a fresh frame: hidden or paused time never becomes ticks.\n lastFrame = null;\n accumulator = 0;\n };\n\n const step = (): Event[] => {\n const inputs = queued;\n queued = [];\n for (const input of inputs) log?.inputs.push({ tick, input });\n const result = simulation.step(state, inputs, rng);\n state = result.state;\n tick += 1;\n return [...(result.events ?? [])];\n };\n\n function onFrame(now: number): void {\n cancelFrame = null;\n if (!running()) return;\n if (lastFrame === null) {\n lastFrame = now;\n return requestFrame();\n }\n accumulator += Math.max(0, now - lastFrame);\n lastFrame = now;\n // A small epsilon keeps 16.666… ms frames from landing a hair short of a tick.\n let steps = Math.floor((accumulator + 1e-6) / stepMs);\n if (steps > maxSteps) {\n // Bounded catch-up: run the cap and drop the backlog, keeping the fraction.\n accumulator %= stepMs;\n steps = maxSteps;\n } else {\n accumulator = Math.max(0, accumulator - steps * stepMs);\n }\n const events: Event[] = [];\n for (let i = 0; i < steps && !finished(); i += 1) {\n const savedRng = rng.save();\n const savedQueue = queued;\n try {\n events.push(...step());\n } catch (error) {\n // A throwing step is a bug: keep the last good tick and stop.\n rng = restoreRng(savedRng);\n queued = savedQueue;\n if (log !== null) log.inputs.splice(log.inputs.length - savedQueue.length);\n broken = true;\n report(error, `The simulation threw at tick ${tick}.`);\n notice = { code: 'InternalError', message: 'Something went wrong. The game has stopped.' };\n break;\n }\n }\n if (steps > 0 || broken) {\n if (!broken) notice = null;\n if (finished() || broken || simulation.checkpoint?.(state, events) === true) persist();\n publish(events);\n }\n requestFrame();\n }\n\n const setPaused = (next: { readonly hidden?: boolean; readonly user?: boolean }): void => {\n const was = paused();\n if (next.hidden !== undefined) hidden = next.hidden;\n if (next.user !== undefined) userPaused = next.user;\n if (was === paused()) return;\n if (paused()) {\n stopFrames();\n persist();\n } else {\n requestFrame();\n }\n publish([]);\n };\n\n const stopVisibility = visibility.subscribe((value) => {\n if (!disposed) setPaused({ hidden: value });\n });\n\n requestFrame();\n\n return {\n get: () => publisher.get(),\n subscribe: (listener) => publisher.subscribe(listener),\n dispatch: (raw) => {\n if (disposed) return;\n if (!isRecord(raw)) return;\n if (raw['kind'] === 'pause' || raw['kind'] === 'resume') return setPaused({ user: raw['kind'] === 'pause' });\n if (raw['kind'] !== 'input') return;\n const reject = (code: string, message: string): void => {\n notice = { code, message };\n publish([]);\n };\n if (finished() || broken) return reject('GameOver', 'The game is over.');\n if (paused()) return reject('Paused', 'The game is paused.');\n const input = decode(simulation.schemas.input, raw['input'], 'input');\n if (!input.ok) return reject(input.error.code, input.error.message);\n // Assigned to the next tick that runs, after everything already queued.\n queued.push(input.value);\n },\n dispose: () => {\n if (disposed) return;\n persist();\n disposed = true;\n stopFrames();\n stopVisibility();\n publisher.close();\n },\n alpha: () => Math.min(1, accumulator / stepMs),\n save: envelope,\n replay,\n };\n};\n"],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export { alwaysVisible, type AiRunner, type AiTask, type Clock, type FrameScheduler, type JobRunner, type Reporter, type RngFactory, type SaveStore, type Task, type Visibility, } from './ports.js';
2
+ export { createLocalSession, inlineAi, restoreLocalSession, type LocalInput, type LocalMeta, type LocalMode, type LocalPorts, type LocalSession, type LocalSessionOptions, type LocalSnapshot, type RestoreOptions, type UndoBlock, } from './local.js';
3
+ export { createFixedStepRunner, restoreFixedStepRunner, type FixedStepInput, type FixedStepOptions, type FixedStepPorts, type FixedStepRestoreOptions, type FixedStepRunner, type FixedStepSnapshot, } from './fixed-step.js';
4
+ export { createRemoteSession, memoryTokens, reconnectDelay, type RemoteGame, type RemoteInput, type RemoteOffer, type RemoteRoom, type RemoteSession, type RemoteSessionOptions, type RemoteSnapshot, type RemoteSocket, type SocketEvents, type SocketFactory, type TokenStore, } from './remote.js';
5
+ export { replayEngine, replaySimulation } from './replay.js';
6
+ export { loadSave, parseEnvelope, type RunnerManifest } from './saves.js';
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ import { alwaysVisible } from "./ports.js";
2
+ import { replayEngine, replaySimulation } from "./replay.js";
3
+ import { loadSave, parseEnvelope } from "./saves.js";
4
+ import { createFixedStepRunner, restoreFixedStepRunner } from "./fixed-step.js";
5
+ import { createLocalSession, inlineAi, restoreLocalSession } from "./local.js";
6
+ import { createRemoteSession, memoryTokens, reconnectDelay } from "./remote.js";
7
+ export { alwaysVisible, createFixedStepRunner, createLocalSession, createRemoteSession, inlineAi, loadSave, memoryTokens, parseEnvelope, reconnectDelay, replayEngine, replaySimulation, restoreFixedStepRunner, restoreLocalSession };
@@ -0,0 +1,130 @@
1
+ import { type AssistKind, type AssistCounts, type DiscreteEngine, type EngineAi, type EngineReplay, type EngineSnapshot, type GameError, type GameSession, type Json, type Migration, type Result, type RngState, type SaveEnvelope, type SeatChange, type SeatId, type SeatInfo, type SessionMeta } from '@joystyk/contracts';
2
+ import { type AiRunner, type Clock, type JobRunner, type Reporter, type RngFactory, type SaveStore, type Visibility } from './ports.js';
3
+ import { type RunnerManifest } from './saves.js';
4
+ /** The local modes. Rooms (P2P and server) have their own session clients. */
5
+ export type LocalMode = 'solo' | 'ai' | 'pass-and-play' | 'daily';
6
+ /**
7
+ * What the game's UI dispatches. A command comes from the viewer's seat unless it names one:
8
+ * in pass-and-play without a hand-off screen it may name any pending human seat, so two
9
+ * people at one device can make simultaneous moves in either order (gp-o2c); elsewhere a
10
+ * named seat must be the viewer's. The platform inputs (`resign`, `seat`) may name another
11
+ * human seat. `reveal` acknowledges the pass-and-play hand-off screen (D-31).
12
+ */
13
+ export type LocalInput<Command> = {
14
+ readonly kind: 'command';
15
+ readonly command: Command;
16
+ readonly seat?: SeatId;
17
+ } | {
18
+ readonly kind: 'resign';
19
+ readonly seat?: SeatId;
20
+ } | {
21
+ readonly kind: 'seat';
22
+ readonly seat: SeatId;
23
+ readonly change: SeatChange;
24
+ } | {
25
+ readonly kind: 'undo';
26
+ } | {
27
+ readonly kind: 'reveal';
28
+ } | {
29
+ readonly kind: 'pause';
30
+ } | {
31
+ readonly kind: 'resume';
32
+ };
33
+ /** Session metadata plus the undo count and assistance, which results screens show (D-32). */
34
+ export type LocalMeta = SessionMeta & {
35
+ /** Undos used in this game. The engine never sees undo, so only the runner can count it. */
36
+ readonly undosUsed: number;
37
+ /** The kinds of assistance used so far, in `ASSIST_KINDS` order. */
38
+ readonly assists: readonly AssistKind[];
39
+ readonly assistCounts: AssistCounts;
40
+ };
41
+ /** Why undo is unavailable right now, as a notice code. */
42
+ export type UndoBlock = 'UndoNotAllowed' | 'HistoryUnavailable' | 'GameOver' | 'UndoLimit' | 'NothingToUndo' | 'UndoBarrier' | 'Paused';
43
+ /** The local runner's snapshot: the contract's `EngineSnapshot` and what local play adds. */
44
+ export type LocalSnapshot<View, Event> = EngineSnapshot<View, Event> & {
45
+ readonly meta: LocalMeta;
46
+ readonly mode: LocalMode;
47
+ /**
48
+ * Pass-and-play only, for games whose manifest sets `handoff` (D-31): the seat the device
49
+ * must pass to. While it is set, `meta.seat` is null and `view` is the spectator view, so
50
+ * nothing private shows; `reveal` ends it. Once the game is over it stays null, and so does
51
+ * `meta.seat`: the final board is the spectator's view.
52
+ */
53
+ readonly handoff: {
54
+ readonly seat: SeatId;
55
+ readonly name: string;
56
+ } | null;
57
+ readonly undo: {
58
+ readonly available: boolean;
59
+ readonly remaining: number | null;
60
+ readonly blockedBy: UndoBlock | null;
61
+ };
62
+ /** AI seats choosing a move right now. */
63
+ readonly thinking: readonly SeatId[];
64
+ readonly paused: boolean;
65
+ /** Active play time when this snapshot was published. For a live stopwatch, call `elapsedMs()`. */
66
+ readonly elapsedMs: number;
67
+ };
68
+ /** A local session: the contract's `GameSession` plus what the launcher needs for saves. */
69
+ export interface LocalSession<Command, View, Event> extends GameSession<LocalSnapshot<View, Event>, LocalInput<Command>> {
70
+ /** Active play time right now. */
71
+ elapsedMs(): number;
72
+ /** The save envelope for the current position. */
73
+ save(): SaveEnvelope;
74
+ /** The replay log so far, or null when the history was lost (a save without a replay). */
75
+ replay(): EngineReplay | null;
76
+ }
77
+ /** Ports and settings shared by new and restored sessions. */
78
+ export type LocalPorts<View> = {
79
+ readonly manifest: RunnerManifest;
80
+ readonly mode: LocalMode;
81
+ /** Restores the game's random state. Default: the kit's `restoreRng`. */
82
+ readonly restoreRng?: RngFactory;
83
+ readonly clock: Clock;
84
+ /**
85
+ * The viewer in solo, vs-AI and daily play; default: the first human seat. Null watches
86
+ * (every seat AI). Pass-and-play ignores it: the viewer follows the seat to act.
87
+ */
88
+ readonly seat?: SeatId | null;
89
+ /**
90
+ * Seed for the AI's own random source, kept apart from the game's so an AI can't predict
91
+ * shuffles. Pass a fresh one; the default forks one from the game's random state. A
92
+ * restored game continues the stream its save recorded (`aiRng`) and ignores this.
93
+ */
94
+ readonly aiSeed?: RngState;
95
+ /** How long the default AI runner waits before choosing, so players can follow. Default 500. */
96
+ readonly aiDelayMs?: number;
97
+ /**
98
+ * Chooses AI moves. Default: the engine's `ai.choose`, inline. Pass `workerAiRunner` when
99
+ * the manifest declares heavy AI; without one, the runner reports it and runs inline.
100
+ */
101
+ readonly ai?: AiRunner<View>;
102
+ readonly visibility?: Visibility;
103
+ readonly storage?: SaveStore;
104
+ readonly jobs?: JobRunner;
105
+ /** Deep-freeze snapshots. Default true; production builds may pass false. */
106
+ readonly freeze?: boolean;
107
+ readonly report?: Reporter;
108
+ };
109
+ export type LocalSessionOptions<View> = LocalPorts<View> & {
110
+ readonly seats: readonly SeatInfo[];
111
+ /** Decoded with the engine's config schema. */
112
+ readonly config: unknown;
113
+ readonly seed: RngState;
114
+ };
115
+ export type RestoreOptions<View> = LocalPorts<View> & {
116
+ readonly migrations: readonly Migration[];
117
+ };
118
+ /**
119
+ * The default AI runner: the engine's own `ai.choose`, after a delay on the clock, drawing
120
+ * from the task's stream. `restore` rebuilds it; default: the kit's `restoreRng`.
121
+ */
122
+ export declare const inlineAi: <View, Command>(ai: EngineAi<View, Command>, clock: Clock, delayMs: number, restore?: RngFactory) => AiRunner<View>;
123
+ /** Starts a new local game. Fails when the seats or config are invalid or `init` refuses them. */
124
+ export declare const createLocalSession: <State extends Json, Command extends Json, View extends Json, Event extends Json, Config extends Json>(engine: DiscreteEngine<State, Command, View, Event, Config>, options: LocalSessionOptions<View>) => Result<LocalSession<Command, View, Event>, GameError>;
125
+ /**
126
+ * Resumes a saved game. `save` is whatever storage returned; it is parsed, migrated and
127
+ * validated here. When the save's replay is missing or no longer matches, the game still
128
+ * resumes, without undo or outstanding jobs, and the reason goes to `report`.
129
+ */
130
+ export declare const restoreLocalSession: <State extends Json, Command extends Json, View extends Json, Event extends Json, Config extends Json>(engine: DiscreteEngine<State, Command, View, Event, Config>, save: unknown, options: RestoreOptions<View>) => Result<LocalSession<Command, View, Event>, GameError>;