@eigeninteractive/testkit 0.4.0 → 0.5.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/dist/{chunk-JJW4WLTM.js → chunk-BQIY636F.js} +41 -11
- package/dist/chunk-BQIY636F.js.map +1 -0
- package/dist/{chunk-VXWO4LVS.js → chunk-YJE4AGHJ.js} +1 -1
- package/dist/chunk-YJE4AGHJ.js.map +1 -0
- package/dist/contract-cli.js +1 -1
- package/dist/index.d.ts +14 -2
- package/dist/index.js +1 -1
- package/dist/inspect-cli.js +1 -1
- package/dist/local-state.d.ts +2 -0
- package/dist/local-state.js +1 -1
- package/package.json +7 -7
- package/dist/chunk-JJW4WLTM.js.map +0 -1
- package/dist/chunk-VXWO4LVS.js.map +0 -1
|
@@ -79,6 +79,18 @@ function parseRatingPoolCase(where, raw) {
|
|
|
79
79
|
expected: raw.expected === null ? null : asString(`${where}.expected`, raw.expected)
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
|
+
function parsePlayerLimitsCase(where, raw) {
|
|
83
|
+
const expected = asObject(`${where}.expected`, raw.expected);
|
|
84
|
+
return {
|
|
85
|
+
kind: "playerLimits",
|
|
86
|
+
name: asString(`${where}.name`, raw.name),
|
|
87
|
+
config: asObject(`${where}.config`, raw.config),
|
|
88
|
+
expected: {
|
|
89
|
+
minPlayers: asNumber(`${where}.expected.minPlayers`, expected.minPlayers),
|
|
90
|
+
maxPlayers: asNumber(`${where}.expected.maxPlayers`, expected.maxPlayers)
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
82
94
|
function parseBotSeatableCase(where, raw) {
|
|
83
95
|
return {
|
|
84
96
|
kind: "botSeatable",
|
|
@@ -99,12 +111,14 @@ function parseTwinFixtureFile(path, json) {
|
|
|
99
111
|
switch (obj.kind) {
|
|
100
112
|
case "action":
|
|
101
113
|
return parseActionCase(where, obj);
|
|
114
|
+
case "playerLimits":
|
|
115
|
+
return parsePlayerLimitsCase(where, obj);
|
|
102
116
|
case "ratingPool":
|
|
103
117
|
return parseRatingPoolCase(where, obj);
|
|
104
118
|
case "botSeatable":
|
|
105
119
|
return parseBotSeatableCase(where, obj);
|
|
106
120
|
default:
|
|
107
|
-
throw new Error(`${where}.kind: expected one of action | ratingPool | botSeatable, got ${JSON.stringify(obj.kind)}`);
|
|
121
|
+
throw new Error(`${where}.kind: expected one of action | playerLimits | ratingPool | botSeatable, got ${JSON.stringify(obj.kind)}`);
|
|
108
122
|
}
|
|
109
123
|
});
|
|
110
124
|
return { schemaVersion, cases };
|
|
@@ -113,12 +127,14 @@ function evaluateTwinCase(rules, kase) {
|
|
|
113
127
|
switch (kase.kind) {
|
|
114
128
|
case "action":
|
|
115
129
|
return evaluateAction(rules, kase);
|
|
130
|
+
case "playerLimits":
|
|
131
|
+
return evaluatePlayerLimits(rules, kase);
|
|
116
132
|
case "ratingPool":
|
|
117
133
|
return evaluateRatingPool(rules, kase);
|
|
118
134
|
case "botSeatable":
|
|
119
135
|
return evaluateBotSeatable(rules, kase);
|
|
120
136
|
default:
|
|
121
|
-
return [`unknown case kind "${kase.kind}", expected action | ratingPool | botSeatable`];
|
|
137
|
+
return [`unknown case kind "${kase.kind}", expected action | playerLimits | ratingPool | botSeatable`];
|
|
122
138
|
}
|
|
123
139
|
}
|
|
124
140
|
function twinFixtureTests(gameModule, fixturesRoot) {
|
|
@@ -240,6 +256,16 @@ function evaluateRatingPool(rules, kase) {
|
|
|
240
256
|
}
|
|
241
257
|
return failures;
|
|
242
258
|
}
|
|
259
|
+
function evaluatePlayerLimits(rules, kase) {
|
|
260
|
+
const failures = [];
|
|
261
|
+
const config = parseWith(rules, "config", kase.config, failures);
|
|
262
|
+
if (config === void 0) return failures;
|
|
263
|
+
const limits = rules.playerLimits({ config });
|
|
264
|
+
if (limits.minPlayers !== kase.expected.minPlayers || limits.maxPlayers !== kase.expected.maxPlayers) {
|
|
265
|
+
failures.push(`playerLimits returned ${limits.minPlayers}-${limits.maxPlayers}, fixture expects ${kase.expected.minPlayers}-${kase.expected.maxPlayers}`);
|
|
266
|
+
}
|
|
267
|
+
return failures;
|
|
268
|
+
}
|
|
243
269
|
function evaluateBotSeatable(rules, kase) {
|
|
244
270
|
const failures = [];
|
|
245
271
|
const gameConfig = parseWith(rules, "config", kase.gameConfig, failures);
|
|
@@ -298,23 +324,27 @@ function deepEquals(a, b) {
|
|
|
298
324
|
// src/game-contract.ts
|
|
299
325
|
import { existsSync, readdirSync as readdirSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
300
326
|
import { basename, relative, resolve } from "node:path";
|
|
327
|
+
import { assertPortableSchema } from "@eigeninteractive/rules";
|
|
301
328
|
var GAME_CONTRACT_FORMAT_VERSION = 1;
|
|
302
329
|
function canonical(value) {
|
|
303
330
|
if (Array.isArray(value)) return value.map(canonical);
|
|
304
331
|
if (value !== null && typeof value === "object") {
|
|
305
332
|
return Object.fromEntries(
|
|
306
|
-
Object.entries(value).sort(([a], [b]) => a
|
|
333
|
+
Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, item]) => [key, canonical(item)])
|
|
307
334
|
);
|
|
308
335
|
}
|
|
309
336
|
return value;
|
|
310
337
|
}
|
|
311
|
-
function jsonSchema(schema, label
|
|
338
|
+
function jsonSchema(schema, label) {
|
|
312
339
|
const options = { target: "draft-2020-12" };
|
|
340
|
+
let emitted;
|
|
313
341
|
try {
|
|
314
|
-
|
|
342
|
+
emitted = canonical(schema["~standard"].jsonSchema.output(options));
|
|
315
343
|
} catch (error) {
|
|
316
|
-
throw new Error(`${label} cannot emit its
|
|
344
|
+
throw new Error(`${label} cannot emit its type as draft-2020-12 JSON Schema`, { cause: error });
|
|
317
345
|
}
|
|
346
|
+
assertPortableSchema(emitted, label);
|
|
347
|
+
return emitted;
|
|
318
348
|
}
|
|
319
349
|
function fixtureFiles2(root) {
|
|
320
350
|
const found = [];
|
|
@@ -361,10 +391,10 @@ function buildGameContract(options) {
|
|
|
361
391
|
version,
|
|
362
392
|
{
|
|
363
393
|
schemas: {
|
|
364
|
-
state: jsonSchema(rules.schemas.state, `v${version} state
|
|
365
|
-
observation: jsonSchema(rules.schemas.observation, `v${version} observation
|
|
366
|
-
action: jsonSchema(rules.schemas.action, `v${version} action
|
|
367
|
-
config: jsonSchema(rules.schemas.config, `v${version} config
|
|
394
|
+
state: jsonSchema(rules.schemas.state, `v${version} state`),
|
|
395
|
+
observation: jsonSchema(rules.schemas.observation, `v${version} observation`),
|
|
396
|
+
action: jsonSchema(rules.schemas.action, `v${version} action`),
|
|
397
|
+
config: jsonSchema(rules.schemas.config, `v${version} config`)
|
|
368
398
|
}
|
|
369
399
|
}
|
|
370
400
|
])
|
|
@@ -450,4 +480,4 @@ export {
|
|
|
450
480
|
emitConfiguredGameContract,
|
|
451
481
|
checkConfiguredGameContract
|
|
452
482
|
};
|
|
453
|
-
//# sourceMappingURL=chunk-
|
|
483
|
+
//# sourceMappingURL=chunk-BQIY636F.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/twin-fixtures.ts","../src/game-contract.ts","../src/contract-command.ts"],"sourcesContent":["/**\n * Twin-drift fixture runner: the TS half of the shared JSON fixtures that\n * keep a version unit's TS and Dart `GameRules` twins in sync. The JSON\n * fixture format is shared verbatim with the Dart runner\n * (`lib/testing/twin_fixtures.dart`): one file, two consumers.\n *\n * One fixture file per concern lives beside the version units at\n * `<fixturesRoot>/v<N>/*.json` and is consumed by BOTH sides: this module\n * runs each case against the TS unit (schemas + `applyAction` +\n * `computeObservation` + the two predicates), while the Dart runner runs the\n * same file against the Dart twin (generated payload parsing,\n * `isValidAction`, `previewAction`, predicate twins). A behavioral divergence\n * then fails one side's CI instead of degrading UX in production.\n *\n * ## Fixture file format\n *\n * ```jsonc\n * {\n * \"schemaVersion\": 1,\n * \"cases\": [\n * {\n * \"kind\": \"action\",\n * \"name\": \"seat 0 marks an empty cell\",\n * \"config\": {},\n * \"state\": { ... }, // TS: applyAction input\n * \"obs\": { ... }, // Dart-only; defaults to `state`\n * \"pending\": [0],\n * \"playerIndex\": 0,\n * \"participantCount\": 2, // optional, default 2\n * \"rngSeed\": \"any string\", // optional, default \"twin-fixtures\"\n * \"action\": { ... },\n * \"expected\": {\n * \"valid\": true, // false ⇒ TS IllegalMoveError,\n * // Dart isValidAction false\n * \"state\": { ... }, // optional, TS envelope.state\n * \"pending\": [1], // optional, TS envelope.pendingPlayers\n * \"outcome\": [ ... ], // optional, TS envelope.outcome\n * // (null asserts the game is ongoing)\n * \"observation\": { ... } // optional, the actor's post-action\n * // view: TS computeObservation slice\n * // data; Dart previewAction (when the\n * // game implements optimism)\n * }\n * },\n * { \"kind\": \"playerLimits\", \"name\": \"...\", \"config\": {},\n * \"expected\": { \"minPlayers\": 2, \"maxPlayers\": 2 } },\n * { \"kind\": \"ratingPool\", \"name\": \"...\", \"access\": \"public\",\n * \"minPlayers\": 2, \"maxPlayers\": 2, \"config\": {}, \"expected\": \"blitz\" },\n * { \"kind\": \"botSeatable\", \"name\": \"...\", \"gameConfig\": {},\n * \"botConfig\": {}, \"expected\": false }\n * ]\n * }\n * ```\n *\n * The `state`/`obs` split exists for hidden-info games (a seat's observation\n * is not the state); perfect-info games omit `obs`. `expected.observation` is\n * the shared behavioral anchor: the TS side must project the post-action\n * state to it, and a Dart `previewAction` that returns non-null must predict\n * it, so the two sides are compared through one recorded value.\n *\n * Wire it up in a game-owned test file running under plain-Node vitest:\n *\n * ```ts\n * import { twinFixtureTests } from \"@eigeninteractive/testkit\";\n * import gameModule from \"../src/module/index.js\";\n *\n * twinFixtureTests(gameModule, new URL(\"../src/module/fixtures/\", import.meta.url));\n * ```\n */\n\nimport { readdirSync, readFileSync } from \"node:fs\";\nimport { type Envelope, type GameAccess, type GameModule, type GameRules, IllegalMoveError, type Json, type JsonObject, type ObservationSlice, type OutcomeEntry } from \"@eigeninteractive/rules\";\nimport Rand from \"rand-seed\";\nimport { it } from \"vitest\";\n\n/** One fixture file: cases targeting one `schemaVersion` unit. */\nexport interface TwinFixtureFile {\n schemaVersion: number;\n cases: TwinFixtureCase[];\n}\n\n/** A game-action case: exercises schemas, `applyAction`, and (through\n * `expected.observation`) `computeObservation` for the acting seat. */\nexport interface ActionCase {\n kind: \"action\";\n name: string;\n config: JsonObject;\n state: JsonObject;\n /** Dart-side observation payload; unused here (defaults to `state`). */\n obs?: JsonObject;\n pending: number[];\n playerIndex: number;\n participantCount?: number;\n rngSeed?: string;\n action: JsonObject;\n expected: {\n valid: boolean;\n state?: JsonObject;\n pending?: number[];\n outcome?: OutcomeEntry[] | null;\n observation?: JsonObject;\n };\n}\n\n/** A `ratingPool` predicate case. Omitted timing fields mean null. */\nexport interface RatingPoolCase {\n kind: \"ratingPool\";\n name: string;\n access: GameAccess;\n turnSeconds?: number | null;\n budgetSeconds?: number | null;\n incrementSeconds?: number | null;\n minPlayers: number;\n maxPlayers: number;\n config: JsonObject;\n expected: string | null;\n}\n\n/** A `playerLimits` case: the seats one config may be played with. */\nexport interface PlayerLimitsCase {\n kind: \"playerLimits\";\n name: string;\n config: JsonObject;\n expected: { minPlayers: number; maxPlayers: number };\n}\n\n/** A `botSeatable` predicate case. */\nexport interface BotSeatableCase {\n kind: \"botSeatable\";\n name: string;\n gameConfig: JsonObject;\n botConfig: JsonObject;\n expected: boolean;\n}\n\nexport type TwinFixtureCase = ActionCase | PlayerLimitsCase | RatingPoolCase | BotSeatableCase;\n\n// ── Fixture validation ────────────────────────────────────────────────────────\n//\n// The case types above are compile-time only; the JSON they describe arrives\n// at runtime from a hand-written file. Asserting `JSON.parse(...) as\n// TwinFixtureFile` would make every field a lie the moment a fixture is\n// mistyped, and because most fields flow straight into a comparison, the\n// symptom would be a confusing `undefined` diff attributed to the game's\n// rules rather than to the fixture. These parsers close that gap: a\n// malformed fixture fails at LOAD, naming the file, the case, and the field.\n//\n// Hand-written rather than schema-library-backed on purpose: `@eigeninteractive/rules`\n// describes an implementor's schemas with `StandardSchemaV1` precisely so the\n// engine never mandates a validation library, and the Dart twin runner is\n// deliberately framework-free. This is ~80 lines and keeps both true.\n\nfunction describe(value: unknown): string {\n if (value === null) return \"null\";\n if (Array.isArray(value)) return \"an array\";\n return typeof value;\n}\n\nfunction fail(where: string, expected: string, got: unknown): never {\n throw new Error(`${where}: expected ${expected}, got ${describe(got)}`);\n}\n\nfunction asObject(where: string, v: unknown): JsonObject {\n if (typeof v !== \"object\" || v === null || Array.isArray(v)) fail(where, \"an object\", v);\n return v as JsonObject;\n}\n\nfunction asString(where: string, v: unknown): string {\n if (typeof v !== \"string\") fail(where, \"a string\", v);\n return v;\n}\n\nfunction asNumber(where: string, v: unknown): number {\n if (typeof v !== \"number\" || !Number.isFinite(v)) fail(where, \"a finite number\", v);\n return v;\n}\n\nfunction asBoolean(where: string, v: unknown): boolean {\n if (typeof v !== \"boolean\") fail(where, \"a boolean\", v);\n return v;\n}\n\nfunction asNumberArray(where: string, v: unknown): number[] {\n if (!Array.isArray(v)) fail(where, \"an array of numbers\", v);\n return v.map((n, i) => asNumber(`${where}[${i}]`, n));\n}\n\n/** Applies `read` only when the key is present and non-null; absent and\n * explicit null both mean \"not specified\" for every optional fixture field. */\nfunction optional<T>(where: string, v: unknown, read: (where: string, v: unknown) => T): T | undefined {\n return v === undefined || v === null ? undefined : read(where, v);\n}\n\nfunction parseActionCase(where: string, raw: JsonObject): ActionCase {\n const expectedRaw = asObject(`${where}.expected`, raw.expected);\n const expected: ActionCase[\"expected\"] = {\n valid: asBoolean(`${where}.expected.valid`, expectedRaw.valid),\n state: optional(`${where}.expected.state`, expectedRaw.state, asObject),\n pending: optional(`${where}.expected.pending`, expectedRaw.pending, asNumberArray),\n observation: optional(`${where}.expected.observation`, expectedRaw.observation, asObject),\n };\n // `outcome` is three-valued: absent (unchecked), null (asserts the game is\n // ongoing), or a list. `checkEnvelope` distinguishes absent from null with\n // an `in` test, so the key must only be set when the fixture set it.\n if (\"outcome\" in expectedRaw) {\n const outcome = expectedRaw.outcome;\n if (outcome !== null && !Array.isArray(outcome)) fail(`${where}.expected.outcome`, \"an array or null\", outcome);\n expected.outcome = outcome as OutcomeEntry[] | null;\n }\n return {\n kind: \"action\",\n name: asString(`${where}.name`, raw.name),\n config: asObject(`${where}.config`, raw.config),\n state: asObject(`${where}.state`, raw.state),\n obs: optional(`${where}.obs`, raw.obs, asObject),\n pending: asNumberArray(`${where}.pending`, raw.pending),\n playerIndex: asNumber(`${where}.playerIndex`, raw.playerIndex),\n participantCount: optional(`${where}.participantCount`, raw.participantCount, asNumber),\n rngSeed: optional(`${where}.rngSeed`, raw.rngSeed, asString),\n action: asObject(`${where}.action`, raw.action),\n expected,\n };\n}\n\nfunction parseRatingPoolCase(where: string, raw: JsonObject): RatingPoolCase {\n const access = asString(`${where}.access`, raw.access);\n if (access !== \"public\" && access !== \"private\" && access !== \"friends\") {\n throw new Error(`${where}.access: expected one of public | private | friends, got ${JSON.stringify(access)}`);\n }\n return {\n kind: \"ratingPool\",\n name: asString(`${where}.name`, raw.name),\n access,\n turnSeconds: optional(`${where}.turnSeconds`, raw.turnSeconds, asNumber) ?? null,\n budgetSeconds: optional(`${where}.budgetSeconds`, raw.budgetSeconds, asNumber) ?? null,\n incrementSeconds: optional(`${where}.incrementSeconds`, raw.incrementSeconds, asNumber) ?? null,\n minPlayers: asNumber(`${where}.minPlayers`, raw.minPlayers),\n maxPlayers: asNumber(`${where}.maxPlayers`, raw.maxPlayers),\n config: asObject(`${where}.config`, raw.config),\n expected: raw.expected === null ? null : asString(`${where}.expected`, raw.expected),\n };\n}\n\nfunction parsePlayerLimitsCase(where: string, raw: JsonObject): PlayerLimitsCase {\n const expected = asObject(`${where}.expected`, raw.expected);\n return {\n kind: \"playerLimits\",\n name: asString(`${where}.name`, raw.name),\n config: asObject(`${where}.config`, raw.config),\n expected: {\n minPlayers: asNumber(`${where}.expected.minPlayers`, expected.minPlayers),\n maxPlayers: asNumber(`${where}.expected.maxPlayers`, expected.maxPlayers),\n },\n };\n}\n\nfunction parseBotSeatableCase(where: string, raw: JsonObject): BotSeatableCase {\n return {\n kind: \"botSeatable\",\n name: asString(`${where}.name`, raw.name),\n gameConfig: asObject(`${where}.gameConfig`, raw.gameConfig),\n botConfig: asObject(`${where}.botConfig`, raw.botConfig),\n expected: asBoolean(`${where}.expected`, raw.expected),\n };\n}\n\n/** Validate one fixture file's parsed JSON, or throw naming the offending\n * file, case, and field. Exported so a repo can lint its fixtures without\n * running them. */\nexport function parseTwinFixtureFile(path: string, json: unknown): TwinFixtureFile {\n const root = asObject(path, json);\n const schemaVersion = asNumber(`${path}.schemaVersion`, root.schemaVersion);\n if (!Array.isArray(root.cases)) fail(`${path}.cases`, \"an array\", root.cases);\n const cases = root.cases.map((raw, i) => {\n // Prefer the case's own name in the location once we can read it: a\n // fixture author finds \"cases[3] (seat 0 wins)\" faster than an index.\n const indexed = `${path}.cases[${i}]`;\n const obj = asObject(indexed, raw);\n const where = typeof obj.name === \"string\" ? `${indexed} (${obj.name})` : indexed;\n switch (obj.kind) {\n case \"action\":\n return parseActionCase(where, obj);\n case \"playerLimits\":\n return parsePlayerLimitsCase(where, obj);\n case \"ratingPool\":\n return parseRatingPoolCase(where, obj);\n case \"botSeatable\":\n return parseBotSeatableCase(where, obj);\n default:\n throw new Error(`${where}.kind: expected one of action | playerLimits | ratingPool | botSeatable, got ${JSON.stringify(obj.kind)}`);\n }\n });\n return { schemaVersion, cases };\n}\n\n/** Run one fixture case against a rules unit, returning failure descriptions\n * (empty ⇒ the case passes). Pure; the file-reading test registrar is\n * {@link twinFixtureTests}. */\nexport function evaluateTwinCase(rules: GameRules, kase: TwinFixtureCase): string[] {\n switch (kase.kind) {\n case \"action\":\n return evaluateAction(rules, kase);\n case \"playerLimits\":\n return evaluatePlayerLimits(rules, kase);\n case \"ratingPool\":\n return evaluateRatingPool(rules, kase);\n case \"botSeatable\":\n return evaluateBotSeatable(rules, kase);\n default:\n return [`unknown case kind \"${(kase as { kind: string }).kind}\", expected action | playerLimits | ratingPool | botSeatable`];\n }\n}\n\n/** Register one vitest test per fixture case found under `fixturesRoot`\n * (layout: `<root>/v<N>/*.json`). Call at the top level of a test module\n * running in a Node environment. */\nexport function twinFixtureTests(gameModule: GameModule, fixturesRoot: string | URL): void {\n for (const filePath of fixtureFiles(fixturesRoot)) {\n const fixture = parseTwinFixtureFile(filePath, JSON.parse(readFileSync(filePath, \"utf8\")));\n const rules = gameModule.versions[fixture.schemaVersion];\n for (const kase of fixture.cases) {\n it(`twin v${fixture.schemaVersion}: ${kase.name}`, () => {\n if (!rules) {\n throw new Error(`gameModule ships no rules unit for schemaVersion ${fixture.schemaVersion} (fixture: ${filePath})`);\n }\n const failures = evaluateTwinCase(rules, kase);\n if (failures.length) throw new Error(`\\n${failures.join(\"\\n\")}`);\n });\n }\n }\n}\n\nfunction* fixtureFiles(root: string | URL): Generator<string> {\n const dirs = readdirSync(root, { withFileTypes: true })\n .filter((e) => e.isDirectory())\n .sort((a, b) => a.name.localeCompare(b.name));\n for (const dir of dirs) {\n const files = readdirSync(`${dir.parentPath}/${dir.name}`, {\n withFileTypes: true,\n })\n .filter((e) => e.isFile() && e.name.endsWith(\".json\"))\n .sort((a, b) => a.name.localeCompare(b.name));\n for (const file of files) yield `${file.parentPath}/${file.name}`;\n }\n}\n\n// ── Case evaluators ───────────────────────────────────────────────────────────\n\nfunction evaluateAction(rules: GameRules, kase: ActionCase): string[] {\n const failures: string[] = [];\n const config = parseWith(rules, \"config\", kase.config, failures);\n const state = parseWith(rules, \"state\", kase.state, failures);\n const action = parseWith(rules, \"action\", kase.action, failures);\n if (config === undefined || state === undefined || action === undefined) {\n return failures;\n }\n\n // The parsed action must be the fixture action: a schema that strips or\n // defaults fields the twin relies on is itself drift.\n if (!deepEquals(action, kase.action)) {\n failures.push(`action schema does not preserve the fixture action, parsed to ${JSON.stringify(action)}`);\n }\n\n const envelope = applyFixtureAction(rules, kase, config, state, action);\n if (typeof envelope === \"string\") {\n if (envelope) failures.push(envelope);\n return failures;\n }\n checkEnvelope(rules, kase, envelope, failures);\n if (kase.expected.observation !== undefined) {\n checkObservation(rules, kase, envelope, config, action, failures);\n }\n return failures;\n}\n\n/** Invoke `applyAction` and reconcile with `expected.valid`. Returns the\n * envelope on a valid accepted move, an error string on failure, or \"\" when\n * an expected-illegal move was correctly rejected (nothing left to check). */\nfunction applyFixtureAction(rules: GameRules, kase: ActionCase, config: JsonObject, state: JsonObject, action: JsonObject): Envelope | string {\n let envelope: Envelope;\n try {\n envelope = rules.applyAction({\n state,\n pending: kase.pending,\n data: action,\n playerIndex: kase.playerIndex,\n rng: new Rand(kase.rngSeed ?? \"twin-fixtures\"),\n config,\n });\n } catch (error) {\n if (!(error instanceof IllegalMoveError)) {\n return `applyAction threw a non-IllegalMoveError: ${error}`;\n }\n return kase.expected.valid ? `applyAction rejected a move the fixture expects to be valid: ${error.message}` : \"\";\n }\n return kase.expected.valid ? envelope : \"applyAction accepted a move the fixture expects to be illegal\";\n}\n\nfunction checkEnvelope(rules: GameRules, kase: ActionCase, envelope: Envelope, failures: string[]): void {\n if (validate(rules, \"state\", envelope.state) === undefined) {\n failures.push(\"applyAction returned state that violates its own schema\");\n }\n const expected = kase.expected;\n if (expected.state !== undefined && !deepEquals(envelope.state, expected.state)) {\n failures.push(`envelope.state mismatch, got ${JSON.stringify(envelope.state)}`);\n }\n if (expected.pending !== undefined && !deepEquals(envelope.pendingPlayers, expected.pending)) {\n failures.push(`envelope.pendingPlayers mismatch, got ${JSON.stringify(envelope.pendingPlayers)}`);\n }\n if (\"outcome\" in expected && !deepEquals(envelope.outcome ?? null, expected.outcome ?? null)) {\n failures.push(`envelope.outcome mismatch, got ${JSON.stringify(envelope.outcome ?? null)}`);\n }\n}\n\nfunction checkObservation(rules: GameRules, kase: ActionCase, envelope: Envelope, config: JsonObject, action: JsonObject, failures: string[]): void {\n let slice: ObservationSlice;\n try {\n slice = rules.computeObservation({\n state: envelope.state,\n pending: envelope.pendingPlayers,\n playerIndex: kase.playerIndex,\n participantCount: kase.participantCount ?? 2,\n config,\n cause: { kind: \"game\", data: action, playerIndex: kase.playerIndex },\n isReplay: false,\n });\n } catch (error) {\n failures.push(`computeObservation threw: ${error}`);\n return;\n }\n if (!deepEquals(slice.data, kase.expected.observation as Json)) {\n failures.push(`actor's observation mismatch, got ${JSON.stringify(slice.data)}`);\n }\n}\n\nfunction evaluateRatingPool(rules: GameRules, kase: RatingPoolCase): string[] {\n const failures: string[] = [];\n const config = parseWith(rules, \"config\", kase.config, failures);\n if (config === undefined) return failures;\n const pool = rules.ratingPool({\n access: kase.access,\n turnSeconds: kase.turnSeconds ?? null,\n budgetSeconds: kase.budgetSeconds ?? null,\n incrementSeconds: kase.incrementSeconds ?? null,\n minPlayers: kase.minPlayers,\n maxPlayers: kase.maxPlayers,\n config,\n });\n if (pool !== kase.expected) {\n failures.push(`ratingPool returned ${JSON.stringify(pool)}, fixture expects ${JSON.stringify(kase.expected)}`);\n }\n return failures;\n}\n\nfunction evaluatePlayerLimits(rules: GameRules, kase: PlayerLimitsCase): string[] {\n const failures: string[] = [];\n const config = parseWith(rules, \"config\", kase.config, failures);\n if (config === undefined) return failures;\n const limits = rules.playerLimits({ config });\n if (limits.minPlayers !== kase.expected.minPlayers || limits.maxPlayers !== kase.expected.maxPlayers) {\n failures.push(`playerLimits returned ${limits.minPlayers}-${limits.maxPlayers}, fixture expects ${kase.expected.minPlayers}-${kase.expected.maxPlayers}`);\n }\n return failures;\n}\n\nfunction evaluateBotSeatable(rules: GameRules, kase: BotSeatableCase): string[] {\n const failures: string[] = [];\n const gameConfig = parseWith(rules, \"config\", kase.gameConfig, failures);\n if (gameConfig === undefined) return failures;\n const seatable = rules.botSeatable({\n gameConfig,\n botConfig: kase.botConfig,\n });\n if (seatable !== kase.expected) {\n failures.push(`botSeatable returned ${seatable}, fixture expects ${kase.expected}`);\n }\n return failures;\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\n/** Run one of the unit's Standard Schemas synchronously, returning the parsed\n * value or undefined on failure (issues written to the second return slot). */\nfunction validate(rules: GameRules, which: \"config\" | \"state\" | \"action\", value: unknown, issues?: string[]): JsonObject | undefined {\n const result = rules.schemas[which][\"~standard\"].validate(value);\n if (result instanceof Promise) {\n issues?.push(`${which} schema validated asynchronously; must be sync`);\n return undefined;\n }\n if (result.issues) {\n issues?.push(\n result.issues\n .map((i) => {\n const path = i.path?.map((p) => (typeof p === \"object\" ? String(p.key) : String(p))).join(\".\");\n return path ? `${path}: ${i.message}` : i.message;\n })\n .join(\"; \"),\n );\n return undefined;\n }\n return result.value as JsonObject;\n}\n\n/** Parse a fixture payload through one of the unit's schemas, recording a\n * failure (and returning undefined) when it does not conform. */\nfunction parseWith(rules: GameRules, which: \"config\" | \"state\" | \"action\", value: JsonObject, failures: string[]): JsonObject | undefined {\n const issues: string[] = [];\n const parsed = validate(rules, which, value, issues);\n if (parsed === undefined) {\n failures.push(`fixture ${which} fails the TS ${which} schema: ${issues.join(\"; \")}`);\n }\n return parsed;\n}\n\n/** Structural JSON equality. Object keys with `undefined` values count as\n * absent (matching how schema libraries model optional fields); array order\n * matters. */\nexport function deepEquals(a: Json | undefined, b: Json | undefined): boolean {\n if (a === undefined || a === null) return b === undefined || b === null;\n if (b === undefined || b === null) return false;\n if (Array.isArray(a) || Array.isArray(b)) {\n if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {\n return false;\n }\n return a.every((item, i) => deepEquals(item, b[i]));\n }\n if (typeof a === \"object\" || typeof b === \"object\") {\n if (typeof a !== \"object\" || typeof b !== \"object\") return false;\n const keys = new Set([...Object.keys(a), ...Object.keys(b)]);\n return [...keys].every((k) => deepEquals((a as JsonObject)[k], (b as JsonObject)[k]));\n }\n return a === b;\n}\n","/**\n * Emit the language-neutral contract a game's Flutter repository consumes.\n *\n * The authoritative inputs live with the TypeScript rules: the four payload\n * schemas and the shared behavioral fixtures. The output is one deterministic\n * JSON file so it can be copied, attached to a release, or fetched by checksum\n * without assuming any repository layout.\n */\n\nimport { existsSync, readdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { basename, relative, resolve } from \"node:path\";\nimport { assertPortableSchema, type GameModule, type StandardJSONSchemaV1 } from \"@eigeninteractive/rules\";\nimport { parseTwinFixtureFile } from \"./twin-fixtures.js\";\n\n/** Current format of the language-neutral contract consumed by EigenInteractive's Dart generator. */\nexport const GAME_CONTRACT_FORMAT_VERSION = 1;\n\n/** One validated twin-fixture document embedded in a {@link GameContract}. */\nexport interface GameContractFixture {\n /** POSIX-style path relative to the supplied fixtures root. */\n path: string;\n /** Validated fixture document, retained in its original JSON shape. */\n document: unknown;\n}\n\n/** The four JSON Schemas emitted for one game `schemaVersion`. */\nexport interface GameContractVersion {\n schemas: {\n state: Record<string, unknown>;\n observation: Record<string, unknown>;\n action: Record<string, unknown>;\n config: Record<string, unknown>;\n };\n}\n\n/** Language-neutral schemas and fixtures shared by a game's Worker and app. */\nexport interface GameContract {\n formatVersion: typeof GAME_CONTRACT_FORMAT_VERSION;\n game: string;\n versions: Record<string, GameContractVersion>;\n fixtures: GameContractFixture[];\n}\n\n/** Inputs for building a {@link GameContract} without writing it. */\nexport interface BuildGameContractOptions {\n /** Stable display name used as the generated Dart type prefix. */\n game: string;\n /** Authoritative TypeScript rules registry. */\n gameModule: GameModule;\n /** Root containing `v<N>/*.json` twin fixtures. */\n fixturesRoot?: string | URL;\n}\n\n/** Inputs for emitting or checking a {@link GameContract} file. */\nexport interface EmitGameContractOptions extends BuildGameContractOptions {\n /** Destination `game-contract.json` path. */\n output: string | URL;\n}\n\n/**\n * Sort object keys so the same schemas always emit the same bytes.\n *\n * Ordering is by UTF-16 code unit — plain `Array#sort` — which is what RFC 8785\n * specifies and what `tool/check-contracts.mjs` uses to digest a manifest. It is\n * *not* `localeCompare`: that collates case-insensitively, so a `$defs` entry\n * named `Move` sorts before `additionalProperties` under one rule and after it\n * under the other. Generated JSON Schema is full of capitalized `$defs` names, so\n * the two orders genuinely disagree, and a digest computed over one would never\n * match a document written by the other.\n */\nfunction canonical(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(canonical);\n if (value !== null && typeof value === \"object\") {\n return Object.fromEntries(\n Object.entries(value as Record<string, unknown>)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([key, item]) => [key, canonical(item)]),\n );\n }\n return value;\n}\n\n/**\n * Emit one payload schema as portable draft-2020-12 JSON Schema.\n *\n * Always the **output** direction. Zod's input schema omits\n * `additionalProperties: false` on objects, because on input a permissive object\n * still strips unknown keys rather than rejecting them — but the contract is what\n * the Dart side generates a validator from, and an open object there means the two\n * halves disagree about an unknown key. Schemas are required to be transform-free\n * (see `GameSchemas`), so the two directions describe the same values and the\n * output one is simply the honest document.\n *\n * The result is then checked against the portable profile, because emission is\n * where an unportable schema can still be fixed cheaply. Skipping this check is\n * how the profile came to be violated in the first place.\n */\nfunction jsonSchema(schema: StandardJSONSchemaV1, label: string): Record<string, unknown> {\n const options = { target: \"draft-2020-12\" as const };\n let emitted: unknown;\n try {\n emitted = canonical(schema[\"~standard\"].jsonSchema.output(options));\n } catch (error) {\n throw new Error(`${label} cannot emit its type as draft-2020-12 JSON Schema`, { cause: error });\n }\n assertPortableSchema(emitted, label);\n return emitted as Record<string, unknown>;\n}\n\nfunction fixtureFiles(root: string): string[] {\n const found: string[] = [];\n const visit = (directory: string): void => {\n for (const entry of readdirSync(directory, { withFileTypes: true })) {\n const path = resolve(directory, entry.name);\n if (entry.isDirectory()) visit(path);\n else if (entry.isFile() && entry.name.endsWith(\".json\")) found.push(path);\n }\n };\n visit(root);\n return found.sort();\n}\n\nfunction fixtures(fixturesRoot: string | URL | undefined, versions: ReadonlySet<string>): GameContractFixture[] {\n if (fixturesRoot === undefined) return [];\n const root = fixturesRoot instanceof URL ? fixturesRoot.pathname : resolve(fixturesRoot);\n return fixtureFiles(root).map((path) => {\n const document = JSON.parse(readFileSync(path, \"utf8\")) as unknown;\n const fixture = parseTwinFixtureFile(path, document);\n const fixturePath = relative(root, path).split(\"\\\\\").join(\"/\");\n const directory = fixturePath.split(\"/\")[0];\n const match = /^v([1-9]\\d*)$/.exec(directory ?? \"\");\n if (match === null) {\n throw new Error(`${fixturePath}: fixtures must live under a v<N>/ directory`);\n }\n const pathVersion = Number(match[1]);\n if (pathVersion !== fixture.schemaVersion) {\n throw new Error(`${fixturePath}: directory v${pathVersion} disagrees with schemaVersion ${fixture.schemaVersion}`);\n }\n if (!versions.has(String(fixture.schemaVersion))) {\n throw new Error(`${fixturePath}: gameModule ships no rules unit for schemaVersion ${fixture.schemaVersion}`);\n }\n return {\n path: fixturePath,\n document: canonical(document),\n };\n });\n}\n\n/** Build a deterministic in-memory contract without touching the filesystem. */\nexport function buildGameContract(options: BuildGameContractOptions): GameContract {\n const game = options.game.trim();\n if (game.length === 0) throw new Error(\"game contract name must not be empty\");\n\n const versions = Object.fromEntries(\n Object.entries(options.gameModule.versions)\n .sort(([a], [b]) => Number(a) - Number(b))\n .map(([version, rules]) => [\n version,\n {\n schemas: {\n state: jsonSchema(rules.schemas.state, `v${version} state`),\n observation: jsonSchema(rules.schemas.observation, `v${version} observation`),\n action: jsonSchema(rules.schemas.action, `v${version} action`),\n config: jsonSchema(rules.schemas.config, `v${version} config`),\n },\n },\n ]),\n );\n\n if (Object.keys(versions).length === 0) {\n throw new Error(\"game contract must contain at least one schema version\");\n }\n\n return {\n formatVersion: GAME_CONTRACT_FORMAT_VERSION,\n game,\n versions,\n fixtures: fixtures(options.fixturesRoot, new Set(Object.keys(versions))),\n };\n}\n\n/** Render one deterministic, newline-terminated contract document. */\nexport function gameContractJson(options: BuildGameContractOptions): string {\n return `${JSON.stringify(buildGameContract(options), null, 2)}\\n`;\n}\n\n/** Emit one deterministic, newline-terminated `game-contract.json`. */\nexport function emitGameContract(options: EmitGameContractOptions): void {\n const output = options.output instanceof URL ? options.output : resolve(options.output);\n writeFileSync(output, gameContractJson(options));\n}\n\n/** Fail when an emitted contract is missing or differs from its inputs. */\nexport function checkGameContract(options: EmitGameContractOptions): void {\n const output = options.output instanceof URL ? options.output : resolve(options.output);\n if (!existsSync(output) || readFileSync(output, \"utf8\") !== gameContractJson(options)) {\n throw new Error(`${output.toString()} is stale; run eigen-contract`);\n }\n}\n\n/** A useful default filename for scripts that accept an output directory. */\nexport const gameContractFilename = (game: string): string =>\n `${basename(game)\n .toLowerCase()\n .replaceAll(/[^a-z0-9]+/g, \"-\")}-game-contract.json`;\n","import { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport type { GameModule } from \"@eigeninteractive/rules\";\nimport { tsImport } from \"tsx/esm/api\";\nimport { checkGameContract, emitGameContract } from \"./game-contract.js\";\n\ninterface EigenPackageConfig {\n game?: unknown;\n module?: unknown;\n fixtures?: unknown;\n contract?: unknown;\n}\n\ninterface PackageManifest {\n eigen?: EigenPackageConfig;\n}\n\nfunction configuredString(value: unknown, fallback: string, field: string): string {\n if (value === undefined) return fallback;\n if (typeof value !== \"string\" || value.trim().length === 0) {\n throw new Error(`package.json eigen.${field} must be a non-empty string`);\n }\n return value;\n}\n\nconst isGameModule = (value: unknown): value is GameModule => value !== null && typeof value === \"object\" && \"versions\" in value && value.versions !== null && typeof value.versions === \"object\";\n\n/**\n * Emit a contract from the conventional game package layout.\n *\n * The package default-exports its GameModule from `src/module/index.ts` and\n * declares `{ \"eigen\": { \"game\": \"…\" } }` in package.json. Paths can be\n * overridden under the same `eigen` object.\n */\nasync function configuredGameContractOptions(root: string) {\n const manifest = JSON.parse(readFileSync(resolve(root, \"package.json\"), \"utf8\")) as PackageManifest;\n const config = manifest.eigen ?? {};\n const game = configuredString(config.game, \"\", \"game\");\n if (game.length === 0) {\n throw new Error('package.json must declare an EigenInteractive game name, e.g. \"eigen\": { \"game\": \"Chess\" }');\n }\n\n const modulePath = configuredString(config.module, \"src/module/index.ts\", \"module\");\n const fixturesPath = configuredString(config.fixtures, \"src/module/fixtures\", \"fixtures\");\n const contractPath = configuredString(config.contract, \"game-contract.json\", \"contract\");\n const imported = (await tsImport(pathToFileURL(resolve(root, modulePath)).href, import.meta.url)) as { default?: unknown };\n\n if (!isGameModule(imported.default)) {\n throw new Error(`${modulePath} must default-export an EigenInteractive GameModule`);\n }\n\n return {\n game,\n gameModule: imported.default,\n fixturesRoot: resolve(root, fixturesPath),\n output: resolve(root, contractPath),\n };\n}\n\n/**\n * Emits `game-contract.json` from an EigenInteractive package's conventional layout.\n *\n * This is the programmatic form of the `eigen-contract` executable. Most\n * games should invoke the executable through their package script.\n */\nexport async function emitConfiguredGameContract(root = process.cwd()): Promise<void> {\n emitGameContract(await configuredGameContractOptions(root));\n}\n\n/**\n * Fails when the conventionally configured contract is absent or stale.\n *\n * Use this in CI through `eigen-contract --check`.\n */\nexport async function checkConfiguredGameContract(root = process.cwd()): Promise<void> {\n checkGameContract(await configuredGameContractOptions(root));\n}\n"],"mappings":"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","names":["readdirSync","readFileSync","fixtureFiles","readdirSync","readFileSync","readFileSync","resolve","readFileSync","resolve"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/local-state.ts"],"sourcesContent":["/**\n * Read a Worker's LOCAL `.wrangler` state directly: the D1 index and the\n * per-game Durable Object databases, opened as plain SQLite files.\n *\n * This is a development and debugging surface, never a runtime one. Nothing here\n * runs on Workers: it is Node reading files that `wrangler dev` left on disk,\n * which is why it lives in the testkit (a `devDependency`) behind its own\n * subpath, and why it reads them through Node's built-in `node:sqlite` rather\n * than a driver package. `better-sqlite3` would work and was tried first, but\n * `drizzle-orm` declares it as an OPTIONAL PEER: installing it anywhere in the\n * workspace re-resolves drizzle's peer set for every importer, which changed the\n * types `packages/server` compiles against and broke its `dts` build. A\n * debugging tool is not worth perturbing the engine's own dependency graph, and\n * the built-in module costs nothing: no native build, no `allowBuilds` opt-in,\n * and no dependency at all.\n *\n * Two stores, two different questions, and the reason a game-shaped reader\n * exists at all:\n *\n * - **D1 is the index.** Discovery, history, ratings, identity. Its `games` row\n * is a display read-model written by the DO as a fire-and-forget mirror.\n * - **The game's DO is the session.** One SQLite database per game, holding the\n * authoritative status, roster and the append-only transition log.\n *\n * A question like \"why is this game not starting\" therefore cannot be answered\n * from either store alone, and answering it from a generic table browser means\n * knowing which of the two to distrust. {@link LocalStore.game} joins them and\n * reports the disagreement as a value, so a stale mirror reads as a stale\n * mirror rather than as a mystery.\n *\n * Every open is read-only, so this is safe to run against a live `wrangler dev`.\n *\n * @module @eigeninteractive/testkit/local-state\n */\n\nimport { existsSync, readdirSync, statSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { DatabaseSync } from \"node:sqlite\";\n\n/** Where `wrangler dev` persists local resources, relative to a Worker root. */\nconst STATE_SUFFIX = join(\".wrangler\", \"state\", \"v3\");\n\n/** Miniflare's own bookkeeping database, present in every resource directory\n * alongside the real ones. It holds `_cf_ALARM`, not user data. */\nconst METADATA_FILE = \"metadata.sqlite\";\n\n/** The table Miniflare writes into every Durable Object database, holding the\n * `idFromName` name the object was addressed by. It is what makes the hashed\n * filename reversible: the engine names a game's DO by its `gameId`, and the\n * hash is one-way, so this row is the only local file-to-game map there is. */\nconst DO_NAME_TABLE = \"__miniflare_do_name\";\n\n/** A local Durable Object database: one game's whole session. */\nexport interface LocalDurableObject {\n /** The Durable Object class, parsed from the directory Miniflare names\n * `<worker>-<ClassName>`. */\n className: string;\n /** The hex `DurableObjectId`, which is the filename: `idFromName(name)`\n * hashed. Not the game id, and not reversible without {@link name}. */\n id: string;\n /** The name the object was addressed by, which for a game DO is its\n * `gameId`. Null only if the object was created by unique id rather than by\n * name, which the engine never does. */\n name: string | null;\n /** Absolute path to the SQLite file. */\n file: string;\n}\n\n/** What a bound parameter may be: SQLite's own value domain, spelled out here\n * rather than re-exported from `node:sqlite` so the public signature does not\n * depend on a built-in module's type names. */\nexport type LocalQueryParam = string | number | bigint | null | Uint8Array;\n\n/** A row of Miniflare's alarm table: the DO's armed alarm, which for a game is\n * its turn deadline plus grace. An absent row means no timer is armed, which is\n * correct for an untimed game and a bug for a timed one mid-turn. */\nexport interface LocalAlarm {\n actorId: string;\n actorName: string | null;\n scheduledTime: number | null;\n}\n\n/** The engine's `games` row: the D1 index entry, a display mirror of the DO. */\nexport interface LocalGameRow {\n id: string;\n createdBy: string | null;\n status: string;\n access: string;\n schemaVersion: number;\n config: unknown;\n turnSeconds: number | null;\n budgetSeconds: number | null;\n incrementSeconds: number | null;\n rated: boolean;\n ratingPool: string | null;\n minPlayers: number;\n maxPlayers: number;\n shortCode: string;\n pendingPlayers: number[] | null;\n turnDeadline: number | null;\n outcomes: unknown;\n finishId: string | null;\n finishedAt: number | null;\n archivedAt: number | null;\n createdAt: number;\n updatedAt: number;\n}\n\n/** One seat, from either store. `username` is filled in from D1's `users` and\n * `bots` tables when the reader has an index to join against. */\nexport interface LocalSeat {\n playerIndex: number;\n userId: string | null;\n botId: string | null;\n type: string;\n username: string | null;\n displayName: string | null;\n}\n\n/** The DO's single `meta` row: the authoritative session header. */\nexport interface LocalGameMeta {\n gameId: string;\n status: string;\n access: string;\n schemaVersion: number;\n config: unknown;\n turnSeconds: number | null;\n budgetSeconds: number | null;\n incrementSeconds: number | null;\n rated: boolean;\n ratingPool: string | null;\n minPlayers: number;\n maxPlayers: number;\n createdBy: string | null;\n rngSeed: string | null;\n}\n\n/** One committed transition, decoded. `action` is null at v0 (the start). */\nexport interface LocalTransition {\n version: number;\n state: unknown;\n action: { type: string; kind: string; playerIndex: number | null; data: unknown } | null;\n pending: number[];\n deadline: number | null;\n playerTimes: number[] | null;\n /** Non-null only while this transition's pending turn consumes a budget\n * bank. Per-action and hook-override deadlines do not set it. */\n turnStartedAt: number | null;\n /** Which seats hold a stored frame at this version. Empty after the finish\n * compaction, which empties `frames` and leaves replay to re-projection. */\n frameSeats: number[];\n}\n\n/** Why the game is where it is, and what would move it. Derived rather than\n * stored: this is the sentence a developer actually wants, and every input to\n * it is already on screen above it. */\nexport interface LocalGameDiagnosis {\n /** One line naming the next expected event, or the reason there is none. */\n summary: string;\n /** Disagreements between the D1 mirror and the DO's own truth. A non-empty\n * list means a mirror write was lost, since the DO is authoritative. */\n mirrorDrift: string[];\n}\n\n/** Everything both stores know about one game. */\nexport interface LocalGameView {\n gameId: string;\n /** The D1 index row, or null when D1 has no such game: either the id came\n * from a DO name and the index was wiped, or the create write never landed. */\n index: LocalGameRow | null;\n /** D1's `participants`, which mirror the DO's roster for display. */\n indexSeats: LocalSeat[];\n /** The DO file, or null when no Durable Object exists for this game yet.\n * That is the normal state of a created-but-never-touched lobby: the DO is\n * created lazily by the first command or socket. */\n durableObject: LocalDurableObject | null;\n meta: LocalGameMeta | null;\n roster: LocalSeat[];\n transitions: LocalTransition[];\n /** Recorded `commandId` dedupe entries, newest first. */\n commands: { commandId: string; createdAt: number; result: unknown }[];\n /** Unapplied finish rows: a surviving row means the D1 finish apply has not\n * succeeded and the admin re-poke is the recovery. */\n outbox: { finishId: string; createdAt: number; outcomes: unknown }[];\n alarm: LocalAlarm | null;\n diagnosis: LocalGameDiagnosis;\n}\n\n/**\n * Walk up from `from` to the nearest directory holding `.wrangler/state/v3`.\n *\n * Walking rather than requiring the Worker root means this works from anywhere\n * inside a game repository, including the app half of a two-package layout,\n * where the state lives a directory or two above the shell's cwd.\n */\nexport function findWranglerState(from: string = process.cwd()): string | null {\n let dir = resolve(from);\n for (;;) {\n const candidate = join(dir, STATE_SUFFIX);\n if (existsSync(candidate)) return candidate;\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\n/**\n * Open a SQLite file for reading only.\n *\n * Read-only is what makes this safe to point at a running `wrangler dev`: no\n * writer lock is taken and no journal is touched. The fallback matters because\n * these databases are in WAL mode, and a read-only connection cannot recover a\n * hot journal or create the `-shm` file it needs; when `wrangler dev` is not\n * running and left one behind, the first open fails and a writable handle is\n * the only way in. Nothing here issues anything but `SELECT`, so the widened\n * handle stays as harmless as the narrow one. The existence check comes first\n * because that fallback would otherwise CREATE a database rather than report a\n * missing one.\n */\nexport function openSqlite(file: string): DatabaseSync {\n if (!existsSync(file)) throw new Error(`no SQLite database at ${file}`);\n try {\n return new DatabaseSync(file, { readOnly: true });\n } catch {\n return new DatabaseSync(file);\n }\n}\n\nfunction sqliteFilesIn(dir: string): string[] {\n if (!existsSync(dir)) return [];\n return readdirSync(dir)\n .filter((entry) => entry.endsWith(\".sqlite\") && entry !== METADATA_FILE)\n .map((entry) => join(dir, entry));\n}\n\nfunction tableExists(db: DatabaseSync, table: string): boolean {\n const row = db.prepare(\"select 1 from sqlite_master where type = 'table' and name = ?\").get(table);\n return row !== undefined;\n}\n\n/** Parse a JSON text column, tolerating a value some other writer left\n * un-encoded. A debugging reader that throws on one malformed cell is useless\n * precisely when it is needed. */\nfunction json(value: unknown): unknown {\n if (typeof value !== \"string\") return value ?? null;\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n}\n\nfunction numbers(value: unknown): number[] {\n const parsed = json(value);\n return Array.isArray(parsed) ? parsed.filter((n): n is number => typeof n === \"number\") : [];\n}\n\nfunction bool(value: unknown): boolean {\n return value === 1 || value === true;\n}\n\n/**\n * A Worker's local state, with its databases opened lazily and held open.\n *\n * Stateful because it owns file handles: construct one, ask it questions, and\n * {@link close} it. A single instance is the unit both the CLI and a test use,\n * so neither has to know which file backs which resource.\n */\nexport class LocalStore {\n /** Absolute path to `.wrangler/state/v3`. */\n readonly root: string;\n\n #d1: { file: string; db: DatabaseSync } | null | undefined;\n readonly #open = new Map<string, DatabaseSync>();\n\n private constructor(root: string) {\n this.root = root;\n }\n\n /**\n * Locate and open the state under `from`, or throw with the one thing the\n * caller can act on: that no Worker has been run here yet.\n */\n static open(from?: string): LocalStore {\n const root = findWranglerState(from);\n if (root === null) {\n throw new Error(`no .wrangler/state/v3 found at or above ${resolve(from ?? process.cwd())}. Run the Worker once (\\`pnpm dev\\`) so it has local state to read.`);\n }\n return new LocalStore(root);\n }\n\n #handle(file: string): DatabaseSync {\n const existing = this.#open.get(file);\n if (existing !== undefined) return existing;\n const db = openSqlite(file);\n this.#open.set(file, db);\n return db;\n }\n\n /**\n * The engine's D1 database, identified by its schema rather than its name.\n *\n * Miniflare names the file after a hash of the database, so there is no\n * mapping back to the binding. Recognising the engine's own `games` table\n * instead is both simpler and more honest about what the caller wants: the\n * database that has games in it, whatever the binding was called.\n */\n d1(): { file: string; db: DatabaseSync } | null {\n if (this.#d1 !== undefined) return this.#d1;\n for (const file of sqliteFilesIn(join(this.root, \"d1\", \"miniflare-D1DatabaseObject\"))) {\n const db = this.#handle(file);\n if (tableExists(db, \"games\")) {\n this.#d1 = { file, db };\n return this.#d1;\n }\n }\n this.#d1 = null;\n return null;\n }\n\n /**\n * Every local Durable Object database, newest-modified first.\n *\n * The directory is `<worker>-<ClassName>`, and a worker name may itself\n * contain a dash, so the class is everything after the LAST one. Ordering by\n * mtime puts the game you just touched at the top, which is nearly always the\n * one being debugged.\n */\n durableObjects(): LocalDurableObject[] {\n const base = join(this.root, \"do\");\n if (!existsSync(base)) return [];\n const found: LocalDurableObject[] = [];\n for (const namespace of readdirSync(base)) {\n const dir = join(base, namespace);\n if (!statSync(dir).isDirectory()) continue;\n const className = namespace.slice(namespace.lastIndexOf(\"-\") + 1);\n for (const file of sqliteFilesIn(dir)) {\n const db = this.#handle(file);\n const name = tableExists(db, DO_NAME_TABLE) ? ((db.prepare(`select name from ${DO_NAME_TABLE} limit 1`).get() as { name?: string } | undefined)?.name ?? null) : null;\n found.push({ className, id: file.slice(file.lastIndexOf(\"/\") + 1, -\".sqlite\".length), name, file });\n }\n }\n return found.sort((a, b) => statSync(b.file).mtimeMs - statSync(a.file).mtimeMs);\n }\n\n /** The armed alarm for a Durable Object, from its namespace's metadata\n * database. Matched on either column because the id is what Miniflare keys\n * by and the name is what a human recognises. */\n alarm(target: LocalDurableObject): LocalAlarm | null {\n const file = join(dirname(target.file), METADATA_FILE);\n if (!existsSync(file)) return null;\n const db = this.#handle(file);\n if (!tableExists(db, \"_cf_ALARM\")) return null;\n const row = db.prepare(\"select actor_id, actor_name, scheduled_time from _cf_ALARM where actor_id = ? or actor_name = ?\").get(target.id, target.name ?? target.id) as { actor_id: string; actor_name: string | null; scheduled_time: number | null } | undefined;\n return row === undefined ? null : { actorId: row.actor_id, actorName: row.actor_name, scheduledTime: row.scheduled_time };\n }\n\n /** The D1 index rows, newest-updated first. */\n games(options: { status?: string; limit?: number } = {}): LocalGameRow[] {\n const d1 = this.d1();\n if (d1 === null) return [];\n const where = options.status === undefined ? \"\" : \"where status = ?\";\n const params = options.status === undefined ? [] : [options.status];\n const rows = d1.db.prepare(`select * from games ${where} order by updated_at desc limit ?`).all(...params, options.limit ?? 50) as Record<string, unknown>[];\n return rows.map(toGameRow);\n }\n\n /**\n * Resolve a game reference the way a developer will type it: a full id, a\n * six-character short code, or an id prefix long enough to be unique.\n *\n * D1 is tried first because it can match a short code, then the DO names, so\n * a game whose index row was wiped is still reachable by id.\n */\n resolveGameId(ref: string): string | null {\n const d1 = this.d1();\n if (d1 !== null) {\n const exact = d1.db.prepare(\"select id from games where id = ? or short_code = ?\").get(ref, ref.toUpperCase()) as { id: string } | undefined;\n if (exact !== undefined) return exact.id;\n const prefixed = d1.db.prepare(\"select id from games where id like ? limit 2\").all(`${ref}%`) as { id: string }[];\n if (prefixed.length === 1) return prefixed[0].id;\n if (prefixed.length > 1) throw new Error(`\"${ref}\" matches more than one game; use a longer prefix`);\n }\n const named = this.durableObjects().filter((o) => o.name !== null && (o.name === ref || o.name.startsWith(ref)));\n if (named.length === 1) return named[0].name;\n if (named.length > 1) throw new Error(`\"${ref}\" matches more than one game; use a longer prefix`);\n return null;\n }\n\n /** The Durable Object holding a game's session, or null if none exists yet. */\n gameDurableObject(gameId: string): LocalDurableObject | null {\n return this.durableObjects().find((o) => o.name === gameId) ?? null;\n }\n\n /** Identity for a set of user and bot ids, for rendering seats as names. */\n #identities(userIds: string[], botIds: string[]): Map<string, { username: string; displayName: string }> {\n const out = new Map<string, { username: string; displayName: string }>();\n const d1 = this.d1();\n if (d1 === null) return out;\n for (const [table, ids] of [\n [\"users\", userIds],\n [\"bots\", botIds],\n ] as const) {\n if (ids.length === 0) continue;\n const placeholders = ids.map(() => \"?\").join(\",\");\n const rows = d1.db.prepare(`select id, username, display_name from ${table} where id in (${placeholders})`).all(...ids) as { id: string; username: string; display_name: string }[];\n for (const row of rows) out.set(row.id, { username: row.username, displayName: row.display_name });\n }\n return out;\n }\n\n #seats(rows: { player_index: number; user_id: string | null; bot_id: string | null; type: string }[]): LocalSeat[] {\n const identities = this.#identities(\n rows.map((r) => r.user_id).filter((id): id is string => id !== null),\n rows.map((r) => r.bot_id).filter((id): id is string => id !== null),\n );\n return rows.map((row) => {\n const identity = identities.get(row.user_id ?? row.bot_id ?? \"\");\n return { playerIndex: row.player_index, userId: row.user_id, botId: row.bot_id, type: row.type, username: identity?.username ?? null, displayName: identity?.displayName ?? null };\n });\n }\n\n /**\n * Join both stores for one game: the index row, the session, the transition\n * log, and the derived diagnosis.\n *\n * Returns null only when neither store has heard of the game.\n */\n game(ref: string): LocalGameView | null {\n const gameId = this.resolveGameId(ref);\n if (gameId === null) return null;\n\n const d1 = this.d1();\n const indexRow = d1 === null ? undefined : (d1.db.prepare(\"select * from games where id = ?\").get(gameId) as Record<string, unknown> | undefined);\n const index = indexRow === undefined ? null : toGameRow(indexRow);\n const indexSeats =\n d1 === null\n ? []\n : this.#seats(\n d1.db.prepare(\"select player_index, user_id, bot_id, type from participants where game_id = ? order by player_index\").all(gameId) as {\n player_index: number;\n user_id: string | null;\n bot_id: string | null;\n type: string;\n }[],\n );\n\n const durableObject = this.gameDurableObject(gameId);\n let meta: LocalGameMeta | null = null;\n let roster: LocalSeat[] = [];\n let transitions: LocalTransition[] = [];\n let commands: LocalGameView[\"commands\"] = [];\n let outbox: LocalGameView[\"outbox\"] = [];\n\n if (durableObject !== null) {\n const db = this.#handle(durableObject.file);\n if (tableExists(db, \"meta\")) {\n const row = db.prepare(\"select * from meta where id = 1\").get() as Record<string, unknown> | undefined;\n if (row !== undefined) meta = toMeta(row);\n }\n if (tableExists(db, \"roster\")) {\n roster = this.#seats(db.prepare(\"select player_index, user_id, bot_id, type from roster order by player_index\").all() as { player_index: number; user_id: string | null; bot_id: string | null; type: string }[]);\n }\n if (tableExists(db, \"transitions\")) {\n const frameSeats = new Map<number, number[]>();\n if (tableExists(db, \"frames\")) {\n for (const row of db.prepare(\"select version, player_index from frames order by version, player_index\").all() as { version: number; player_index: number }[]) {\n frameSeats.set(row.version, [...(frameSeats.get(row.version) ?? []), row.player_index]);\n }\n }\n transitions = (db.prepare(\"select * from transitions order by version\").all() as Record<string, unknown>[]).map((row) => toTransition(row, frameSeats));\n }\n if (tableExists(db, \"commands\")) {\n commands = (db.prepare(\"select command_id, response, created_at from commands order by created_at desc\").all() as { command_id: string; response: unknown; created_at: number }[]).map((row) => ({\n commandId: row.command_id,\n createdAt: row.created_at,\n result: json(row.response),\n }));\n }\n if (tableExists(db, \"outbox\")) {\n outbox = (db.prepare(\"select finish_id, outcomes, created_at from outbox order by created_at\").all() as { finish_id: string; outcomes: unknown; created_at: number }[]).map((row) => ({\n finishId: row.finish_id,\n createdAt: row.created_at,\n outcomes: json(row.outcomes),\n }));\n }\n }\n\n const alarm = durableObject === null ? null : this.alarm(durableObject);\n const view: Omit<LocalGameView, \"diagnosis\"> = { gameId, index, indexSeats, durableObject, meta, roster, transitions, commands, outbox, alarm };\n return { ...view, diagnosis: diagnose(view) };\n }\n\n /** Run a read-only statement against D1 or one game's Durable Object: the\n * escape hatch for a question this reader does not model. */\n query(sql: string, options: { game?: string; params?: LocalQueryParam[] } = {}): Record<string, unknown>[] {\n const db = options.game === undefined ? this.d1()?.db : this.#gameDb(options.game);\n if (db === undefined || db === null) {\n throw new Error(options.game === undefined ? \"no local D1 database found in this Worker's state\" : `no local Durable Object found for game ${options.game}`);\n }\n return db.prepare(sql).all(...(options.params ?? [])) as Record<string, unknown>[];\n }\n\n /** Table names and row counts, for D1 or one game's Durable Object. */\n tables(options: { game?: string } = {}): { name: string; rows: number }[] {\n const db = options.game === undefined ? this.d1()?.db : this.#gameDb(options.game);\n if (db === undefined || db === null) return [];\n const names = (db.prepare(\"select name from sqlite_master where type = 'table' order by name\").all() as { name: string }[]).map((r) => r.name);\n return names.map((name) => ({ name, rows: (db.prepare(`select count(*) as n from \"${name}\"`).get() as { n: number }).n }));\n }\n\n #gameDb(ref: string): DatabaseSync | null {\n const gameId = this.resolveGameId(ref);\n const target = gameId === null ? null : this.gameDurableObject(gameId);\n return target === null ? null : this.#handle(target.file);\n }\n\n /** Release every open handle. */\n close(): void {\n for (const db of this.#open.values()) db.close();\n this.#open.clear();\n this.#d1 = undefined;\n }\n}\n\nfunction toGameRow(row: Record<string, unknown>): LocalGameRow {\n return {\n id: row.id as string,\n createdBy: (row.created_by as string | null) ?? null,\n status: row.status as string,\n access: row.access as string,\n schemaVersion: row.schema_version as number,\n config: json(row.config),\n turnSeconds: (row.turn_seconds as number | null) ?? null,\n budgetSeconds: (row.budget_seconds as number | null) ?? null,\n incrementSeconds: (row.increment_seconds as number | null) ?? null,\n rated: bool(row.rated),\n ratingPool: (row.rating_pool as string | null) ?? null,\n minPlayers: row.min_players as number,\n maxPlayers: row.max_players as number,\n shortCode: row.short_code as string,\n pendingPlayers: row.pending_players === null || row.pending_players === undefined ? null : numbers(row.pending_players),\n turnDeadline: (row.turn_deadline as number | null) ?? null,\n outcomes: json(row.outcomes),\n finishId: (row.finish_id as string | null) ?? null,\n finishedAt: (row.finished_at as number | null) ?? null,\n archivedAt: (row.archived_at as number | null) ?? null,\n createdAt: row.created_at as number,\n updatedAt: row.updated_at as number,\n };\n}\n\nfunction toMeta(row: Record<string, unknown>): LocalGameMeta {\n return {\n gameId: row.game_id as string,\n status: row.status as string,\n access: row.access as string,\n schemaVersion: row.schema_version as number,\n config: json(row.config),\n turnSeconds: (row.turn_seconds as number | null) ?? null,\n budgetSeconds: (row.budget_seconds as number | null) ?? null,\n incrementSeconds: (row.increment_seconds as number | null) ?? null,\n rated: bool(row.rated),\n ratingPool: (row.rating_pool as string | null) ?? null,\n minPlayers: row.min_players as number,\n maxPlayers: row.max_players as number,\n createdBy: (row.created_by as string | null) ?? null,\n rngSeed: (row.rng_seed as string | null) ?? null,\n };\n}\n\nfunction toTransition(row: Record<string, unknown>, frameSeats: Map<number, number[]>): LocalTransition {\n const version = row.version as number;\n const action = json(row.action) as LocalTransition[\"action\"];\n return {\n version,\n state: json(row.state),\n action: action ?? null,\n pending: numbers(row.pending),\n deadline: (row.deadline as number | null) ?? null,\n playerTimes: row.player_times === null || row.player_times === undefined ? null : numbers(row.player_times),\n turnStartedAt: (row.turn_started_at as number | null) ?? null,\n frameSeats: frameSeats.get(version) ?? [],\n };\n}\n\n/**\n * Say what the game is waiting for, and whether the two stores agree.\n *\n * The engine's statuses each have exactly one thing that moves them, so this is\n * a total function over `status` rather than a heuristic. The cases developers\n * actually hit are the first two: a lobby that will not fill, and a full lobby\n * whose creator has not pressed start.\n */\nfunction diagnose(view: Omit<LocalGameView, \"diagnosis\">): LocalGameDiagnosis {\n const status = view.meta?.status ?? view.index?.status ?? \"unknown\";\n const seated = (view.meta === null ? view.indexSeats : view.roster).length;\n const min = view.meta?.minPlayers ?? view.index?.minPlayers ?? 0;\n const max = view.meta?.maxPlayers ?? view.index?.maxPlayers ?? 0;\n const latest = view.transitions.at(-1) ?? null;\n\n const mirrorDrift: string[] = [];\n if (view.index !== null && view.meta !== null) {\n if (view.index.status !== view.meta.status) {\n mirrorDrift.push(`D1 says status=${view.index.status}, the Durable Object says ${view.meta.status}. The DO is authoritative; a mirror write was lost.`);\n }\n if (view.indexSeats.length !== view.roster.length) {\n mirrorDrift.push(`D1 mirrors ${view.indexSeats.length} seat(s), the Durable Object holds ${view.roster.length}.`);\n }\n const mirroredPending = view.index.pendingPlayers ?? null;\n const actualPending = latest?.pending ?? null;\n if (status === \"active\" && JSON.stringify(mirroredPending) !== JSON.stringify(actualPending)) {\n mirrorDrift.push(`D1 mirrors pendingPlayers=${JSON.stringify(mirroredPending)}, the newest transition has ${JSON.stringify(actualPending)}.`);\n }\n }\n if (view.index === null && view.meta !== null) {\n mirrorDrift.push(\"no D1 index row for a game whose Durable Object exists; discovery and history lists cannot show it.\");\n }\n\n let summary: string;\n switch (status) {\n case \"waiting\":\n summary = `waiting for players: ${seated}/${min} needed to become ready (${max} max). The next event is a join, or add-bot from the creator.`;\n break;\n case \"ready\":\n summary = `ready and not started: ${seated}/${max} seats filled. Nothing happens until the CREATOR calls POST /api/engine/games/${view.gameId}/start. A start is explicit and creator-only; filling the lobby does not start a game.`;\n break;\n case \"active\":\n if (latest === null) {\n summary = \"status is active but no transition is committed, which should be impossible: a start commits v0 in the same transaction that sets the status.\";\n } else {\n const waitingOn = latest.pending.length === 0 ? \"nobody, which means the game should have finished at this version\" : `seat(s) ${latest.pending.join(\", \")}`;\n const timer = latest.deadline === null ? \"untimed, so no alarm is armed and only a move or a forfeit moves it\" : `deadline ${new Date(latest.deadline).toISOString()}`;\n summary = `at v${latest.version}, waiting on ${waitingOn}. ${timer}.`;\n }\n break;\n case \"finished\":\n summary = view.outbox.length > 0 ? `finished, but ${view.outbox.length} outbox row(s) survive: the D1 finish apply has not succeeded, so ratings and history are not published yet.` : `finished at v${latest?.version ?? \"?\"}; the outbox is clear, so D1 has the outcomes.`;\n break;\n case \"aborted\":\n summary = \"aborted: cancelled by its creator, reaped as abandoned, or torn down by an account purge. Nothing moves an aborted game.\";\n break;\n default:\n summary = view.durableObject === null ? \"created in D1 but its Durable Object does not exist yet. That is normal for a lobby nobody has touched: the DO is created lazily by the first command or socket.\" : `unrecognised status ${status}.`;\n }\n\n return { summary, mirrorDrift };\n}\n"],"mappings":"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","names":[]}
|
package/dist/contract-cli.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -144,6 +144,8 @@ declare function projectView(rules: GameRules, args: {
|
|
|
144
144
|
* // game implements optimism)
|
|
145
145
|
* }
|
|
146
146
|
* },
|
|
147
|
+
* { "kind": "playerLimits", "name": "...", "config": {},
|
|
148
|
+
* "expected": { "minPlayers": 2, "maxPlayers": 2 } },
|
|
147
149
|
* { "kind": "ratingPool", "name": "...", "access": "public",
|
|
148
150
|
* "minPlayers": 2, "maxPlayers": 2, "config": {}, "expected": "blitz" },
|
|
149
151
|
* { "kind": "botSeatable", "name": "...", "gameConfig": {},
|
|
@@ -208,6 +210,16 @@ interface RatingPoolCase {
|
|
|
208
210
|
config: JsonObject;
|
|
209
211
|
expected: string | null;
|
|
210
212
|
}
|
|
213
|
+
/** A `playerLimits` case: the seats one config may be played with. */
|
|
214
|
+
interface PlayerLimitsCase {
|
|
215
|
+
kind: "playerLimits";
|
|
216
|
+
name: string;
|
|
217
|
+
config: JsonObject;
|
|
218
|
+
expected: {
|
|
219
|
+
minPlayers: number;
|
|
220
|
+
maxPlayers: number;
|
|
221
|
+
};
|
|
222
|
+
}
|
|
211
223
|
/** A `botSeatable` predicate case. */
|
|
212
224
|
interface BotSeatableCase {
|
|
213
225
|
kind: "botSeatable";
|
|
@@ -216,7 +228,7 @@ interface BotSeatableCase {
|
|
|
216
228
|
botConfig: JsonObject;
|
|
217
229
|
expected: boolean;
|
|
218
230
|
}
|
|
219
|
-
type TwinFixtureCase = ActionCase | RatingPoolCase | BotSeatableCase;
|
|
231
|
+
type TwinFixtureCase = ActionCase | PlayerLimitsCase | RatingPoolCase | BotSeatableCase;
|
|
220
232
|
/** Validate one fixture file's parsed JSON, or throw naming the offending
|
|
221
233
|
* file, case, and field. Exported so a repo can lint its fixtures without
|
|
222
234
|
* running them. */
|
|
@@ -234,4 +246,4 @@ declare function twinFixtureTests(gameModule: GameModule, fixturesRoot: string |
|
|
|
234
246
|
* matters. */
|
|
235
247
|
declare function deepEquals(a: Json | undefined, b: Json | undefined): boolean;
|
|
236
248
|
|
|
237
|
-
export { type ActionCase, type BotSeatableCase, type BuildGameContractOptions, type EmitGameContractOptions, GAME_CONTRACT_FORMAT_VERSION, type GameContract, type GameContractFixture, type GameContractVersion, type RatingPoolCase, type TwinFixtureCase, type TwinFixtureFile, buildGameContract, checkConfiguredGameContract, checkGameContract, deepEquals, emitConfiguredGameContract, emitGameContract, evaluateTwinCase, gameContractFilename, gameContractJson, parseTwinFixtureFile, projectView, twinFixtureTests };
|
|
249
|
+
export { type ActionCase, type BotSeatableCase, type BuildGameContractOptions, type EmitGameContractOptions, GAME_CONTRACT_FORMAT_VERSION, type GameContract, type GameContractFixture, type GameContractVersion, type PlayerLimitsCase, type RatingPoolCase, type TwinFixtureCase, type TwinFixtureFile, buildGameContract, checkConfiguredGameContract, checkGameContract, deepEquals, emitConfiguredGameContract, emitGameContract, evaluateTwinCase, gameContractFilename, gameContractJson, parseTwinFixtureFile, projectView, twinFixtureTests };
|
package/dist/index.js
CHANGED
package/dist/inspect-cli.js
CHANGED
package/dist/local-state.d.ts
CHANGED
|
@@ -127,6 +127,8 @@ interface LocalTransition {
|
|
|
127
127
|
pending: number[];
|
|
128
128
|
deadline: number | null;
|
|
129
129
|
playerTimes: number[] | null;
|
|
130
|
+
/** Non-null only while this transition's pending turn consumes a budget
|
|
131
|
+
* bank. Per-action and hook-override deadlines do not set it. */
|
|
130
132
|
turnStartedAt: number | null;
|
|
131
133
|
/** Which seats hold a stored frame at this version. Empty after the finish
|
|
132
134
|
* compaction, which empties `frames` and leaves replay to re-projection. */
|
package/dist/local-state.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eigeninteractive/testkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Test helpers for games built on the EigenInteractive engine: drive rules through the real kernel and run shared twin fixtures, with no Worker, database or network.",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -36,25 +36,25 @@
|
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"rand-seed": "^3.0.0",
|
|
38
38
|
"tsx": "^4.23.1",
|
|
39
|
-
"@eigeninteractive/kernel": "0.
|
|
39
|
+
"@eigeninteractive/kernel": "0.5.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/node": "^26.1.1",
|
|
43
43
|
"vitest": "^4.1.10",
|
|
44
|
-
"@eigeninteractive/rules": "0.
|
|
44
|
+
"@eigeninteractive/rules": "0.5.0"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"vitest": "^4.1.0",
|
|
48
|
-
"@eigeninteractive/rules": "^0.
|
|
48
|
+
"@eigeninteractive/rules": "^0.5.0"
|
|
49
49
|
},
|
|
50
50
|
"homepage": "https://eigeninteractive.com",
|
|
51
51
|
"repository": {
|
|
52
52
|
"type": "git",
|
|
53
|
-
"url": "git+https://github.com/eigeninteractive/eigen-
|
|
54
|
-
"directory": "packages/testkit"
|
|
53
|
+
"url": "git+https://github.com/eigeninteractive/eigen-platform.git",
|
|
54
|
+
"directory": "server/packages/testkit"
|
|
55
55
|
},
|
|
56
56
|
"bugs": {
|
|
57
|
-
"url": "https://github.com/eigeninteractive/eigen-
|
|
57
|
+
"url": "https://github.com/eigeninteractive/eigen-platform/issues"
|
|
58
58
|
},
|
|
59
59
|
"publishConfig": {
|
|
60
60
|
"access": "public"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/twin-fixtures.ts","../src/game-contract.ts","../src/contract-command.ts"],"sourcesContent":["/**\n * Twin-drift fixture runner: the TS half of the shared JSON fixtures that\n * keep a version unit's TS and Dart `GameRules` twins in sync. The JSON\n * fixture format is shared verbatim with the Dart runner\n * (`lib/testing/twin_fixtures.dart`): one file, two consumers.\n *\n * One fixture file per concern lives beside the version units at\n * `<fixturesRoot>/v<N>/*.json` and is consumed by BOTH sides: this module\n * runs each case against the TS unit (schemas + `applyAction` +\n * `computeObservation` + the two predicates), while the Dart runner runs the\n * same file against the Dart twin (generated payload parsing,\n * `isValidAction`, `previewAction`, predicate twins). A behavioral divergence\n * then fails one side's CI instead of degrading UX in production.\n *\n * ## Fixture file format\n *\n * ```jsonc\n * {\n * \"schemaVersion\": 1,\n * \"cases\": [\n * {\n * \"kind\": \"action\",\n * \"name\": \"seat 0 marks an empty cell\",\n * \"config\": {},\n * \"state\": { ... }, // TS: applyAction input\n * \"obs\": { ... }, // Dart-only; defaults to `state`\n * \"pending\": [0],\n * \"playerIndex\": 0,\n * \"participantCount\": 2, // optional, default 2\n * \"rngSeed\": \"any string\", // optional, default \"twin-fixtures\"\n * \"action\": { ... },\n * \"expected\": {\n * \"valid\": true, // false ⇒ TS IllegalMoveError,\n * // Dart isValidAction false\n * \"state\": { ... }, // optional, TS envelope.state\n * \"pending\": [1], // optional, TS envelope.pendingPlayers\n * \"outcome\": [ ... ], // optional, TS envelope.outcome\n * // (null asserts the game is ongoing)\n * \"observation\": { ... } // optional, the actor's post-action\n * // view: TS computeObservation slice\n * // data; Dart previewAction (when the\n * // game implements optimism)\n * }\n * },\n * { \"kind\": \"ratingPool\", \"name\": \"...\", \"access\": \"public\",\n * \"minPlayers\": 2, \"maxPlayers\": 2, \"config\": {}, \"expected\": \"blitz\" },\n * { \"kind\": \"botSeatable\", \"name\": \"...\", \"gameConfig\": {},\n * \"botConfig\": {}, \"expected\": false }\n * ]\n * }\n * ```\n *\n * The `state`/`obs` split exists for hidden-info games (a seat's observation\n * is not the state); perfect-info games omit `obs`. `expected.observation` is\n * the shared behavioral anchor: the TS side must project the post-action\n * state to it, and a Dart `previewAction` that returns non-null must predict\n * it, so the two sides are compared through one recorded value.\n *\n * Wire it up in a game-owned test file running under plain-Node vitest:\n *\n * ```ts\n * import { twinFixtureTests } from \"@eigeninteractive/testkit\";\n * import gameModule from \"../src/module/index.js\";\n *\n * twinFixtureTests(gameModule, new URL(\"../src/module/fixtures/\", import.meta.url));\n * ```\n */\n\nimport { readdirSync, readFileSync } from \"node:fs\";\nimport { type Envelope, type GameAccess, type GameModule, type GameRules, IllegalMoveError, type Json, type JsonObject, type ObservationSlice, type OutcomeEntry } from \"@eigeninteractive/rules\";\nimport Rand from \"rand-seed\";\nimport { it } from \"vitest\";\n\n/** One fixture file: cases targeting one `schemaVersion` unit. */\nexport interface TwinFixtureFile {\n schemaVersion: number;\n cases: TwinFixtureCase[];\n}\n\n/** A game-action case: exercises schemas, `applyAction`, and (through\n * `expected.observation`) `computeObservation` for the acting seat. */\nexport interface ActionCase {\n kind: \"action\";\n name: string;\n config: JsonObject;\n state: JsonObject;\n /** Dart-side observation payload; unused here (defaults to `state`). */\n obs?: JsonObject;\n pending: number[];\n playerIndex: number;\n participantCount?: number;\n rngSeed?: string;\n action: JsonObject;\n expected: {\n valid: boolean;\n state?: JsonObject;\n pending?: number[];\n outcome?: OutcomeEntry[] | null;\n observation?: JsonObject;\n };\n}\n\n/** A `ratingPool` predicate case. Omitted timing fields mean null. */\nexport interface RatingPoolCase {\n kind: \"ratingPool\";\n name: string;\n access: GameAccess;\n turnSeconds?: number | null;\n budgetSeconds?: number | null;\n incrementSeconds?: number | null;\n minPlayers: number;\n maxPlayers: number;\n config: JsonObject;\n expected: string | null;\n}\n\n/** A `botSeatable` predicate case. */\nexport interface BotSeatableCase {\n kind: \"botSeatable\";\n name: string;\n gameConfig: JsonObject;\n botConfig: JsonObject;\n expected: boolean;\n}\n\nexport type TwinFixtureCase = ActionCase | RatingPoolCase | BotSeatableCase;\n\n// ── Fixture validation ────────────────────────────────────────────────────────\n//\n// The case types above are compile-time only; the JSON they describe arrives\n// at runtime from a hand-written file. Asserting `JSON.parse(...) as\n// TwinFixtureFile` would make every field a lie the moment a fixture is\n// mistyped, and because most fields flow straight into a comparison, the\n// symptom would be a confusing `undefined` diff attributed to the game's\n// rules rather than to the fixture. These parsers close that gap: a\n// malformed fixture fails at LOAD, naming the file, the case, and the field.\n//\n// Hand-written rather than schema-library-backed on purpose: `@eigeninteractive/rules`\n// describes an implementor's schemas with `StandardSchemaV1` precisely so the\n// engine never mandates a validation library, and the Dart twin runner is\n// deliberately framework-free. This is ~80 lines and keeps both true.\n\nfunction describe(value: unknown): string {\n if (value === null) return \"null\";\n if (Array.isArray(value)) return \"an array\";\n return typeof value;\n}\n\nfunction fail(where: string, expected: string, got: unknown): never {\n throw new Error(`${where}: expected ${expected}, got ${describe(got)}`);\n}\n\nfunction asObject(where: string, v: unknown): JsonObject {\n if (typeof v !== \"object\" || v === null || Array.isArray(v)) fail(where, \"an object\", v);\n return v as JsonObject;\n}\n\nfunction asString(where: string, v: unknown): string {\n if (typeof v !== \"string\") fail(where, \"a string\", v);\n return v;\n}\n\nfunction asNumber(where: string, v: unknown): number {\n if (typeof v !== \"number\" || !Number.isFinite(v)) fail(where, \"a finite number\", v);\n return v;\n}\n\nfunction asBoolean(where: string, v: unknown): boolean {\n if (typeof v !== \"boolean\") fail(where, \"a boolean\", v);\n return v;\n}\n\nfunction asNumberArray(where: string, v: unknown): number[] {\n if (!Array.isArray(v)) fail(where, \"an array of numbers\", v);\n return v.map((n, i) => asNumber(`${where}[${i}]`, n));\n}\n\n/** Applies `read` only when the key is present and non-null; absent and\n * explicit null both mean \"not specified\" for every optional fixture field. */\nfunction optional<T>(where: string, v: unknown, read: (where: string, v: unknown) => T): T | undefined {\n return v === undefined || v === null ? undefined : read(where, v);\n}\n\nfunction parseActionCase(where: string, raw: JsonObject): ActionCase {\n const expectedRaw = asObject(`${where}.expected`, raw.expected);\n const expected: ActionCase[\"expected\"] = {\n valid: asBoolean(`${where}.expected.valid`, expectedRaw.valid),\n state: optional(`${where}.expected.state`, expectedRaw.state, asObject),\n pending: optional(`${where}.expected.pending`, expectedRaw.pending, asNumberArray),\n observation: optional(`${where}.expected.observation`, expectedRaw.observation, asObject),\n };\n // `outcome` is three-valued: absent (unchecked), null (asserts the game is\n // ongoing), or a list. `checkEnvelope` distinguishes absent from null with\n // an `in` test, so the key must only be set when the fixture set it.\n if (\"outcome\" in expectedRaw) {\n const outcome = expectedRaw.outcome;\n if (outcome !== null && !Array.isArray(outcome)) fail(`${where}.expected.outcome`, \"an array or null\", outcome);\n expected.outcome = outcome as OutcomeEntry[] | null;\n }\n return {\n kind: \"action\",\n name: asString(`${where}.name`, raw.name),\n config: asObject(`${where}.config`, raw.config),\n state: asObject(`${where}.state`, raw.state),\n obs: optional(`${where}.obs`, raw.obs, asObject),\n pending: asNumberArray(`${where}.pending`, raw.pending),\n playerIndex: asNumber(`${where}.playerIndex`, raw.playerIndex),\n participantCount: optional(`${where}.participantCount`, raw.participantCount, asNumber),\n rngSeed: optional(`${where}.rngSeed`, raw.rngSeed, asString),\n action: asObject(`${where}.action`, raw.action),\n expected,\n };\n}\n\nfunction parseRatingPoolCase(where: string, raw: JsonObject): RatingPoolCase {\n const access = asString(`${where}.access`, raw.access);\n if (access !== \"public\" && access !== \"private\" && access !== \"friends\") {\n throw new Error(`${where}.access: expected one of public | private | friends, got ${JSON.stringify(access)}`);\n }\n return {\n kind: \"ratingPool\",\n name: asString(`${where}.name`, raw.name),\n access,\n turnSeconds: optional(`${where}.turnSeconds`, raw.turnSeconds, asNumber) ?? null,\n budgetSeconds: optional(`${where}.budgetSeconds`, raw.budgetSeconds, asNumber) ?? null,\n incrementSeconds: optional(`${where}.incrementSeconds`, raw.incrementSeconds, asNumber) ?? null,\n minPlayers: asNumber(`${where}.minPlayers`, raw.minPlayers),\n maxPlayers: asNumber(`${where}.maxPlayers`, raw.maxPlayers),\n config: asObject(`${where}.config`, raw.config),\n expected: raw.expected === null ? null : asString(`${where}.expected`, raw.expected),\n };\n}\n\nfunction parseBotSeatableCase(where: string, raw: JsonObject): BotSeatableCase {\n return {\n kind: \"botSeatable\",\n name: asString(`${where}.name`, raw.name),\n gameConfig: asObject(`${where}.gameConfig`, raw.gameConfig),\n botConfig: asObject(`${where}.botConfig`, raw.botConfig),\n expected: asBoolean(`${where}.expected`, raw.expected),\n };\n}\n\n/** Validate one fixture file's parsed JSON, or throw naming the offending\n * file, case, and field. Exported so a repo can lint its fixtures without\n * running them. */\nexport function parseTwinFixtureFile(path: string, json: unknown): TwinFixtureFile {\n const root = asObject(path, json);\n const schemaVersion = asNumber(`${path}.schemaVersion`, root.schemaVersion);\n if (!Array.isArray(root.cases)) fail(`${path}.cases`, \"an array\", root.cases);\n const cases = root.cases.map((raw, i) => {\n // Prefer the case's own name in the location once we can read it: a\n // fixture author finds \"cases[3] (seat 0 wins)\" faster than an index.\n const indexed = `${path}.cases[${i}]`;\n const obj = asObject(indexed, raw);\n const where = typeof obj.name === \"string\" ? `${indexed} (${obj.name})` : indexed;\n switch (obj.kind) {\n case \"action\":\n return parseActionCase(where, obj);\n case \"ratingPool\":\n return parseRatingPoolCase(where, obj);\n case \"botSeatable\":\n return parseBotSeatableCase(where, obj);\n default:\n throw new Error(`${where}.kind: expected one of action | ratingPool | botSeatable, got ${JSON.stringify(obj.kind)}`);\n }\n });\n return { schemaVersion, cases };\n}\n\n/** Run one fixture case against a rules unit, returning failure descriptions\n * (empty ⇒ the case passes). Pure; the file-reading test registrar is\n * {@link twinFixtureTests}. */\nexport function evaluateTwinCase(rules: GameRules, kase: TwinFixtureCase): string[] {\n switch (kase.kind) {\n case \"action\":\n return evaluateAction(rules, kase);\n case \"ratingPool\":\n return evaluateRatingPool(rules, kase);\n case \"botSeatable\":\n return evaluateBotSeatable(rules, kase);\n default:\n return [`unknown case kind \"${(kase as { kind: string }).kind}\", expected action | ratingPool | botSeatable`];\n }\n}\n\n/** Register one vitest test per fixture case found under `fixturesRoot`\n * (layout: `<root>/v<N>/*.json`). Call at the top level of a test module\n * running in a Node environment. */\nexport function twinFixtureTests(gameModule: GameModule, fixturesRoot: string | URL): void {\n for (const filePath of fixtureFiles(fixturesRoot)) {\n const fixture = parseTwinFixtureFile(filePath, JSON.parse(readFileSync(filePath, \"utf8\")));\n const rules = gameModule.versions[fixture.schemaVersion];\n for (const kase of fixture.cases) {\n it(`twin v${fixture.schemaVersion}: ${kase.name}`, () => {\n if (!rules) {\n throw new Error(`gameModule ships no rules unit for schemaVersion ${fixture.schemaVersion} (fixture: ${filePath})`);\n }\n const failures = evaluateTwinCase(rules, kase);\n if (failures.length) throw new Error(`\\n${failures.join(\"\\n\")}`);\n });\n }\n }\n}\n\nfunction* fixtureFiles(root: string | URL): Generator<string> {\n const dirs = readdirSync(root, { withFileTypes: true })\n .filter((e) => e.isDirectory())\n .sort((a, b) => a.name.localeCompare(b.name));\n for (const dir of dirs) {\n const files = readdirSync(`${dir.parentPath}/${dir.name}`, {\n withFileTypes: true,\n })\n .filter((e) => e.isFile() && e.name.endsWith(\".json\"))\n .sort((a, b) => a.name.localeCompare(b.name));\n for (const file of files) yield `${file.parentPath}/${file.name}`;\n }\n}\n\n// ── Case evaluators ───────────────────────────────────────────────────────────\n\nfunction evaluateAction(rules: GameRules, kase: ActionCase): string[] {\n const failures: string[] = [];\n const config = parseWith(rules, \"config\", kase.config, failures);\n const state = parseWith(rules, \"state\", kase.state, failures);\n const action = parseWith(rules, \"action\", kase.action, failures);\n if (config === undefined || state === undefined || action === undefined) {\n return failures;\n }\n\n // The parsed action must be the fixture action: a schema that strips or\n // defaults fields the twin relies on is itself drift.\n if (!deepEquals(action, kase.action)) {\n failures.push(`action schema does not preserve the fixture action, parsed to ${JSON.stringify(action)}`);\n }\n\n const envelope = applyFixtureAction(rules, kase, config, state, action);\n if (typeof envelope === \"string\") {\n if (envelope) failures.push(envelope);\n return failures;\n }\n checkEnvelope(rules, kase, envelope, failures);\n if (kase.expected.observation !== undefined) {\n checkObservation(rules, kase, envelope, config, action, failures);\n }\n return failures;\n}\n\n/** Invoke `applyAction` and reconcile with `expected.valid`. Returns the\n * envelope on a valid accepted move, an error string on failure, or \"\" when\n * an expected-illegal move was correctly rejected (nothing left to check). */\nfunction applyFixtureAction(rules: GameRules, kase: ActionCase, config: JsonObject, state: JsonObject, action: JsonObject): Envelope | string {\n let envelope: Envelope;\n try {\n envelope = rules.applyAction({\n state,\n pending: kase.pending,\n data: action,\n playerIndex: kase.playerIndex,\n rng: new Rand(kase.rngSeed ?? \"twin-fixtures\"),\n config,\n });\n } catch (error) {\n if (!(error instanceof IllegalMoveError)) {\n return `applyAction threw a non-IllegalMoveError: ${error}`;\n }\n return kase.expected.valid ? `applyAction rejected a move the fixture expects to be valid: ${error.message}` : \"\";\n }\n return kase.expected.valid ? envelope : \"applyAction accepted a move the fixture expects to be illegal\";\n}\n\nfunction checkEnvelope(rules: GameRules, kase: ActionCase, envelope: Envelope, failures: string[]): void {\n if (validate(rules, \"state\", envelope.state) === undefined) {\n failures.push(\"applyAction returned state that violates its own schema\");\n }\n const expected = kase.expected;\n if (expected.state !== undefined && !deepEquals(envelope.state, expected.state)) {\n failures.push(`envelope.state mismatch, got ${JSON.stringify(envelope.state)}`);\n }\n if (expected.pending !== undefined && !deepEquals(envelope.pendingPlayers, expected.pending)) {\n failures.push(`envelope.pendingPlayers mismatch, got ${JSON.stringify(envelope.pendingPlayers)}`);\n }\n if (\"outcome\" in expected && !deepEquals(envelope.outcome ?? null, expected.outcome ?? null)) {\n failures.push(`envelope.outcome mismatch, got ${JSON.stringify(envelope.outcome ?? null)}`);\n }\n}\n\nfunction checkObservation(rules: GameRules, kase: ActionCase, envelope: Envelope, config: JsonObject, action: JsonObject, failures: string[]): void {\n let slice: ObservationSlice;\n try {\n slice = rules.computeObservation({\n state: envelope.state,\n pending: envelope.pendingPlayers,\n playerIndex: kase.playerIndex,\n participantCount: kase.participantCount ?? 2,\n config,\n cause: { kind: \"game\", data: action, playerIndex: kase.playerIndex },\n isReplay: false,\n });\n } catch (error) {\n failures.push(`computeObservation threw: ${error}`);\n return;\n }\n if (!deepEquals(slice.data, kase.expected.observation as Json)) {\n failures.push(`actor's observation mismatch, got ${JSON.stringify(slice.data)}`);\n }\n}\n\nfunction evaluateRatingPool(rules: GameRules, kase: RatingPoolCase): string[] {\n const failures: string[] = [];\n const config = parseWith(rules, \"config\", kase.config, failures);\n if (config === undefined) return failures;\n const pool = rules.ratingPool({\n access: kase.access,\n turnSeconds: kase.turnSeconds ?? null,\n budgetSeconds: kase.budgetSeconds ?? null,\n incrementSeconds: kase.incrementSeconds ?? null,\n minPlayers: kase.minPlayers,\n maxPlayers: kase.maxPlayers,\n config,\n });\n if (pool !== kase.expected) {\n failures.push(`ratingPool returned ${JSON.stringify(pool)}, fixture expects ${JSON.stringify(kase.expected)}`);\n }\n return failures;\n}\n\nfunction evaluateBotSeatable(rules: GameRules, kase: BotSeatableCase): string[] {\n const failures: string[] = [];\n const gameConfig = parseWith(rules, \"config\", kase.gameConfig, failures);\n if (gameConfig === undefined) return failures;\n const seatable = rules.botSeatable({\n gameConfig,\n botConfig: kase.botConfig,\n });\n if (seatable !== kase.expected) {\n failures.push(`botSeatable returned ${seatable}, fixture expects ${kase.expected}`);\n }\n return failures;\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\n/** Run one of the unit's Standard Schemas synchronously, returning the parsed\n * value or undefined on failure (issues written to the second return slot). */\nfunction validate(rules: GameRules, which: \"config\" | \"state\" | \"action\", value: unknown, issues?: string[]): JsonObject | undefined {\n const result = rules.schemas[which][\"~standard\"].validate(value);\n if (result instanceof Promise) {\n issues?.push(`${which} schema validated asynchronously; must be sync`);\n return undefined;\n }\n if (result.issues) {\n issues?.push(\n result.issues\n .map((i) => {\n const path = i.path?.map((p) => (typeof p === \"object\" ? String(p.key) : String(p))).join(\".\");\n return path ? `${path}: ${i.message}` : i.message;\n })\n .join(\"; \"),\n );\n return undefined;\n }\n return result.value as JsonObject;\n}\n\n/** Parse a fixture payload through one of the unit's schemas, recording a\n * failure (and returning undefined) when it does not conform. */\nfunction parseWith(rules: GameRules, which: \"config\" | \"state\" | \"action\", value: JsonObject, failures: string[]): JsonObject | undefined {\n const issues: string[] = [];\n const parsed = validate(rules, which, value, issues);\n if (parsed === undefined) {\n failures.push(`fixture ${which} fails the TS ${which} schema: ${issues.join(\"; \")}`);\n }\n return parsed;\n}\n\n/** Structural JSON equality. Object keys with `undefined` values count as\n * absent (matching how schema libraries model optional fields); array order\n * matters. */\nexport function deepEquals(a: Json | undefined, b: Json | undefined): boolean {\n if (a === undefined || a === null) return b === undefined || b === null;\n if (b === undefined || b === null) return false;\n if (Array.isArray(a) || Array.isArray(b)) {\n if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {\n return false;\n }\n return a.every((item, i) => deepEquals(item, b[i]));\n }\n if (typeof a === \"object\" || typeof b === \"object\") {\n if (typeof a !== \"object\" || typeof b !== \"object\") return false;\n const keys = new Set([...Object.keys(a), ...Object.keys(b)]);\n return [...keys].every((k) => deepEquals((a as JsonObject)[k], (b as JsonObject)[k]));\n }\n return a === b;\n}\n","/**\n * Emit the language-neutral contract a game's Flutter repository consumes.\n *\n * The authoritative inputs live with the TypeScript rules: the four payload\n * schemas and the shared behavioral fixtures. The output is one deterministic\n * JSON file so it can be copied, attached to a release, or fetched by checksum\n * without assuming any repository layout.\n */\n\nimport { existsSync, readdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { basename, relative, resolve } from \"node:path\";\nimport type { GameModule, StandardJSONSchemaV1 } from \"@eigeninteractive/rules\";\nimport { parseTwinFixtureFile } from \"./twin-fixtures.js\";\n\n/** Current format of the language-neutral contract consumed by EigenInteractive's Dart generator. */\nexport const GAME_CONTRACT_FORMAT_VERSION = 1;\n\n/** One validated twin-fixture document embedded in a {@link GameContract}. */\nexport interface GameContractFixture {\n /** POSIX-style path relative to the supplied fixtures root. */\n path: string;\n /** Validated fixture document, retained in its original JSON shape. */\n document: unknown;\n}\n\n/** The four JSON Schemas emitted for one game `schemaVersion`. */\nexport interface GameContractVersion {\n schemas: {\n state: Record<string, unknown>;\n observation: Record<string, unknown>;\n action: Record<string, unknown>;\n config: Record<string, unknown>;\n };\n}\n\n/** Language-neutral schemas and fixtures shared by a game's Worker and app. */\nexport interface GameContract {\n formatVersion: typeof GAME_CONTRACT_FORMAT_VERSION;\n game: string;\n versions: Record<string, GameContractVersion>;\n fixtures: GameContractFixture[];\n}\n\n/** Inputs for building a {@link GameContract} without writing it. */\nexport interface BuildGameContractOptions {\n /** Stable display name used as the generated Dart type prefix. */\n game: string;\n /** Authoritative TypeScript rules registry. */\n gameModule: GameModule;\n /** Root containing `v<N>/*.json` twin fixtures. */\n fixturesRoot?: string | URL;\n}\n\n/** Inputs for emitting or checking a {@link GameContract} file. */\nexport interface EmitGameContractOptions extends BuildGameContractOptions {\n /** Destination `game-contract.json` path. */\n output: string | URL;\n}\n\nfunction canonical(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(canonical);\n if (value !== null && typeof value === \"object\") {\n return Object.fromEntries(\n Object.entries(value as Record<string, unknown>)\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, item]) => [key, canonical(item)]),\n );\n }\n return value;\n}\n\nfunction jsonSchema(schema: StandardJSONSchemaV1, label: string, direction: \"input\" | \"output\"): Record<string, unknown> {\n const options = { target: \"draft-2020-12\" as const };\n try {\n return canonical(schema[\"~standard\"].jsonSchema[direction](options)) as Record<string, unknown>;\n } catch (error) {\n throw new Error(`${label} cannot emit its ${direction} type as draft-2020-12 JSON Schema`, { cause: error });\n }\n}\n\nfunction fixtureFiles(root: string): string[] {\n const found: string[] = [];\n const visit = (directory: string): void => {\n for (const entry of readdirSync(directory, { withFileTypes: true })) {\n const path = resolve(directory, entry.name);\n if (entry.isDirectory()) visit(path);\n else if (entry.isFile() && entry.name.endsWith(\".json\")) found.push(path);\n }\n };\n visit(root);\n return found.sort();\n}\n\nfunction fixtures(fixturesRoot: string | URL | undefined, versions: ReadonlySet<string>): GameContractFixture[] {\n if (fixturesRoot === undefined) return [];\n const root = fixturesRoot instanceof URL ? fixturesRoot.pathname : resolve(fixturesRoot);\n return fixtureFiles(root).map((path) => {\n const document = JSON.parse(readFileSync(path, \"utf8\")) as unknown;\n const fixture = parseTwinFixtureFile(path, document);\n const fixturePath = relative(root, path).split(\"\\\\\").join(\"/\");\n const directory = fixturePath.split(\"/\")[0];\n const match = /^v([1-9]\\d*)$/.exec(directory ?? \"\");\n if (match === null) {\n throw new Error(`${fixturePath}: fixtures must live under a v<N>/ directory`);\n }\n const pathVersion = Number(match[1]);\n if (pathVersion !== fixture.schemaVersion) {\n throw new Error(`${fixturePath}: directory v${pathVersion} disagrees with schemaVersion ${fixture.schemaVersion}`);\n }\n if (!versions.has(String(fixture.schemaVersion))) {\n throw new Error(`${fixturePath}: gameModule ships no rules unit for schemaVersion ${fixture.schemaVersion}`);\n }\n return {\n path: fixturePath,\n document: canonical(document),\n };\n });\n}\n\n/** Build a deterministic in-memory contract without touching the filesystem. */\nexport function buildGameContract(options: BuildGameContractOptions): GameContract {\n const game = options.game.trim();\n if (game.length === 0) throw new Error(\"game contract name must not be empty\");\n\n const versions = Object.fromEntries(\n Object.entries(options.gameModule.versions)\n .sort(([a], [b]) => Number(a) - Number(b))\n .map(([version, rules]) => [\n version,\n {\n schemas: {\n state: jsonSchema(rules.schemas.state, `v${version} state`, \"output\"),\n observation: jsonSchema(rules.schemas.observation, `v${version} observation`, \"output\"),\n action: jsonSchema(rules.schemas.action, `v${version} action`, \"input\"),\n config: jsonSchema(rules.schemas.config, `v${version} config`, \"output\"),\n },\n },\n ]),\n );\n\n if (Object.keys(versions).length === 0) {\n throw new Error(\"game contract must contain at least one schema version\");\n }\n\n return {\n formatVersion: GAME_CONTRACT_FORMAT_VERSION,\n game,\n versions,\n fixtures: fixtures(options.fixturesRoot, new Set(Object.keys(versions))),\n };\n}\n\n/** Render one deterministic, newline-terminated contract document. */\nexport function gameContractJson(options: BuildGameContractOptions): string {\n return `${JSON.stringify(buildGameContract(options), null, 2)}\\n`;\n}\n\n/** Emit one deterministic, newline-terminated `game-contract.json`. */\nexport function emitGameContract(options: EmitGameContractOptions): void {\n const output = options.output instanceof URL ? options.output : resolve(options.output);\n writeFileSync(output, gameContractJson(options));\n}\n\n/** Fail when an emitted contract is missing or differs from its inputs. */\nexport function checkGameContract(options: EmitGameContractOptions): void {\n const output = options.output instanceof URL ? options.output : resolve(options.output);\n if (!existsSync(output) || readFileSync(output, \"utf8\") !== gameContractJson(options)) {\n throw new Error(`${output.toString()} is stale; run eigen-contract`);\n }\n}\n\n/** A useful default filename for scripts that accept an output directory. */\nexport const gameContractFilename = (game: string): string =>\n `${basename(game)\n .toLowerCase()\n .replaceAll(/[^a-z0-9]+/g, \"-\")}-game-contract.json`;\n","import { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport type { GameModule } from \"@eigeninteractive/rules\";\nimport { tsImport } from \"tsx/esm/api\";\nimport { checkGameContract, emitGameContract } from \"./game-contract.js\";\n\ninterface EigenPackageConfig {\n game?: unknown;\n module?: unknown;\n fixtures?: unknown;\n contract?: unknown;\n}\n\ninterface PackageManifest {\n eigen?: EigenPackageConfig;\n}\n\nfunction configuredString(value: unknown, fallback: string, field: string): string {\n if (value === undefined) return fallback;\n if (typeof value !== \"string\" || value.trim().length === 0) {\n throw new Error(`package.json eigen.${field} must be a non-empty string`);\n }\n return value;\n}\n\nconst isGameModule = (value: unknown): value is GameModule => value !== null && typeof value === \"object\" && \"versions\" in value && value.versions !== null && typeof value.versions === \"object\";\n\n/**\n * Emit a contract from the conventional game package layout.\n *\n * The package default-exports its GameModule from `src/module/index.ts` and\n * declares `{ \"eigen\": { \"game\": \"…\" } }` in package.json. Paths can be\n * overridden under the same `eigen` object.\n */\nasync function configuredGameContractOptions(root: string) {\n const manifest = JSON.parse(readFileSync(resolve(root, \"package.json\"), \"utf8\")) as PackageManifest;\n const config = manifest.eigen ?? {};\n const game = configuredString(config.game, \"\", \"game\");\n if (game.length === 0) {\n throw new Error('package.json must declare an EigenInteractive game name, e.g. \"eigen\": { \"game\": \"Chess\" }');\n }\n\n const modulePath = configuredString(config.module, \"src/module/index.ts\", \"module\");\n const fixturesPath = configuredString(config.fixtures, \"src/module/fixtures\", \"fixtures\");\n const contractPath = configuredString(config.contract, \"game-contract.json\", \"contract\");\n const imported = (await tsImport(pathToFileURL(resolve(root, modulePath)).href, import.meta.url)) as { default?: unknown };\n\n if (!isGameModule(imported.default)) {\n throw new Error(`${modulePath} must default-export an EigenInteractive GameModule`);\n }\n\n return {\n game,\n gameModule: imported.default,\n fixturesRoot: resolve(root, fixturesPath),\n output: resolve(root, contractPath),\n };\n}\n\n/**\n * Emits `game-contract.json` from an EigenInteractive package's conventional layout.\n *\n * This is the programmatic form of the `eigen-contract` executable. Most\n * games should invoke the executable through their package script.\n */\nexport async function emitConfiguredGameContract(root = process.cwd()): Promise<void> {\n emitGameContract(await configuredGameContractOptions(root));\n}\n\n/**\n * Fails when the conventionally configured contract is absent or stale.\n *\n * Use this in CI through `eigen-contract --check`.\n */\nexport async function checkConfiguredGameContract(root = process.cwd()): Promise<void> {\n checkGameContract(await configuredGameContractOptions(root));\n}\n"],"mappings":"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","names":["readdirSync","readFileSync","fixtureFiles","readdirSync","readFileSync","readFileSync","resolve","readFileSync","resolve"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/local-state.ts"],"sourcesContent":["/**\n * Read a Worker's LOCAL `.wrangler` state directly: the D1 index and the\n * per-game Durable Object databases, opened as plain SQLite files.\n *\n * This is a development and debugging surface, never a runtime one. Nothing here\n * runs on Workers: it is Node reading files that `wrangler dev` left on disk,\n * which is why it lives in the testkit (a `devDependency`) behind its own\n * subpath, and why it reads them through Node's built-in `node:sqlite` rather\n * than a driver package. `better-sqlite3` would work and was tried first, but\n * `drizzle-orm` declares it as an OPTIONAL PEER: installing it anywhere in the\n * workspace re-resolves drizzle's peer set for every importer, which changed the\n * types `packages/server` compiles against and broke its `dts` build. A\n * debugging tool is not worth perturbing the engine's own dependency graph, and\n * the built-in module costs nothing: no native build, no `allowBuilds` opt-in,\n * and no dependency at all.\n *\n * Two stores, two different questions, and the reason a game-shaped reader\n * exists at all:\n *\n * - **D1 is the index.** Discovery, history, ratings, identity. Its `games` row\n * is a display read-model written by the DO as a fire-and-forget mirror.\n * - **The game's DO is the session.** One SQLite database per game, holding the\n * authoritative status, roster and the append-only transition log.\n *\n * A question like \"why is this game not starting\" therefore cannot be answered\n * from either store alone, and answering it from a generic table browser means\n * knowing which of the two to distrust. {@link LocalStore.game} joins them and\n * reports the disagreement as a value, so a stale mirror reads as a stale\n * mirror rather than as a mystery.\n *\n * Every open is read-only, so this is safe to run against a live `wrangler dev`.\n *\n * @module @eigeninteractive/testkit/local-state\n */\n\nimport { existsSync, readdirSync, statSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { DatabaseSync } from \"node:sqlite\";\n\n/** Where `wrangler dev` persists local resources, relative to a Worker root. */\nconst STATE_SUFFIX = join(\".wrangler\", \"state\", \"v3\");\n\n/** Miniflare's own bookkeeping database, present in every resource directory\n * alongside the real ones. It holds `_cf_ALARM`, not user data. */\nconst METADATA_FILE = \"metadata.sqlite\";\n\n/** The table Miniflare writes into every Durable Object database, holding the\n * `idFromName` name the object was addressed by. It is what makes the hashed\n * filename reversible: the engine names a game's DO by its `gameId`, and the\n * hash is one-way, so this row is the only local file-to-game map there is. */\nconst DO_NAME_TABLE = \"__miniflare_do_name\";\n\n/** A local Durable Object database: one game's whole session. */\nexport interface LocalDurableObject {\n /** The Durable Object class, parsed from the directory Miniflare names\n * `<worker>-<ClassName>`. */\n className: string;\n /** The hex `DurableObjectId`, which is the filename: `idFromName(name)`\n * hashed. Not the game id, and not reversible without {@link name}. */\n id: string;\n /** The name the object was addressed by, which for a game DO is its\n * `gameId`. Null only if the object was created by unique id rather than by\n * name, which the engine never does. */\n name: string | null;\n /** Absolute path to the SQLite file. */\n file: string;\n}\n\n/** What a bound parameter may be: SQLite's own value domain, spelled out here\n * rather than re-exported from `node:sqlite` so the public signature does not\n * depend on a built-in module's type names. */\nexport type LocalQueryParam = string | number | bigint | null | Uint8Array;\n\n/** A row of Miniflare's alarm table: the DO's armed alarm, which for a game is\n * its turn deadline plus grace. An absent row means no timer is armed, which is\n * correct for an untimed game and a bug for a timed one mid-turn. */\nexport interface LocalAlarm {\n actorId: string;\n actorName: string | null;\n scheduledTime: number | null;\n}\n\n/** The engine's `games` row: the D1 index entry, a display mirror of the DO. */\nexport interface LocalGameRow {\n id: string;\n createdBy: string | null;\n status: string;\n access: string;\n schemaVersion: number;\n config: unknown;\n turnSeconds: number | null;\n budgetSeconds: number | null;\n incrementSeconds: number | null;\n rated: boolean;\n ratingPool: string | null;\n minPlayers: number;\n maxPlayers: number;\n shortCode: string;\n pendingPlayers: number[] | null;\n turnDeadline: number | null;\n outcomes: unknown;\n finishId: string | null;\n finishedAt: number | null;\n archivedAt: number | null;\n createdAt: number;\n updatedAt: number;\n}\n\n/** One seat, from either store. `username` is filled in from D1's `users` and\n * `bots` tables when the reader has an index to join against. */\nexport interface LocalSeat {\n playerIndex: number;\n userId: string | null;\n botId: string | null;\n type: string;\n username: string | null;\n displayName: string | null;\n}\n\n/** The DO's single `meta` row: the authoritative session header. */\nexport interface LocalGameMeta {\n gameId: string;\n status: string;\n access: string;\n schemaVersion: number;\n config: unknown;\n turnSeconds: number | null;\n budgetSeconds: number | null;\n incrementSeconds: number | null;\n rated: boolean;\n ratingPool: string | null;\n minPlayers: number;\n maxPlayers: number;\n createdBy: string | null;\n rngSeed: string | null;\n}\n\n/** One committed transition, decoded. `action` is null at v0 (the start). */\nexport interface LocalTransition {\n version: number;\n state: unknown;\n action: { type: string; kind: string; playerIndex: number | null; data: unknown } | null;\n pending: number[];\n deadline: number | null;\n playerTimes: number[] | null;\n turnStartedAt: number | null;\n /** Which seats hold a stored frame at this version. Empty after the finish\n * compaction, which empties `frames` and leaves replay to re-projection. */\n frameSeats: number[];\n}\n\n/** Why the game is where it is, and what would move it. Derived rather than\n * stored: this is the sentence a developer actually wants, and every input to\n * it is already on screen above it. */\nexport interface LocalGameDiagnosis {\n /** One line naming the next expected event, or the reason there is none. */\n summary: string;\n /** Disagreements between the D1 mirror and the DO's own truth. A non-empty\n * list means a mirror write was lost, since the DO is authoritative. */\n mirrorDrift: string[];\n}\n\n/** Everything both stores know about one game. */\nexport interface LocalGameView {\n gameId: string;\n /** The D1 index row, or null when D1 has no such game: either the id came\n * from a DO name and the index was wiped, or the create write never landed. */\n index: LocalGameRow | null;\n /** D1's `participants`, which mirror the DO's roster for display. */\n indexSeats: LocalSeat[];\n /** The DO file, or null when no Durable Object exists for this game yet.\n * That is the normal state of a created-but-never-touched lobby: the DO is\n * created lazily by the first command or socket. */\n durableObject: LocalDurableObject | null;\n meta: LocalGameMeta | null;\n roster: LocalSeat[];\n transitions: LocalTransition[];\n /** Recorded `commandId` dedupe entries, newest first. */\n commands: { commandId: string; createdAt: number; result: unknown }[];\n /** Unapplied finish rows: a surviving row means the D1 finish apply has not\n * succeeded and the admin re-poke is the recovery. */\n outbox: { finishId: string; createdAt: number; outcomes: unknown }[];\n alarm: LocalAlarm | null;\n diagnosis: LocalGameDiagnosis;\n}\n\n/**\n * Walk up from `from` to the nearest directory holding `.wrangler/state/v3`.\n *\n * Walking rather than requiring the Worker root means this works from anywhere\n * inside a game repository, including the app half of a two-package layout,\n * where the state lives a directory or two above the shell's cwd.\n */\nexport function findWranglerState(from: string = process.cwd()): string | null {\n let dir = resolve(from);\n for (;;) {\n const candidate = join(dir, STATE_SUFFIX);\n if (existsSync(candidate)) return candidate;\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\n/**\n * Open a SQLite file for reading only.\n *\n * Read-only is what makes this safe to point at a running `wrangler dev`: no\n * writer lock is taken and no journal is touched. The fallback matters because\n * these databases are in WAL mode, and a read-only connection cannot recover a\n * hot journal or create the `-shm` file it needs; when `wrangler dev` is not\n * running and left one behind, the first open fails and a writable handle is\n * the only way in. Nothing here issues anything but `SELECT`, so the widened\n * handle stays as harmless as the narrow one. The existence check comes first\n * because that fallback would otherwise CREATE a database rather than report a\n * missing one.\n */\nexport function openSqlite(file: string): DatabaseSync {\n if (!existsSync(file)) throw new Error(`no SQLite database at ${file}`);\n try {\n return new DatabaseSync(file, { readOnly: true });\n } catch {\n return new DatabaseSync(file);\n }\n}\n\nfunction sqliteFilesIn(dir: string): string[] {\n if (!existsSync(dir)) return [];\n return readdirSync(dir)\n .filter((entry) => entry.endsWith(\".sqlite\") && entry !== METADATA_FILE)\n .map((entry) => join(dir, entry));\n}\n\nfunction tableExists(db: DatabaseSync, table: string): boolean {\n const row = db.prepare(\"select 1 from sqlite_master where type = 'table' and name = ?\").get(table);\n return row !== undefined;\n}\n\n/** Parse a JSON text column, tolerating a value some other writer left\n * un-encoded. A debugging reader that throws on one malformed cell is useless\n * precisely when it is needed. */\nfunction json(value: unknown): unknown {\n if (typeof value !== \"string\") return value ?? null;\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n}\n\nfunction numbers(value: unknown): number[] {\n const parsed = json(value);\n return Array.isArray(parsed) ? parsed.filter((n): n is number => typeof n === \"number\") : [];\n}\n\nfunction bool(value: unknown): boolean {\n return value === 1 || value === true;\n}\n\n/**\n * A Worker's local state, with its databases opened lazily and held open.\n *\n * Stateful because it owns file handles: construct one, ask it questions, and\n * {@link close} it. A single instance is the unit both the CLI and a test use,\n * so neither has to know which file backs which resource.\n */\nexport class LocalStore {\n /** Absolute path to `.wrangler/state/v3`. */\n readonly root: string;\n\n #d1: { file: string; db: DatabaseSync } | null | undefined;\n readonly #open = new Map<string, DatabaseSync>();\n\n private constructor(root: string) {\n this.root = root;\n }\n\n /**\n * Locate and open the state under `from`, or throw with the one thing the\n * caller can act on: that no Worker has been run here yet.\n */\n static open(from?: string): LocalStore {\n const root = findWranglerState(from);\n if (root === null) {\n throw new Error(`no .wrangler/state/v3 found at or above ${resolve(from ?? process.cwd())}. Run the Worker once (\\`pnpm dev\\`) so it has local state to read.`);\n }\n return new LocalStore(root);\n }\n\n #handle(file: string): DatabaseSync {\n const existing = this.#open.get(file);\n if (existing !== undefined) return existing;\n const db = openSqlite(file);\n this.#open.set(file, db);\n return db;\n }\n\n /**\n * The engine's D1 database, identified by its schema rather than its name.\n *\n * Miniflare names the file after a hash of the database, so there is no\n * mapping back to the binding. Recognising the engine's own `games` table\n * instead is both simpler and more honest about what the caller wants: the\n * database that has games in it, whatever the binding was called.\n */\n d1(): { file: string; db: DatabaseSync } | null {\n if (this.#d1 !== undefined) return this.#d1;\n for (const file of sqliteFilesIn(join(this.root, \"d1\", \"miniflare-D1DatabaseObject\"))) {\n const db = this.#handle(file);\n if (tableExists(db, \"games\")) {\n this.#d1 = { file, db };\n return this.#d1;\n }\n }\n this.#d1 = null;\n return null;\n }\n\n /**\n * Every local Durable Object database, newest-modified first.\n *\n * The directory is `<worker>-<ClassName>`, and a worker name may itself\n * contain a dash, so the class is everything after the LAST one. Ordering by\n * mtime puts the game you just touched at the top, which is nearly always the\n * one being debugged.\n */\n durableObjects(): LocalDurableObject[] {\n const base = join(this.root, \"do\");\n if (!existsSync(base)) return [];\n const found: LocalDurableObject[] = [];\n for (const namespace of readdirSync(base)) {\n const dir = join(base, namespace);\n if (!statSync(dir).isDirectory()) continue;\n const className = namespace.slice(namespace.lastIndexOf(\"-\") + 1);\n for (const file of sqliteFilesIn(dir)) {\n const db = this.#handle(file);\n const name = tableExists(db, DO_NAME_TABLE) ? ((db.prepare(`select name from ${DO_NAME_TABLE} limit 1`).get() as { name?: string } | undefined)?.name ?? null) : null;\n found.push({ className, id: file.slice(file.lastIndexOf(\"/\") + 1, -\".sqlite\".length), name, file });\n }\n }\n return found.sort((a, b) => statSync(b.file).mtimeMs - statSync(a.file).mtimeMs);\n }\n\n /** The armed alarm for a Durable Object, from its namespace's metadata\n * database. Matched on either column because the id is what Miniflare keys\n * by and the name is what a human recognises. */\n alarm(target: LocalDurableObject): LocalAlarm | null {\n const file = join(dirname(target.file), METADATA_FILE);\n if (!existsSync(file)) return null;\n const db = this.#handle(file);\n if (!tableExists(db, \"_cf_ALARM\")) return null;\n const row = db.prepare(\"select actor_id, actor_name, scheduled_time from _cf_ALARM where actor_id = ? or actor_name = ?\").get(target.id, target.name ?? target.id) as { actor_id: string; actor_name: string | null; scheduled_time: number | null } | undefined;\n return row === undefined ? null : { actorId: row.actor_id, actorName: row.actor_name, scheduledTime: row.scheduled_time };\n }\n\n /** The D1 index rows, newest-updated first. */\n games(options: { status?: string; limit?: number } = {}): LocalGameRow[] {\n const d1 = this.d1();\n if (d1 === null) return [];\n const where = options.status === undefined ? \"\" : \"where status = ?\";\n const params = options.status === undefined ? [] : [options.status];\n const rows = d1.db.prepare(`select * from games ${where} order by updated_at desc limit ?`).all(...params, options.limit ?? 50) as Record<string, unknown>[];\n return rows.map(toGameRow);\n }\n\n /**\n * Resolve a game reference the way a developer will type it: a full id, a\n * six-character short code, or an id prefix long enough to be unique.\n *\n * D1 is tried first because it can match a short code, then the DO names, so\n * a game whose index row was wiped is still reachable by id.\n */\n resolveGameId(ref: string): string | null {\n const d1 = this.d1();\n if (d1 !== null) {\n const exact = d1.db.prepare(\"select id from games where id = ? or short_code = ?\").get(ref, ref.toUpperCase()) as { id: string } | undefined;\n if (exact !== undefined) return exact.id;\n const prefixed = d1.db.prepare(\"select id from games where id like ? limit 2\").all(`${ref}%`) as { id: string }[];\n if (prefixed.length === 1) return prefixed[0].id;\n if (prefixed.length > 1) throw new Error(`\"${ref}\" matches more than one game; use a longer prefix`);\n }\n const named = this.durableObjects().filter((o) => o.name !== null && (o.name === ref || o.name.startsWith(ref)));\n if (named.length === 1) return named[0].name;\n if (named.length > 1) throw new Error(`\"${ref}\" matches more than one game; use a longer prefix`);\n return null;\n }\n\n /** The Durable Object holding a game's session, or null if none exists yet. */\n gameDurableObject(gameId: string): LocalDurableObject | null {\n return this.durableObjects().find((o) => o.name === gameId) ?? null;\n }\n\n /** Identity for a set of user and bot ids, for rendering seats as names. */\n #identities(userIds: string[], botIds: string[]): Map<string, { username: string; displayName: string }> {\n const out = new Map<string, { username: string; displayName: string }>();\n const d1 = this.d1();\n if (d1 === null) return out;\n for (const [table, ids] of [\n [\"users\", userIds],\n [\"bots\", botIds],\n ] as const) {\n if (ids.length === 0) continue;\n const placeholders = ids.map(() => \"?\").join(\",\");\n const rows = d1.db.prepare(`select id, username, display_name from ${table} where id in (${placeholders})`).all(...ids) as { id: string; username: string; display_name: string }[];\n for (const row of rows) out.set(row.id, { username: row.username, displayName: row.display_name });\n }\n return out;\n }\n\n #seats(rows: { player_index: number; user_id: string | null; bot_id: string | null; type: string }[]): LocalSeat[] {\n const identities = this.#identities(\n rows.map((r) => r.user_id).filter((id): id is string => id !== null),\n rows.map((r) => r.bot_id).filter((id): id is string => id !== null),\n );\n return rows.map((row) => {\n const identity = identities.get(row.user_id ?? row.bot_id ?? \"\");\n return { playerIndex: row.player_index, userId: row.user_id, botId: row.bot_id, type: row.type, username: identity?.username ?? null, displayName: identity?.displayName ?? null };\n });\n }\n\n /**\n * Join both stores for one game: the index row, the session, the transition\n * log, and the derived diagnosis.\n *\n * Returns null only when neither store has heard of the game.\n */\n game(ref: string): LocalGameView | null {\n const gameId = this.resolveGameId(ref);\n if (gameId === null) return null;\n\n const d1 = this.d1();\n const indexRow = d1 === null ? undefined : (d1.db.prepare(\"select * from games where id = ?\").get(gameId) as Record<string, unknown> | undefined);\n const index = indexRow === undefined ? null : toGameRow(indexRow);\n const indexSeats =\n d1 === null\n ? []\n : this.#seats(\n d1.db.prepare(\"select player_index, user_id, bot_id, type from participants where game_id = ? order by player_index\").all(gameId) as {\n player_index: number;\n user_id: string | null;\n bot_id: string | null;\n type: string;\n }[],\n );\n\n const durableObject = this.gameDurableObject(gameId);\n let meta: LocalGameMeta | null = null;\n let roster: LocalSeat[] = [];\n let transitions: LocalTransition[] = [];\n let commands: LocalGameView[\"commands\"] = [];\n let outbox: LocalGameView[\"outbox\"] = [];\n\n if (durableObject !== null) {\n const db = this.#handle(durableObject.file);\n if (tableExists(db, \"meta\")) {\n const row = db.prepare(\"select * from meta where id = 1\").get() as Record<string, unknown> | undefined;\n if (row !== undefined) meta = toMeta(row);\n }\n if (tableExists(db, \"roster\")) {\n roster = this.#seats(db.prepare(\"select player_index, user_id, bot_id, type from roster order by player_index\").all() as { player_index: number; user_id: string | null; bot_id: string | null; type: string }[]);\n }\n if (tableExists(db, \"transitions\")) {\n const frameSeats = new Map<number, number[]>();\n if (tableExists(db, \"frames\")) {\n for (const row of db.prepare(\"select version, player_index from frames order by version, player_index\").all() as { version: number; player_index: number }[]) {\n frameSeats.set(row.version, [...(frameSeats.get(row.version) ?? []), row.player_index]);\n }\n }\n transitions = (db.prepare(\"select * from transitions order by version\").all() as Record<string, unknown>[]).map((row) => toTransition(row, frameSeats));\n }\n if (tableExists(db, \"commands\")) {\n commands = (db.prepare(\"select command_id, response, created_at from commands order by created_at desc\").all() as { command_id: string; response: unknown; created_at: number }[]).map((row) => ({\n commandId: row.command_id,\n createdAt: row.created_at,\n result: json(row.response),\n }));\n }\n if (tableExists(db, \"outbox\")) {\n outbox = (db.prepare(\"select finish_id, outcomes, created_at from outbox order by created_at\").all() as { finish_id: string; outcomes: unknown; created_at: number }[]).map((row) => ({\n finishId: row.finish_id,\n createdAt: row.created_at,\n outcomes: json(row.outcomes),\n }));\n }\n }\n\n const alarm = durableObject === null ? null : this.alarm(durableObject);\n const view: Omit<LocalGameView, \"diagnosis\"> = { gameId, index, indexSeats, durableObject, meta, roster, transitions, commands, outbox, alarm };\n return { ...view, diagnosis: diagnose(view) };\n }\n\n /** Run a read-only statement against D1 or one game's Durable Object: the\n * escape hatch for a question this reader does not model. */\n query(sql: string, options: { game?: string; params?: LocalQueryParam[] } = {}): Record<string, unknown>[] {\n const db = options.game === undefined ? this.d1()?.db : this.#gameDb(options.game);\n if (db === undefined || db === null) {\n throw new Error(options.game === undefined ? \"no local D1 database found in this Worker's state\" : `no local Durable Object found for game ${options.game}`);\n }\n return db.prepare(sql).all(...(options.params ?? [])) as Record<string, unknown>[];\n }\n\n /** Table names and row counts, for D1 or one game's Durable Object. */\n tables(options: { game?: string } = {}): { name: string; rows: number }[] {\n const db = options.game === undefined ? this.d1()?.db : this.#gameDb(options.game);\n if (db === undefined || db === null) return [];\n const names = (db.prepare(\"select name from sqlite_master where type = 'table' order by name\").all() as { name: string }[]).map((r) => r.name);\n return names.map((name) => ({ name, rows: (db.prepare(`select count(*) as n from \"${name}\"`).get() as { n: number }).n }));\n }\n\n #gameDb(ref: string): DatabaseSync | null {\n const gameId = this.resolveGameId(ref);\n const target = gameId === null ? null : this.gameDurableObject(gameId);\n return target === null ? null : this.#handle(target.file);\n }\n\n /** Release every open handle. */\n close(): void {\n for (const db of this.#open.values()) db.close();\n this.#open.clear();\n this.#d1 = undefined;\n }\n}\n\nfunction toGameRow(row: Record<string, unknown>): LocalGameRow {\n return {\n id: row.id as string,\n createdBy: (row.created_by as string | null) ?? null,\n status: row.status as string,\n access: row.access as string,\n schemaVersion: row.schema_version as number,\n config: json(row.config),\n turnSeconds: (row.turn_seconds as number | null) ?? null,\n budgetSeconds: (row.budget_seconds as number | null) ?? null,\n incrementSeconds: (row.increment_seconds as number | null) ?? null,\n rated: bool(row.rated),\n ratingPool: (row.rating_pool as string | null) ?? null,\n minPlayers: row.min_players as number,\n maxPlayers: row.max_players as number,\n shortCode: row.short_code as string,\n pendingPlayers: row.pending_players === null || row.pending_players === undefined ? null : numbers(row.pending_players),\n turnDeadline: (row.turn_deadline as number | null) ?? null,\n outcomes: json(row.outcomes),\n finishId: (row.finish_id as string | null) ?? null,\n finishedAt: (row.finished_at as number | null) ?? null,\n archivedAt: (row.archived_at as number | null) ?? null,\n createdAt: row.created_at as number,\n updatedAt: row.updated_at as number,\n };\n}\n\nfunction toMeta(row: Record<string, unknown>): LocalGameMeta {\n return {\n gameId: row.game_id as string,\n status: row.status as string,\n access: row.access as string,\n schemaVersion: row.schema_version as number,\n config: json(row.config),\n turnSeconds: (row.turn_seconds as number | null) ?? null,\n budgetSeconds: (row.budget_seconds as number | null) ?? null,\n incrementSeconds: (row.increment_seconds as number | null) ?? null,\n rated: bool(row.rated),\n ratingPool: (row.rating_pool as string | null) ?? null,\n minPlayers: row.min_players as number,\n maxPlayers: row.max_players as number,\n createdBy: (row.created_by as string | null) ?? null,\n rngSeed: (row.rng_seed as string | null) ?? null,\n };\n}\n\nfunction toTransition(row: Record<string, unknown>, frameSeats: Map<number, number[]>): LocalTransition {\n const version = row.version as number;\n const action = json(row.action) as LocalTransition[\"action\"];\n return {\n version,\n state: json(row.state),\n action: action ?? null,\n pending: numbers(row.pending),\n deadline: (row.deadline as number | null) ?? null,\n playerTimes: row.player_times === null || row.player_times === undefined ? null : numbers(row.player_times),\n turnStartedAt: (row.turn_started_at as number | null) ?? null,\n frameSeats: frameSeats.get(version) ?? [],\n };\n}\n\n/**\n * Say what the game is waiting for, and whether the two stores agree.\n *\n * The engine's statuses each have exactly one thing that moves them, so this is\n * a total function over `status` rather than a heuristic. The cases developers\n * actually hit are the first two: a lobby that will not fill, and a full lobby\n * whose creator has not pressed start.\n */\nfunction diagnose(view: Omit<LocalGameView, \"diagnosis\">): LocalGameDiagnosis {\n const status = view.meta?.status ?? view.index?.status ?? \"unknown\";\n const seated = (view.meta === null ? view.indexSeats : view.roster).length;\n const min = view.meta?.minPlayers ?? view.index?.minPlayers ?? 0;\n const max = view.meta?.maxPlayers ?? view.index?.maxPlayers ?? 0;\n const latest = view.transitions.at(-1) ?? null;\n\n const mirrorDrift: string[] = [];\n if (view.index !== null && view.meta !== null) {\n if (view.index.status !== view.meta.status) {\n mirrorDrift.push(`D1 says status=${view.index.status}, the Durable Object says ${view.meta.status}. The DO is authoritative; a mirror write was lost.`);\n }\n if (view.indexSeats.length !== view.roster.length) {\n mirrorDrift.push(`D1 mirrors ${view.indexSeats.length} seat(s), the Durable Object holds ${view.roster.length}.`);\n }\n const mirroredPending = view.index.pendingPlayers ?? null;\n const actualPending = latest?.pending ?? null;\n if (status === \"active\" && JSON.stringify(mirroredPending) !== JSON.stringify(actualPending)) {\n mirrorDrift.push(`D1 mirrors pendingPlayers=${JSON.stringify(mirroredPending)}, the newest transition has ${JSON.stringify(actualPending)}.`);\n }\n }\n if (view.index === null && view.meta !== null) {\n mirrorDrift.push(\"no D1 index row for a game whose Durable Object exists; discovery and history lists cannot show it.\");\n }\n\n let summary: string;\n switch (status) {\n case \"waiting\":\n summary = `waiting for players: ${seated}/${min} needed to become ready (${max} max). The next event is a join, or add-bot from the creator.`;\n break;\n case \"ready\":\n summary = `ready and not started: ${seated}/${max} seats filled. Nothing happens until the CREATOR calls POST /api/engine/games/${view.gameId}/start. A start is explicit and creator-only; filling the lobby does not start a game.`;\n break;\n case \"active\":\n if (latest === null) {\n summary = \"status is active but no transition is committed, which should be impossible: a start commits v0 in the same transaction that sets the status.\";\n } else {\n const waitingOn = latest.pending.length === 0 ? \"nobody, which means the game should have finished at this version\" : `seat(s) ${latest.pending.join(\", \")}`;\n const timer = latest.deadline === null ? \"untimed, so no alarm is armed and only a move or a forfeit moves it\" : `deadline ${new Date(latest.deadline).toISOString()}`;\n summary = `at v${latest.version}, waiting on ${waitingOn}. ${timer}.`;\n }\n break;\n case \"finished\":\n summary = view.outbox.length > 0 ? `finished, but ${view.outbox.length} outbox row(s) survive: the D1 finish apply has not succeeded, so ratings and history are not published yet.` : `finished at v${latest?.version ?? \"?\"}; the outbox is clear, so D1 has the outcomes.`;\n break;\n case \"aborted\":\n summary = \"aborted: cancelled by its creator, reaped as abandoned, or torn down by an account purge. Nothing moves an aborted game.\";\n break;\n default:\n summary = view.durableObject === null ? \"created in D1 but its Durable Object does not exist yet. That is normal for a lobby nobody has touched: the DO is created lazily by the first command or socket.\" : `unrecognised status ${status}.`;\n }\n\n return { summary, mirrorDrift };\n}\n"],"mappings":"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","names":[]}
|