// Dispatch a game id to its source: the local engine (an offline vs_ai game) or the network // gateway. The same game screen (game/Game.svelte) drives both through the shared game-loop subset // (GameLoopSource); a game id decides which. A network game's calls are unchanged — gameSource // returns the gateway itself for it — so the seam adds the local path without touching online play. // // The offline engine is kept OUT of the app entry bundle: this module statically imports only the // gateway, the tiny id helper and types; the LocalSource is dynamically imported on first use of a // local game, so it loads as a separate chunk only when someone plays offline. import { gateway } from './gateway'; import { isLocalGameId } from './localgame/id'; import type { GameLoopSource, LocalSource } from './localgame/source'; let loaded: Promise | null = null; function load(): Promise { if (!loaded) loaded = import('./localgame/source').then((m) => new m.LocalSource()); return loaded; } /** * localSource is a lazy handle on the single offline LocalSource. It is obtainable synchronously, * but each method dynamically imports the engine on first use, so the offline code stays out of the * app entry bundle until a local game is actually played. It exposes the game-loop methods plus the * local-only create() and the robot-reply event subscription events(). */ export const localSource = { // Offline scoring is self-contained, so the local source ignores includeAlphabet, the evaluate // abort signal, and the draft (drafts are not persisted offline); the proxy accepts the full // gateway signatures but forwards only what the local source uses. gameState: (id, _includeAlphabet) => load().then((s) => s.gameState(id)), gameHistory: (id) => load().then((s) => s.gameHistory(id)), submitPlay: (id, tiles, variant) => load().then((s) => s.submitPlay(id, tiles, variant)), pass: (id) => load().then((s) => s.pass(id)), exchange: (id, tiles, variant) => load().then((s) => s.exchange(id, tiles, variant)), resign: (id) => load().then((s) => s.resign(id)), hint: (id) => load().then((s) => s.hint(id)), evaluate: (id, tiles, variant, _signal) => load().then((s) => s.evaluate(id, tiles, variant)), checkWord: (id, word, variant) => load().then((s) => s.checkWord(id, word, variant)), draftGet: (_id) => load().then((s) => s.draftGet()), draftSave: (_id, _json) => load().then((s) => s.draftSave()), create: (opts) => load().then((s) => s.create(opts)), list: () => load().then((s) => s.list()), // events must return the unsubscribe synchronously (the screen subscribes in onMount), so it wires // the real subscription once the engine loads and, until then, cancels a pending subscribe. events: (id, onEvent) => { let unsub = (): void => {}; let cancelled = false; void load().then((s) => { if (!cancelled) unsub = s.events(id, onEvent); }); return () => { cancelled = true; unsub(); }; }, } satisfies GameLoopSource & Pick; /** * gameSource returns the source that runs the game with the given id: the local engine for a local * id, otherwise the network gateway (which satisfies the same game-loop interface). */ export function gameSource(id: string): GameLoopSource { return isLocalGameId(id) ? localSource : gateway; } export { isLocalGameId } from './localgame/id';