@joystyk/launcher 0.0.0 → 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.
- package/CHANGELOG.md +38 -0
- package/README.md +245 -2
- package/dist/browser/daily-prefetch.d.ts +8 -0
- package/dist/browser/daily-prefetch.js +32 -0
- package/dist/browser/daily-prefetch.js.map +1 -0
- package/dist/browser/index.d.ts +3 -0
- package/dist/browser/index.js +4 -0
- package/dist/browser/rooms.d.ts +2 -0
- package/dist/browser/rooms.js +49 -0
- package/dist/browser/rooms.js.map +1 -0
- package/dist/browser/services.d.ts +127 -0
- package/dist/browser/services.js +95 -0
- package/dist/browser/services.js.map +1 -0
- package/dist/browser/stage.d.ts +28 -0
- package/dist/browser/stage.js +1195 -0
- package/dist/browser/stage.js.map +1 -0
- package/dist/fixture/engine.d.ts +60 -0
- package/dist/fixture/engine.js +164 -0
- package/dist/fixture/engine.js.map +1 -0
- package/dist/fixture/index.d.ts +52 -0
- package/dist/fixture/index.js +63 -0
- package/dist/fixture/index.js.map +1 -0
- package/dist/fixture/module.d.ts +2 -0
- package/dist/fixture/module.js +135 -0
- package/dist/fixture/module.js.map +1 -0
- package/dist/fixture/probe.d.ts +10 -0
- package/dist/fixture/probe.js +19 -0
- package/dist/fixture/probe.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/launcher.d.ts +190 -0
- package/dist/launcher.js +4614 -0
- package/dist/launcher.js.map +1 -0
- package/dist/model.d.ts +431 -0
- package/dist/model.js +566 -0
- package/dist/model.js.map +1 -0
- package/dist/report-problem.d.ts +39 -0
- package/dist/report-problem.js +359 -0
- package/dist/report-problem.js.map +1 -0
- package/dist/report.d.ts +18 -0
- package/dist/report.js +32 -0
- package/dist/report.js.map +1 -0
- package/dist/room-drivers.d.ts +26 -0
- package/dist/room-drivers.js +92 -0
- package/dist/room-drivers.js.map +1 -0
- package/dist/safe-point.d.ts +33 -0
- package/dist/safe-point.js +44 -0
- package/dist/safe-point.js.map +1 -0
- package/dist/save-slots.d.ts +6 -0
- package/dist/save-slots.js +18 -0
- package/dist/save-slots.js.map +1 -0
- package/dist/theme.d.ts +35 -0
- package/dist/theme.js +111 -0
- package/dist/theme.js.map +1 -0
- package/package.json +67 -11
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stage.js","names":["game"],"sources":["../../src/browser/stage.ts"],"sourcesContent":["// <jy-game-stage>: the part of the launcher with a lifecycle. `<jy-launcher>` renders it with\n// a `control` property (plain data, model.ts `StageControl`) and listens to its outputs.\n// When connected it loads the game through the app's loaders, opens the player's storage and\n// looks for a save; when told to begin it creates the local session (or the fixed-step\n// runner, or the game's own Layer 1 session), mounts the canvas with a PlatformApi, and\n// reports what the frame needs from each snapshot. Disconnecting it disposes everything; the\n// session writes its last save on the way out.\n//\n// The daily puzzle (D-46) is a local session on today's published config: after the\n// AI-content notice, the stage downloads today's puzzle through `@joystyk/daily/browser`\n// (cached for offline play), or reports today's earlier result when the player has already\n// finished it; at the outcome it records the result and reports the streaks and share text.\n//\n// Online with friends, it opens a server room instead: it creates the room (or checks the\n// code it was given), opens the remote session, reports the room after every change, performs\n// the lobby's requests, and mounts the game when the match starts (again at each rematch).\n//\n// The canvas is a `<div>` in the stage's own shadow root, so the frame's styles never reach\n// the game. It carries the game's theme tokens (`--game-accent`, `--game-surface`,\n// `--game-ink`; see ../theme.ts) and the player's color scheme, which resolves any\n// `light-dark()` pair in them; the frame keeps Joystyk's tokens (D-07).\nimport { OUTPUT_EVENT } from '@gyral/core';\nimport {\n err,\n setupOptions,\n gameError,\n ok,\n type AnyGameSession,\n type GameEntry,\n type GameError,\n type GameModule,\n type Json,\n type Mounted,\n type Outcome,\n type PlatformApi,\n type Preferences,\n type Result,\n type SaveEnvelope,\n type OutcomeResult,\n type SeatInfo,\n type SessionSnapshot,\n type ShareSummary,\n type Validator,\n} from '@joystyk/contracts';\nimport {\n dailyWordStats,\n type DailyWordStats,\n creditDate,\n parsePuzzleId,\n shareText,\n utcDate,\n type DailyOutcome,\n type DailyResult,\n type ShareRow,\n type Streaks,\n} from '@joystyk/daily';\nimport {\n contentHashOf,\n createDailyClient,\n createDailyResults,\n nextReleaseLocal,\n type DailyResults,\n} from '@joystyk/daily/browser';\nimport { restoreRng } from '@joystyk/kit/rng';\nimport {\n createFixedStepRunner,\n createLocalSession,\n createRemoteSession,\n restoreFixedStepRunner,\n restoreLocalSession,\n type LocalMode,\n type LocalSession,\n type RemoteSession,\n type RemoteSnapshot,\n type SaveStore as RunnerSaveStore,\n} from '@joystyk/session';\nimport { createGameAudio, watchGameAudio, workerAiRunner, workerJobRunner } from '@joystyk/session/browser';\nimport { DEFAULT_PREFERENCES, resolvePreferences, type SavedGame, type StoredPreferences } from '@joystyk/storage';\nimport {\n accessGate,\n clockOptions,\n GUEST_NAME,\n type ClockSetting,\n type DailyFrame,\n type FrameState,\n type RoomFrame,\n type RoomOfferFrame,\n type RoomRequest,\n type RoomSeat,\n type RoomTimer,\n type StageControl,\n type StageOutput,\n} from '../model.ts';\nimport { SAVE_REQUEST_EVENT, type SaveRequestDetail } from '../safe-point.ts';\nimport { canvasTheme } from '../theme.ts';\nimport { saveDescription } from '../save-slots.ts';\nimport { pageRooms } from './rooms.ts';\nimport {\n findLauncherServices,\n type GameLoader,\n type LauncherServices,\n type PlayerContext,\n type RoomServices,\n} from './services.ts';\n\nexport const STAGE_TAG = 'jy-game-stage';\n\nconst STAGE_STYLES = `\n :host { display: block; }\n :host([hidden]) { display: none; }\n .canvas {\n display: block;\n min-block-size: inherit;\n border-radius: inherit;\n background: var(--game-surface, transparent);\n color: var(--game-ink, inherit);\n color-scheme: inherit;\n }\n`;\n\nlet sheet: CSSStyleSheet | null = null;\nconst stageSheet = (): CSSStyleSheet => {\n if (sheet === null) {\n sheet = new CSSStyleSheet();\n sheet.replaceSync(STAGE_STYLES);\n }\n return sheet;\n};\n\ntype Inputs = (input: { readonly kind: 'pause' | 'resume' | 'reveal' }) => void;\n\ntype Running = {\n readonly session: AnyGameSession;\n readonly inputs: Inputs | null;\n /** The engine's config for a rematch, read at the outcome, or null. */\n readonly rematch: () => Json | null;\n /** The engine's state now (a local engine session), for the daily share summary; null otherwise. */\n readonly state: () => Json | null;\n /** The save envelope for the position now (local and fixed-step sessions); null for rooms and Layer 1 games. */\n readonly checkpoint: (() => SaveEnvelope) | null;\n};\n\nconst noRematch = (): null => null;\nconst noState = (): null => null;\n\ntype Loaded = {\n readonly loader: GameLoader;\n readonly entry: GameEntry<HTMLElement>;\n readonly module: GameModule<HTMLElement>;\n readonly player: PlayerContext;\n readonly preferences: StoredPreferences;\n save: SavedGame | null;\n};\n\nconst json = (value: unknown): string => JSON.stringify(value);\n\n/** Runs a synchronous Standard Schema; an async one is refused. */\nconst decodeSync = <T>(schema: Validator<T>, value: unknown): Result<T, GameError> => {\n const result = schema['~standard'].validate(value);\n if (result instanceof Promise) return err(gameError('AsyncSchema', 'The game setup could not be checked.'));\n return result.issues === undefined\n ? ok(result.value)\n : err(gameError('InvalidSetup', 'The game setup is not valid.', { issues: result.issues.map((i) => i.message) }));\n};\n\nconst describeSave = (describe: (state: Json) => unknown, state: Json) => {\n try {\n return decodeSync(saveDescription, describe(state));\n } catch {\n return err(gameError('InvalidSaveSlot', 'The saved game could not be grouped.'));\n }\n};\n\n/**\n * A save slot per local mode, adapted to the runner's port. After the game ends, writes are\n * dropped and the slot is cleared, so a finished game is never offered for resuming. The\n * runner clears it too while the game isn't worth saving (the engine's `worthSaving`).\n */\nconst saveSlot = (\n player: PlayerContext,\n gameId: string,\n slot: string,\n groupOf?: (save: SaveEnvelope) => string | null,\n initialGroup: string | null = null,\n) => {\n let finished = false;\n let group = initialGroup;\n let chain: Promise<void> = Promise.resolve();\n const port: RunnerSaveStore = {\n write: (save: SaveEnvelope) => {\n if (finished) return Promise.resolve();\n const write = chain.then(async () => {\n group = groupOf?.(save) ?? null;\n if (group !== null) {\n const grouped = await player.storage.saves.write(gameId, group, save);\n if (!grouped.ok) throw new Error(grouped.error.message);\n }\n const written = await player.storage.saves.write(gameId, slot, save);\n if (!written.ok) throw new Error(written.error.message);\n });\n chain = write.catch(() => {});\n return write;\n },\n clear: () => {\n if (finished) return Promise.resolve();\n const cleared = chain.then(async () => {\n if (group !== null) {\n await player.storage.saves.delete(gameId, group);\n group = null;\n }\n const deleted = await player.storage.saves.delete(gameId, slot);\n if (!deleted.ok) throw new Error(deleted.error.message);\n });\n chain = cleared.catch(() => {});\n return cleared;\n },\n };\n return {\n port,\n clear: (): Promise<void> => {\n chain = chain.then(async () => {\n if (group !== null) {\n await player.storage.saves.delete(gameId, group);\n group = null;\n }\n await player.storage.saves.delete(gameId, slot);\n });\n return chain;\n },\n finish: (): Promise<void> => {\n finished = true;\n chain = chain.then(async () => {\n if (group !== null) {\n await player.storage.saves.delete(gameId, group);\n group = null;\n }\n await player.storage.saves.delete(gameId, slot);\n });\n return chain;\n },\n };\n};\n\nconst handoffOf = (snapshot: SessionSnapshot): FrameState['handoff'] => {\n const handoff = (snapshot as { readonly handoff?: unknown }).handoff;\n if (typeof handoff !== 'object' || handoff === null) return null;\n const { seat, name } = handoff as { readonly seat?: unknown; readonly name?: unknown };\n return typeof seat === 'number' && typeof name === 'string' ? { seat, name } : null;\n};\n\n/** What the frame needs from a snapshot, as plain data. */\nexport const frameOf = (snapshot: SessionSnapshot): FrameState => ({\n seats: snapshot.meta.seats.map((seat) => ({ ...seat })),\n handoff: handoffOf(snapshot),\n outcome: snapshot.meta.outcome === null ? null : (JSON.parse(JSON.stringify(snapshot.meta.outcome)) as Outcome),\n safePoint: snapshot.meta.safePoint,\n notice: snapshot.meta.notice?.message ?? null,\n});\n\n/** The room's clock setting, as the lobby shows it. */\nconst clockOf = (settings: {\n readonly decisionMs: number | null;\n readonly bank: { readonly initialMs: number; readonly incrementMs: number } | null;\n}): ClockSetting =>\n settings.bank !== null\n ? { kind: 'bank', initialMs: settings.bank.initialMs, incrementMs: settings.bank.incrementMs }\n : settings.decisionMs !== null\n ? { kind: 'move', ms: settings.decisionMs }\n : { kind: 'off' };\n\nconst seatName = (seats: readonly RoomSeat[], seat: number): string =>\n seats.find((s) => s.id === seat)?.name ?? `Seat ${String(seat + 1)}`;\n\n/**\n * The decision clock that is running, counted down from when the room reported it; or, on a\n * paused table (D-58), the clock that waits, holding its time.\n */\nconst timerOf = (snapshot: RemoteSnapshot<Json, Json>, seats: readonly RoomSeat[], now: number): RoomTimer | null => {\n const room = snapshot.room;\n const clocks = snapshot.meta.clocks ?? [];\n const shown = clocks.find((clock) => clock.running) ?? clocks.find((clock) => clock.paused === true);\n if (room === null || shown === undefined || room.phase !== 'playing') return null;\n const left = shown.running ? Math.max(0, shown.remainingMs - (now - room.clocksReadAt)) : shown.remainingMs;\n return {\n seat: shown.seat,\n name: seatName(seats, shown.seat),\n seconds: Math.ceil(left / 1000),\n paused: !shown.running,\n };\n};\n\n/** The room's offer, with the players it waits for and an end offer's seconds left. */\nconst offerOf = (\n snapshot: RemoteSnapshot<Json, Json>,\n seats: readonly RoomSeat[],\n now: number,\n): RoomOfferFrame | null => {\n const room = snapshot.room;\n const offer = room?.offer ?? null;\n if (room === null || offer === null) return null;\n // A rematch waits for every seated player; an end offer for the other players who are here.\n const voters = seats.filter(\n (s) => s.claimed && s.controller === 'human' && (offer.kind === 'rematch' || (s.id !== offer.by && s.connected)),\n );\n const elapsed = room.paused ? 0 : Math.max(0, now - room.clocksReadAt);\n return {\n id: offer.id,\n kind: offer.kind,\n by: offer.by,\n byName: seatName(seats, offer.by),\n status: offer.status,\n accepted: [...offer.accepted],\n declinedBy: offer.declinedBy,\n declinedName: offer.declinedBy === null ? null : seatName(seats, offer.declinedBy),\n waiting: offer.status === 'open' ? voters.filter((s) => !offer.accepted.includes(s.id)).map((s) => s.name) : [],\n seconds: offer.endsInMs === null ? null : Math.ceil(Math.max(0, offer.endsInMs - elapsed) / 1000),\n };\n};\n\n/** What the lobby and the bar need from a room snapshot, as plain data. */\nexport const roomFrameOf = (\n snapshot: RemoteSnapshot<Json, Json>,\n context: {\n readonly code: string;\n readonly link: string;\n readonly manifest: GameEntry['manifest'];\n readonly now: number;\n },\n): RoomFrame => {\n const room = snapshot.room;\n const seats: RoomSeat[] = (room?.seats ?? []).map((seat) => ({\n id: seat.id,\n name: seat.name,\n controller: seat.controller,\n aiLevel: seat.aiLevel,\n claimed: seat.claimed,\n ready: seat.ready,\n connected: room?.connected[seat.id] ?? false,\n }));\n const { manifest } = context;\n return {\n code: context.code,\n link: context.link,\n phase: room?.phase ?? (snapshot.meta.connection === 'closed' ? 'closed' : 'connecting'),\n connection: snapshot.meta.connection === 'local' ? 'online' : snapshot.meta.connection,\n seat: snapshot.meta.seat,\n host: room?.host ?? null,\n seats,\n minSeats: Math.max(2, manifest.players.min),\n clock: room === null ? { kind: 'off' } : clockOf(room.settings),\n clockOptions: clockOptions(manifest.clock),\n aiLevel: room?.settings.aiLevel ?? null,\n aiLevels: manifest.ai === null ? [] : [...manifest.ai.levels],\n spectators: room?.spectators ?? 0,\n timer: timerOf(snapshot, seats, context.now),\n notice: snapshot.meta.notice?.message ?? null,\n noticeCode: snapshot.meta.notice?.code ?? null,\n match: room?.match ?? 0,\n offer: offerOf(snapshot, seats, context.now),\n };\n};\n\n/** A finished game's result as a daily result records it. */\nconst dailyOutcome = (result: OutcomeResult): DailyOutcome =>\n result === 'won' ? 'won' : result === 'lost' || result === 'abandoned' ? 'lost' : 'completed';\n\n/** The game page's address for share text: no query string or fragment (a room's code, say). */\nconst pageUrl = (): string | undefined => {\n const url = `${location.origin}${location.pathname}`;\n return /^https?:\\/\\/[^\\s?#]+$/.test(url) ? url : undefined;\n};\n\n/** The results panel's daily part: share text in both forms, streaks and the next release. */\nconst dailyFrame = (\n title: string,\n result: DailyResult,\n streaks: Streaks | null,\n headline: string,\n now: () => number,\n earlier: boolean,\n statistics?: DailyWordStats,\n): DailyFrame => {\n const date = creditDate(result);\n const rows: ShareRow[] = result.shareRows.map((symbols, i) => ({\n symbols,\n words: result.shareWords?.[i] ?? `Row ${String(i + 1)}`,\n ...(result.shareDetails?.[i] == null ? {} : { detail: result.shareDetails[i]! }),\n }));\n const url = pageUrl();\n const input = { title, date, headline, rows, ...(url === undefined ? {} : { url }) };\n let share = '';\n let shareWords = '';\n try {\n share = shareText(input, 'symbols');\n shareWords = shareText(input, 'words');\n } catch {\n // A game whose rows break the share rules (letters in a symbol row) shares nothing rather than a spoiler.\n }\n const next = nextReleaseLocal({ now });\n const canLose = result.outcome !== 'completed';\n return {\n date,\n ...(statistics === undefined ? {} : { statistics }),\n share,\n shareWords,\n streak: streaks?.current ?? 0,\n bestStreak: streaks?.best ?? 0,\n winStreak: canLose ? (streaks?.winCurrent ?? 0) : null,\n bestWinStreak: canLose ? (streaks?.winBest ?? 0) : null,\n next: `New puzzle at ${next.time} (${next.countdown})`,\n earlier: earlier ? { result: result.outcome, summary: result.summary } : null,\n };\n};\n\n/** One game on one stage, from loading to disposal. */\nconst run = (host: HTMLElement, canvas: HTMLElement, first: StageControl) => {\n let control = first;\n let disposed = false;\n const preparations = new Set<{ dispose(): void }>();\n let loaded: Loaded | null = null;\n let begun = false;\n let running: Running | null = null;\n let mounted: Mounted | null = null;\n let stopAudio: (() => void) | null = null;\n let slot: ReturnType<typeof saveSlot> | null = null;\n let paused = false;\n let reveals = control.reveals;\n let lastPreferences = '';\n let lastFrame = '';\n let finished = false;\n const stops: (() => void)[] = [];\n const services: LauncherServices | null = findLauncherServices(host);\n let platform: PlatformApi | null = null;\n let saveChoices: readonly {\n slot: string;\n label: string;\n savedAt: number;\n elapsedMs: number;\n players: readonly string[];\n }[] = [];\n /** Online with friends: the room, once open. */\n let remote: {\n readonly session: RemoteSession<Json, Json, Json>;\n readonly code: string;\n readonly link: string;\n /** The match the canvas shows, or null before it mounts. */\n mountedMatch: number | null;\n lastRoom: string;\n ticker: (() => void) | null;\n } | null = null;\n\n const emit = (out: StageOutput): void => {\n if (!disposed) host.dispatchEvent(new CustomEvent(OUTPUT_EVENT, { detail: out, bubbles: true }));\n };\n const fail = (message: string, error?: unknown): void => {\n if (error !== undefined) services?.ports.report(error, message);\n emit({ _tag: 'Failed', message });\n };\n\n // --- Loading and the save check ------------------------------------------------------------\n\n const prepare = async (svc: LauncherServices): Promise<void> => {\n const loader = svc.games[control.gameId];\n if (loader === undefined) return fail(\"This game isn't available here yet.\");\n const entry = await loader.entry();\n if (disposed) return;\n const { manifest } = entry;\n if (manifest.id !== control.gameId || !manifest.modes.includes(control.mode))\n return fail(\"This game can't be played in that mode.\");\n if (control.mode === 'server' && (control.room === null || manifest.players.max < 2))\n return fail(\"This game can't be played online here.\");\n if (accessGate(manifest.access) !== 'open') return fail('This game needs a pass to play.');\n const [module, player] = await Promise.all([entry.load(), svc.openPlayer()]);\n if (disposed) return;\n const stored = await player.storage.preferences.get();\n const preferences = stored.ok ? stored.value : DEFAULT_PREFERENCES;\n loaded = { loader, entry, module, player, preferences, save: await findSave(entry, module, player) };\n if (disposed) return;\n lastPreferences = json(preferences);\n // A preference changed in another tab: the launcher's menu and motion follow it, and\n // `lastPreferences` moves too, so `sync` doesn't write it back (gp-d4v). The game hears\n // of it through `platformFor`.\n stops.push(\n player.storage.preferences.subscribe((next, source) => {\n if (source !== 'other-tab' || json(next) === lastPreferences) return;\n lastPreferences = json(next);\n emit({ _tag: 'Preferences', preferences: next });\n }),\n );\n const save = loaded.save;\n emit({\n _tag: 'Prepared',\n preferences,\n save:\n save === null\n ? null\n : {\n savedAt: save.savedAt,\n elapsedMs: save.envelope.elapsedMs ?? 0,\n players: save.envelope.seats.map((seat) => seat.name),\n ...(saveChoices.length === 0 ? {} : { slot: save.slot, choices: saveChoices }),\n },\n });\n sync();\n };\n\n /** The saved game in this mode's slot, if it can still be played on. Others are cleared. */\n const findSave = async (\n entry: GameEntry<HTMLElement>,\n module: GameModule<HTMLElement>,\n player: PlayerContext,\n requested = control.mode as string,\n one = false,\n ): Promise<SavedGame | null> => {\n const { manifest } = entry;\n // A room keeps its own state on the server; there is nothing to resume on this device.\n if (control.mode === 'server') return null;\n const rules = module.engine ?? module.simulation;\n if (!one && module.engine?.saveSlot !== undefined) {\n const listed = await player.storage.saves.list(manifest.id);\n if (!listed.ok) return null;\n const groups = new Map<string, { save: SavedGame; label: string }>();\n for (const saved of listed.value) {\n if (saved.slot !== control.mode && !saved.slot.startsWith(`${control.mode}-`)) continue;\n const checked = await findSave(entry, module, player, saved.slot, true);\n if (checked === null) continue;\n const description = describeSave(module.engine.saveSlot, checked.envelope.state);\n if (!description.ok) continue;\n const key: string = `${control.mode}-${description.value.key}`;\n if (saved.slot !== control.mode && saved.slot !== key) continue;\n const old = groups.get(key);\n if (old === undefined || checked.savedAt > old.save.savedAt) {\n groups.set(key, { save: checked, label: description.value.label });\n }\n }\n const offered: { save: SavedGame; label: string }[] = [];\n for (const [key, group] of groups) {\n if (group.save.slot !== key) {\n // Migrate the validated legacy save even when an invalid grouped save exists.\n const migrated = await player.storage.saves.write(manifest.id, key, group.save.envelope);\n if (!migrated.ok) continue;\n }\n offered.push({ ...group, save: { ...group.save, slot: key } });\n }\n offered.sort((a, b) => b.save.savedAt - a.save.savedAt || a.save.slot.localeCompare(b.save.slot));\n saveChoices = offered.map(({ save, label }) => ({\n slot: save.slot,\n label,\n savedAt: save.savedAt,\n elapsedMs: save.envelope.elapsedMs ?? 0,\n players: save.envelope.seats.map((seat) => seat.name),\n }));\n return offered[0]?.save ?? null;\n }\n const found =\n rules === undefined\n ? await player.storage.saves.read(manifest.id, requested)\n : await player.storage.saves.load(manifest.id, requested, {\n saveVersion: manifest.versions.save,\n migrations: module.migrations,\n state: rules.schemas.state,\n });\n if (!found.ok || found.value === null) return null;\n const save = found.value;\n let over = false;\n if (rules !== undefined) {\n if (save.envelope.rulesVersion !== rules.rulesVersion) {\n emit({\n _tag: 'Notice',\n message: 'Your saved game was played under older rules, so it was set aside. A new game starts instead.',\n });\n over = true;\n } else {\n try {\n over =\n (module.engine?.outcome(save.envelope.state) ?? module.simulation?.outcome(save.envelope.state) ?? null) !==\n null;\n // An untouched game, saved before the engine said so, isn't offered either.\n over ||= module.engine?.worthSaving?.(save.envelope.state) === false;\n } catch (error) {\n services?.ports.report(error, 'Checking a saved game failed.');\n over = true;\n }\n }\n }\n if (!over) return save;\n await player.storage.saves.delete(manifest.id, requested);\n return null;\n };\n\n // --- Starting the game ---------------------------------------------------------------------\n\n const seatsWithNames = (player: PlayerContext): readonly SeatInfo[] =>\n control.seats.map((seat) =>\n seat.controller === 'human' && seat.name === GUEST_NAME ? { ...seat, name: player.displayName } : seat,\n );\n\n /**\n * The engine's Config: a rematch's, the preparer's output, or the setup itself (empty\n * until setup screens exist, so a game's config schema supplies its defaults).\n */\n const configFor = async (svc: LauncherServices, entry: GameEntry<HTMLElement>): Promise<Result<Json, GameError>> => {\n if (control.config !== null) return ok(control.config);\n const raw = control.setup ?? {};\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw))\n return err(gameError('InvalidSetup', 'Choose valid game options.'));\n const selected = raw as { readonly [key: string]: Json };\n const { level: aiLevel, ...options } = selected;\n if (\n control.mode === 'ai' &&\n entry.manifest.players.max === 1 &&\n aiLevel !== undefined &&\n (typeof aiLevel !== 'string' || !entry.manifest.ai?.levels.includes(aiLevel))\n )\n return err(gameError('InvalidSetup', 'Choose a valid computer level.'));\n const decodedSetup = decodeSync(\n setupOptions(entry.manifest.options ?? [], control.mode),\n control.mode === 'ai' && entry.manifest.players.max === 1 ? options : selected,\n );\n if (!decodedSetup.ok) return decodedSetup;\n const setup: Json =\n control.mode === 'ai' && entry.manifest.players.max === 1\n ? { ...decodedSetup.value, level: aiLevel ?? entry.manifest.ai?.levels[0] ?? 'medium' }\n : decodedSetup.value;\n if (loaded?.module.configFromSetup !== undefined) return loaded.module.configFromSetup(setup, control.mode);\n if (entry.loadAsyncPreparer === undefined && entry.loadPreparer === undefined) return ok(setup);\n const execution = entry.loadAsyncPreparer === undefined ? 'main' : 'worker';\n const started = performance.now();\n try {\n if (entry.loadAsyncPreparer !== undefined) {\n const bridge = await entry.loadAsyncPreparer();\n if (disposed) {\n bridge.dispose();\n return err(gameError('Disposed', 'The game was closed.'));\n }\n preparations.add(bridge);\n try {\n const decoded = decodeSync(bridge.setup, setup);\n return decoded.ok ? await bridge.prepare(decoded.value, restoreRng(svc.newSeed())) : decoded;\n } finally {\n bridge.dispose();\n preparations.delete(bridge);\n }\n }\n const preparer = await entry.loadPreparer!();\n const decoded = decodeSync(preparer.setup, setup);\n return decoded.ok ? preparer.prepare(decoded.value, restoreRng(svc.newSeed())) : decoded;\n } finally {\n const duration = performance.now() - started;\n const name = `joystyk:prepare:${entry.manifest.id}:${execution}`;\n performance.clearMeasures(name);\n performance.measure(name, { start: started, duration });\n if (import.meta.env.DEV)\n console.debug('[Joystyk preparation]', { game: entry.manifest.id, execution, durationMs: duration });\n }\n };\n\n const start = async (svc: LauncherServices, game: Loaded, how: 'new' | 'resume'): Promise<void> => {\n const mode = control.mode;\n if (mode === 'server') return startRoom(svc, game);\n const { entry, player, loader } = game;\n const { manifest } = entry;\n let resume = how === 'resume' ? game.save : null;\n if (how === 'resume' && control.resumeSlot !== undefined) {\n if (!saveChoices.some((choice) => choice.slot === control.resumeSlot))\n return fail('That saved game is no longer available.');\n resume = await findSave(entry, game.module, player, control.resumeSlot, true);\n if (resume === null) return fail('That saved game could not be resumed.');\n }\n const groupOf =\n game.module.engine?.saveSlot === undefined\n ? undefined\n : (save: SaveEnvelope): string | null => {\n const described = describeSave(game.module.engine!.saveSlot!, save.state);\n if (!described.ok) {\n svc.ports.report(described.error, 'The engine supplied an invalid save slot.');\n return null;\n }\n return `${mode}-${described.value.key}`;\n };\n const saves = saveSlot(\n player,\n manifest.id,\n mode,\n groupOf,\n resume !== null && game.module.engine?.saveSlot !== undefined ? resume.slot : null,\n );\n const write = saves.port.write;\n saves.port.write = (save) =>\n write(\n mode === 'daily' && daily !== null\n ? { ...save, daily: { puzzleId: daily.puzzleId, contentHash: daily.contentHash } }\n : save,\n );\n slot = saves;\n let given: Json | null = null;\n if (mode === 'daily') {\n if (resume !== null) await keepDaily(svc, game, resume);\n else {\n const today = await openDaily(svc, game);\n if (today === null || disposed) return;\n given = today;\n }\n }\n if (resume === null && game.save !== null) await saves.clear();\n if (disposed) return;\n const seats = seatsWithNames(player);\n const created = await createSession(svc, game, mode, seats, resume, loader, given);\n if (disposed) {\n if (created.ok) created.value.session.dispose();\n return;\n }\n if (!created.ok) {\n if (resume === null) return fail(created.error.message, created.error);\n // A save that can't be resumed is cleared, and a new game starts in its place.\n emit({ _tag: 'Notice', message: `${created.error.message} A new game starts instead.` });\n await saves.clear();\n game.save = null;\n return start(svc, game, 'new');\n }\n running = created.value;\n mount(svc, game, created.value.session);\n };\n\n const createSession = async (\n svc: LauncherServices,\n game: Loaded,\n mode: LocalMode,\n seats: readonly SeatInfo[],\n resume: SavedGame | null,\n loader: GameLoader,\n /** The daily puzzle's config, which replaces the game's own setup. */\n given: Json | null = null,\n ): Promise<Result<Running, GameError>> => {\n const { entry, module } = game;\n const { manifest } = entry;\n const storage = slot?.port;\n const common = {\n manifest,\n report: svc.ports.report,\n visibility: svc.ports.visibility,\n ...(storage === undefined ? {} : { storage }),\n freeze: svc.freeze ?? true,\n };\n if (module.engine !== undefined) {\n const heavy = manifest.ai?.heavy === true && loader.aiWorker !== undefined;\n const local = {\n ...common,\n mode,\n clock: svc.ports.clock,\n aiSeed: svc.newSeed(),\n aiDelayMs: svc.aiDelayMs ?? 500,\n ...(heavy && loader.aiWorker !== undefined ? { ai: workerAiRunner(loader.aiWorker) } : {}),\n ...(loader.jobWorker === undefined ? {} : { jobs: workerJobRunner(loader.jobWorker) }),\n };\n const engine = module.engine;\n const wrap = (session: LocalSession<Json, Json, Json>): Running => ({\n session,\n inputs: (input) => session.dispatch(input),\n state: () => session.save().state,\n checkpoint: () => session.save(),\n rematch: () => {\n const replay = session.replay();\n if (engine.rematch === undefined || replay === null) return null;\n try {\n return engine.rematch(replay.config, session.save().state);\n } catch (error) {\n svc.ports.report(error, 'The engine failed to name a rematch config.');\n return null;\n }\n },\n });\n if (resume !== null) {\n const restored = restoreLocalSession(engine, resume.envelope, { ...local, migrations: module.migrations });\n return restored.ok ? ok(wrap(restored.value)) : restored;\n }\n const choices =\n given === null ? null : decodeSync(setupOptions(manifest.options ?? [], mode), control.setup ?? {});\n if (choices !== null && !choices.ok) return choices;\n const config =\n given === null\n ? await configFor(svc, entry)\n : module.configFromSetup !== undefined && choices?.ok\n ? module.configFromSetup(choices.value, mode, given)\n : ok(given);\n if (!config.ok) return config;\n const created = createLocalSession(engine, { ...local, seats, config: config.value, seed: svc.newSeed() });\n return created.ok ? ok(wrap(created.value)) : created;\n }\n if (module.simulation !== undefined) {\n const fixed = { ...common, frames: svc.ports.frames };\n const pausable =\n (runner: { dispatch(input: { readonly kind: 'pause' | 'resume' }): void }): Inputs =>\n (input) => {\n if (input.kind !== 'reveal') runner.dispatch({ kind: input.kind });\n };\n if (resume !== null) {\n const restored = restoreFixedStepRunner(module.simulation, resume.envelope, {\n ...fixed,\n startPaused: true,\n migrations: module.migrations,\n });\n return restored.ok\n ? ok({\n session: restored.value,\n inputs: pausable(restored.value),\n rematch: noRematch,\n state: () => restored.value.save().state,\n checkpoint: () => restored.value.save(),\n })\n : restored;\n }\n const config = given === null ? await configFor(svc, entry) : ok(given);\n if (!config.ok) return config;\n const created = createFixedStepRunner(module.simulation, {\n ...fixed,\n seats,\n config: config.value,\n seed: svc.newSeed(),\n });\n return created.ok\n ? ok({\n session: created.value,\n inputs: pausable(created.value),\n rematch: noRematch,\n state: () => created.value.save().state,\n checkpoint: () => created.value.save(),\n })\n : created;\n }\n if (module.createSession !== undefined) {\n const config = await configFor(svc, entry);\n if (!config.ok) return config;\n try {\n const session = module.createSession({\n mode,\n seats,\n seat: seats.find((seat) => seat.controller === 'human')?.id ?? null,\n config: config.value,\n seed: svc.newSeed(),\n save: resume?.envelope ?? null,\n });\n return ok({ session, inputs: null, rematch: noRematch, state: noState, checkpoint: null });\n } catch (error) {\n return err(gameError('SessionFailed', \"The game couldn't start.\", { error: String(error) }));\n }\n }\n return err(gameError('NoSession', \"This game can't be played yet.\"));\n };\n\n // --- The daily puzzle ----------------------------------------------------------------------\n\n /** The daily mode's content client and results, once today's puzzle is open. */\n let daily: {\n readonly results: DailyResults;\n readonly now: () => number;\n /** The puzzle being played: its id and content hash, kept with the result. */\n readonly puzzleId: string;\n readonly contentHash: string;\n } | null = null;\n\n /**\n * A resumed daily game finishes on the puzzle its save holds, even after the next release\n * (D-46: a puzzle started is a puzzle kept); its result counts for that puzzle's date.\n */\n const keepDaily = async (svc: LauncherServices, game: Loaded, resume: SavedGame): Promise<void> => {\n const { player } = game;\n if (player.device === undefined) return;\n if (\n resume.envelope.daily !== undefined &&\n parsePuzzleId(resume.envelope.daily.puzzleId)?.gameId === game.entry.manifest.id\n ) {\n daily = {\n results: createDailyResults(player.device, player.playerId),\n now: svc.daily?.now ?? Date.now,\n ...resume.envelope.daily,\n };\n emit({ _tag: 'Puzzle', puzzleId: daily.puzzleId });\n return;\n }\n const config = resume.envelope.replay?.config ?? null;\n if (config === null || typeof config !== 'object' || Array.isArray(config)) return;\n const id = (config as { readonly [key: string]: Json })['puzzleId'];\n const hash = await contentHashOf(config);\n if (typeof id !== 'string' || parsePuzzleId(id) === null || hash === null) return;\n daily = {\n results: createDailyResults(player.device, player.playerId),\n now: svc.daily?.now ?? Date.now,\n puzzleId: id,\n contentHash: hash,\n };\n emit({ _tag: 'Puzzle', puzzleId: id });\n };\n\n /**\n * Today's puzzle for a new daily game. Returns the config to start with, or null after\n * reporting why not: no puzzle (offline with nothing cached, or not published yet), or\n * today's puzzle already finished, whose result it reports instead.\n */\n const openDaily = async (svc: LauncherServices, game: Loaded): Promise<Json | null> => {\n const { entry, module, player } = game;\n const rules = module.engine ?? module.simulation;\n if (rules === undefined || player.device === undefined) {\n fail(\"This game's daily puzzle can't be played here.\");\n return null;\n }\n const now = svc.daily?.now ?? Date.now;\n const results = createDailyResults(player.device, player.playerId);\n const client = createDailyClient<Json>({\n baseUrl: svc.daily?.baseUrl ?? '/daily',\n gameId: entry.manifest.id,\n config: rules.schemas.config as Validator<Json>,\n storage: player.device,\n ...(svc.daily?.fetch === undefined ? {} : { fetch: svc.daily.fetch }),\n });\n const today = utcDate(now());\n const puzzle = await client.puzzle(today);\n if (disposed) return null;\n if (!puzzle.ok) {\n fail(puzzle.error.message);\n return null;\n }\n // A day is done when every puzzle it holds is: one result, or one per variant (Crossword's).\n const variants: readonly (string | null)[] = entry.manifest.dailyVariants ?? [null];\n let earlier: DailyResult | null = null;\n for (const variant of variants) {\n const found = await results.get(puzzle.value.puzzleId, variant);\n if (disposed) return null;\n earlier = found.ok ? found.value : null;\n if (earlier === null) break;\n }\n if (earlier !== null) {\n const streaks = await results.streaks(entry.manifest.id, today);\n if (disposed) return null;\n emit({\n _tag: 'Daily',\n daily: dailyFrame(\n entry.manifest.name,\n earlier,\n streaks.ok ? streaks.value : null,\n earlier.summary,\n now,\n true,\n entry.manifest.id === 'daily-word' ? await wordStatistics(results, today) : undefined,\n ),\n });\n return null;\n }\n daily = { results, now, puzzleId: puzzle.value.puzzleId, contentHash: puzzle.value.contentHash };\n emit({ _tag: 'Puzzle', puzzleId: puzzle.value.puzzleId });\n // Housekeeping: downloads older than yesterday are no longer playable (v1 has no archive).\n void client.prune(today).catch(() => {});\n return puzzle.value.config;\n };\n\n const wordStatistics = async (results: DailyResults, today: string): Promise<DailyWordStats | undefined> => {\n const all = await results.list('daily-word');\n return all.ok ? dailyWordStats(all.value, today) : undefined;\n };\n\n /** Records the finished daily puzzle, then reports its streaks and share text. */\n const recordDaily = async (svc: LauncherServices, game: Loaded, outcome: Outcome): Promise<void> => {\n const open = daily;\n const engine = game.module.engine;\n const simulation = game.module.simulation;\n if (open === null || (engine === undefined && simulation === undefined)) return;\n let summary: ShareSummary | null = null;\n try {\n const state = running?.state() ?? null;\n summary = state === null ? null : (engine?.share?.(state, outcome) ?? simulation?.share?.(state) ?? null);\n } catch (error) {\n svc.ports.report(error, 'The engine failed to summarize the daily result for sharing.');\n }\n const rows = summary?.rows ?? [];\n const today = utcDate(open.now());\n const result: DailyResult = {\n puzzleId: open.puzzleId,\n // A day with variants keeps one result per variant (`GameManifest.dailyVariants`).\n variant:\n summary?.variant !== undefined && (game.entry.manifest.dailyVariants ?? []).includes(summary.variant)\n ? summary.variant\n : null,\n contentHash: open.contentHash,\n outcome: dailyOutcome(outcome.result),\n assisted: summary?.assisted ?? ((outcome.assists?.length ?? 0) > 0 || (outcome.undosUsed ?? 0) > 0),\n summary: outcome.summary.slice(0, 200),\n stats: { ...(outcome.stats ?? {}) },\n shareRows: rows.map((row) => row.symbols),\n shareWords: rows.map((row) => row.words),\n ...(rows.some((row) => row.detail !== undefined) ? { shareDetails: rows.map((row) => row.detail ?? null) } : {}),\n finishedOn: today,\n };\n const recorded = await open.results.record(result, today);\n if (disposed) return;\n if (!recorded.ok) {\n emit({ _tag: 'Notice', message: \"Your result couldn't be saved on this device.\" });\n return;\n }\n const kept = recorded.value.result;\n emit({\n _tag: 'Daily',\n daily: dailyFrame(\n game.entry.manifest.name,\n kept,\n recorded.value.streaks,\n recorded.value.isNew ? (summary?.headline ?? kept.summary) : kept.summary,\n open.now,\n false,\n game.entry.manifest.id === 'daily-word' ? await wordStatistics(open.results, today) : undefined,\n ),\n });\n };\n\n // --- Online with friends ----------------------------------------------------------------------\n\n /** Opens the room: creates it (the creator holds seat 0) or checks the code, then connects. */\n const startRoom = async (svc: LauncherServices, game: Loaded): Promise<void> => {\n const { entry, module, player } = game;\n const { manifest } = entry;\n const plan = control.room;\n const engine = module.engine;\n if (plan === null || engine === undefined) return fail(\"This game can't be played online yet.\");\n const rooms: RoomServices = svc.rooms ?? pageRooms();\n let code: string;\n if (plan.action === 'create') {\n const created = await rooms.create(manifest.id, player.displayName);\n if (disposed) return;\n if (!created.ok) return fail(created.message);\n // The token goes to storage before the socket opens, so the creator joins seat 0 as host.\n rooms.tokens.set(created.value.room, created.value.token);\n code = created.value.room;\n if (created.value.notice !== null) emit({ _tag: 'Notice', message: created.value.notice });\n } else {\n const found = await rooms.find(plan.code);\n if (disposed) return;\n if (!found.ok) return fail(found.message);\n if (found.value.gameId !== manifest.id)\n return fail('That code belongs to a room for another game. Open the invite link to reach its page.');\n code = plan.code;\n }\n const session = createRemoteSession<Json, Json, Json>({\n game: {\n id: manifest.id,\n build: manifest.versions.build,\n rulesVersion: engine.rulesVersion,\n schemas: engine.schemas,\n },\n room: code,\n name: player.displayName,\n watch: plan.action === 'join' && plan.watch,\n connect: rooms.connect,\n tokens: rooms.tokens,\n clock: svc.ports.clock,\n actionId: () => rooms.actionId(),\n freeze: svc.freeze ?? true,\n report: svc.ports.report,\n });\n running = { session, inputs: null, rematch: noRematch, state: noState, checkpoint: null };\n remote = { session, code, link: rooms.inviteLink(code), mountedMatch: null, lastRoom: '', ticker: null };\n platform = platformFor(svc, game);\n stops.push(session.subscribe((snapshot) => onRoom(svc, game, snapshot)));\n sync();\n };\n\n /** Reports the room, ticks its clock, and mounts the game when a match is there to show. */\n const onRoom = (svc: LauncherServices, game: Loaded, snapshot: RemoteSnapshot<Json, Json>): void => {\n const open = remote;\n if (open === null || disposed) return;\n reportRoom(svc, game);\n const match = snapshot.room?.match ?? 0;\n if (snapshot.view !== null && open.mountedMatch !== match) {\n // A new match (a rematch, or the first look) gets a fresh canvas.\n try {\n mounted?.dispose();\n } finally {\n mounted = null;\n canvas.replaceChildren();\n }\n open.mountedMatch = match;\n finished = false;\n lastFrame = '';\n mountCanvas(game.module, open.session);\n }\n if (open.mountedMatch === null) return;\n const frame = frameOf(snapshot);\n if (frame.outcome !== null && !finished) {\n finished = true;\n void game.player.storage.stats.record(game.entry.manifest.id, frame.outcome);\n }\n const text = json(frame);\n if (text === lastFrame) return;\n lastFrame = text;\n emit({ _tag: 'Frame', frame });\n };\n\n /** Emits the room when it changed; while a clock runs, again as its seconds tick. */\n const reportRoom = (svc: LauncherServices, game: Loaded): void => {\n const open = remote;\n if (open === null || disposed) return;\n const room = roomFrameOf(open.session.get(), {\n code: open.code,\n link: open.link,\n manifest: game.entry.manifest,\n now: svc.ports.clock.now(),\n });\n const text = json(room);\n if (text !== open.lastRoom) {\n open.lastRoom = text;\n emit({ _tag: 'Room', room });\n }\n open.ticker?.();\n // A running clock, or an offer to end the match counting down, is reported as it ticks.\n const ticking =\n (room.timer !== null && !room.timer.paused) || (room.offer?.status === 'open' && room.offer.seconds !== null);\n open.ticker = ticking ? svc.ports.clock.after(250, () => reportRoom(svc, game)) : null;\n };\n\n /** Turns a lobby request into the room control (or session input) it stands for. */\n const perform = (session: RemoteSession<Json, Json, Json>, request: RoomRequest): void => {\n switch (request.kind) {\n case 'ready':\n return session.dispatch({ kind: 'control', control: { kind: 'ready', ready: request.ready } });\n case 'clock': {\n const { setting } = request;\n const control =\n setting.kind === 'off'\n ? { kind: 'settings' as const, decisionMs: null, bank: null }\n : setting.kind === 'move'\n ? { kind: 'settings' as const, decisionMs: setting.ms }\n : { kind: 'settings' as const, bank: { initialMs: setting.initialMs, incrementMs: setting.incrementMs } };\n return session.dispatch({ kind: 'control', control });\n }\n case 'ai-level':\n return session.dispatch({ kind: 'control', control: { kind: 'settings', aiLevel: request.level } });\n case 'fill-with-ai':\n case 'remove-seat':\n return session.dispatch({ kind: 'control', control: { kind: request.kind, seat: request.seat } });\n case 'start':\n return session.dispatch({ kind: 'control', control: { kind: 'start' } });\n case 'offer':\n return session.dispatch({ kind: 'control', control: { kind: 'offer', offer: request.offer } });\n case 'answer':\n return session.dispatch({ kind: 'control', control: { kind: request.answer, offer: request.offer } });\n case 'reconnect':\n return session.dispatch({ kind: 'reconnect' });\n case 'leave':\n // The seat is given up over the socket before the stage goes away.\n session.dispatch({ kind: 'leave' });\n emit({ _tag: 'ExitRequested' });\n return;\n }\n };\n\n // --- Mounting and the PlatformApi ----------------------------------------------------------\n\n const mount = (svc: LauncherServices, game: Loaded, session: AnyGameSession): void => {\n const { entry, player } = game;\n const { manifest } = entry;\n platform = platformFor(svc, game);\n stops.push(\n session.subscribe((snapshot) => {\n const frame = frameOf(snapshot);\n if (frame.outcome !== null && !finished) {\n finished = true;\n void slot?.finish();\n void player.storage.stats.record(manifest.id, frame.outcome);\n if (control.mode === 'daily') {\n void recordDaily(svc, game, frame.outcome).catch((error: unknown) =>\n fail(\"Your result couldn't be saved.\", error),\n );\n } else {\n const rematch = running?.rematch() ?? null;\n if (rematch !== null) emit({ _tag: 'Rematch', config: rematch });\n }\n }\n const text = json(frame);\n if (text === lastFrame) return;\n lastFrame = text;\n emit({ _tag: 'Frame', frame });\n }),\n );\n mountCanvas(game.module, session);\n };\n\n /** Draws the game into the canvas with the stage's PlatformApi. */\n const mountCanvas = (module: GameModule<HTMLElement>, session: AnyGameSession): void => {\n if (platform === null) return;\n stopAudio?.();\n const audio = createGameAudio(platform.preferences, control.gameId);\n const withAudio: PlatformApi = { ...platform, audio };\n const stopSounds = watchGameAudio(session, withAudio, control.gameId);\n stopAudio = () => {\n stopSounds();\n audio.dispose();\n };\n try {\n mounted = module.mount(canvas, session, withAudio);\n } catch (error) {\n stopAudio();\n stopAudio = null;\n fail(\"The game couldn't start.\", error);\n }\n sync();\n };\n\n /** The PlatformApi for this stage's game: its theme on the canvas, live preferences, storage. */\n const platformFor = (svc: LauncherServices, game: Loaded): PlatformApi => {\n const { entry, player } = game;\n const { manifest } = entry;\n const theme = canvasTheme(manifest.theme);\n canvas.style.setProperty('--game-accent', theme.accent);\n canvas.style.setProperty('--game-surface', theme.surface);\n canvas.style.setProperty('--game-ink', theme.ink);\n\n let stored = game.preferences;\n let resolved: Preferences = resolvePreferences(stored, svc.system.get());\n const listeners = new Set<(preferences: Preferences) => void>();\n const publish = (): void => {\n const next = resolvePreferences(stored, svc.system.get());\n canvas.style.colorScheme = next.colorScheme;\n if (json(next) === json(resolved)) return;\n resolved = next;\n for (const listener of [...listeners]) listener(next);\n };\n canvas.style.colorScheme = resolved.colorScheme;\n stops.push(\n player.storage.preferences.subscribe((next) => {\n stored = next;\n publish();\n }),\n svc.system.subscribe(publish),\n );\n\n return {\n storage: player.storage.gameStorage(manifest.id),\n preferences: {\n get: () => resolved,\n subscribe: (listener) => {\n const entry = (preferences: Preferences): void => listener(preferences);\n listeners.add(entry);\n return () => {\n listeners.delete(entry);\n };\n },\n },\n requestExit: () => {\n // Leaving a room from inside the canvas gives the seat up, like the frame's own button.\n if (remote !== null) remote.session.dispatch({ kind: 'leave' });\n emit({ _tag: 'ExitRequested' });\n },\n announce: (message, politeness) => emit({ _tag: 'Announce', message, politeness }),\n };\n };\n\n // --- Following the launcher ----------------------------------------------------------------\n\n /** Acts on what changed in `control`: begin, pause, reveals and preferences. */\n const sync = (): void => {\n if (disposed || services === null || loaded === null) return;\n if (control.preferences !== null && json(control.preferences) !== lastPreferences) {\n lastPreferences = json(control.preferences);\n void loaded.player.storage.preferences.update(control.preferences);\n }\n if (!begun && control.begin !== null) {\n begun = true;\n const game = loaded;\n start(services, game, control.begin).catch((error: unknown) => fail(\"The game couldn't start.\", error));\n return;\n }\n if (running?.inputs === undefined || running.inputs === null) return;\n if (control.paused !== paused) {\n paused = control.paused;\n running.inputs({ kind: paused ? 'pause' : 'resume' });\n }\n while (reveals < control.reveals) {\n reveals += 1;\n running.inputs({ kind: 'reveal' });\n }\n };\n\n // --- A forced update ---------------------------------------------------------------------\n\n /**\n * Stops play and writes the position now, for a forced security update (`requestSave`):\n * a real-time game pauses first, which also checkpoints it. The write queues behind the\n * session's own, so whatever lands last is this position or a later one. A finished game\n * has nothing to keep; rooms keep their state on the server.\n */\n const saveNow = async (): Promise<void> => {\n const game = running;\n const saves = slot;\n if (game === null || game.checkpoint === null || saves === null || finished) return;\n game.inputs?.({ kind: 'pause' });\n await saves.port.write(game.checkpoint());\n };\n const onSaveRequest = (event: Event): void => {\n (event as CustomEvent<Partial<SaveRequestDetail> | null>).detail?.waitUntil?.(saveNow());\n };\n const doc = host.ownerDocument;\n doc.addEventListener(SAVE_REQUEST_EVENT, onSaveRequest);\n stops.push(() => doc.removeEventListener(SAVE_REQUEST_EVENT, onSaveRequest));\n\n if (services === null) {\n queueMicrotask(() =>\n fail(\"This page can't start games: the launcher has no services. Call provideLauncherServices above it.\"),\n );\n } else {\n stops.push(services.onNotice((message) => emit({ _tag: 'Notice', message })));\n prepare(services).catch((error: unknown) =>\n fail(\"The game couldn't load. Check your connection, then try again.\", error),\n );\n }\n\n return {\n roomRequest: (request: RoomRequest): void => {\n if (disposed || remote === null) throw new Error('The room is not open.');\n perform(remote.session, request);\n },\n update: (next: StageControl): void => {\n control = next;\n sync();\n },\n dispose: (): void => {\n if (disposed) return;\n disposed = true;\n for (const bridge of preparations) bridge.dispose();\n preparations.clear();\n remote?.ticker?.();\n for (const stop of stops.splice(0)) stop();\n stopAudio?.();\n stopAudio = null;\n try {\n mounted?.dispose();\n } finally {\n running?.session.dispose();\n canvas.replaceChildren();\n }\n },\n };\n};\n\n/** The stage element. Set `control`; listen for `gyral-output` events. */\nexport class GameStage extends HTMLElement {\n #control: StageControl | null = null;\n #game: ReturnType<typeof run> | null = null;\n\n get control(): StageControl | null {\n return this.#control;\n }\n\n set control(value: StageControl | null) {\n this.#control = value;\n if (value === null) return;\n if (this.#game !== null) this.#game.update(value);\n else if (this.isConnected) this.#start(value);\n }\n\n /** A one-shot command from the launcher; the remote room is the authority. */\n roomRequest(request: RoomRequest): void {\n if (this.#game === null) throw new Error('The game stage is not running.');\n this.#game.roomRequest(request);\n }\n\n connectedCallback(): void {\n // A property set before the element was defined shadows the accessor; take it over.\n if (Object.hasOwn(this, 'control')) {\n const value = (this as unknown as { control: StageControl | null }).control;\n delete (this as unknown as { control?: unknown }).control;\n this.control = value;\n }\n if (this.#game === null && this.#control !== null) this.#start(this.#control);\n }\n\n disconnectedCallback(): void {\n this.#game?.dispose();\n this.#game = null;\n }\n\n #start(control: StageControl): void {\n const root = this.shadowRoot ?? this.attachShadow({ mode: 'open' });\n root.adoptedStyleSheets = [stageSheet()];\n const canvas = document.createElement('div');\n canvas.className = 'canvas';\n canvas.setAttribute('part', 'canvas');\n root.replaceChildren(canvas);\n this.#game = run(this, canvas, control);\n }\n}\n\nif (customElements.get(STAGE_TAG) === undefined) customElements.define(STAGE_TAG, GameStage);\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'jy-game-stage': GameStage;\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type SeatId } from '@joystyk/contracts';
|
|
2
|
+
/** `firstSeat`: who moves first (default seat 0); a rematch swaps it. */
|
|
3
|
+
export type TallyConfig = {
|
|
4
|
+
readonly target?: number;
|
|
5
|
+
readonly firstSeat?: 0 | 1;
|
|
6
|
+
};
|
|
7
|
+
export type TallyState = {
|
|
8
|
+
readonly target: number;
|
|
9
|
+
readonly names: readonly string[];
|
|
10
|
+
readonly ai: readonly boolean[];
|
|
11
|
+
readonly scores: readonly number[];
|
|
12
|
+
/** One private number per seat; a seat sees only its own. */
|
|
13
|
+
readonly secrets: readonly number[];
|
|
14
|
+
readonly turn: SeatId;
|
|
15
|
+
/** The seat that moved first. */
|
|
16
|
+
readonly first: SeatId;
|
|
17
|
+
readonly moves: number;
|
|
18
|
+
readonly hints: number;
|
|
19
|
+
readonly winner: SeatId | null;
|
|
20
|
+
readonly resigned: SeatId | null;
|
|
21
|
+
};
|
|
22
|
+
export type TallyCommand = {
|
|
23
|
+
readonly type: 'add';
|
|
24
|
+
readonly amount: 1 | 2;
|
|
25
|
+
} | {
|
|
26
|
+
readonly type: 'hint';
|
|
27
|
+
};
|
|
28
|
+
export type TallyEvent = {
|
|
29
|
+
readonly type: 'added';
|
|
30
|
+
readonly seat: SeatId;
|
|
31
|
+
readonly amount: number;
|
|
32
|
+
} | {
|
|
33
|
+
readonly type: 'hinted';
|
|
34
|
+
readonly seat: SeatId;
|
|
35
|
+
} | {
|
|
36
|
+
readonly type: 'resigned';
|
|
37
|
+
readonly seat: SeatId;
|
|
38
|
+
};
|
|
39
|
+
/** What one seat sees: everything but the other seats' secrets. */
|
|
40
|
+
export type TallyView = {
|
|
41
|
+
readonly target: number;
|
|
42
|
+
readonly names: readonly string[];
|
|
43
|
+
readonly scores: readonly number[];
|
|
44
|
+
readonly turn: SeatId;
|
|
45
|
+
readonly moves: number;
|
|
46
|
+
readonly winner: SeatId | null;
|
|
47
|
+
/** The viewer's own secret; null for a spectator (and behind the hand-off screen). */
|
|
48
|
+
readonly secret: number | null;
|
|
49
|
+
};
|
|
50
|
+
export declare const tallyEngine: import("@joystyk/contracts").DiscreteEngine<TallyState, TallyCommand, TallyView, {
|
|
51
|
+
readonly type: "added";
|
|
52
|
+
readonly seat: SeatId;
|
|
53
|
+
readonly amount: number;
|
|
54
|
+
} | {
|
|
55
|
+
readonly type: "hinted";
|
|
56
|
+
readonly seat: SeatId;
|
|
57
|
+
} | {
|
|
58
|
+
readonly type: "resigned";
|
|
59
|
+
readonly seat: SeatId;
|
|
60
|
+
}, TallyConfig>;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { defineEngine, err, gameError, ok } from "@joystyk/contracts";
|
|
2
|
+
//#region src/fixture/engine.ts
|
|
3
|
+
/** A Standard Schema from a type guard. */
|
|
4
|
+
var schema = (guard, name) => ({ "~standard": {
|
|
5
|
+
version: 1,
|
|
6
|
+
vendor: "joystyk-fixture",
|
|
7
|
+
validate: (value) => guard(value) ? { value } : { issues: [{ message: `Expected ${name}.` }] }
|
|
8
|
+
} });
|
|
9
|
+
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
var isIntArray = (value) => Array.isArray(value) && value.every((n) => Number.isInteger(n));
|
|
11
|
+
var isConfig = (value) => isObject(value) && (value["target"] === void 0 || Number.isInteger(value["target"]) && value["target"] >= 2 && value["target"] <= 50) && (value["firstSeat"] === void 0 || value["firstSeat"] === 0 || value["firstSeat"] === 1);
|
|
12
|
+
var isState = (value) => isObject(value) && Number.isInteger(value["target"]) && isIntArray(value["scores"]) && isIntArray(value["secrets"]) && Array.isArray(value["names"]) && Array.isArray(value["ai"]) && Number.isInteger(value["turn"]) && Number.isInteger(value["first"]) && Number.isInteger(value["moves"]) && Number.isInteger(value["hints"]);
|
|
13
|
+
var isCommand = (value) => isObject(value) && (value["type"] === "add" && (value["amount"] === 1 || value["amount"] === 2) || value["type"] === "hint");
|
|
14
|
+
var isView = (value) => isObject(value) && isIntArray(value["scores"]);
|
|
15
|
+
var isEvent = (value) => isObject(value) && typeof value["type"] === "string";
|
|
16
|
+
var outcomeOf = (state) => {
|
|
17
|
+
const players = state.scores.length;
|
|
18
|
+
if (state.resigned !== null && players === 1) return {
|
|
19
|
+
result: "abandoned",
|
|
20
|
+
placements: [{
|
|
21
|
+
seat: 0,
|
|
22
|
+
place: 1,
|
|
23
|
+
score: state.scores[0] ?? 0
|
|
24
|
+
}],
|
|
25
|
+
summary: "Gave up."
|
|
26
|
+
};
|
|
27
|
+
if (state.winner === null) return null;
|
|
28
|
+
const winner = state.winner;
|
|
29
|
+
const placements = state.scores.map((score, seat) => ({
|
|
30
|
+
seat,
|
|
31
|
+
place: seat === winner ? 1 : 2,
|
|
32
|
+
score
|
|
33
|
+
}));
|
|
34
|
+
if (players === 1) return {
|
|
35
|
+
result: "completed",
|
|
36
|
+
placements,
|
|
37
|
+
summary: `Reached ${String(state.target)} in ${String(state.moves)} moves.`,
|
|
38
|
+
stats: { moves: state.moves }
|
|
39
|
+
};
|
|
40
|
+
const human = state.ai.findIndex((ai) => !ai);
|
|
41
|
+
return {
|
|
42
|
+
result: state.ai.some((ai) => ai) && human !== winner ? "lost" : "won",
|
|
43
|
+
placements,
|
|
44
|
+
summary: `${state.names[winner] ?? "Someone"} reached ${String(state.target)} first.`,
|
|
45
|
+
stats: { moves: state.moves }
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
var tallyEngine = defineEngine({
|
|
49
|
+
rulesVersion: 1,
|
|
50
|
+
schemas: {
|
|
51
|
+
config: schema(isConfig, "a Tally setup"),
|
|
52
|
+
command: schema(isCommand, "a Tally move"),
|
|
53
|
+
state: schema(isState, "a Tally game"),
|
|
54
|
+
view: schema(isView, "a Tally view"),
|
|
55
|
+
event: schema(isEvent, "a Tally event")
|
|
56
|
+
},
|
|
57
|
+
init: (config, seats, rng) => seats.length < 1 || seats.length > 2 ? err(gameError("Seats", "Tally is for one or two players.")) : ok({
|
|
58
|
+
target: config.target ?? 5,
|
|
59
|
+
names: seats.map((seat) => seat.name),
|
|
60
|
+
ai: seats.map((seat) => seat.controller === "ai"),
|
|
61
|
+
scores: seats.map(() => 0),
|
|
62
|
+
secrets: seats.map(() => rng.int(10, 100)),
|
|
63
|
+
turn: Math.min(config.firstSeat ?? 0, seats.length - 1),
|
|
64
|
+
first: Math.min(config.firstSeat ?? 0, seats.length - 1),
|
|
65
|
+
moves: 0,
|
|
66
|
+
hints: 0,
|
|
67
|
+
winner: null,
|
|
68
|
+
resigned: null
|
|
69
|
+
}),
|
|
70
|
+
apply: (state, input) => {
|
|
71
|
+
if (outcomeOf(state) !== null) return err(gameError("GameOver", "The game is over."));
|
|
72
|
+
const players = state.scores.length;
|
|
73
|
+
switch (input.kind) {
|
|
74
|
+
case "command": {
|
|
75
|
+
if (input.seat !== state.turn) return err(gameError("NotYourTurn", "It's not your turn."));
|
|
76
|
+
if (input.command.type === "hint") {
|
|
77
|
+
const next = {
|
|
78
|
+
...state,
|
|
79
|
+
hints: state.hints + 1
|
|
80
|
+
};
|
|
81
|
+
return ok({
|
|
82
|
+
state: next,
|
|
83
|
+
beats: [{ event: {
|
|
84
|
+
type: "hinted",
|
|
85
|
+
seat: input.seat
|
|
86
|
+
} }],
|
|
87
|
+
assist: "hint"
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const amount = input.command.amount;
|
|
91
|
+
const scores = state.scores.map((score, seat) => seat === input.seat ? score + amount : score);
|
|
92
|
+
const reached = (scores[input.seat] ?? 0) >= state.target;
|
|
93
|
+
const next = {
|
|
94
|
+
...state,
|
|
95
|
+
scores,
|
|
96
|
+
moves: state.moves + 1,
|
|
97
|
+
turn: (state.turn + 1) % players,
|
|
98
|
+
winner: reached ? input.seat : null
|
|
99
|
+
};
|
|
100
|
+
return ok({
|
|
101
|
+
state: next,
|
|
102
|
+
beats: [{
|
|
103
|
+
event: {
|
|
104
|
+
type: "added",
|
|
105
|
+
seat: input.seat,
|
|
106
|
+
amount
|
|
107
|
+
},
|
|
108
|
+
state: next
|
|
109
|
+
}]
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
case "resign": {
|
|
113
|
+
const next = {
|
|
114
|
+
...state,
|
|
115
|
+
resigned: input.seat,
|
|
116
|
+
winner: players === 1 ? null : (input.seat + 1) % players
|
|
117
|
+
};
|
|
118
|
+
return ok({
|
|
119
|
+
state: next,
|
|
120
|
+
beats: [{ event: {
|
|
121
|
+
type: "resigned",
|
|
122
|
+
seat: input.seat
|
|
123
|
+
} }]
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
case "seat": return ok({
|
|
127
|
+
state: {
|
|
128
|
+
...state,
|
|
129
|
+
ai: state.ai.map((ai, seat) => seat === input.seat && input.change.type === "controller" ? input.change.controller === "ai" : ai)
|
|
130
|
+
},
|
|
131
|
+
beats: []
|
|
132
|
+
});
|
|
133
|
+
case "timeout":
|
|
134
|
+
case "job": return err(gameError("Unsupported", "Tally has no clock and no jobs."));
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
pending: (state) => outcomeOf(state) === null ? [state.turn] : [],
|
|
138
|
+
viewFor: (state, viewer) => ({
|
|
139
|
+
target: state.target,
|
|
140
|
+
names: state.names,
|
|
141
|
+
scores: state.scores,
|
|
142
|
+
turn: state.turn,
|
|
143
|
+
moves: state.moves,
|
|
144
|
+
winner: state.winner,
|
|
145
|
+
secret: viewer === null ? null : state.secrets[viewer] ?? null
|
|
146
|
+
}),
|
|
147
|
+
eventFor: (event) => event,
|
|
148
|
+
outcome: outcomeOf,
|
|
149
|
+
rematch: (config, finished) => finished.scores.length < 2 ? config : {
|
|
150
|
+
...config,
|
|
151
|
+
firstSeat: finished.first === 0 ? 1 : 0
|
|
152
|
+
},
|
|
153
|
+
ai: {
|
|
154
|
+
levels: ["easy", "hard"],
|
|
155
|
+
choose: (_view, _seat, level) => ({
|
|
156
|
+
type: "add",
|
|
157
|
+
amount: level === "hard" ? 2 : 1
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
//#endregion
|
|
162
|
+
export { tallyEngine };
|
|
163
|
+
|
|
164
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.js","names":[],"sources":["../../src/fixture/engine.ts"],"sourcesContent":["// Tally, the launcher's fixture game: race to a target by adding one or two to your own\n// score. It is deliberately tiny, yet it exercises what the launcher must handle: solo,\n// vs-AI and pass-and-play play, a private number per seat (so the hand-off screen matters),\n// an AI with two levels, an assist (a hint), resignation and an outcome. It exists for tests\n// and development builds only and is never listed in production.\nimport { defineEngine, err, gameError, ok, type Outcome, type SeatId, type Validator } from '@joystyk/contracts';\n\n/** A Standard Schema from a type guard. */\nconst schema = <T>(guard: (value: unknown) => value is T, name: string): Validator<T> => ({\n '~standard': {\n version: 1,\n vendor: 'joystyk-fixture',\n validate: (value) => (guard(value) ? { value } : { issues: [{ message: `Expected ${name}.` }] }),\n },\n});\n\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\nconst isIntArray = (value: unknown): value is number[] =>\n Array.isArray(value) && value.every((n) => Number.isInteger(n));\n\n/** `firstSeat`: who moves first (default seat 0); a rematch swaps it. */\nexport type TallyConfig = { readonly target?: number; readonly firstSeat?: 0 | 1 };\n\nexport type TallyState = {\n readonly target: number;\n readonly names: readonly string[];\n readonly ai: readonly boolean[];\n readonly scores: readonly number[];\n /** One private number per seat; a seat sees only its own. */\n readonly secrets: readonly number[];\n readonly turn: SeatId;\n /** The seat that moved first. */\n readonly first: SeatId;\n readonly moves: number;\n readonly hints: number;\n readonly winner: SeatId | null;\n readonly resigned: SeatId | null;\n};\n\nexport type TallyCommand = { readonly type: 'add'; readonly amount: 1 | 2 } | { readonly type: 'hint' };\n\nexport type TallyEvent =\n | { readonly type: 'added'; readonly seat: SeatId; readonly amount: number }\n | { readonly type: 'hinted'; readonly seat: SeatId }\n | { readonly type: 'resigned'; readonly seat: SeatId };\n\n/** What one seat sees: everything but the other seats' secrets. */\nexport type TallyView = {\n readonly target: number;\n readonly names: readonly string[];\n readonly scores: readonly number[];\n readonly turn: SeatId;\n readonly moves: number;\n readonly winner: SeatId | null;\n /** The viewer's own secret; null for a spectator (and behind the hand-off screen). */\n readonly secret: number | null;\n};\n\nconst isConfig = (value: unknown): value is TallyConfig =>\n isObject(value) &&\n (value['target'] === undefined ||\n (Number.isInteger(value['target']) && (value['target'] as number) >= 2 && (value['target'] as number) <= 50)) &&\n (value['firstSeat'] === undefined || value['firstSeat'] === 0 || value['firstSeat'] === 1);\n\nconst isState = (value: unknown): value is TallyState =>\n isObject(value) &&\n Number.isInteger(value['target']) &&\n isIntArray(value['scores']) &&\n isIntArray(value['secrets']) &&\n Array.isArray(value['names']) &&\n Array.isArray(value['ai']) &&\n Number.isInteger(value['turn']) &&\n Number.isInteger(value['first']) &&\n Number.isInteger(value['moves']) &&\n Number.isInteger(value['hints']);\n\nconst isCommand = (value: unknown): value is TallyCommand =>\n isObject(value) &&\n ((value['type'] === 'add' && (value['amount'] === 1 || value['amount'] === 2)) || value['type'] === 'hint');\n\nconst isView = (value: unknown): value is TallyView => isObject(value) && isIntArray(value['scores']);\nconst isEvent = (value: unknown): value is TallyEvent => isObject(value) && typeof value['type'] === 'string';\n\nconst outcomeOf = (state: TallyState): Outcome | null => {\n const players = state.scores.length;\n if (state.resigned !== null && players === 1) {\n return {\n result: 'abandoned',\n placements: [{ seat: 0, place: 1, score: state.scores[0] ?? 0 }],\n summary: 'Gave up.',\n };\n }\n if (state.winner === null) return null;\n const winner = state.winner;\n const placements = state.scores.map((score, seat) => ({ seat, place: seat === winner ? 1 : 2, score }));\n if (players === 1)\n return {\n result: 'completed',\n placements,\n summary: `Reached ${String(state.target)} in ${String(state.moves)} moves.`,\n stats: { moves: state.moves },\n };\n const human = state.ai.findIndex((ai) => !ai);\n const vsAi = state.ai.some((ai) => ai);\n const result = vsAi && human !== winner ? 'lost' : 'won';\n return {\n result,\n placements,\n summary: `${state.names[winner] ?? 'Someone'} reached ${String(state.target)} first.`,\n stats: { moves: state.moves },\n };\n};\n\nexport const tallyEngine = defineEngine({\n rulesVersion: 1,\n schemas: {\n config: schema(isConfig, 'a Tally setup'),\n command: schema(isCommand, 'a Tally move'),\n state: schema(isState, 'a Tally game'),\n view: schema(isView, 'a Tally view'),\n event: schema(isEvent, 'a Tally event'),\n },\n init: (config: TallyConfig, seats, rng) =>\n seats.length < 1 || seats.length > 2\n ? err(gameError('Seats', 'Tally is for one or two players.'))\n : ok<TallyState>({\n target: config.target ?? 5,\n names: seats.map((seat) => seat.name),\n ai: seats.map((seat) => seat.controller === 'ai'),\n scores: seats.map(() => 0),\n secrets: seats.map(() => rng.int(10, 100)),\n turn: Math.min(config.firstSeat ?? 0, seats.length - 1),\n first: Math.min(config.firstSeat ?? 0, seats.length - 1),\n moves: 0,\n hints: 0,\n winner: null,\n resigned: null,\n }),\n apply: (state: TallyState, input) => {\n if (outcomeOf(state) !== null) return err(gameError('GameOver', 'The game is over.'));\n const players = state.scores.length;\n switch (input.kind) {\n case 'command': {\n if (input.seat !== state.turn) return err(gameError('NotYourTurn', \"It's not your turn.\"));\n if (input.command.type === 'hint') {\n const next = { ...state, hints: state.hints + 1 };\n return ok({\n state: next,\n beats: [{ event: { type: 'hinted', seat: input.seat } as TallyEvent }],\n assist: 'hint' as const,\n });\n }\n const amount = input.command.amount;\n const scores = state.scores.map((score, seat) => (seat === input.seat ? score + amount : score));\n const reached = (scores[input.seat] ?? 0) >= state.target;\n const next: TallyState = {\n ...state,\n scores,\n moves: state.moves + 1,\n turn: (state.turn + 1) % players,\n winner: reached ? input.seat : null,\n };\n return ok({\n state: next,\n beats: [{ event: { type: 'added', seat: input.seat, amount } as TallyEvent, state: next }],\n });\n }\n case 'resign': {\n const next: TallyState = {\n ...state,\n resigned: input.seat,\n winner: players === 1 ? null : (input.seat + 1) % players,\n };\n return ok({ state: next, beats: [{ event: { type: 'resigned', seat: input.seat } as TallyEvent }] });\n }\n case 'seat':\n return ok({\n state: {\n ...state,\n ai: state.ai.map((ai, seat) =>\n seat === input.seat && input.change.type === 'controller' ? input.change.controller === 'ai' : ai,\n ),\n },\n beats: [],\n });\n case 'timeout':\n case 'job':\n return err(gameError('Unsupported', 'Tally has no clock and no jobs.'));\n }\n },\n pending: (state: TallyState) => (outcomeOf(state) === null ? [state.turn] : []),\n viewFor: (state: TallyState, viewer): TallyView => ({\n target: state.target,\n names: state.names,\n scores: state.scores,\n turn: state.turn,\n moves: state.moves,\n winner: state.winner,\n secret: viewer === null ? null : (state.secrets[viewer] ?? null),\n }),\n eventFor: (event: TallyEvent) => event,\n outcome: outcomeOf,\n // A rematch keeps the target and swaps who moves first.\n rematch: (config: TallyConfig, finished: TallyState): TallyConfig =>\n finished.scores.length < 2 ? config : { ...config, firstSeat: finished.first === 0 ? 1 : 0 },\n ai: {\n levels: ['easy', 'hard'],\n choose: (_view: TallyView, _seat, level): TallyCommand => ({ type: 'add', amount: level === 'hard' ? 2 : 1 }),\n },\n});\n"],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type GameEntry } from '@joystyk/contracts';
|
|
2
|
+
export declare const TALLY_ID = "test-tally";
|
|
3
|
+
export declare const tallyManifest: {
|
|
4
|
+
readonly id: "test-tally";
|
|
5
|
+
readonly name: "Tally";
|
|
6
|
+
readonly summary: "A test game for the launcher: race to five by adding one or two to your score.";
|
|
7
|
+
readonly category: "arcade";
|
|
8
|
+
readonly tags: readonly ["test"];
|
|
9
|
+
readonly modes: readonly ["solo", "ai", "pass-and-play", "server"];
|
|
10
|
+
readonly players: {
|
|
11
|
+
readonly min: 1;
|
|
12
|
+
readonly max: 2;
|
|
13
|
+
};
|
|
14
|
+
readonly ai: {
|
|
15
|
+
readonly levels: readonly ["easy", "hard"];
|
|
16
|
+
};
|
|
17
|
+
readonly layers: {
|
|
18
|
+
readonly engine: true;
|
|
19
|
+
readonly simulation: false;
|
|
20
|
+
};
|
|
21
|
+
readonly privateState: true;
|
|
22
|
+
readonly handoff: true;
|
|
23
|
+
readonly essentialMotion: false;
|
|
24
|
+
readonly access: "free";
|
|
25
|
+
readonly versions: {
|
|
26
|
+
readonly build: "0.0.1";
|
|
27
|
+
readonly rules: 1;
|
|
28
|
+
readonly save: 1;
|
|
29
|
+
readonly minProtocol: 1;
|
|
30
|
+
};
|
|
31
|
+
readonly undo: "none";
|
|
32
|
+
readonly clock: null;
|
|
33
|
+
readonly assets: "/games/test-tally/manifest.json";
|
|
34
|
+
readonly theme: {
|
|
35
|
+
readonly accent: {
|
|
36
|
+
readonly light: "oklch(46% 0.15 145)";
|
|
37
|
+
readonly dark: "oklch(80% 0.16 145)";
|
|
38
|
+
};
|
|
39
|
+
readonly surface: {
|
|
40
|
+
readonly light: "oklch(96% 0.03 145)";
|
|
41
|
+
readonly dark: "oklch(25% 0.04 145)";
|
|
42
|
+
};
|
|
43
|
+
readonly ink: {
|
|
44
|
+
readonly light: "oklch(24% 0.04 145)";
|
|
45
|
+
readonly dark: "oklch(95% 0.02 145)";
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
/** The game's entry: the manifest now, the module (engine and canvas) when someone plays. */
|
|
50
|
+
export declare const tallyEntry: GameEntry<HTMLElement>;
|
|
51
|
+
export { tallyEngine, type TallyCommand, type TallyConfig, type TallyEvent, type TallyState, type TallyView, } from './engine.js';
|
|
52
|
+
export { tallyProbe } from './probe.js';
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { tallyEngine } from "./engine.js";
|
|
2
|
+
import { tallyProbe } from "./probe.js";
|
|
3
|
+
import { defineManifest } from "@joystyk/contracts";
|
|
4
|
+
//#region src/fixture/index.ts
|
|
5
|
+
var TALLY_ID = "test-tally";
|
|
6
|
+
var tallyManifest = defineManifest({
|
|
7
|
+
id: TALLY_ID,
|
|
8
|
+
name: "Tally",
|
|
9
|
+
summary: "A test game for the launcher: race to five by adding one or two to your score.",
|
|
10
|
+
category: "arcade",
|
|
11
|
+
tags: ["test"],
|
|
12
|
+
modes: [
|
|
13
|
+
"solo",
|
|
14
|
+
"ai",
|
|
15
|
+
"pass-and-play",
|
|
16
|
+
"server"
|
|
17
|
+
],
|
|
18
|
+
players: {
|
|
19
|
+
min: 1,
|
|
20
|
+
max: 2
|
|
21
|
+
},
|
|
22
|
+
ai: { levels: ["easy", "hard"] },
|
|
23
|
+
layers: {
|
|
24
|
+
engine: true,
|
|
25
|
+
simulation: false
|
|
26
|
+
},
|
|
27
|
+
privateState: true,
|
|
28
|
+
handoff: true,
|
|
29
|
+
essentialMotion: false,
|
|
30
|
+
access: "free",
|
|
31
|
+
versions: {
|
|
32
|
+
build: "0.0.1",
|
|
33
|
+
rules: 1,
|
|
34
|
+
save: 1,
|
|
35
|
+
minProtocol: 1
|
|
36
|
+
},
|
|
37
|
+
undo: "none",
|
|
38
|
+
clock: null,
|
|
39
|
+
assets: `/games/${TALLY_ID}/manifest.json`,
|
|
40
|
+
theme: {
|
|
41
|
+
accent: {
|
|
42
|
+
light: "oklch(46% 0.15 145)",
|
|
43
|
+
dark: "oklch(80% 0.16 145)"
|
|
44
|
+
},
|
|
45
|
+
surface: {
|
|
46
|
+
light: "oklch(96% 0.03 145)",
|
|
47
|
+
dark: "oklch(25% 0.04 145)"
|
|
48
|
+
},
|
|
49
|
+
ink: {
|
|
50
|
+
light: "oklch(24% 0.04 145)",
|
|
51
|
+
dark: "oklch(95% 0.02 145)"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
/** The game's entry: the manifest now, the module (engine and canvas) when someone plays. */
|
|
56
|
+
var tallyEntry = {
|
|
57
|
+
manifest: tallyManifest,
|
|
58
|
+
load: async () => (await import("./module.js")).tallyModule
|
|
59
|
+
};
|
|
60
|
+
//#endregion
|
|
61
|
+
export { TALLY_ID, tallyEngine, tallyEntry, tallyManifest, tallyProbe };
|
|
62
|
+
|
|
63
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/fixture/index.ts"],"sourcesContent":["// @joystyk/launcher/fixture: Tally, a test game for the launcher's own tests and for apps\n// that register it in test builds (apps/site does, behind JOYSTYK_TEST_GAMES=1). It is not\n// a real game: never list it in production.\nimport { defineManifest, type GameEntry } from '@joystyk/contracts';\n\nexport const TALLY_ID = 'test-tally';\n\nexport const tallyManifest = defineManifest({\n id: TALLY_ID,\n name: 'Tally',\n summary: 'A test game for the launcher: race to five by adding one or two to your score.',\n category: 'arcade',\n tags: ['test'],\n modes: ['solo', 'ai', 'pass-and-play', 'server'],\n players: { min: 1, max: 2 },\n ai: { levels: ['easy', 'hard'] },\n layers: { engine: true, simulation: false },\n privateState: true,\n handoff: true,\n essentialMotion: false,\n access: 'free',\n versions: { build: '0.0.1', rules: 1, save: 1, minProtocol: 1 },\n undo: 'none',\n clock: null,\n assets: `/games/${TALLY_ID}/manifest.json`,\n // A pale green board in light mode and a deep green one in dark mode, each with its ink.\n theme: {\n accent: { light: 'oklch(46% 0.15 145)', dark: 'oklch(80% 0.16 145)' },\n surface: { light: 'oklch(96% 0.03 145)', dark: 'oklch(25% 0.04 145)' },\n ink: { light: 'oklch(24% 0.04 145)', dark: 'oklch(95% 0.02 145)' },\n },\n});\n\n/** The game's entry: the manifest now, the module (engine and canvas) when someone plays. */\nexport const tallyEntry: GameEntry<HTMLElement> = {\n manifest: tallyManifest,\n load: async () => (await import('./module.ts')).tallyModule,\n};\n\nexport {\n tallyEngine,\n type TallyCommand,\n type TallyConfig,\n type TallyEvent,\n type TallyState,\n type TallyView,\n} from './engine.ts';\nexport { tallyProbe } from './probe.ts';\n"],"mappings":";;;;AAKA,IAAa,WAAW;AAExB,IAAa,gBAAgB,eAAe;CAC1C,IAAI;CACJ,MAAM;CACN,SAAS;CACT,UAAU;CACV,MAAM,CAAC,MAAM;CACb,OAAO;EAAC;EAAQ;EAAM;EAAiB;CAAQ;CAC/C,SAAS;EAAE,KAAK;EAAG,KAAK;CAAE;CAC1B,IAAI,EAAE,QAAQ,CAAC,QAAQ,MAAM,EAAE;CAC/B,QAAQ;EAAE,QAAQ;EAAM,YAAY;CAAM;CAC1C,cAAc;CACd,SAAS;CACT,iBAAiB;CACjB,QAAQ;CACR,UAAU;EAAE,OAAO;EAAS,OAAO;EAAG,MAAM;EAAG,aAAa;CAAE;CAC9D,MAAM;CACN,OAAO;CACP,QAAQ,UAAU,SAAS;CAE3B,OAAO;EACL,QAAQ;GAAE,OAAO;GAAuB,MAAM;EAAsB;EACpE,SAAS;GAAE,OAAO;GAAuB,MAAM;EAAsB;EACrE,KAAK;GAAE,OAAO;GAAuB,MAAM;EAAsB;CACnE;AACF,CAAC;;AAGD,IAAa,aAAqC;CAChD,UAAU;CACV,MAAM,aAAa,MAAM,OAAO,eAAA,CAAgB;AAClD"}
|