@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 @@
1
+ {"version":3,"file":"local.js","names":[],"sources":["../src/local.ts"],"sourcesContent":["// The local runner: plays a Layer 2 engine in the browser, as the game's single authority\n// (core belief 10). It validates every input, drives AI seats, runs jobs, measures play\n// time, implements undo by replay, saves after every accepted change, and publishes\n// snapshots by the Layer 1 rules. See README.md for the design.\nimport {\n ASSIST_KINDS,\n err,\n gameError,\n isJson,\n ok,\n undoPolicyFor,\n type AssistKind,\n type AssistCounts,\n type Beat,\n type DiscreteEngine,\n type EngineAi,\n type EngineInput,\n type EngineReplay,\n type EngineSnapshot,\n type GameError,\n type GameSession,\n type JobOutcome,\n type JobRequest,\n type JobSpec,\n type Json,\n type Migration,\n type Notice,\n type Result,\n type Rng,\n type RngState,\n type SaveEnvelope,\n type SeatChange,\n type SeatId,\n type SeatInfo,\n type SessionMeta,\n type ViewBeat,\n} from '@joystyk/contracts';\nimport {\n alwaysVisible,\n type AiRunner,\n type Clock,\n type JobRunner,\n type Reporter,\n type RngFactory,\n type SaveStore,\n type Visibility,\n} from './ports.ts';\nimport { createPublisher } from './publisher.ts';\nimport { applySeatChange, parseEngineInput, rebuild, recordOf, safeApply, type InputRecord } from './replay.ts';\nimport { advanceAi, aiDecisionSeed, aiRestore, defaultRestoreRng, deriveAiSeed } from './rngs.ts';\nimport { isSeatInfo, loadSave, type RunnerManifest } from './saves.ts';\nimport { createStopwatch } from './stopwatch.ts';\nimport { compact, decode, isRecord, jsonClone, jsonEqual } from './values.ts';\n\n/** The local modes. Rooms (P2P and server) have their own session clients. */\nexport type LocalMode = 'solo' | 'ai' | 'pass-and-play' | 'daily';\n\n/**\n * What the game's UI dispatches. A command comes from the viewer's seat unless it names one:\n * in pass-and-play without a hand-off screen it may name any pending human seat, so two\n * people at one device can make simultaneous moves in either order (gp-o2c); elsewhere a\n * named seat must be the viewer's. The platform inputs (`resign`, `seat`) may name another\n * human seat. `reveal` acknowledges the pass-and-play hand-off screen (D-31).\n */\nexport type LocalInput<Command> =\n | { readonly kind: 'command'; readonly command: Command; readonly seat?: SeatId }\n | { readonly kind: 'resign'; readonly seat?: SeatId }\n | { readonly kind: 'seat'; readonly seat: SeatId; readonly change: SeatChange }\n | { readonly kind: 'undo' }\n | { readonly kind: 'reveal' }\n | { readonly kind: 'pause' }\n | { readonly kind: 'resume' };\n\n/** Session metadata plus the undo count and assistance, which results screens show (D-32). */\nexport type LocalMeta = SessionMeta & {\n /** Undos used in this game. The engine never sees undo, so only the runner can count it. */\n readonly undosUsed: number;\n /** The kinds of assistance used so far, in `ASSIST_KINDS` order. */\n readonly assists: readonly AssistKind[];\n readonly assistCounts: AssistCounts;\n};\n\n/** Why undo is unavailable right now, as a notice code. */\nexport type UndoBlock =\n 'UndoNotAllowed' | 'HistoryUnavailable' | 'GameOver' | 'UndoLimit' | 'NothingToUndo' | 'UndoBarrier' | 'Paused';\n\n/** The local runner's snapshot: the contract's `EngineSnapshot` and what local play adds. */\nexport type LocalSnapshot<View, Event> = EngineSnapshot<View, Event> & {\n readonly meta: LocalMeta;\n readonly mode: LocalMode;\n /**\n * Pass-and-play only, for games whose manifest sets `handoff` (D-31): the seat the device\n * must pass to. While it is set, `meta.seat` is null and `view` is the spectator view, so\n * nothing private shows; `reveal` ends it. Once the game is over it stays null, and so does\n * `meta.seat`: the final board is the spectator's view.\n */\n readonly handoff: { readonly seat: SeatId; readonly name: string } | null;\n readonly undo: {\n readonly available: boolean;\n readonly remaining: number | null;\n readonly blockedBy: UndoBlock | null;\n };\n /** AI seats choosing a move right now. */\n readonly thinking: readonly SeatId[];\n readonly paused: boolean;\n /** Active play time when this snapshot was published. For a live stopwatch, call `elapsedMs()`. */\n readonly elapsedMs: number;\n};\n\n/** A local session: the contract's `GameSession` plus what the launcher needs for saves. */\nexport interface LocalSession<Command, View, Event> extends GameSession<\n LocalSnapshot<View, Event>,\n LocalInput<Command>\n> {\n /** Active play time right now. */\n elapsedMs(): number;\n /** The save envelope for the current position. */\n save(): SaveEnvelope;\n /** The replay log so far, or null when the history was lost (a save without a replay). */\n replay(): EngineReplay | null;\n}\n\n/** Ports and settings shared by new and restored sessions. */\nexport type LocalPorts<View> = {\n readonly manifest: RunnerManifest;\n readonly mode: LocalMode;\n /** Restores the game's random state. Default: the kit's `restoreRng`. */\n readonly restoreRng?: RngFactory;\n readonly clock: Clock;\n /**\n * The viewer in solo, vs-AI and daily play; default: the first human seat. Null watches\n * (every seat AI). Pass-and-play ignores it: the viewer follows the seat to act.\n */\n readonly seat?: SeatId | null;\n /**\n * Seed for the AI's own random source, kept apart from the game's so an AI can't predict\n * shuffles. Pass a fresh one; the default forks one from the game's random state. A\n * restored game continues the stream its save recorded (`aiRng`) and ignores this.\n */\n readonly aiSeed?: RngState;\n /** How long the default AI runner waits before choosing, so players can follow. Default 500. */\n readonly aiDelayMs?: number;\n /**\n * Chooses AI moves. Default: the engine's `ai.choose`, inline. Pass `workerAiRunner` when\n * the manifest declares heavy AI; without one, the runner reports it and runs inline.\n */\n readonly ai?: AiRunner<View>;\n readonly visibility?: Visibility;\n readonly storage?: SaveStore;\n readonly jobs?: JobRunner;\n /** Deep-freeze snapshots. Default true; production builds may pass false. */\n readonly freeze?: boolean;\n readonly report?: Reporter;\n};\n\nexport type LocalSessionOptions<View> = LocalPorts<View> & {\n readonly seats: readonly SeatInfo[];\n /** Decoded with the engine's config schema. */\n readonly config: unknown;\n readonly seed: RngState;\n};\n\nexport type RestoreOptions<View> = LocalPorts<View> & {\n readonly migrations: readonly Migration[];\n};\n\ntype History<Command, Config> = {\n readonly config: Config;\n readonly seats: readonly SeatInfo[];\n readonly seed: RngState;\n records: InputRecord<Command>[];\n};\n\ntype Start<State, Command, Config> = {\n readonly state: State;\n readonly rng: Rng;\n readonly seats: readonly SeatInfo[];\n readonly history: History<Command, Config> | null;\n readonly elapsedMs: number;\n /** Whether the play clock has started. */\n readonly started: boolean;\n readonly undosUsed: number;\n readonly assists: readonly AssistKind[];\n readonly assistCounts: AssistCounts;\n /** The AI stream's saved state, or null to start one. */\n readonly aiRng: RngState | null;\n};\n\nconst notice = (code: string, message: string): Notice => ({ code, message });\n\nconst UNDO_MESSAGES: { readonly [K in UndoBlock]: string } = {\n UndoNotAllowed: \"This game doesn't allow undo here.\",\n HistoryUnavailable: 'Undo is unavailable for this saved game.',\n GameOver: 'The game is over.',\n UndoLimit: 'No undos left.',\n NothingToUndo: 'There is nothing to undo.',\n UndoBarrier: \"That move can't be undone.\",\n Paused: 'The game is paused.',\n};\n\n/**\n * The default AI runner: the engine's own `ai.choose`, after a delay on the clock, drawing\n * from the task's stream. `restore` rebuilds it; default: the kit's `restoreRng`.\n */\nexport const inlineAi = <View, Command>(\n ai: EngineAi<View, Command>,\n clock: Clock,\n delayMs: number,\n restore: RngFactory = defaultRestoreRng,\n): AiRunner<View> => ({\n choose: (task) => {\n let cancel = (): void => {};\n const result = new Promise<unknown>((resolve, reject) => {\n cancel = clock.after(delayMs, () => {\n try {\n resolve(ai.choose(task.view, task.seat, task.level, restore(task.rng)));\n } catch (error) {\n reject(error instanceof Error ? error : new Error(String(error)));\n }\n });\n });\n return { result, cancel: () => cancel() };\n },\n});\n\n/** The kinds in `ASSIST_KINDS` order, each once. */\nconst assistList = (used: ReadonlySet<AssistKind>): readonly AssistKind[] =>\n ASSIST_KINDS.filter((kind) => used.has(kind));\n\nconst checkSeats = (seats: readonly SeatInfo[], levels: readonly string[] | null): GameError | null => {\n if (seats.length === 0 || !seats.every((seat, i) => isSeatInfo(seat) && seat.id === i)) {\n return gameError('InvalidSeats', 'The seats must be numbered 0, 1, 2 and so on.');\n }\n const unknown = seats.find((seat) => seat.aiLevel !== undefined && levels !== null && !levels.includes(seat.aiLevel));\n return unknown === undefined\n ? null\n : gameError('InvalidSeats', 'An AI seat names a level the game lacks.', { seat: unknown.id });\n};\n\n/** Starts a new local game. Fails when the seats or config are invalid or `init` refuses them. */\nexport const createLocalSession = <\n State extends Json,\n Command extends Json,\n View extends Json,\n Event extends Json,\n Config extends Json,\n>(\n engine: DiscreteEngine<State, Command, View, Event, Config>,\n options: LocalSessionOptions<View>,\n): Result<LocalSession<Command, View, Event>, GameError> => {\n const seatProblem = checkSeats(options.seats, engine.ai?.levels ?? null);\n if (seatProblem !== null) return err(seatProblem);\n const config = decode(engine.schemas.config, options.config, 'game setup');\n if (!config.ok) return config;\n const start = { config: config.value, seats: options.seats, seed: options.seed };\n const built = rebuild(engine, start, [], options.restoreRng ?? defaultRestoreRng);\n if (!built.ok) return built;\n return ok(\n run(engine, options, {\n state: built.value.state,\n rng: built.value.rng,\n seats: options.seats,\n history: { ...start, records: [] },\n elapsedMs: 0,\n started: false,\n undosUsed: 0,\n assists: [],\n assistCounts: {},\n aiRng: null,\n }),\n );\n};\n\nconst restoreHistory = <\n State extends Json,\n Command extends Json,\n View extends Json,\n Event extends Json,\n Config extends Json,\n>(\n engine: DiscreteEngine<State, Command, View, Event, Config>,\n envelope: SaveEnvelope,\n state: State,\n restoreRng: RngFactory,\n): Result<History<Command, Config>, GameError> => {\n const lost = (why: string): Result<never, GameError> =>\n err(gameError('HistoryUnavailable', 'The saved moves could not be replayed.', { why }));\n const replay: unknown = envelope.replay;\n if (!isRecord(replay)) return lost('no replay');\n if (replay['layer'] !== 2 || replay['gameId'] !== envelope.gameId || replay['rulesVersion'] !== engine.rulesVersion) {\n return lost('replay header');\n }\n const config = decode(engine.schemas.config, replay['config'], 'saved setup');\n if (!config.ok) return config;\n const seats = replay['seats'];\n if (!Array.isArray(seats) || checkSeats(seats as SeatInfo[], null) !== null) return lost('replay seats');\n const seed = replay['seed'];\n if (!isRecord(seed) || typeof seed['algorithm'] !== 'string' || !Array.isArray(seed['words']))\n return lost('replay seed');\n const rawInputs = replay['inputs'];\n if (!Array.isArray(rawInputs)) return lost('replay inputs');\n const inputs: EngineInput<Command>[] = [];\n for (const raw of rawInputs) {\n const input = parseEngineInput(raw, engine.schemas.command);\n if (!input.ok) return input;\n inputs.push(input.value);\n }\n const start = { config: config.value, seats: seats as SeatInfo[], seed: seed as RngState };\n const rebuilt = rebuild(engine, start, inputs, restoreRng);\n if (!rebuilt.ok) return rebuilt;\n // The replay must land exactly where the save says the game stands.\n if (!jsonEqual(rebuilt.value.state, state) || !jsonEqual(rebuilt.value.rng.save(), envelope.rng))\n return lost('replay diverged');\n return ok({ ...start, records: [...rebuilt.value.records] });\n};\n\n/**\n * Resumes a saved game. `save` is whatever storage returned; it is parsed, migrated and\n * validated here. When the save's replay is missing or no longer matches, the game still\n * resumes, without undo or outstanding jobs, and the reason goes to `report`.\n */\nexport const restoreLocalSession = <\n State extends Json,\n Command extends Json,\n View extends Json,\n Event extends Json,\n Config extends Json,\n>(\n engine: DiscreteEngine<State, Command, View, Event, Config>,\n save: unknown,\n options: RestoreOptions<View>,\n): Result<LocalSession<Command, View, Event>, GameError> => {\n const loaded = loadSave(save, options.manifest, engine.rulesVersion, options.migrations, engine.schemas.state);\n if (!loaded.ok) return loaded;\n const { envelope, state } = loaded.value;\n const seatProblem = checkSeats(envelope.seats, engine.ai?.levels ?? null);\n if (seatProblem !== null) return err(seatProblem);\n const restoreRng = options.restoreRng ?? defaultRestoreRng;\n const history = restoreHistory(engine, envelope, state, restoreRng);\n if (!history.ok) options.report?.(history.error, 'Restored a save without its history.');\n // The save's record is authoritative (undo leaves no trace in the replay); the replay's flags only add to it.\n const assists = new Set<AssistKind>(envelope.assists ?? []);\n const fullRecords = history.ok ? history.value.records : [];\n const records = fullRecords.slice(\n Math.max(\n 0,\n fullRecords.findLastIndex((record) => record.restartsGame),\n ),\n );\n for (const record of records) if (record.assist !== null) assists.add(record.assist);\n const replayCounts: Partial<Record<AssistKind, number>> = {};\n for (const record of records)\n if (record.assist !== null) replayCounts[record.assist] = (replayCounts[record.assist] ?? 0) + 1;\n const assistCounts: Partial<Record<AssistKind, number>> = {};\n for (const kind of ASSIST_KINDS) {\n const count = Math.max(\n envelope.assistCounts?.[kind] ?? 0,\n replayCounts[kind] ?? 0,\n kind === 'undo' ? (envelope.undosUsed ?? 0) : 0,\n assists.has(kind) ? 1 : 0,\n );\n if (count > 0) {\n assistCounts[kind] = count;\n assists.add(kind);\n }\n }\n // A save from before `clockStarted` existed: the time it recorded, or the input that started the clock.\n const clockInput =\n engine.clockStart === 'transition' ? records.some((record) => record.startsClock) : records.length > 0;\n const started = envelope.clockStarted ?? ((envelope.elapsedMs ?? 0) > 0 || clockInput);\n return ok(\n run(engine, options, {\n state,\n rng: restoreRng(envelope.rng),\n seats: envelope.seats,\n history: history.ok ? history.value : null,\n elapsedMs: envelope.elapsedMs ?? 0,\n started,\n undosUsed: envelope.undosUsed ?? 0,\n assists: assistList(assists),\n assistCounts,\n aiRng: envelope.aiRng ?? null,\n }),\n );\n};\n\nconst outstandingJobs = <Command>(records: readonly InputRecord<Command>[]): Map<string, JobRequest> => {\n const jobs = new Map<string, JobRequest>();\n for (const record of records) {\n if (record.restartsGame) jobs.clear();\n if (record.input.kind === 'job') jobs.delete(record.input.jobId);\n for (const job of record.jobs) jobs.set(job.id, job);\n }\n return jobs;\n};\n\ntype JobResult = { readonly outcome: JobOutcome; readonly presentation: Json | null };\n\nconst FAILED: JobResult = { outcome: { ok: false, reason: 'failed' }, presentation: null };\n\nconst parseJobResult = (raw: unknown, spec: JobSpec): JobResult => {\n if (!isRecord(raw)) return FAILED;\n if (raw['ok'] === false && (raw['reason'] === 'timeout' || raw['reason'] === 'failed')) {\n return { outcome: { ok: false, reason: raw['reason'] }, presentation: null };\n }\n if (raw['ok'] !== true) return FAILED;\n const value = decode(spec.result, raw['value'], 'job result');\n if (!value.ok || !isJson(value.value)) return FAILED;\n const presentation = raw['presentation'];\n // The engine gets the value only; presentation goes to the snapshot (game-contract.md, \"Async work\").\n return {\n outcome: { ok: true, value: value.value },\n presentation: presentation !== undefined && isJson(presentation) ? presentation : null,\n };\n};\n\nconst run = <State extends Json, Command extends Json, View extends Json, Event extends Json, Config extends Json>(\n engine: DiscreteEngine<State, Command, View, Event, Config>,\n options: LocalPorts<View>,\n start: Start<State, Command, Config>,\n): LocalSession<Command, View, Event> => {\n const { manifest, mode, clock } = options;\n const restoreRng = options.restoreRng ?? defaultRestoreRng;\n const report: Reporter = options.report ?? (() => {});\n const visibility = options.visibility ?? alwaysVisible;\n const handoffScreen = mode === 'pass-and-play' && manifest.handoff;\n\n let state = start.state;\n let rng = start.rng;\n let seats = start.seats;\n let history = start.history;\n let revision = 0;\n let undosUsed = start.undosUsed;\n const assists = new Set<AssistKind>(start.assists);\n const assistCounts: Partial<Record<AssistKind, number>> = { ...start.assistCounts };\n const undoPolicy = undoPolicyFor(manifest.undo, mode);\n let currentNotice: Notice | null = null;\n let presentation: Json | null = null;\n let paused = false;\n let disposed = false;\n let outstanding = history === null ? new Map<string, JobRequest>() : outstandingJobs(history.records);\n const jobTasks = new Map<string, { readonly cancel: () => void }>();\n const aiTasks = new Map<SeatId, { readonly key: string; readonly cancel: () => void }>();\n\n // The AI's own stream (saved as `aiRng`). It moves only when an AI move is accepted, so a\n // save taken while the AI is thinking restores to the same choice.\n const restoreAi = aiRestore(restoreRng);\n let aiState: RngState = start.aiRng ?? options.aiSeed ?? deriveAiSeed(rng.save(), restoreRng);\n if (manifest.ai?.heavy === true && options.ai === undefined && engine.ai !== undefined) {\n report(\n new Error(`${manifest.id} declares heavy AI but no worker AI runner was given.`),\n 'Running heavy AI on the main thread.',\n );\n }\n const aiRunner: AiRunner<View> | null =\n options.ai ?? (engine.ai === undefined ? null : inlineAi(engine.ai, clock, options.aiDelayMs ?? 500, restoreAi));\n\n const stopwatch = createStopwatch(clock, {\n elapsedMs: start.elapsedMs,\n started: start.started,\n finished: engine.outcome(state) !== null,\n hidden: visibility.hidden(),\n });\n\n // --- Who is looking at the device ---------------------------------------------------------\n\n const isHuman = (seat: SeatId): boolean => seats[seat]?.controller === 'human';\n const firstHuman = (): SeatId | null => seats.find((seat) => seat.controller === 'human')?.id ?? null;\n let viewer: SeatId | null =\n mode === 'pass-and-play' ? null : options.seat === undefined ? firstHuman() : options.seat;\n let handoff: SeatId | null = null;\n let wasHandoffRequired = handoffScreen;\n\n /**\n * Pass-and-play: the viewer follows the human seat to act, through the hand-off screen. At\n * the outcome a hand-off game shows the spectator's view, so the final board reveals no\n * hand to whoever holds the device next; an engine whose rules reveal hands at the end\n * shows them in `viewFor(state, null)`.\n */\n const updateViewer = (): void => {\n if (mode !== 'pass-and-play') return;\n if (engine.outcome(state) !== null) {\n handoff = null;\n if (handoffScreen) viewer = null;\n return;\n }\n const humans = engine.pending(state).filter(isHuman);\n const current = handoff ?? viewer;\n const required = handoffScreen && engine.handoffRequired?.(state) !== false;\n const enteringPrivate = required && !wasHandoffRequired;\n wasHandoffRequired = required;\n if (humans.length === 0) {\n if (!required && handoff !== null) {\n handoff = null;\n viewer = firstHuman();\n }\n if (viewer === null && handoff === null && !handoffScreen) viewer = firstHuman();\n return;\n }\n if (current !== null && humans.includes(current) && !enteringPrivate) {\n if (!required && handoff !== null) {\n viewer = current;\n handoff = null;\n }\n return;\n }\n // The next human to act after the current one, in seat order.\n const after = current ?? -1;\n const target = enteringPrivate\n ? Math.min(...humans)\n : ([...humans].sort((a, b) => a - b).find((seat) => seat > after) ?? Math.min(...humans));\n if (required) {\n viewer = null;\n handoff = target;\n } else {\n viewer = target;\n handoff = null;\n }\n };\n\n // --- Undo (D-32) --------------------------------------------------------------------------\n\n const undoPlan = (): { readonly cut: number } | { readonly block: UndoBlock } => {\n const policy = undoPolicy;\n // The manifest's rule for this mode; two people outside pass-and-play make a multiplayer game.\n if (\n policy === 'none' ||\n (mode !== 'pass-and-play' && seats.filter((seat) => seat.controller === 'human').length > 1)\n ) {\n return { block: 'UndoNotAllowed' };\n }\n if (history === null) return { block: 'HistoryUnavailable' };\n if (engine.outcome(state) !== null) return { block: 'GameOver' };\n if (paused) return { block: 'Paused' };\n if (typeof policy === 'object' && undosUsed >= policy.limited) return { block: 'UndoLimit' };\n // Back to the start of the human's previous decision, taking the AI's replies with it.\n const cut = history.records.findLastIndex(\n (record) => record.input.kind === 'command' && record.seatsBefore[record.input.seat]?.controller === 'human',\n );\n if (cut < 0) return { block: 'NothingToUndo' };\n // Never step back past a barrier, nor past anything before it.\n if (history.records.slice(cut).some((record) => record.barrier)) return { block: 'UndoBarrier' };\n return { cut };\n };\n\n const undoState = (): LocalSnapshot<View, Event>['undo'] => {\n const plan = undoPlan();\n const policy = undoPolicy;\n const remaining = policy === 'none' ? 0 : policy === 'unlimited' ? null : Math.max(0, policy.limited - undosUsed);\n return 'cut' in plan\n ? { available: true, remaining, blockedBy: null }\n : { available: false, remaining, blockedBy: plan.block };\n };\n\n // --- Snapshots ----------------------------------------------------------------------------\n\n const redact = (beats: readonly Beat<State, Event>[]): ViewBeat<View, Event>[] =>\n beats.flatMap((beat) => {\n const event = engine.eventFor(beat.event, viewer);\n if (event === null) return [];\n return [beat.state === undefined ? { event } : { event, view: engine.viewFor(beat.state, viewer) }];\n });\n\n const snapshot = (beats: readonly Beat<State, Event>[]): LocalSnapshot<View, Event> => {\n const outcome = engine.outcome(state);\n const elapsedMs = stopwatch.elapsed();\n return {\n meta: {\n connection: 'local',\n seat: viewer,\n revision,\n seats,\n // The platform's records replace anything the engine wrote in these fields.\n outcome:\n outcome === null\n ? null\n : { ...outcome, elapsedMs, undosUsed, assists: assistList(assists), assistCounts: { ...assistCounts } },\n safePoint: engine.safePoint?.(state) ?? outcome !== null,\n notice: currentNotice,\n undosUsed,\n assists: assistList(assists),\n assistCounts: { ...assistCounts },\n },\n view: engine.viewFor(state, viewer),\n beats: redact(beats),\n pending: engine.pending(state),\n presentation,\n mode,\n handoff: handoff === null ? null : { seat: handoff, name: seats[handoff]?.name ?? '' },\n undo: undoState(),\n thinking: [...aiTasks.keys()].sort((a, b) => a - b),\n paused,\n elapsedMs,\n };\n };\n\n // --- Saves --------------------------------------------------------------------------------\n\n const replay = (): EngineReplay | null =>\n history === null\n ? null\n : {\n layer: 2,\n gameId: manifest.id,\n rulesVersion: engine.rulesVersion,\n config: history.config,\n seats: history.seats,\n seed: history.seed,\n inputs: history.records.map((record) => record.input),\n };\n\n const envelope = (): SaveEnvelope =>\n compact({\n gameId: manifest.id,\n build: manifest.versions.build,\n rulesVersion: engine.rulesVersion,\n saveVersion: manifest.versions.save,\n rng: rng.save(),\n seats,\n state,\n elapsedMs: stopwatch.elapsed(),\n clockStarted: stopwatch.started(),\n replay: replay() ?? undefined,\n aiRng: aiState,\n undosUsed,\n assists: assistList(assists),\n assistCounts: { ...assistCounts },\n });\n\n /** The engine's `worthSaving`; an engine that throws is reported, and the game is saved. */\n const worthSaving = (): boolean => {\n try {\n return engine.worthSaving?.(state) ?? true;\n } catch (error) {\n report(error, 'The engine threw in worthSaving.');\n return true;\n }\n };\n\n let saving: Promise<void> = Promise.resolve();\n // Whether the slot may still hold a save: a restored game's, or one written over. Cleared\n // once while the position isn't worth saving, then left alone.\n let slotMayHoldSave = true;\n /**\n * Queues a write of the current position; resolves once it has landed (or failed). A\n * position the engine says isn't worth saving is never written, and a save it replaces\n * (after an undo back to the start) is cleared, so the launcher offers no \"Continue\".\n */\n const persist = (): Promise<void> => {\n const store = options.storage;\n if (store === undefined) return Promise.resolve();\n if (!worthSaving()) {\n if (!slotMayHoldSave || store.clear === undefined) return saving;\n slotMayHoldSave = false;\n const clear = store.clear.bind(store);\n saving = saving.then(clear).catch((error: unknown) => report(error, 'Clearing an untouched save failed.'));\n return saving;\n }\n slotMayHoldSave = true;\n const save = envelope();\n saving = saving\n .then(() => store.write(save))\n .catch((error: unknown) => {\n report(error, 'Saving the game failed.');\n enqueue(() => {\n currentNotice = notice('SaveFailed', \"The game couldn't be saved on this device.\");\n publisher.publish(snapshot([]));\n });\n });\n return saving;\n };\n\n // --- The ordered input path ---------------------------------------------------------------\n\n const queue: (() => void)[] = [];\n let draining = false;\n /** Every change runs here, one at a time, so a listener that dispatches never sees a torn state. */\n const enqueue = (work: () => void): void => {\n if (disposed) return;\n queue.push(work);\n if (draining) return;\n draining = true;\n try {\n while (queue.length > 0 && !disposed) {\n const next = queue.shift()!;\n try {\n next();\n } catch (error) {\n report(error, 'The local session failed to process an input.');\n }\n }\n } finally {\n draining = false;\n queue.length = 0;\n }\n };\n\n const reject = (problem: Notice): void => {\n currentNotice = { code: problem.code, message: problem.message };\n publisher.publish(snapshot([]));\n };\n\n /** Publishes the new position, then saves it, then starts the jobs it requested (rule 1). */\n const settle = (beats: readonly Beat<State, Event>[]): void => {\n updateViewer();\n scheduleAi();\n publisher.publish(snapshot(beats));\n void persist().then(() => enqueue(startJobs));\n };\n\n /** Applies one input. On success it publishes; on failure it returns the engine's error. */\n const accept = (input: EngineInput<Command>, jobPresentation: Json | null = null): GameError | null => {\n const applied = safeApply(engine, state, input, rng, restoreRng);\n rng = applied.rng;\n if ('thrown' in applied) report(applied.thrown, 'The engine threw in apply.');\n if (!applied.result.ok) return applied.result.error;\n const transition = applied.result.value;\n const record = recordOf(input, transition, seats);\n if (record.restartsGame) {\n cancelAi();\n cancelJobs();\n outstanding.clear();\n stopwatch.restart();\n undosUsed = 0;\n assists.clear();\n for (const key of ASSIST_KINDS) delete assistCounts[key];\n }\n\n history?.records.push(record);\n if (record.assist !== null) {\n assists.add(record.assist);\n assistCounts[record.assist] = (assistCounts[record.assist] ?? 0) + 1;\n }\n seats = applySeatChange(seats, input);\n state = transition.state;\n if (input.kind === 'job') outstanding.delete(input.jobId);\n for (const job of transition.jobs ?? []) outstanding.set(job.id, job);\n revision += 1;\n currentNotice = null;\n presentation = jobPresentation;\n // The clock starts at the first accepted input, or at the engine's `startsClock` transition.\n if (engine.clockStart !== 'transition' || record.startsClock) stopwatch.start();\n if (engine.outcome(state) !== null) stopwatch.finish();\n settle(transition.beats);\n return null;\n };\n\n // --- AI seats -----------------------------------------------------------------------------\n\n const cancelAi = (): void => {\n for (const task of aiTasks.values()) task.cancel();\n aiTasks.clear();\n };\n\n /** Starts a choice for every pending AI seat whose decision changed; cancels the rest. */\n const scheduleAi = (): void => {\n const wanted = new Map<SeatId, string>();\n if (!paused && !disposed && aiRunner !== null && engine.outcome(state) === null) {\n for (const seat of engine.pending(state)) {\n if (seats[seat]?.controller !== 'ai') continue;\n wanted.set(seat, JSON.stringify(engine.decisionKey?.(state, seat) ?? engine.viewFor(state, seat)));\n }\n }\n for (const [seat, task] of aiTasks) {\n if (wanted.get(seat) === task.key) continue;\n task.cancel();\n aiTasks.delete(seat);\n }\n for (const [seat, key] of wanted) if (!aiTasks.has(seat)) startAi(seat, key);\n };\n\n const aiFailed = (seat: SeatId, error: unknown): void => {\n report(error, `The AI for seat ${seat} failed.`);\n reject(notice('AiFailed', `${seats[seat]?.name ?? 'The computer'} couldn't move.`));\n };\n\n const startAi = (seat: SeatId, key: string): void => {\n if (aiRunner === null) return;\n // A copy of the seat's view and nothing else: the AI can't see more, or mutate state.\n const view = jsonClone(engine.viewFor(state, seat));\n const level = seats[seat]?.aiLevel ?? engine.ai?.levels[0] ?? 'default';\n const task = aiRunner.choose({ view, seat, level, rng: aiDecisionSeed(aiState, seat, restoreAi) });\n const entry = { key, cancel: () => task.cancel() };\n aiTasks.set(seat, entry);\n task.result.then(\n (raw) =>\n enqueue(() => {\n if (aiTasks.get(seat) !== entry) return;\n aiTasks.delete(seat);\n const command = decode(engine.schemas.command, raw, 'AI move');\n if (!command.ok) return aiFailed(seat, command.error);\n // Move the AI stream before accepting, so the AI choices this move triggers get fresh children.\n const before = aiState;\n aiState = advanceAi(aiState, seats.length, restoreAi);\n const problem = accept({ kind: 'command', seat, command: command.value });\n if (problem !== null) {\n aiState = before;\n aiFailed(seat, problem);\n }\n }),\n (error: unknown) =>\n enqueue(() => {\n if (aiTasks.get(seat) !== entry) return;\n aiTasks.delete(seat);\n aiFailed(seat, error);\n }),\n );\n };\n\n // --- Jobs ---------------------------------------------------------------------------------\n\n const cancelJobs = (): void => {\n for (const task of jobTasks.values()) task.cancel();\n jobTasks.clear();\n };\n\n const startJobs = (): void => {\n for (const job of outstanding.values()) if (!jobTasks.has(job.id)) launch(job);\n };\n\n const launch = (job: JobRequest): void => {\n let stopTimer = (): void => {};\n let task: { cancel(): void } | null = null;\n const entry = { cancel: () => (stopTimer(), task?.cancel()) };\n jobTasks.set(job.id, entry);\n const finish = (result: JobResult): void =>\n enqueue(() => {\n // Rule 2: only the job the game still awaits applies; a late result is dropped.\n if (jobTasks.get(job.id) !== entry) return;\n jobTasks.delete(job.id);\n stopTimer();\n if (!outstanding.has(job.id)) return;\n const problem = accept({ kind: 'job', jobId: job.id, outcome: result.outcome }, result.presentation);\n if (problem !== null) {\n // The engine no longer wants it (rule 2); not the player's concern.\n outstanding.delete(job.id);\n report(problem, `The engine rejected the result of job ${job.id}.`);\n }\n });\n const spec = engine.jobs?.[job.kind];\n const payload = spec === undefined ? null : decode(spec.payload, job.payload, 'job payload');\n if (\n spec === undefined ||\n spec.where === 'server' ||\n options.jobs === undefined ||\n payload === null ||\n !payload.ok\n ) {\n // The engine decides what a failure means (rule 3).\n report(new Error(`Job ${job.id} (${job.kind}) can't run locally.`), 'Job unavailable.');\n void Promise.resolve().then(() => finish(FAILED));\n return;\n }\n const running = options.jobs.run(job, spec);\n task = running;\n stopTimer = clock.after(spec.timeoutMs, () => {\n running.cancel();\n finish({ outcome: { ok: false, reason: 'timeout' }, presentation: null });\n });\n running.result.then(\n (raw) => finish(parseJobResult(raw, spec)),\n (error: unknown) => {\n report(error, `Job ${job.id} failed.`);\n finish(FAILED);\n },\n );\n };\n\n // --- Inputs from the UI -------------------------------------------------------------------\n\n const gameOver = (): boolean => engine.outcome(state) !== null;\n\n const undo = (): void => {\n const plan = undoPlan();\n if ('block' in plan) return reject(notice(plan.block, UNDO_MESSAGES[plan.block]));\n if (history === null) return;\n const inputs = history.records.slice(0, plan.cut).map((record) => record.input);\n const rebuilt = rebuild(engine, history, inputs, restoreRng);\n if (!rebuilt.ok) {\n report(rebuilt.error, 'Undo could not replay the game.');\n return reject(notice('InternalError', 'Undo failed. The game continues from here.'));\n }\n cancelAi();\n cancelJobs();\n state = rebuilt.value.state;\n rng = rebuilt.value.rng;\n seats = rebuilt.value.seats;\n history.records = [...rebuilt.value.records];\n outstanding = outstandingJobs(history.records);\n undosUsed += 1;\n assists.add('undo');\n assistCounts.undo = undosUsed;\n revision += 1;\n currentNotice = null;\n presentation = null;\n settle([]);\n };\n\n /**\n * The seat a command comes from. Without a named seat it is the viewer's. A named seat must\n * be a person at this device who is pending or has an explicitly authorized untimed side\n * command: in pass-and-play without a hand-off screen, any such human seat, so simultaneous games such as War take both players' moves in\n * either order; everywhere else, only the viewer's own seat (gp-o2c).\n */\n const commandSeat = (named: unknown, command: Command): Result<SeatId, Notice> => {\n const watching = (): Result<SeatId, Notice> =>\n err(\n handoff === null\n ? notice('Spectator', 'Watchers cannot move.')\n : notice('HandoffPending', 'Pass the device first.'),\n );\n if (named === undefined) return viewer === null ? watching() : ok(viewer);\n const notHere = err(notice('InvalidSeat', \"That seat isn't played on this device.\"));\n if (typeof named !== 'number' || seats[named] === undefined || !isHuman(named)) return notHere;\n if (mode !== 'pass-and-play') return named === viewer ? ok(named) : notHere;\n if (handoffScreen && named !== viewer)\n return viewer === null ? watching() : err(notice('HandoffPending', 'Pass the device first.'));\n if (gameOver()) return err(notice('GameOver', 'The game is over.'));\n if (!engine.pending(state).includes(named) && engine.sideCommand?.(state, named, command) !== true)\n return err(notice('NotYourTurn', `It isn't ${seats[named]?.name ?? 'that seat'}'s turn.`));\n return ok(named);\n };\n\n const handle = (raw: unknown): void => {\n const invalid = notice('InvalidInput', \"That action isn't recognized.\");\n if (!isRecord(raw)) return reject(invalid);\n const kind = raw['kind'];\n const changesGame = kind === 'command' || kind === 'resign' || kind === 'seat' || kind === 'undo';\n if (changesGame && paused) return reject(notice('Paused', 'The game is paused.'));\n switch (kind) {\n case 'command': {\n const command = decode(engine.schemas.command, raw['command'], 'move');\n if (!command.ok) return reject(command.error);\n const seat = commandSeat(raw['seat'], command.value);\n if (!seat.ok) return reject(seat.error);\n const problem = accept({ kind: 'command', seat: seat.value, command: command.value });\n return problem === null ? undefined : reject(problem);\n }\n case 'resign':\n case 'seat': {\n const seat = raw['seat'] === undefined && kind === 'resign' ? viewer : raw['seat'];\n if (typeof seat !== 'number' || seats[seat] === undefined)\n return reject(notice('InvalidSeat', 'Choose a seat first.'));\n if (gameOver()) return reject(notice('GameOver', 'The game is over.'));\n let input: EngineInput<Command>;\n if (kind === 'resign') {\n if (!isHuman(seat)) return reject(notice('InvalidSeat', 'Only a player can resign.'));\n input = { kind: 'resign', seat };\n } else {\n const parsed = parseEngineInput({ kind: 'seat', seat, change: raw['change'] }, engine.schemas.command);\n if (!parsed.ok) return reject(invalid);\n input = parsed.value;\n const change = (parsed.value as { readonly change: SeatChange }).change;\n if (\n change.type === 'controller' &&\n change.aiLevel !== undefined &&\n engine.ai !== undefined &&\n !engine.ai.levels.includes(change.aiLevel)\n ) {\n return reject(notice('InvalidSeat', \"The game doesn't have that AI level.\"));\n }\n }\n const problem = accept(input);\n return problem === null ? undefined : reject(problem);\n }\n case 'undo':\n return undo();\n case 'reveal': {\n if (handoff === null) return reject(notice('NoHandoff', 'There is nothing to reveal.'));\n viewer = handoff;\n handoff = null;\n revision += 1;\n currentNotice = null;\n return publisher.publish(snapshot([]));\n }\n case 'pause':\n case 'resume': {\n if (paused === (kind === 'pause')) return;\n paused = kind === 'pause';\n stopwatch.setPaused(paused);\n scheduleAi();\n publisher.publish(snapshot([]));\n if (paused) void persist();\n return;\n }\n default:\n return reject(invalid);\n }\n };\n\n // --- Start --------------------------------------------------------------------------------\n\n updateViewer();\n scheduleAi();\n const publisher = createPublisher(snapshot([]), { freeze: options.freeze ?? true, report });\n const stopVisibility = visibility.subscribe((hidden) =>\n enqueue(() => {\n stopwatch.setHidden(hidden);\n // A hidden page may never come back: save the play time now.\n if (hidden) void persist();\n }),\n );\n // Save the starting position (a reload can't redeal), then resume any outstanding jobs.\n void persist().then(() => enqueue(startJobs));\n\n return {\n get: () => publisher.get(),\n subscribe: (listener) => publisher.subscribe(listener),\n dispatch: (input) => enqueue(() => handle(input)),\n dispose: () => {\n if (disposed) return;\n void persist();\n disposed = true;\n queue.length = 0;\n cancelAi();\n cancelJobs();\n stopVisibility();\n publisher.close();\n },\n elapsedMs: () => stopwatch.elapsed(),\n save: envelope,\n replay,\n };\n};\n"],"mappings":"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"}
@@ -0,0 +1,70 @@
1
+ import type { JobRequest, JobSpec, Rng, RngState, SaveEnvelope, SeatId } from '@joystyk/contracts';
2
+ /** Work that finishes later and can be abandoned. After `cancel`, the result is ignored. */
3
+ export type Task<T> = {
4
+ readonly result: Promise<T>;
5
+ cancel(): void;
6
+ };
7
+ /** Monotonic time and timers. */
8
+ export interface Clock {
9
+ /** Milliseconds from an arbitrary origin; never goes backwards. */
10
+ now(): number;
11
+ /** Runs `callback` after `ms` milliseconds. The returned function cancels it. */
12
+ after(ms: number, callback: () => void): () => void;
13
+ }
14
+ /** Whether the page is hidden (another tab, a locked phone). */
15
+ export interface Visibility {
16
+ hidden(): boolean;
17
+ /** Calls `listener` on every change. The returned function unsubscribes. */
18
+ subscribe(listener: (hidden: boolean) => void): () => void;
19
+ }
20
+ /** Where a runner writes its save envelope after each accepted change. */
21
+ export interface SaveStore {
22
+ write(save: SaveEnvelope): Promise<void>;
23
+ /**
24
+ * Removes the saved game, when the position is no longer worth resuming (the engine's
25
+ * `worthSaving`). Without it, the runner only stops writing.
26
+ */
27
+ clear?(): Promise<void>;
28
+ }
29
+ /**
30
+ * Performs an engine's asynchronous jobs (Greed's dice throw). The runner validates the
31
+ * payload before calling it and parses the result afterwards, so the result is `unknown`.
32
+ * It enforces `spec.timeoutMs` itself.
33
+ */
34
+ export interface JobRunner {
35
+ run(job: JobRequest, spec: JobSpec): Task<unknown>;
36
+ }
37
+ /**
38
+ * What an AI runner receives: one seat's view, never the state, and the random state the
39
+ * choice must draw from (restore it with the kit's `restoreRng`). All of it is JSON, so it
40
+ * can be posted to a worker as is.
41
+ */
42
+ export type AiTask<View> = {
43
+ readonly view: View;
44
+ readonly seat: SeatId;
45
+ readonly level: string;
46
+ /** This decision's own stream, forked from the AI's saved stream. Always the kit's SFC32. */
47
+ readonly rng: RngState;
48
+ };
49
+ /**
50
+ * Chooses commands for AI seats. The default runs the engine's `ai.choose` after a short
51
+ * delay on the clock; a game whose manifest declares heavy AI gets one that runs in a worker
52
+ * (`workerAiRunner` in `@joystyk/session/browser`). The command is parsed with the engine's
53
+ * command schema, so the result is `unknown`.
54
+ */
55
+ export interface AiRunner<View> {
56
+ choose(task: AiTask<View>): Task<unknown>;
57
+ }
58
+ /** Schedules one callback for the next display frame. The returned function cancels it. */
59
+ export interface FrameScheduler {
60
+ request(callback: (now: number) => void): () => void;
61
+ }
62
+ /** Rebuilds a seeded random source from its saved state. The default is the kit's `restoreRng`. */
63
+ export type RngFactory = (state: RngState) => Rng;
64
+ /**
65
+ * Receives bugs the runner caught and survived: an engine that threw, a listener that threw,
66
+ * a failed save. The player sees a notice; this is for logs.
67
+ */
68
+ export type Reporter = (error: unknown, context: string) => void;
69
+ /** A page that is never hidden: the default when no visibility port is given. */
70
+ export declare const alwaysVisible: Visibility;
package/dist/ports.js ADDED
@@ -0,0 +1,10 @@
1
+ //#region src/ports.ts
2
+ /** A page that is never hidden: the default when no visibility port is given. */
3
+ var alwaysVisible = {
4
+ hidden: () => false,
5
+ subscribe: () => () => {}
6
+ };
7
+ //#endregion
8
+ export { alwaysVisible };
9
+
10
+ //# sourceMappingURL=ports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ports.js","names":[],"sources":["../src/ports.ts"],"sourcesContent":["// The runners' ports: everything outside pure computation arrives through one of these, so\n// the core stays headless and tests drive time, visibility, storage, frames and jobs by hand.\n// Browser implementations live in `@joystyk/session/browser`.\nimport type { JobRequest, JobSpec, Rng, RngState, SaveEnvelope, SeatId } from '@joystyk/contracts';\n\n/** Work that finishes later and can be abandoned. After `cancel`, the result is ignored. */\nexport type Task<T> = {\n readonly result: Promise<T>;\n cancel(): void;\n};\n\n/** Monotonic time and timers. */\nexport interface Clock {\n /** Milliseconds from an arbitrary origin; never goes backwards. */\n now(): number;\n /** Runs `callback` after `ms` milliseconds. The returned function cancels it. */\n after(ms: number, callback: () => void): () => void;\n}\n\n/** Whether the page is hidden (another tab, a locked phone). */\nexport interface Visibility {\n hidden(): boolean;\n /** Calls `listener` on every change. The returned function unsubscribes. */\n subscribe(listener: (hidden: boolean) => void): () => void;\n}\n\n/** Where a runner writes its save envelope after each accepted change. */\nexport interface SaveStore {\n write(save: SaveEnvelope): Promise<void>;\n /**\n * Removes the saved game, when the position is no longer worth resuming (the engine's\n * `worthSaving`). Without it, the runner only stops writing.\n */\n clear?(): Promise<void>;\n}\n\n/**\n * Performs an engine's asynchronous jobs (Greed's dice throw). The runner validates the\n * payload before calling it and parses the result afterwards, so the result is `unknown`.\n * It enforces `spec.timeoutMs` itself.\n */\nexport interface JobRunner {\n run(job: JobRequest, spec: JobSpec): Task<unknown>;\n}\n\n/**\n * What an AI runner receives: one seat's view, never the state, and the random state the\n * choice must draw from (restore it with the kit's `restoreRng`). All of it is JSON, so it\n * can be posted to a worker as is.\n */\nexport type AiTask<View> = {\n readonly view: View;\n readonly seat: SeatId;\n readonly level: string;\n /** This decision's own stream, forked from the AI's saved stream. Always the kit's SFC32. */\n readonly rng: RngState;\n};\n\n/**\n * Chooses commands for AI seats. The default runs the engine's `ai.choose` after a short\n * delay on the clock; a game whose manifest declares heavy AI gets one that runs in a worker\n * (`workerAiRunner` in `@joystyk/session/browser`). The command is parsed with the engine's\n * command schema, so the result is `unknown`.\n */\nexport interface AiRunner<View> {\n choose(task: AiTask<View>): Task<unknown>;\n}\n\n/** Schedules one callback for the next display frame. The returned function cancels it. */\nexport interface FrameScheduler {\n request(callback: (now: number) => void): () => void;\n}\n\n/** Rebuilds a seeded random source from its saved state. The default is the kit's `restoreRng`. */\nexport type RngFactory = (state: RngState) => Rng;\n\n/**\n * Receives bugs the runner caught and survived: an engine that threw, a listener that threw,\n * a failed save. The player sees a notice; this is for logs.\n */\nexport type Reporter = (error: unknown, context: string) => void;\n\n/** A page that is never hidden: the default when no visibility port is given. */\nexport const alwaysVisible: Visibility = {\n hidden: () => false,\n subscribe: () => () => {},\n};\n"],"mappings":";;AAmFA,IAAa,gBAA4B;CACvC,cAAc;CACd,uBAAuB,CAAC;AAC1B"}
@@ -0,0 +1,11 @@
1
+ import type { Reporter } from './ports.js';
2
+ export type Publisher<Snapshot> = {
3
+ get(): Snapshot;
4
+ subscribe(listener: (snapshot: Snapshot) => void): () => void;
5
+ publish(snapshot: Snapshot): void;
6
+ close(): void;
7
+ };
8
+ export declare const createPublisher: <Snapshot>(initial: Snapshot, options: {
9
+ readonly freeze: boolean;
10
+ readonly report: Reporter;
11
+ }) => Publisher<Snapshot>;
@@ -0,0 +1,44 @@
1
+ import { deepFreeze } from "./values.js";
2
+ //#region src/publisher.ts
3
+ var createPublisher = (initial, options) => {
4
+ const seal = (snapshot) => options.freeze ? deepFreeze(snapshot) : snapshot;
5
+ let current = seal(initial);
6
+ let closed = false;
7
+ const listeners = /* @__PURE__ */ new Set();
8
+ const call = (listener, snapshot) => {
9
+ try {
10
+ listener(snapshot);
11
+ } catch (error) {
12
+ options.report(error, "A session listener threw.");
13
+ }
14
+ };
15
+ return {
16
+ get: () => current,
17
+ subscribe: (listener) => {
18
+ if (closed) return () => {};
19
+ const entry = (snapshot) => call(listener, snapshot);
20
+ listeners.add(entry);
21
+ call(listener, current);
22
+ return () => {
23
+ listeners.delete(entry);
24
+ };
25
+ },
26
+ publish: (snapshot) => {
27
+ if (closed) return;
28
+ current = seal(snapshot);
29
+ const published = current;
30
+ for (const listener of [...listeners]) {
31
+ if (closed || !listeners.has(listener)) continue;
32
+ listener(published);
33
+ }
34
+ },
35
+ close: () => {
36
+ closed = true;
37
+ listeners.clear();
38
+ }
39
+ };
40
+ };
41
+ //#endregion
42
+ export { createPublisher };
43
+
44
+ //# sourceMappingURL=publisher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publisher.js","names":[],"sources":["../src/publisher.ts"],"sourcesContent":["// The Layer 1 publishing rules, shared by every runner in this package:\n// 1. get() returns the latest published snapshot.\n// 2. subscribe() delivers the current snapshot at once; unsubscribing twice is safe.\n// 4. Each snapshot is one object, published whole (atomic).\n// 5. Snapshots are deep-frozen when `freeze` is on (development builds).\n// 6. After close(), nothing publishes and no listener is called.\nimport type { Reporter } from './ports.ts';\nimport { deepFreeze } from './values.ts';\n\nexport type Publisher<Snapshot> = {\n get(): Snapshot;\n subscribe(listener: (snapshot: Snapshot) => void): () => void;\n publish(snapshot: Snapshot): void;\n close(): void;\n};\n\nexport const createPublisher = <Snapshot>(\n initial: Snapshot,\n options: { readonly freeze: boolean; readonly report: Reporter },\n): Publisher<Snapshot> => {\n const seal = (snapshot: Snapshot): Snapshot => (options.freeze ? deepFreeze(snapshot) : snapshot);\n let current = seal(initial);\n let closed = false;\n const listeners = new Set<(snapshot: Snapshot) => void>();\n\n const call = (listener: (snapshot: Snapshot) => void, snapshot: Snapshot): void => {\n try {\n listener(snapshot);\n } catch (error) {\n // One broken subscriber must not starve the others or tear the session's state.\n options.report(error, 'A session listener threw.');\n }\n };\n\n return {\n get: () => current,\n subscribe: (listener) => {\n if (closed) return () => {};\n // Wrap so the same function can subscribe twice and unsubscribe each independently.\n const entry = (snapshot: Snapshot): void => call(listener, snapshot);\n listeners.add(entry);\n call(listener, current);\n return () => {\n listeners.delete(entry);\n };\n },\n publish: (snapshot) => {\n if (closed) return;\n current = seal(snapshot);\n const published = current;\n for (const listener of [...listeners]) {\n // A listener removed by an earlier one in this round is skipped.\n if (closed || !listeners.has(listener)) continue;\n listener(published);\n }\n },\n close: () => {\n closed = true;\n listeners.clear();\n },\n };\n};\n"],"mappings":";;AAgBA,IAAa,mBACX,SACA,YACwB;CACxB,MAAM,QAAQ,aAAkC,QAAQ,SAAS,WAAW,QAAQ,IAAI;CACxF,IAAI,UAAU,KAAK,OAAO;CAC1B,IAAI,SAAS;CACb,MAAM,4BAAY,IAAI,IAAkC;CAExD,MAAM,QAAQ,UAAwC,aAA6B;EACjF,IAAI;GACF,SAAS,QAAQ;EACnB,SAAS,OAAO;GAEd,QAAQ,OAAO,OAAO,2BAA2B;EACnD;CACF;CAEA,OAAO;EACL,WAAW;EACX,YAAY,aAAa;GACvB,IAAI,QAAQ,aAAa,CAAC;GAE1B,MAAM,SAAS,aAA6B,KAAK,UAAU,QAAQ;GACnE,UAAU,IAAI,KAAK;GACnB,KAAK,UAAU,OAAO;GACtB,aAAa;IACX,UAAU,OAAO,KAAK;GACxB;EACF;EACA,UAAU,aAAa;GACrB,IAAI,QAAQ;GACZ,UAAU,KAAK,QAAQ;GACvB,MAAM,YAAY;GAClB,KAAK,MAAM,YAAY,CAAC,GAAG,SAAS,GAAG;IAErC,IAAI,UAAU,CAAC,UAAU,IAAI,QAAQ,GAAG;IACxC,SAAS,SAAS;GACpB;EACF;EACA,aAAa;GACX,SAAS;GACT,UAAU,MAAM;EAClB;CACF;AACF"}
@@ -0,0 +1,128 @@
1
+ import type { GameSession, Json, SeatId, SessionMeta, Validator, ViewBeat } from '@joystyk/contracts';
2
+ import { type GamePin, type PublicSeat, type RoomControl, type RoomOffer, type RoomPhase, type RoomSettings } from '@joystyk/rooms/protocol';
3
+ import type { Clock, Reporter } from './ports.js';
4
+ /** One open connection to the room server. */
5
+ export interface RemoteSocket {
6
+ send(text: string): void;
7
+ close(code?: number, reason?: string): void;
8
+ }
9
+ /** What a socket reports. `close` fires once, also when the connection never opened. */
10
+ export type SocketEvents = {
11
+ open(): void;
12
+ message(text: string): void;
13
+ close(code: number): void;
14
+ };
15
+ /** Opens a connection to the room server's `/ws`. The browser adapter wraps `WebSocket`. */
16
+ export type SocketFactory = (events: SocketEvents) => RemoteSocket;
17
+ /** Where seat tokens live, one per room code. The browser adapter uses local storage, so every tab shares them. */
18
+ export interface TokenStore {
19
+ get(room: string): string | null;
20
+ set(room: string, token: string): void;
21
+ delete(room: string): void;
22
+ }
23
+ /** Tokens kept in memory only: for tests, and for a page whose storage is blocked. */
24
+ export declare const memoryTokens: () => TokenStore;
25
+ /**
26
+ * What the game's UI and the launcher dispatch. Commands always come from the viewer's seat.
27
+ * `resign` is the platform's resign control. `control` carries a room control (ready,
28
+ * settings, start, offers …) from the launcher's lobby and results. `reconnect` takes the seat back
29
+ * after another tab replaced this one; `leave` gives the seat up and closes the session.
30
+ * Local-only inputs (`pause`, `resume`, `reveal`, `undo`) are ignored: a room never pauses.
31
+ */
32
+ export type RemoteInput<Command> =
33
+ /** A command for this connection's seat. A named seat must be that seat: a room never acts for another (gp-o2c). */
34
+ {
35
+ readonly kind: 'command';
36
+ readonly command: Command;
37
+ readonly seat?: SeatId;
38
+ } | {
39
+ readonly kind: 'resign';
40
+ } | {
41
+ readonly kind: 'control';
42
+ readonly control: RoomControl;
43
+ } | {
44
+ readonly kind: 'reconnect';
45
+ } | {
46
+ readonly kind: 'leave';
47
+ } | {
48
+ readonly kind: 'resync';
49
+ };
50
+ /** A rematch or end-match offer, with an end offer's time left measured at `clocksReadAt`. */
51
+ export type RemoteOffer = RoomOffer & {
52
+ /** An end offer: milliseconds until the match ends if nobody declines. Null for a rematch. */
53
+ readonly endsInMs: number | null;
54
+ };
55
+ /** The room around the match: what the launcher's lobby and frame draw. */
56
+ export type RemoteRoom = {
57
+ readonly code: string;
58
+ readonly epoch: number;
59
+ readonly phase: RoomPhase;
60
+ readonly host: SeatId | null;
61
+ readonly seats: readonly PublicSeat[];
62
+ readonly settings: RoomSettings;
63
+ readonly game: GamePin;
64
+ /** Indexed by seat: whether a player is connected to it. */
65
+ readonly connected: readonly boolean[];
66
+ readonly spectators: number;
67
+ /** No player is connected, so the table is paused and its clocks hold their time (D-58). */
68
+ readonly paused: boolean;
69
+ /** Counts matches in this room; it rises at every start and rematch. */
70
+ readonly match: number;
71
+ /** When the clocks in `meta.clocks` (and the offer's time left) were read, on this session's clock. */
72
+ readonly clocksReadAt: number;
73
+ /** The latest rematch or end-match offer (D-61). */
74
+ readonly offer: RemoteOffer | null;
75
+ };
76
+ /** The remote client's snapshot: the contract's `EngineSnapshot`, plus the room. `view` is null in a lobby. */
77
+ export type RemoteSnapshot<View, Event> = {
78
+ readonly meta: SessionMeta;
79
+ readonly view: View | null;
80
+ readonly beats: readonly ViewBeat<View, Event>[];
81
+ readonly pending: readonly SeatId[];
82
+ readonly presentation: Json | null;
83
+ /** Null until the first snapshot arrives. */
84
+ readonly room: RemoteRoom | null;
85
+ };
86
+ export interface RemoteSession<Command, View, Event> extends GameSession<RemoteSnapshot<View, Event>, RemoteInput<Command>> {
87
+ /** The room code this session plays in. */
88
+ readonly code: string;
89
+ }
90
+ /** The engine parts the client needs: its schemas, to decode what the server sends. */
91
+ export type RemoteGame<Command, View, Event> = {
92
+ readonly id: string;
93
+ /** The build this page carries, sent with `join`. */
94
+ readonly build: string;
95
+ readonly rulesVersion: number;
96
+ readonly schemas: {
97
+ readonly command: Validator<Command>;
98
+ readonly view: Validator<View>;
99
+ readonly event: Validator<Event>;
100
+ };
101
+ };
102
+ export type RemoteSessionOptions<Command, View, Event> = {
103
+ readonly game: RemoteGame<Command, View, Event>;
104
+ /** The six-character room code. */
105
+ readonly room: string;
106
+ /** The display name to claim a seat with (the server filters it again). */
107
+ readonly name: string;
108
+ /** Ask to watch even if a seat is free (D-33). */
109
+ readonly watch?: boolean;
110
+ readonly connect: SocketFactory;
111
+ readonly tokens: TokenStore;
112
+ readonly clock: Clock;
113
+ /** A fresh action ID: 8–40 URL-safe characters, unique for the seat across reloads. */
114
+ readonly actionId: () => string;
115
+ /** A number in [0, 1) for the reconnect jitter. Default: always 0.5. */
116
+ readonly random?: () => number;
117
+ /** Deep-freeze snapshots. Default true. */
118
+ readonly freeze?: boolean;
119
+ readonly report?: Reporter;
120
+ };
121
+ /** Greed's backoff: min(10 s, 500 ms · 2ⁿ) plus up to 250 ms of jitter. */
122
+ export declare const reconnectDelay: (attempt: number, random: number) => number;
123
+ /**
124
+ * Opens a session in a server room. The socket opens at once; the first snapshot arrives
125
+ * after the server's `welcome`. Nothing throws: problems become `meta.notice` and
126
+ * `meta.connection`.
127
+ */
128
+ export declare const createRemoteSession: <Command, View, Event>(options: RemoteSessionOptions<Command, View, Event>) => RemoteSession<Command, View, Event>;