@eigeninteractive/testkit 0.6.1 → 0.7.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/README.md +26 -0
- package/dist/contract-cli.js +1 -1
- package/dist/{contract-command-aCqO5CyJ.js → contract-command-Dn5ANpqD.js} +459 -23
- package/dist/contract-command-Dn5ANpqD.js.map +1 -0
- package/dist/index.d.ts +122 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/package.json +5 -5
- package/src/index.ts +9 -0
- package/src/twin-fixtures.ts +592 -31
- package/dist/contract-command-aCqO5CyJ.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract-command-Dn5ANpqD.js","names":["fixtureFiles"],"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 + the four hooks + the two\n * predicates, and whole matches through the real kernel), while the Dart\n * runner runs the 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 * \"kind\": \"initialState\",\n * \"name\": \"a fresh board seats both players\",\n * \"config\": { ... },\n * \"playerCount\": 2,\n * \"rngSeed\": \"any string\", // optional, default \"twin-fixtures\"\n * \"expected\": { // the envelope block the action case\n * \"state\": { ... }, // records, minus `valid`\n * \"pending\": [0, 1],\n * \"outcome\": null\n * }\n * },\n * {\n * \"kind\": \"lifecycle\",\n * \"name\": \"a forfeit hands the match to the other seat\",\n * \"config\": { ... },\n * \"state\": { ... }, // applyLifecycle input\n * \"pending\": [0, 1],\n * \"type\": \"forfeit\", // timeout | forfeit | autoForfeit\n * \"playerIndex\": 0, // required for forfeit/autoForfeit,\n * // forbidden for timeout\n * \"participantCount\": 2, // optional, default 2\n * \"rngSeed\": \"any string\", // optional, default \"twin-fixtures\"\n * \"expected\": { \"state\": { ... }, \"pending\": [], \"outcome\": [ ... ] }\n * },\n * {\n * \"kind\": \"transcript\",\n * \"name\": \"a best-of-one match played to its finish\",\n * \"config\": { ... },\n * \"playerCount\": 2,\n * \"seed\": \"the game's base rng seed\",\n * \"transitions\": [\n * { \"kind\": \"game\", \"playerIndex\": 0, \"data\": { ... } },\n * { \"kind\": \"lifecycle\", \"type\": \"forfeit\", \"playerIndex\": 1 }\n * ],\n * \"expected\": {\n * \"version\": 3, // the final committed state version\n * \"status\": \"finished\", // active | finished\n * \"state\": { ... }, // optional, as above\n * \"pending\": [],\n * \"outcome\": [ ... ]\n * }\n * },\n * {\n * \"kind\": \"rng\",\n * \"name\": \"the stream initialState draws from\",\n * \"seed\": \"twin-fixtures\",\n * \"version\": 0,\n * \"seat\": 1, // optional; present ⇒ the bot stream\n * \"draws\": [0.123, 0.456] // 1..64 values, compared exactly\n * }\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 * ## What each kind records\n *\n * `action`, `playerLimits`, `ratingPool` and `botSeatable` pin one hook or\n * predicate. The four kinds below exist for offline play (architecture\n * decision 0012): a device runs the Dart twin through a Dart port of the\n * kernel, so the pieces the online client never had to reproduce — the\n * opening state, the lifecycle hooks, a whole match, and the random stream\n * itself — each need a recorded behavior of their own.\n *\n * - `initialState` runs `initialState` with `deriveRng(rngSeed, 0)`, the\n * version-0 stream the engine's `start` commit derives, and checks the\n * returned envelope exactly as the action case does.\n * - `lifecycle` runs `applyLifecycle` with the `data` payload the kernel\n * builds (`{type:\"timeout\"}`, or `{type, playerIndex}`), then applies the\n * kernel's forfeit guard: a forfeit must remove its own seat from pending.\n * Like the action case, and unlike `initialState`, it draws from the raw\n * `rngSeed` stream: a standalone hook case has no version to derive from.\n * - `transcript` replays an ordered match through the REAL kernel `commit()`,\n * so every engine guard, the pending/version bookkeeping, and the\n * per-transition `deriveRng(seed, version)` streams all participate. It is\n * the case a Dart local kernel has to reproduce move for move, on the\n * untimed table and with the human/bot roster {@link evaluateTranscript}\n * documents.\n * - `rng` records raw `deriveRng` draws. Generated from the TypeScript kernel\n * by {@link rngFixtureCase} / {@link writeRngFixture} and compared with\n * `===`, no tolerance: the Dart port is required to be bit-identical, and a\n * port that is merely close is a port that desynchronizes one draw later.\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, writeFileSync } from \"node:fs\";\nimport { assertForfeitPending, type CommitInput, type CommitPlan, commit, deriveRng, GameBugError, type GameRow, type Intent, isRejected, type Rejected, type Seat } from \"@eigeninteractive/kernel\";\nimport { type Envelope, type GameAccess, type GameModule, type GameRules, IllegalMoveError, type Json, type JsonObject, type LifecycleAction, type LifecycleType, type ObservationSlice, type OutcomeEntry, type Rng } from \"@eigeninteractive/rules\";\nimport Rand from \"rand-seed\";\nimport { it } from \"vitest\";\n\n/** The stream a case draws from when it names no `rngSeed`. */\nconst DEFAULT_RNG_SEED = \"twin-fixtures\";\n\n/** One fixture file: cases targeting one `schemaVersion` unit. */\nexport interface TwinFixtureFile {\n schemaVersion: number;\n cases: TwinFixtureCase[];\n}\n\n/** What a case may assert about the envelope a hook (or a whole transcript)\n * produced. Every field is optional: a fixture pins what it means to pin.\n * `outcome` is three-valued — absent leaves the outcome unchecked, `null`\n * asserts the game is ongoing, a list asserts it ended exactly so. */\nexport interface ExpectedEnvelope {\n state?: JsonObject;\n pending?: number[];\n outcome?: OutcomeEntry[] | null;\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: ExpectedEnvelope & {\n valid: boolean;\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\n/** The opening transition: what `initialState` returns for one config and\n * seat count, drawing from the version-0 stream a `start` commit derives. */\nexport interface InitialStateCase {\n kind: \"initialState\";\n name: string;\n config: JsonObject;\n playerCount: number;\n rngSeed?: string;\n expected: ExpectedEnvelope;\n}\n\n/** An `applyLifecycle` case: one engine-driven transition (a turn that ran\n * out, a resign, a purged account) resolved against a stated position. */\nexport interface LifecycleCase {\n kind: \"lifecycle\";\n name: string;\n config: JsonObject;\n state: JsonObject;\n pending: number[];\n type: LifecycleType;\n /** The forfeiting seat. Required for `forfeit`/`autoForfeit`; a `timeout`\n * carries no seat (its victims are `pending`), so it must be omitted. */\n playerIndex?: number;\n participantCount?: number;\n rngSeed?: string;\n expected: ExpectedEnvelope;\n}\n\n/** One transition of a {@link TranscriptCase}: a seat's move, or a resign. */\nexport type TranscriptTransition = { kind: \"game\"; playerIndex: number; data: JsonObject } | { kind: \"lifecycle\"; type: \"forfeit\"; playerIndex: number };\n\n/** A whole match, replayed through the real kernel from a stated base seed.\n * The one case that pins the *engine's* bookkeeping — versions, pending\n * hand-off, the per-transition RNG streams — rather than a single hook. */\nexport interface TranscriptCase {\n kind: \"transcript\";\n name: string;\n config: JsonObject;\n playerCount: number;\n /** The game's base RNG seed: every transition's stream derives from it. */\n seed: string;\n transitions: TranscriptTransition[];\n expected: ExpectedEnvelope & {\n version: number;\n status: \"active\" | \"finished\";\n };\n}\n\n/** Recorded draws from one derived RNG stream. Generated by\n * {@link rngFixtureCase}, never hand-written: the values ARE the TypeScript\n * kernel's, and the Dart port has to reproduce them bit for bit. */\nexport interface RngCase {\n kind: \"rng\";\n name: string;\n seed: string;\n /** The state version the stream belongs to (`deriveRng`'s second arg). */\n version: number;\n /** Present ⇒ the bot stream for that seat, keyed `\"<seed>:bot<seat>\"`. */\n seat?: number;\n draws: number[];\n}\n\nexport type TwinFixtureCase = ActionCase | PlayerLimitsCase | RatingPoolCase | BotSeatableCase | InitialStateCase | LifecycleCase | TranscriptCase | RngCase;\n\n/** Every `kind` a fixture case may declare, in the order the error messages\n * and the evaluator switch list them. */\nconst CASE_KINDS = [\"action\", \"playerLimits\", \"ratingPool\", \"botSeatable\", \"initialState\", \"lifecycle\", \"transcript\", \"rng\"] as const;\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/** A seat index, a seat count, or a state version: the fixture format has no\n * fractional or negative counts anywhere. */\nfunction asIndex(where: string, v: unknown): number {\n const n = asNumber(where, v);\n if (!Number.isInteger(n) || n < 0) {\n throw new Error(`${where}: expected a non-negative integer, got ${JSON.stringify(v)}`);\n }\n return 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\n/** The `expected` block every hook-running case shares. */\nfunction parseExpectedEnvelope(where: string, raw: JsonObject): ExpectedEnvelope {\n const expected: ExpectedEnvelope = {\n state: optional(`${where}.state`, raw.state, asObject),\n pending: optional(`${where}.pending`, raw.pending, asNumberArray),\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 raw) {\n const outcome = raw.outcome;\n if (outcome !== null && !Array.isArray(outcome)) fail(`${where}.outcome`, \"an array or null\", outcome);\n expected.outcome = outcome as OutcomeEntry[] | null;\n }\n return expected;\n}\n\nfunction parseActionCase(where: string, raw: JsonObject): ActionCase {\n const expectedRaw = asObject(`${where}.expected`, raw.expected);\n const expected: ActionCase[\"expected\"] = {\n ...parseExpectedEnvelope(`${where}.expected`, expectedRaw),\n valid: asBoolean(`${where}.expected.valid`, expectedRaw.valid),\n observation: optional(`${where}.expected.observation`, expectedRaw.observation, asObject),\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\nfunction parseInitialStateCase(where: string, raw: JsonObject): InitialStateCase {\n return {\n kind: \"initialState\",\n name: asString(`${where}.name`, raw.name),\n config: asObject(`${where}.config`, raw.config),\n playerCount: asIndex(`${where}.playerCount`, raw.playerCount),\n rngSeed: optional(`${where}.rngSeed`, raw.rngSeed, asString),\n expected: parseExpectedEnvelope(`${where}.expected`, asObject(`${where}.expected`, raw.expected)),\n };\n}\n\nfunction parseLifecycleCase(where: string, raw: JsonObject): LifecycleCase {\n const type = asString(`${where}.type`, raw.type);\n if (type !== \"timeout\" && type !== \"forfeit\" && type !== \"autoForfeit\") {\n throw new Error(`${where}.type: expected one of timeout | forfeit | autoForfeit, got ${JSON.stringify(raw.type)}`);\n }\n // A timeout resolves whoever is pending and carries no seat, so a\n // `playerIndex` on one is not a harmless extra: it is a fixture author\n // expecting a seat the hook will never be told about.\n const playerIndex = optional(`${where}.playerIndex`, raw.playerIndex, asIndex);\n if (type === \"timeout\" && playerIndex !== undefined) {\n throw new Error(`${where}.playerIndex: a timeout carries no seat (its victims are \"pending\"); remove it`);\n }\n if (type !== \"timeout\" && playerIndex === undefined) {\n throw new Error(`${where}.playerIndex: a ${type} must name the forfeiting seat`);\n }\n return {\n kind: \"lifecycle\",\n name: asString(`${where}.name`, raw.name),\n config: asObject(`${where}.config`, raw.config),\n state: asObject(`${where}.state`, raw.state),\n pending: asNumberArray(`${where}.pending`, raw.pending),\n type,\n playerIndex,\n participantCount: optional(`${where}.participantCount`, raw.participantCount, asNumber),\n rngSeed: optional(`${where}.rngSeed`, raw.rngSeed, asString),\n expected: parseExpectedEnvelope(`${where}.expected`, asObject(`${where}.expected`, raw.expected)),\n };\n}\n\nfunction parseTranscriptTransition(where: string, raw: unknown): TranscriptTransition {\n const obj = asObject(where, raw);\n switch (obj.kind) {\n case \"game\":\n return {\n kind: \"game\",\n playerIndex: asIndex(`${where}.playerIndex`, obj.playerIndex),\n data: asObject(`${where}.data`, obj.data),\n };\n case \"lifecycle\": {\n // Only `forfeit` is transcriptable: a timeout needs an expired clock and\n // a local game is untimed, and `autoForfeit` is an account purge, which\n // no transcript of a played match can contain.\n const type = asString(`${where}.type`, obj.type);\n if (type !== \"forfeit\") {\n throw new Error(`${where}.type: expected forfeit, got ${JSON.stringify(obj.type)}`);\n }\n return {\n kind: \"lifecycle\",\n type,\n playerIndex: asIndex(`${where}.playerIndex`, obj.playerIndex),\n };\n }\n default:\n throw new Error(`${where}.kind: expected one of game | lifecycle, got ${JSON.stringify(obj.kind)}`);\n }\n}\n\nfunction parseTranscriptCase(where: string, raw: JsonObject): TranscriptCase {\n const expectedRaw = asObject(`${where}.expected`, raw.expected);\n const status = asString(`${where}.expected.status`, expectedRaw.status);\n if (status !== \"active\" && status !== \"finished\") {\n throw new Error(`${where}.expected.status: expected one of active | finished, got ${JSON.stringify(expectedRaw.status)}`);\n }\n if (!Array.isArray(raw.transitions)) fail(`${where}.transitions`, \"an array\", raw.transitions);\n return {\n kind: \"transcript\",\n name: asString(`${where}.name`, raw.name),\n config: asObject(`${where}.config`, raw.config),\n playerCount: asIndex(`${where}.playerCount`, raw.playerCount),\n seed: asString(`${where}.seed`, raw.seed),\n transitions: raw.transitions.map((transition, i) => parseTranscriptTransition(`${where}.transitions[${i}]`, transition)),\n expected: {\n ...parseExpectedEnvelope(`${where}.expected`, expectedRaw),\n version: asIndex(`${where}.expected.version`, expectedRaw.version),\n status,\n },\n };\n}\n\n/** The draw count one rng case may record. One is enough to catch a wrong\n * seeding; the cap keeps a fixture file a document rather than a data dump,\n * and a stream that agrees for 64 draws does not diverge at 65. */\nconst RNG_DRAW_LIMIT = 64;\n\nfunction parseRngCase(where: string, raw: JsonObject): RngCase {\n if (!Array.isArray(raw.draws)) fail(`${where}.draws`, \"an array of numbers\", raw.draws);\n if (raw.draws.length === 0 || raw.draws.length > RNG_DRAW_LIMIT) {\n throw new Error(`${where}.draws: expected between 1 and ${RNG_DRAW_LIMIT} draws, got ${raw.draws.length}`);\n }\n return {\n kind: \"rng\",\n name: asString(`${where}.name`, raw.name),\n seed: asString(`${where}.seed`, raw.seed),\n version: asIndex(`${where}.version`, raw.version),\n seat: optional(`${where}.seat`, raw.seat, asIndex),\n draws: asNumberArray(`${where}.draws`, raw.draws),\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 case \"initialState\":\n return parseInitialStateCase(where, obj);\n case \"lifecycle\":\n return parseLifecycleCase(where, obj);\n case \"transcript\":\n return parseTranscriptCase(where, obj);\n case \"rng\":\n return parseRngCase(where, obj);\n default:\n throw new Error(`${where}.kind: expected one of ${CASE_KINDS.join(\" | \")}, 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}.\n *\n * `schemaVersion` is the version the case targets. It never selects\n * behavior — the caller already resolved `rules` from it — and only labels\n * the engine guard messages a `lifecycle` or `transcript` case can provoke;\n * {@link twinFixtureTests} passes the fixture file's. */\nexport function evaluateTwinCase(rules: GameRules, kase: TwinFixtureCase, schemaVersion = 1): 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 case \"initialState\":\n return evaluateInitialState(rules, kase);\n case \"lifecycle\":\n return evaluateLifecycle(rules, kase, schemaVersion);\n case \"transcript\":\n return evaluateTranscript(rules, kase, schemaVersion);\n case \"rng\":\n return evaluateRng(kase);\n default:\n return [`unknown case kind \"${(kase as { kind: string }).kind}\", expected ${CASE_KINDS.join(\" | \")}`];\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, fixture.schemaVersion);\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, \"applyAction\", kase.expected, 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: hookRng(kase.rngSeed),\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\n/** The stream a standalone hook case draws from. Seeded with the fixture's\n * `rngSeed` verbatim rather than through `deriveRng`, because a single hook\n * case stands outside any game: it has no committed version to derive a\n * stream from. The `initialState` case is the exception — a game's opening\n * transition is always version 0 — and a `transcript` case, which runs the\n * real kernel, gets the real per-version streams. */\nfunction hookRng(rngSeed: string | undefined): Rng {\n return new Rand(rngSeed ?? DEFAULT_RNG_SEED);\n}\n\n/** Compare a hook's envelope with what a case recorded, and re-validate the\n * state it returned exactly as the engine does before committing it. */\nfunction checkEnvelope(rules: GameRules, hook: string, expected: ExpectedEnvelope, envelope: Envelope, failures: string[]): void {\n if (validate(rules, \"state\", envelope.state) === undefined) {\n failures.push(`${hook} returned state that violates its own schema`);\n }\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\nfunction evaluateInitialState(rules: GameRules, kase: InitialStateCase): string[] {\n const failures: string[] = [];\n const config = parseWith(rules, \"config\", kase.config, failures);\n if (config === undefined) return failures;\n let envelope: Envelope;\n try {\n envelope = rules.initialState({\n config,\n // Exactly what a `start` commit passes: the opening transition commits\n // as version 0, so its stream is the version-0 one.\n rng: deriveRng(kase.rngSeed ?? DEFAULT_RNG_SEED, 0),\n playerCount: kase.playerCount,\n });\n } catch (error) {\n failures.push(`initialState threw: ${error}`);\n return failures;\n }\n checkEnvelope(rules, \"initialState\", kase.expected, envelope, failures);\n return failures;\n}\n\nfunction evaluateLifecycle(rules: GameRules, kase: LifecycleCase, schemaVersion: number): string[] {\n const failures: string[] = [];\n const config = parseWith(rules, \"config\", kase.config, failures);\n const state = parseWith(rules, \"state\", kase.state, failures);\n if (config === undefined || state === undefined) return failures;\n\n // Built exactly as `commit()` builds it, because the payload is the hook's\n // only word on which seat is leaving: a `timeout` names none and resolves\n // the whole pending set, and the parser already refused any other pairing.\n const data: LifecycleAction = kase.type === \"timeout\" ? { type: \"timeout\" } : { type: kase.type, playerIndex: kase.playerIndex as number };\n\n let envelope: Envelope;\n try {\n envelope = rules.applyLifecycle({\n state,\n pending: kase.pending,\n type: kase.type,\n data,\n rng: hookRng(kase.rngSeed),\n config,\n });\n } catch (error) {\n failures.push(`applyLifecycle threw: ${error}`);\n return failures;\n }\n checkEnvelope(rules, \"applyLifecycle\", kase.expected, envelope, failures);\n if (data.type !== \"timeout\") {\n // The engine's own guard, run here rather than restated: a forfeit that\n // leaves its seat pending is a game bug the kernel would refuse to commit,\n // and a fixture recording one would record an unreachable behavior for the\n // Dart twin to reproduce.\n try {\n assertForfeitPending(data.playerIndex, envelope, schemaVersion);\n } catch (error) {\n failures.push(`${error instanceof Error ? error.message : error}`);\n }\n }\n return failures;\n}\n\n/**\n * Replay an ordered match through the real kernel.\n *\n * The table is the one a local (offline) game plays on, and the conventions\n * below are a contract with the Dart local kernel that replays the same file:\n *\n * - untimed and unrated (`turnSeconds`, `budgetSeconds`, `incrementSeconds`\n * all null), so no deadline is ever armed and the clock is never read;\n * - a roster of `playerCount` identified seats: seat 0 is the human\n * `user-0`, every other seat is the bot `bot-<seat>`;\n * - the match opens with a `start` intent carrying the case's `seed`, so\n * every transition draws from `deriveRng(seed, version)`;\n * - each transition commits at `expectedVersion` = the current version (a\n * device is the only actor, so it is never stale), with `actor` `\"user\"`\n * for seat 0 and `\"bot\"` for the rest.\n *\n * A rejection or a broken invariant fails the case naming the transition that\n * produced it: an engine that refuses the transcript is as much a divergence\n * as a wrong final state.\n */\nfunction evaluateTranscript(rules: GameRules, kase: TranscriptCase, schemaVersion: number): string[] {\n const failures: string[] = [];\n // `commit()` parses the config itself; parsing here too reports a bad\n // fixture config as a fixture problem rather than as an engine throw.\n if (parseWith(rules, \"config\", kase.config, failures) === undefined) return failures;\n\n const game: GameRow = {\n status: \"ready\",\n schemaVersion,\n config: kase.config,\n turnSeconds: null,\n budgetSeconds: null,\n incrementSeconds: null,\n rated: false,\n ratingPool: null,\n };\n const roster: Seat[] = Array.from({ length: kase.playerCount }, (_, seat) => (seat === 0 ? { playerIndex: 0, userId: \"user-0\", botId: null, type: \"human\" } : { playerIndex: seat, userId: null, botId: `bot-${seat}`, type: \"bot\" }));\n\n let plan = runCommit({ game, state: null, roster, intent: { kind: \"start\", seed: kase.seed }, now: TRANSCRIPT_NOW, rules });\n if (typeof plan === \"string\") return [`start: ${plan}`];\n game.status = \"active\";\n\n for (const [index, transition] of kase.transitions.entries()) {\n const intent: Intent =\n transition.kind === \"game\"\n ? {\n kind: \"action\",\n seat: transition.playerIndex,\n expectedVersion: plan.nextState.version,\n data: transition.data,\n actor: transition.playerIndex === 0 ? \"user\" : \"bot\",\n }\n : { kind: \"lifecycle\", type: \"forfeit\", seat: transition.playerIndex };\n const result = runCommit({ game, state: plan.nextState, roster, intent, now: TRANSCRIPT_NOW, rules });\n if (typeof result === \"string\") return [`transitions[${index}] (${describeTransition(transition)}): ${result}`];\n plan = result;\n // A finished game refuses everything that follows, so a transcript with a\n // move after its last one fails on that move rather than silently here.\n if (plan.outcomes !== null) game.status = \"finished\";\n }\n\n const status = plan.outcomes === null ? \"active\" : \"finished\";\n if (plan.nextState.version !== kase.expected.version) {\n failures.push(`the match committed as version ${plan.nextState.version}, fixture expects ${kase.expected.version}`);\n }\n if (status !== kase.expected.status) {\n failures.push(`the match is ${status}, fixture expects ${kase.expected.status}`);\n }\n checkEnvelope(rules, \"the final transition\", kase.expected, { state: plan.nextState.state, pendingPlayers: plan.nextState.pending, ...(plan.outcomes === null ? {} : { outcome: plan.outcomes }) }, failures);\n return failures;\n}\n\n/** The commit instant every transcript transition carries. An untimed game\n * never reads it, and a fixed value keeps the case a pure function of the\n * transcript. */\nconst TRANSCRIPT_NOW = 0;\n\n/** One kernel commit, with both failure species rendered as a string: a\n * rejection (a value) and a broken invariant (a throw) are equally a\n * transcript that does not replay. */\nfunction runCommit(input: CommitInput): CommitPlan | string {\n let result: CommitPlan | Rejected;\n try {\n result = commit(input);\n } catch (error) {\n if (error instanceof GameBugError) return `the engine refused the hook result: ${error.message}`;\n return `commit threw: ${error}`;\n }\n if (isRejected(result)) return `the engine rejected the intent (${result.code}): ${result.message}`;\n return result;\n}\n\nfunction describeTransition(transition: TranscriptTransition): string {\n return transition.kind === \"game\" ? `seat ${transition.playerIndex} plays ${JSON.stringify(transition.data)}` : `seat ${transition.playerIndex} forfeits`;\n}\n\nfunction evaluateRng(kase: RngCase): string[] {\n const rng = rngStream(kase.seed, kase.version, kase.seat);\n const failures: string[] = [];\n for (const [index, expected] of kase.draws.entries()) {\n const draw = rng.next();\n // Exact equality, deliberately: a port that is merely close has already\n // diverged, and the next draw off a shared stream proves it loudly.\n if (draw !== expected) failures.push(`draw[${index}] is ${draw}, fixture records ${expected}`);\n }\n return failures;\n}\n\n/** The stream an {@link RngCase} describes: a transition's own stream, or,\n * when the case names a seat, the bot stream the Durable Object derives for\n * that seat before running its brain. */\nfunction rngStream(seed: string, version: number, seat: number | undefined): Rng {\n return seat === undefined ? deriveRng(seed, version) : deriveRng(`${seed}:bot${seat}`, version);\n}\n\n// ── Generating rng fixtures ───────────────────────────────────────────────────\n//\n// An `rng` case is the one kind nobody writes by hand: its values are whatever\n// the TypeScript kernel's generator produces, and the point of recording them\n// is that the Dart port has to agree digit for digit. So the testkit generates\n// them, and a game re-runs the generator when it wants more coverage.\n\n/** What stream to record, and how much of it. */\nexport interface RngFixtureCaseOptions {\n /** The case name, used as the test name on both sides. */\n name: string;\n /** The game's base seed. */\n seed: string;\n /** The state version whose stream to record. */\n version: number;\n /** Record the bot stream for this seat instead of the transition's own. */\n seat?: number;\n /** How many draws to record (1 to 64). */\n count: number;\n}\n\n/** Record one derived stream from the real kernel as a fixture case. */\nexport function rngFixtureCase(options: RngFixtureCaseOptions): RngCase {\n const { name, seed, version, seat, count } = options;\n if (!Number.isInteger(count) || count < 1 || count > RNG_DRAW_LIMIT) {\n throw new Error(`rngFixtureCase(${JSON.stringify(name)}): count must be an integer between 1 and ${RNG_DRAW_LIMIT}, got ${count}`);\n }\n const rng = rngStream(seed, version, seat);\n return {\n kind: \"rng\",\n name,\n seed,\n version,\n ...(seat === undefined ? {} : { seat }),\n draws: Array.from({ length: count }, () => rng.next()),\n };\n}\n\n/**\n * Write a fixture file of generated `rng` cases.\n *\n * The `schemaVersion` is read from the `v<N>/` directory in `path`, the same\n * rule `eigen-contract` enforces over every fixture file, so the two can\n * never be written out of agreement. The document is validated before it is\n * written: a generator that emits something the runners would refuse to load\n * should fail at the generator.\n *\n * The output is plain two-space JSON. Only the values mean anything to either\n * runner, so a repo that formats JSON should run its formatter afterwards.\n *\n * ```ts\n * import { rngFixtureCase, writeRngFixture } from \"@eigeninteractive/testkit\";\n *\n * writeRngFixture(\"src/module/fixtures/v1/rng.json\", [\n * rngFixtureCase({ name: \"version 0\", seed: \"twin-fixtures\", version: 0, count: 16 }),\n * rngFixtureCase({ name: \"seat 1's bot stream\", seed: \"twin-fixtures\", version: 3, seat: 1, count: 16 }),\n * ]);\n * ```\n */\nexport function writeRngFixture(path: string, cases: RngCase[]): void {\n const match = /(?:^|\\/)v([1-9]\\d*)\\/[^/]+$/.exec(path.split(\"\\\\\").join(\"/\"));\n if (match === null) {\n throw new Error(`${path}: an rng fixture must be written into a v<N>/ directory, which names the schemaVersion it targets`);\n }\n const document = { schemaVersion: Number(match[1]), cases };\n parseTwinFixtureFile(path, document);\n writeFileSync(path, `${JSON.stringify(document, null, 2)}\\n`);\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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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GameAccess, GameModule, GameRules, Json, JsonObject, OutcomeEntry, TransitionCause } from "@eigeninteractive/rules";
|
|
1
|
+
import { GameAccess, GameModule, GameRules, Json, JsonObject, LifecycleType, OutcomeEntry, TransitionCause } from "@eigeninteractive/rules";
|
|
2
2
|
import { CommitInput, CommitPlan, DEADLINE_GRACE_MS, Effect, GameRow, Intent, ObservationFrame, RejectCode, Rejected, Seat, SeatView, SeatView as SeatView$1, StateRow, commit, deriveRng, isRejected, randomSeed } from "@eigeninteractive/kernel";
|
|
3
3
|
//#region src/contract-command.d.ts
|
|
4
4
|
/**
|
|
@@ -87,6 +87,15 @@ interface TwinFixtureFile {
|
|
|
87
87
|
schemaVersion: number;
|
|
88
88
|
cases: TwinFixtureCase[];
|
|
89
89
|
}
|
|
90
|
+
/** What a case may assert about the envelope a hook (or a whole transcript)
|
|
91
|
+
* produced. Every field is optional: a fixture pins what it means to pin.
|
|
92
|
+
* `outcome` is three-valued — absent leaves the outcome unchecked, `null`
|
|
93
|
+
* asserts the game is ongoing, a list asserts it ended exactly so. */
|
|
94
|
+
interface ExpectedEnvelope {
|
|
95
|
+
state?: JsonObject;
|
|
96
|
+
pending?: number[];
|
|
97
|
+
outcome?: OutcomeEntry[] | null;
|
|
98
|
+
}
|
|
90
99
|
/** A game-action case: exercises schemas, `applyAction`, and (through
|
|
91
100
|
* `expected.observation`) `computeObservation` for the acting seat. */
|
|
92
101
|
interface ActionCase {
|
|
@@ -101,11 +110,8 @@ interface ActionCase {
|
|
|
101
110
|
participantCount?: number;
|
|
102
111
|
rngSeed?: string;
|
|
103
112
|
action: JsonObject;
|
|
104
|
-
expected: {
|
|
113
|
+
expected: ExpectedEnvelope & {
|
|
105
114
|
valid: boolean;
|
|
106
|
-
state?: JsonObject;
|
|
107
|
-
pending?: number[];
|
|
108
|
-
outcome?: OutcomeEntry[] | null;
|
|
109
115
|
observation?: JsonObject;
|
|
110
116
|
};
|
|
111
117
|
}
|
|
@@ -140,23 +146,130 @@ interface BotSeatableCase {
|
|
|
140
146
|
botConfig: JsonObject;
|
|
141
147
|
expected: boolean;
|
|
142
148
|
}
|
|
143
|
-
|
|
149
|
+
/** The opening transition: what `initialState` returns for one config and
|
|
150
|
+
* seat count, drawing from the version-0 stream a `start` commit derives. */
|
|
151
|
+
interface InitialStateCase {
|
|
152
|
+
kind: "initialState";
|
|
153
|
+
name: string;
|
|
154
|
+
config: JsonObject;
|
|
155
|
+
playerCount: number;
|
|
156
|
+
rngSeed?: string;
|
|
157
|
+
expected: ExpectedEnvelope;
|
|
158
|
+
}
|
|
159
|
+
/** An `applyLifecycle` case: one engine-driven transition (a turn that ran
|
|
160
|
+
* out, a resign, a purged account) resolved against a stated position. */
|
|
161
|
+
interface LifecycleCase {
|
|
162
|
+
kind: "lifecycle";
|
|
163
|
+
name: string;
|
|
164
|
+
config: JsonObject;
|
|
165
|
+
state: JsonObject;
|
|
166
|
+
pending: number[];
|
|
167
|
+
type: LifecycleType;
|
|
168
|
+
/** The forfeiting seat. Required for `forfeit`/`autoForfeit`; a `timeout`
|
|
169
|
+
* carries no seat (its victims are `pending`), so it must be omitted. */
|
|
170
|
+
playerIndex?: number;
|
|
171
|
+
participantCount?: number;
|
|
172
|
+
rngSeed?: string;
|
|
173
|
+
expected: ExpectedEnvelope;
|
|
174
|
+
}
|
|
175
|
+
/** One transition of a {@link TranscriptCase}: a seat's move, or a resign. */
|
|
176
|
+
type TranscriptTransition = {
|
|
177
|
+
kind: "game";
|
|
178
|
+
playerIndex: number;
|
|
179
|
+
data: JsonObject;
|
|
180
|
+
} | {
|
|
181
|
+
kind: "lifecycle";
|
|
182
|
+
type: "forfeit";
|
|
183
|
+
playerIndex: number;
|
|
184
|
+
};
|
|
185
|
+
/** A whole match, replayed through the real kernel from a stated base seed.
|
|
186
|
+
* The one case that pins the *engine's* bookkeeping — versions, pending
|
|
187
|
+
* hand-off, the per-transition RNG streams — rather than a single hook. */
|
|
188
|
+
interface TranscriptCase {
|
|
189
|
+
kind: "transcript";
|
|
190
|
+
name: string;
|
|
191
|
+
config: JsonObject;
|
|
192
|
+
playerCount: number;
|
|
193
|
+
/** The game's base RNG seed: every transition's stream derives from it. */
|
|
194
|
+
seed: string;
|
|
195
|
+
transitions: TranscriptTransition[];
|
|
196
|
+
expected: ExpectedEnvelope & {
|
|
197
|
+
version: number;
|
|
198
|
+
status: "active" | "finished";
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/** Recorded draws from one derived RNG stream. Generated by
|
|
202
|
+
* {@link rngFixtureCase}, never hand-written: the values ARE the TypeScript
|
|
203
|
+
* kernel's, and the Dart port has to reproduce them bit for bit. */
|
|
204
|
+
interface RngCase {
|
|
205
|
+
kind: "rng";
|
|
206
|
+
name: string;
|
|
207
|
+
seed: string;
|
|
208
|
+
/** The state version the stream belongs to (`deriveRng`'s second arg). */
|
|
209
|
+
version: number;
|
|
210
|
+
/** Present ⇒ the bot stream for that seat, keyed `"<seed>:bot<seat>"`. */
|
|
211
|
+
seat?: number;
|
|
212
|
+
draws: number[];
|
|
213
|
+
}
|
|
214
|
+
type TwinFixtureCase = ActionCase | PlayerLimitsCase | RatingPoolCase | BotSeatableCase | InitialStateCase | LifecycleCase | TranscriptCase | RngCase;
|
|
144
215
|
/** Validate one fixture file's parsed JSON, or throw naming the offending
|
|
145
216
|
* file, case, and field. Exported so a repo can lint its fixtures without
|
|
146
217
|
* running them. */
|
|
147
218
|
export declare function parseTwinFixtureFile(path: string, json: unknown): TwinFixtureFile;
|
|
148
219
|
/** Run one fixture case against a rules unit, returning failure descriptions
|
|
149
220
|
* (empty ⇒ the case passes). Pure; the file-reading test registrar is
|
|
150
|
-
* {@link twinFixtureTests}.
|
|
151
|
-
|
|
221
|
+
* {@link twinFixtureTests}.
|
|
222
|
+
*
|
|
223
|
+
* `schemaVersion` is the version the case targets. It never selects
|
|
224
|
+
* behavior — the caller already resolved `rules` from it — and only labels
|
|
225
|
+
* the engine guard messages a `lifecycle` or `transcript` case can provoke;
|
|
226
|
+
* {@link twinFixtureTests} passes the fixture file's. */
|
|
227
|
+
export declare function evaluateTwinCase(rules: GameRules, kase: TwinFixtureCase, schemaVersion?: number): string[];
|
|
152
228
|
/** Register one vitest test per fixture case found under `fixturesRoot`
|
|
153
229
|
* (layout: `<root>/v<N>/*.json`). Call at the top level of a test module
|
|
154
230
|
* running in a Node environment. */
|
|
155
231
|
export declare function twinFixtureTests(gameModule: GameModule, fixturesRoot: string | URL): void;
|
|
232
|
+
/** What stream to record, and how much of it. */
|
|
233
|
+
interface RngFixtureCaseOptions {
|
|
234
|
+
/** The case name, used as the test name on both sides. */
|
|
235
|
+
name: string;
|
|
236
|
+
/** The game's base seed. */
|
|
237
|
+
seed: string;
|
|
238
|
+
/** The state version whose stream to record. */
|
|
239
|
+
version: number;
|
|
240
|
+
/** Record the bot stream for this seat instead of the transition's own. */
|
|
241
|
+
seat?: number;
|
|
242
|
+
/** How many draws to record (1 to 64). */
|
|
243
|
+
count: number;
|
|
244
|
+
}
|
|
245
|
+
/** Record one derived stream from the real kernel as a fixture case. */
|
|
246
|
+
export declare function rngFixtureCase(options: RngFixtureCaseOptions): RngCase;
|
|
247
|
+
/**
|
|
248
|
+
* Write a fixture file of generated `rng` cases.
|
|
249
|
+
*
|
|
250
|
+
* The `schemaVersion` is read from the `v<N>/` directory in `path`, the same
|
|
251
|
+
* rule `eigen-contract` enforces over every fixture file, so the two can
|
|
252
|
+
* never be written out of agreement. The document is validated before it is
|
|
253
|
+
* written: a generator that emits something the runners would refuse to load
|
|
254
|
+
* should fail at the generator.
|
|
255
|
+
*
|
|
256
|
+
* The output is plain two-space JSON. Only the values mean anything to either
|
|
257
|
+
* runner, so a repo that formats JSON should run its formatter afterwards.
|
|
258
|
+
*
|
|
259
|
+
* ```ts
|
|
260
|
+
* import { rngFixtureCase, writeRngFixture } from "@eigeninteractive/testkit";
|
|
261
|
+
*
|
|
262
|
+
* writeRngFixture("src/module/fixtures/v1/rng.json", [
|
|
263
|
+
* rngFixtureCase({ name: "version 0", seed: "twin-fixtures", version: 0, count: 16 }),
|
|
264
|
+
* rngFixtureCase({ name: "seat 1's bot stream", seed: "twin-fixtures", version: 3, seat: 1, count: 16 }),
|
|
265
|
+
* ]);
|
|
266
|
+
* ```
|
|
267
|
+
*/
|
|
268
|
+
export declare function writeRngFixture(path: string, cases: RngCase[]): void;
|
|
156
269
|
/** Structural JSON equality. Object keys with `undefined` values count as
|
|
157
270
|
* absent (matching how schema libraries model optional fields); array order
|
|
158
271
|
* matters. */
|
|
159
272
|
export declare function deepEquals(a: Json | undefined, b: Json | undefined): boolean;
|
|
160
273
|
//#endregion
|
|
161
|
-
export { type ActionCase, type BotSeatableCase, type BuildGameContractOptions, type CommitInput, type CommitPlan, DEADLINE_GRACE_MS, type Effect, type EmitGameContractOptions, type GameContract, type GameContractFixture, type GameContractVersion, type GameRow, type Intent, type ObservationFrame, type PlayerLimitsCase, type RatingPoolCase, type RejectCode, type Rejected, type Seat, type SeatView, type StateRow, type TwinFixtureCase, type TwinFixtureFile, commit, deriveRng, isRejected, randomSeed };
|
|
274
|
+
export { type ActionCase, type BotSeatableCase, type BuildGameContractOptions, type CommitInput, type CommitPlan, DEADLINE_GRACE_MS, type Effect, type EmitGameContractOptions, type ExpectedEnvelope, type GameContract, type GameContractFixture, type GameContractVersion, type GameRow, type InitialStateCase, type Intent, type LifecycleCase, type ObservationFrame, type PlayerLimitsCase, type RatingPoolCase, type RejectCode, type Rejected, type RngCase, type RngFixtureCaseOptions, type Seat, type SeatView, type StateRow, type TranscriptCase, type TranscriptTransition, type TwinFixtureCase, type TwinFixtureFile, commit, deriveRng, isRejected, randomSeed };
|
|
162
275
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/contract-command.ts","../src/game-contract.ts","../src/kernel-scenarios.ts","../src/twin-fixtures.ts"],"mappings":";;;;;;;;;wBAkEsB,2BAA2B,gBAAuB;;;;;;wBASlD,4BAA4B,gBAAuB;;;;qBC5D5D;;UAGI;;EAEf;;EAEA;;;UAIe;EACf;IACE,OAAO;IACP,aAAa;IACb,QAAQ;IACR,QAAQ;;;;UAKK;EACf,sBAAsB;EACtB;EACA,UAAU,eAAe;EACzB,UAAU;;;UAIK;;EAEf;;EAEA,YAAY;;EAEZ,wBAAwB;;;UAIT,gCAAgC;;EAE/C,iBAAiB;;;wBA6FH,kBAAkB,SAAS,2BAA2B;;wBAiCtD,iBAAiB,SAAS;;wBAK1B,iBAAiB,SAAS;;wBAM1B,kBAAkB,SAAS;;qBAQ9B,uBAAwB;;;;;;wBCnKrB,YACd,OAAO,WACP;EACE,OAAO;EACP;;EAEA;EACA,QAAQ;EACR,QAAQ;EACR;EACA;IAED;;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/contract-command.ts","../src/game-contract.ts","../src/kernel-scenarios.ts","../src/twin-fixtures.ts"],"mappings":";;;;;;;;;wBAkEsB,2BAA2B,gBAAuB;;;;;;wBASlD,4BAA4B,gBAAuB;;;;qBC5D5D;;UAGI;;EAEf;;EAEA;;;UAIe;EACf;IACE,OAAO;IACP,aAAa;IACb,QAAQ;IACR,QAAQ;;;;UAKK;EACf,sBAAsB;EACtB;EACA,UAAU,eAAe;EACzB,UAAU;;;UAIK;;EAEf;;EAEA,YAAY;;EAEZ,wBAAwB;;;UAIT,gCAAgC;;EAE/C,iBAAiB;;;wBA6FH,kBAAkB,SAAS,2BAA2B;;wBAiCtD,iBAAiB,SAAS;;wBAK1B,iBAAiB,SAAS;;wBAM1B,kBAAkB,SAAS;;qBAQ9B,uBAAwB;;;;;;wBCnKrB,YACd,OAAO,WACP;EACE,OAAO;EACP;;EAEA;EACA,QAAQ;EACR,QAAQ;EACR;EACA;IAED;;;;UC6Gc;EACf;EACA,OAAO;;;;;;UAOQ;EACf,QAAQ;EACR;EACA,UAAU;;;;UAKK;EACf;EACA;EACA,QAAQ;EACR,OAAO;;EAEP,MAAM;EACN;EACA;EACA;EACA;EACA,QAAQ;EACR,UAAU;IACR;IACA,cAAc;;;;UAKD;EACf;EACA;EACA,QAAQ;EACR;EACA;EACA;EACA;EACA;EACA,QAAQ;EACR;;;UAIe;EACf;EACA;EACA,QAAQ;EACR;IAAY;IAAoB;;;;UAIjB;EACf;EACA;EACA,YAAY;EACZ,WAAW;EACX;;;;UAKe;EACf;EACA;EACA,QAAQ;EACR;EACA;EACA,UAAU;;;;UAKK;EACf;EACA;EACA,QAAQ;EACR,OAAO;EACP;EACA,MAAM;;;EAGN;EACA;EACA;EACA,UAAU;;;KAIA;EAAyB;EAAc;EAAqB,MAAM;;EAAiB;EAAmB;EAAiB;;;;;UAKlH;EACf;EACA;EACA,QAAQ;EACR;;EAEA;EACA,aAAa;EACb,UAAU;IACR;IACA;;;;;;UAOa;EACf;EACA;EACA;;EAEA;;EAEA;EACA;;KAGU,kBAAkB,aAAa,mBAAmB,iBAAiB,kBAAkB,mBAAmB,gBAAgB,iBAAiB;;;;wBA0QrI,qBAAqB,cAAc,gBAAgB;;;;;;;;;wBA0CnD,iBAAiB,OAAO,WAAW,MAAM,iBAAiB;;;;wBA0B1D,iBAAiB,YAAY,YAAY,uBAAuB;;UAsW/D;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;;wBAIc,eAAe,SAAS,wBAAwB;;;;;;;;;;;;;;;;;;;;;;wBAqChD,gBAAgB,cAAc,OAAO;;;;wBAgDrC,WAAW,GAAG,kBAAkB,GAAG"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as checkGameContract, c as gameContractJson, d as parseTwinFixtureFile, f as
|
|
1
|
+
import { a as checkGameContract, c as gameContractJson, d as parseTwinFixtureFile, f as rngFixtureCase, i as buildGameContract, l as deepEquals, m as writeRngFixture, n as emitConfiguredGameContract, o as emitGameContract, p as twinFixtureTests, r as GAME_CONTRACT_FORMAT_VERSION, s as gameContractFilename, t as checkConfiguredGameContract, u as evaluateTwinCase } from "./contract-command-Dn5ANpqD.js";
|
|
2
2
|
import { DEADLINE_GRACE_MS, assertHookPayload, commit, deriveRng, isRejected, randomSeed } from "@eigeninteractive/kernel";
|
|
3
3
|
//#region src/kernel-scenarios.ts
|
|
4
4
|
/**
|
|
@@ -31,6 +31,6 @@ function projectView(rules, args) {
|
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
33
|
//#endregion
|
|
34
|
-
export { DEADLINE_GRACE_MS, GAME_CONTRACT_FORMAT_VERSION, buildGameContract, checkConfiguredGameContract, checkGameContract, commit, deepEquals, deriveRng, emitConfiguredGameContract, emitGameContract, evaluateTwinCase, gameContractFilename, gameContractJson, isRejected, parseTwinFixtureFile, projectView, randomSeed, twinFixtureTests };
|
|
34
|
+
export { DEADLINE_GRACE_MS, GAME_CONTRACT_FORMAT_VERSION, buildGameContract, checkConfiguredGameContract, checkGameContract, commit, deepEquals, deriveRng, emitConfiguredGameContract, emitGameContract, evaluateTwinCase, gameContractFilename, gameContractJson, isRejected, parseTwinFixtureFile, projectView, randomSeed, rngFixtureCase, twinFixtureTests, writeRngFixture };
|
|
35
35
|
|
|
36
36
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eigeninteractive/testkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|
|
@@ -37,16 +37,16 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"rand-seed": "^3.0.0",
|
|
39
39
|
"tsx": "^4.23.13",
|
|
40
|
-
"@eigeninteractive/kernel": "0.
|
|
40
|
+
"@eigeninteractive/kernel": "0.7.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@types/node": "^26.
|
|
43
|
+
"@types/node": "^26.5.0",
|
|
44
44
|
"vitest": "^4.1.11",
|
|
45
|
-
"@eigeninteractive/rules": "0.
|
|
45
|
+
"@eigeninteractive/rules": "0.7.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"vitest": "^4.1.0",
|
|
49
|
-
"@eigeninteractive/rules": "^0.
|
|
49
|
+
"@eigeninteractive/rules": "^0.7.0"
|
|
50
50
|
},
|
|
51
51
|
"homepage": "https://eigeninteractive.com",
|
|
52
52
|
"repository": {
|
package/src/index.ts
CHANGED
|
@@ -43,11 +43,20 @@ export {
|
|
|
43
43
|
type ActionCase,
|
|
44
44
|
type BotSeatableCase,
|
|
45
45
|
deepEquals,
|
|
46
|
+
type ExpectedEnvelope,
|
|
46
47
|
evaluateTwinCase,
|
|
48
|
+
type InitialStateCase,
|
|
49
|
+
type LifecycleCase,
|
|
47
50
|
type PlayerLimitsCase,
|
|
48
51
|
parseTwinFixtureFile,
|
|
49
52
|
type RatingPoolCase,
|
|
53
|
+
type RngCase,
|
|
54
|
+
type RngFixtureCaseOptions,
|
|
55
|
+
rngFixtureCase,
|
|
56
|
+
type TranscriptCase,
|
|
57
|
+
type TranscriptTransition,
|
|
50
58
|
type TwinFixtureCase,
|
|
51
59
|
type TwinFixtureFile,
|
|
52
60
|
twinFixtureTests,
|
|
61
|
+
writeRngFixture,
|
|
53
62
|
} from "./twin-fixtures.js";
|