@eigeninteractive/testkit 0.5.1 → 0.6.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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract-command-aCqO5CyJ.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 + `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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"}
package/dist/index.d.ts CHANGED
@@ -1,11 +1,162 @@
1
+ import { GameAccess, GameModule, GameRules, Json, JsonObject, OutcomeEntry, TransitionCause } from "@eigeninteractive/rules";
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
+ //#region src/contract-command.d.ts
1
4
  /**
2
- * `@eigeninteractive/testkit`: drive a game's rules through the real kernel without a
3
- * Worker, a database or a network. Build a table, submit actions as seats,
4
- * assert on the resulting transitions and per-seat observations.
5
+ * Emits `game-contract.json` from an EigenInteractive package's conventional layout.
5
6
  *
6
- * @module @eigeninteractive/testkit
7
+ * This is the programmatic form of the `eigen-contract` executable. Most
8
+ * games should invoke the executable through their package script.
7
9
  */
8
- export { checkConfiguredGameContract, emitConfiguredGameContract } from "./contract-command.js";
9
- export { type BuildGameContractOptions, buildGameContract, checkGameContract, type EmitGameContractOptions, emitGameContract, GAME_CONTRACT_FORMAT_VERSION, type GameContract, type GameContractFixture, type GameContractVersion, gameContractFilename, gameContractJson, } from "./game-contract.js";
10
- export { type CommitInput, type CommitPlan, commit, DEADLINE_GRACE_MS, deriveRng, type Effect, type GameRow, type Intent, isRejected, type ObservationFrame, projectView, type RejectCode, type Rejected, randomSeed, type Seat, type SeatView, type StateRow, } from "./kernel-scenarios.js";
11
- export { type ActionCase, type BotSeatableCase, deepEquals, evaluateTwinCase, type PlayerLimitsCase, parseTwinFixtureFile, type RatingPoolCase, type TwinFixtureCase, type TwinFixtureFile, twinFixtureTests, } from "./twin-fixtures.js";
10
+ declare function emitConfiguredGameContract(root?: string): Promise<void>;
11
+ /**
12
+ * Fails when the conventionally configured contract is absent or stale.
13
+ *
14
+ * Use this in CI through `eigen-contract --check`.
15
+ */
16
+ declare function checkConfiguredGameContract(root?: string): Promise<void>;
17
+ //#endregion
18
+ //#region src/game-contract.d.ts
19
+ /** Current format of the language-neutral contract consumed by EigenInteractive's Dart generator. */
20
+ declare const GAME_CONTRACT_FORMAT_VERSION = 1;
21
+ /** One validated twin-fixture document embedded in a {@link GameContract}. */
22
+ interface GameContractFixture {
23
+ /** POSIX-style path relative to the supplied fixtures root. */
24
+ path: string;
25
+ /** Validated fixture document, retained in its original JSON shape. */
26
+ document: unknown;
27
+ }
28
+ /** The four JSON Schemas emitted for one game `schemaVersion`. */
29
+ interface GameContractVersion {
30
+ schemas: {
31
+ state: Record<string, unknown>;
32
+ observation: Record<string, unknown>;
33
+ action: Record<string, unknown>;
34
+ config: Record<string, unknown>;
35
+ };
36
+ }
37
+ /** Language-neutral schemas and fixtures shared by a game's Worker and app. */
38
+ interface GameContract {
39
+ formatVersion: typeof GAME_CONTRACT_FORMAT_VERSION;
40
+ game: string;
41
+ versions: Record<string, GameContractVersion>;
42
+ fixtures: GameContractFixture[];
43
+ }
44
+ /** Inputs for building a {@link GameContract} without writing it. */
45
+ interface BuildGameContractOptions {
46
+ /** Stable display name used as the generated Dart type prefix. */
47
+ game: string;
48
+ /** Authoritative TypeScript rules registry. */
49
+ gameModule: GameModule;
50
+ /** Root containing `v<N>/*.json` twin fixtures. */
51
+ fixturesRoot?: string | URL;
52
+ }
53
+ /** Inputs for emitting or checking a {@link GameContract} file. */
54
+ interface EmitGameContractOptions extends BuildGameContractOptions {
55
+ /** Destination `game-contract.json` path. */
56
+ output: string | URL;
57
+ }
58
+ /** Build a deterministic in-memory contract without touching the filesystem. */
59
+ declare function buildGameContract(options: BuildGameContractOptions): GameContract;
60
+ /** Render one deterministic, newline-terminated contract document. */
61
+ declare function gameContractJson(options: BuildGameContractOptions): string;
62
+ /** Emit one deterministic, newline-terminated `game-contract.json`. */
63
+ declare function emitGameContract(options: EmitGameContractOptions): void;
64
+ /** Fail when an emitted contract is missing or differs from its inputs. */
65
+ declare function checkGameContract(options: EmitGameContractOptions): void;
66
+ /** A useful default filename for scripts that accept an output directory. */
67
+ declare const gameContractFilename: (game: string) => string;
68
+ //#endregion
69
+ //#region src/kernel-scenarios.d.ts
70
+ /** Project one seat's view of a state: the stored-frame shape the same-view
71
+ * rule compares (`commit()`'s `staleViews` input). Convenience for scenario
72
+ * tests that replay a simultaneous-move race. */
73
+ declare function projectView(rules: GameRules, args: {
74
+ state: JsonObject;
75
+ pending: number[];
76
+ /** The seat to project for, or null for a viewer. */
77
+ seat: number | null;
78
+ config: JsonObject;
79
+ cause?: TransitionCause;
80
+ participantCount?: number;
81
+ isReplay?: boolean;
82
+ }): SeatView$1;
83
+ //#endregion
84
+ //#region src/twin-fixtures.d.ts
85
+ /** One fixture file: cases targeting one `schemaVersion` unit. */
86
+ interface TwinFixtureFile {
87
+ schemaVersion: number;
88
+ cases: TwinFixtureCase[];
89
+ }
90
+ /** A game-action case: exercises schemas, `applyAction`, and (through
91
+ * `expected.observation`) `computeObservation` for the acting seat. */
92
+ interface ActionCase {
93
+ kind: "action";
94
+ name: string;
95
+ config: JsonObject;
96
+ state: JsonObject;
97
+ /** Dart-side observation payload; unused here (defaults to `state`). */
98
+ obs?: JsonObject;
99
+ pending: number[];
100
+ playerIndex: number;
101
+ participantCount?: number;
102
+ rngSeed?: string;
103
+ action: JsonObject;
104
+ expected: {
105
+ valid: boolean;
106
+ state?: JsonObject;
107
+ pending?: number[];
108
+ outcome?: OutcomeEntry[] | null;
109
+ observation?: JsonObject;
110
+ };
111
+ }
112
+ /** A `ratingPool` predicate case. Omitted timing fields mean null. */
113
+ interface RatingPoolCase {
114
+ kind: "ratingPool";
115
+ name: string;
116
+ access: GameAccess;
117
+ turnSeconds?: number | null;
118
+ budgetSeconds?: number | null;
119
+ incrementSeconds?: number | null;
120
+ minPlayers: number;
121
+ maxPlayers: number;
122
+ config: JsonObject;
123
+ expected: string | null;
124
+ }
125
+ /** A `playerLimits` case: the seats one config may be played with. */
126
+ interface PlayerLimitsCase {
127
+ kind: "playerLimits";
128
+ name: string;
129
+ config: JsonObject;
130
+ expected: {
131
+ minPlayers: number;
132
+ maxPlayers: number;
133
+ };
134
+ }
135
+ /** A `botSeatable` predicate case. */
136
+ interface BotSeatableCase {
137
+ kind: "botSeatable";
138
+ name: string;
139
+ gameConfig: JsonObject;
140
+ botConfig: JsonObject;
141
+ expected: boolean;
142
+ }
143
+ type TwinFixtureCase = ActionCase | PlayerLimitsCase | RatingPoolCase | BotSeatableCase;
144
+ /** Validate one fixture file's parsed JSON, or throw naming the offending
145
+ * file, case, and field. Exported so a repo can lint its fixtures without
146
+ * running them. */
147
+ declare function parseTwinFixtureFile(path: string, json: unknown): TwinFixtureFile;
148
+ /** Run one fixture case against a rules unit, returning failure descriptions
149
+ * (empty ⇒ the case passes). Pure; the file-reading test registrar is
150
+ * {@link twinFixtureTests}. */
151
+ declare function evaluateTwinCase(rules: GameRules, kase: TwinFixtureCase): string[];
152
+ /** Register one vitest test per fixture case found under `fixturesRoot`
153
+ * (layout: `<root>/v<N>/*.json`). Call at the top level of a test module
154
+ * running in a Node environment. */
155
+ declare function twinFixtureTests(gameModule: GameModule, fixturesRoot: string | URL): void;
156
+ /** Structural JSON equality. Object keys with `undefined` values count as
157
+ * absent (matching how schema libraries model optional fields); array order
158
+ * matters. */
159
+ declare function deepEquals(a: Json | undefined, b: Json | undefined): boolean;
160
+ //#endregion
161
+ export { type ActionCase, type BotSeatableCase, type BuildGameContractOptions, type CommitInput, type CommitPlan, DEADLINE_GRACE_MS, type Effect, type EmitGameContractOptions, GAME_CONTRACT_FORMAT_VERSION, 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, buildGameContract, checkConfiguredGameContract, checkGameContract, commit, deepEquals, deriveRng, emitConfiguredGameContract, emitGameContract, evaluateTwinCase, gameContractFilename, gameContractJson, isRejected, parseTwinFixtureFile, projectView, randomSeed, twinFixtureTests };
162
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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":";;;;;;;;;iBAkEsB,2BAA2B,gBAAuB;;;;;;iBASlD,4BAA4B,gBAAuB;;;;cC5D5D;;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;;;iBA6FH,kBAAkB,SAAS,2BAA2B;;iBAiCtD,iBAAiB,SAAS;;iBAK1B,iBAAiB,SAAS;;iBAM1B,kBAAkB,SAAS;;cAQ9B,uBAAwB;;;;;;iBCnKrB,YACd,OAAO,WACP;EACE,OAAO;EACP;;EAEA;EACA,QAAQ;EACR,QAAQ;EACR;EACA;IAED;;;;UC0Bc;EACf;EACA,OAAO;;;;UAKQ;EACf;EACA;EACA,QAAQ;EACR,OAAO;;EAEP,MAAM;EACN;EACA;EACA;EACA;EACA,QAAQ;EACR;IACE;IACA,QAAQ;IACR;IACA,UAAU;IACV,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;;KAGU,kBAAkB,aAAa,mBAAmB,iBAAiB;;;;iBAsI/D,qBAAqB,cAAc,gBAAgB;;;;iBA6BnD,iBAAiB,OAAO,WAAW,MAAM;;;;iBAkBzC,iBAAiB,YAAY,YAAY,uBAAuB;;;;iBAyMhE,WAAW,GAAG,kBAAkB,GAAG"}
package/dist/index.js CHANGED
@@ -1,58 +1,36 @@
1
- import {
2
- GAME_CONTRACT_FORMAT_VERSION,
3
- buildGameContract,
4
- checkConfiguredGameContract,
5
- checkGameContract,
6
- deepEquals,
7
- emitConfiguredGameContract,
8
- emitGameContract,
9
- evaluateTwinCase,
10
- gameContractFilename,
11
- gameContractJson,
12
- parseTwinFixtureFile,
13
- twinFixtureTests
14
- } from "./chunk-BQIY636F.js";
15
-
16
- // src/kernel-scenarios.ts
17
- import { assertHookPayload } from "@eigeninteractive/kernel";
18
- import {
19
- commit,
20
- DEADLINE_GRACE_MS,
21
- deriveRng,
22
- isRejected,
23
- randomSeed
24
- } from "@eigeninteractive/kernel";
1
+ import { a as checkGameContract, c as gameContractJson, d as parseTwinFixtureFile, f as twinFixtureTests, i as buildGameContract, l as deepEquals, n as emitConfiguredGameContract, o as emitGameContract, r as GAME_CONTRACT_FORMAT_VERSION, s as gameContractFilename, t as checkConfiguredGameContract, u as evaluateTwinCase } from "./contract-command-aCqO5CyJ.js";
2
+ import { DEADLINE_GRACE_MS, assertHookPayload, commit, deriveRng, isRejected, randomSeed } from "@eigeninteractive/kernel";
3
+ //#region src/kernel-scenarios.ts
4
+ /**
5
+ * The kernel-scenario surface for game tests: how an implementor drives real
6
+ * engine transitions over their rules unit (same-view pairs, timing/grace
7
+ * behavior, lifecycle outcomes) without depending on the engine's internals.
8
+ *
9
+ * `@eigeninteractive/kernel` is an internal package: implementors ship `@eigeninteractive/rules` +
10
+ * `@eigeninteractive/server` and dev-depend on `@eigeninteractive/testkit` alone. Everything a
11
+ * game's test suite legitimately needs from the kernel is re-exported here,
12
+ * so the kernel's own surface stays free to churn.
13
+ */
14
+ /** Project one seat's view of a state: the stored-frame shape the same-view
15
+ * rule compares (`commit()`'s `staleViews` input). Convenience for scenario
16
+ * tests that replay a simultaneous-move race. */
25
17
  function projectView(rules, args) {
26
- const slice = rules.computeObservation({
27
- state: args.state,
28
- pending: args.pending,
29
- playerIndex: args.seat,
30
- participantCount: args.participantCount ?? 2,
31
- config: args.config,
32
- cause: args.cause ?? null,
33
- isReplay: args.isReplay ?? false
34
- });
35
- assertHookPayload(rules.schemas.observation, slice.data, `computeObservation for ${args.seat === null ? "viewer" : `seat ${args.seat}`}`);
36
- return { data: slice.data, pendingPlayers: slice.pendingPlayers };
18
+ const slice = rules.computeObservation({
19
+ state: args.state,
20
+ pending: args.pending,
21
+ playerIndex: args.seat,
22
+ participantCount: args.participantCount ?? 2,
23
+ config: args.config,
24
+ cause: args.cause ?? null,
25
+ isReplay: args.isReplay ?? false
26
+ });
27
+ assertHookPayload(rules.schemas.observation, slice.data, `computeObservation for ${args.seat === null ? "viewer" : `seat ${args.seat}`}`);
28
+ return {
29
+ data: slice.data,
30
+ pendingPlayers: slice.pendingPlayers
31
+ };
37
32
  }
38
- export {
39
- DEADLINE_GRACE_MS,
40
- GAME_CONTRACT_FORMAT_VERSION,
41
- buildGameContract,
42
- checkConfiguredGameContract,
43
- checkGameContract,
44
- commit,
45
- deepEquals,
46
- deriveRng,
47
- emitConfiguredGameContract,
48
- emitGameContract,
49
- evaluateTwinCase,
50
- gameContractFilename,
51
- gameContractJson,
52
- isRejected,
53
- parseTwinFixtureFile,
54
- projectView,
55
- randomSeed,
56
- twinFixtureTests
57
- };
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 };
35
+
58
36
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/kernel-scenarios.ts"],"sourcesContent":["/**\n * The kernel-scenario surface for game tests: how an implementor drives real\n * engine transitions over their rules unit (same-view pairs, timing/grace\n * behavior, lifecycle outcomes) without depending on the engine's internals.\n *\n * `@eigeninteractive/kernel` is an internal package: implementors ship `@eigeninteractive/rules` +\n * `@eigeninteractive/server` and dev-depend on `@eigeninteractive/testkit` alone. Everything a\n * game's test suite legitimately needs from the kernel is re-exported here,\n * so the kernel's own surface stays free to churn.\n */\n\nimport { assertHookPayload, type SeatView } from \"@eigeninteractive/kernel\";\nimport type { GameRules, JsonObject, TransitionCause } from \"@eigeninteractive/rules\";\n\nexport type {\n CommitInput,\n CommitPlan,\n Effect,\n GameRow,\n Intent,\n ObservationFrame,\n RejectCode,\n Rejected,\n Seat,\n SeatView,\n StateRow,\n} from \"@eigeninteractive/kernel\";\nexport {\n commit,\n DEADLINE_GRACE_MS,\n deriveRng,\n isRejected,\n randomSeed,\n} from \"@eigeninteractive/kernel\";\n\n/** Project one seat's view of a state: the stored-frame shape the same-view\n * rule compares (`commit()`'s `staleViews` input). Convenience for scenario\n * tests that replay a simultaneous-move race. */\nexport function projectView(\n rules: GameRules,\n args: {\n state: JsonObject;\n pending: number[];\n /** The seat to project for, or null for a viewer. */\n seat: number | null;\n config: JsonObject;\n cause?: TransitionCause;\n participantCount?: number;\n isReplay?: boolean;\n },\n): SeatView {\n const slice = rules.computeObservation({\n state: args.state,\n pending: args.pending,\n playerIndex: args.seat,\n participantCount: args.participantCount ?? 2,\n config: args.config,\n cause: args.cause ?? null,\n isReplay: args.isReplay ?? false,\n });\n assertHookPayload(rules.schemas.observation, slice.data, `computeObservation for ${args.seat === null ? \"viewer\" : `seat ${args.seat}`}`);\n return { data: slice.data, pendingPlayers: slice.pendingPlayers };\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAWA,SAAS,yBAAwC;AAgBjD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAKA,SAAS,YACd,OACA,MAUU;AACV,QAAM,QAAQ,MAAM,mBAAmB;AAAA,IACrC,OAAO,KAAK;AAAA,IACZ,SAAS,KAAK;AAAA,IACd,aAAa,KAAK;AAAA,IAClB,kBAAkB,KAAK,oBAAoB;AAAA,IAC3C,QAAQ,KAAK;AAAA,IACb,OAAO,KAAK,SAAS;AAAA,IACrB,UAAU,KAAK,YAAY;AAAA,EAC7B,CAAC;AACD,oBAAkB,MAAM,QAAQ,aAAa,MAAM,MAAM,0BAA0B,KAAK,SAAS,OAAO,WAAW,QAAQ,KAAK,IAAI,EAAE,EAAE;AACxI,SAAO,EAAE,MAAM,MAAM,MAAM,gBAAgB,MAAM,eAAe;AAClE;","names":[]}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/kernel-scenarios.ts"],"sourcesContent":["/**\n * The kernel-scenario surface for game tests: how an implementor drives real\n * engine transitions over their rules unit (same-view pairs, timing/grace\n * behavior, lifecycle outcomes) without depending on the engine's internals.\n *\n * `@eigeninteractive/kernel` is an internal package: implementors ship `@eigeninteractive/rules` +\n * `@eigeninteractive/server` and dev-depend on `@eigeninteractive/testkit` alone. Everything a\n * game's test suite legitimately needs from the kernel is re-exported here,\n * so the kernel's own surface stays free to churn.\n */\n\nimport { assertHookPayload, type SeatView } from \"@eigeninteractive/kernel\";\nimport type { GameRules, JsonObject, TransitionCause } from \"@eigeninteractive/rules\";\n\nexport type {\n CommitInput,\n CommitPlan,\n Effect,\n GameRow,\n Intent,\n ObservationFrame,\n RejectCode,\n Rejected,\n Seat,\n SeatView,\n StateRow,\n} from \"@eigeninteractive/kernel\";\nexport {\n commit,\n DEADLINE_GRACE_MS,\n deriveRng,\n isRejected,\n randomSeed,\n} from \"@eigeninteractive/kernel\";\n\n/** Project one seat's view of a state: the stored-frame shape the same-view\n * rule compares (`commit()`'s `staleViews` input). Convenience for scenario\n * tests that replay a simultaneous-move race. */\nexport function projectView(\n rules: GameRules,\n args: {\n state: JsonObject;\n pending: number[];\n /** The seat to project for, or null for a viewer. */\n seat: number | null;\n config: JsonObject;\n cause?: TransitionCause;\n participantCount?: number;\n isReplay?: boolean;\n },\n): SeatView {\n const slice = rules.computeObservation({\n state: args.state,\n pending: args.pending,\n playerIndex: args.seat,\n participantCount: args.participantCount ?? 2,\n config: args.config,\n cause: args.cause ?? null,\n isReplay: args.isReplay ?? false,\n });\n assertHookPayload(rules.schemas.observation, slice.data, `computeObservation for ${args.seat === null ? \"viewer\" : `seat ${args.seat}`}`);\n return { data: slice.data, pendingPlayers: slice.pendingPlayers };\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAsCA,SAAgB,YACd,OACA,MAUU;CACV,MAAM,QAAQ,MAAM,mBAAmB;EACrC,OAAO,KAAK;EACZ,SAAS,KAAK;EACd,aAAa,KAAK;EAClB,kBAAkB,KAAK,oBAAoB;EAC3C,QAAQ,KAAK;EACb,OAAO,KAAK,SAAS;EACrB,UAAU,KAAK,YAAY;CAC7B,CAAC;CACD,kBAAkB,MAAM,QAAQ,aAAa,MAAM,MAAM,0BAA0B,KAAK,SAAS,OAAO,WAAW,QAAQ,KAAK,QAAQ;CACxI,OAAO;EAAE,MAAM,MAAM;EAAM,gBAAgB,MAAM;CAAe;AAClE"}
@@ -1,16 +1 @@
1
- #!/usr/bin/env node
2
- /**
3
- * `eigen-inspect`: read a Worker's local `.wrangler` state from the terminal.
4
- *
5
- * Deliberately narrow. Wrangler already ships a first-party browser for local
6
- * resources (press `e` in `wrangler dev`, or open `/cdn-cgi/explorer`), and
7
- * generic table browsing is its job, not this tool's. What that view cannot do
8
- * is answer a question in the engine's own terms, because a game's truth is
9
- * split across two stores on purpose: the D1 index and the game's own Durable
10
- * Object. This reads both, decodes the JSON columns, prints the transition log
11
- * as a timeline, and says what the game is waiting for.
12
- *
13
- * It also runs with no dev server, no browser and no account, and emits
14
- * `--json` for a script or an agent.
15
- */
16
- export {};
1
+ export {}