@alpaca-software/40kdc-data 0.4.0 → 0.4.6
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 +9 -3
- package/dist/data/bundle.generated.js +1 -1
- package/dist/data/bundle.generated.js.map +1 -1
- package/dist/fix-corner-ruin.d.ts +2 -0
- package/dist/fix-corner-ruin.d.ts.map +1 -0
- package/dist/fix-corner-ruin.js +160 -0
- package/dist/fix-corner-ruin.js.map +1 -0
- package/dist/gen-conformance.js +151 -0
- package/dist/gen-conformance.js.map +1 -1
- package/dist/generated.d.ts +23 -0
- package/dist/generated.d.ts.map +1 -1
- package/dist/generated.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +162 -0
- package/dist/runner.js.map +1 -1
- package/dist/scoring/index.d.ts +28 -6
- package/dist/scoring/index.d.ts.map +1 -1
- package/dist/scoring/index.js +31 -7
- package/dist/scoring/index.js.map +1 -1
- package/dist/terrain/index.d.ts +2 -2
- package/dist/terrain/index.d.ts.map +1 -1
- package/dist/terrain/index.js +1 -1
- package/dist/terrain/index.js.map +1 -1
- package/dist/terrain/solve.d.ts +41 -0
- package/dist/terrain/solve.d.ts.map +1 -1
- package/dist/terrain/solve.js +100 -0
- package/dist/terrain/solve.js.map +1 -1
- package/package.json +2 -2
- package/schemas/core/secondary-card.schema.json +10 -0
- package/schemas/core/terrain-layout.schema.json +18 -0
- package/schemas/enrichment/ability-dsl/condition.schema.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fix-corner-ruin.d.ts","sourceRoot":"","sources":["../src/fix-corner-ruin.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-shot correction (refuses to re-run once applied) of the `corner-ruin-left` /
|
|
3
|
+
* `corner-ruin-right` catalog templates: the long leg is 7″ but the physical
|
|
4
|
+
* piece is 6.5″. Shrinks the leg in `data/core/terrain-templates.json` and
|
|
5
|
+
* compensates every layout piece referencing those templates in
|
|
6
|
+
* `data/core/terrain-layouts.json` so the resolved outer corner stays exactly
|
|
7
|
+
* where it was placed.
|
|
8
|
+
*
|
|
9
|
+
* Pieces are centroid-anchored (`board = position + R·M·(v − c)`), so changing
|
|
10
|
+
* the footprint moves the centroid `c` and would shift the whole piece. Keeping
|
|
11
|
+
* any unchanged vertex fixed requires `position += R·M·(c_new − c_old)` — the
|
|
12
|
+
* same correction in board space and in a parent area's centroid-local frame
|
|
13
|
+
* (the area transform is rigid and applied after).
|
|
14
|
+
*
|
|
15
|
+
* Pieces with an inline baked `footprint` (the migrated gw-11e-crucible corner
|
|
16
|
+
* segments, which carry the template id only as provenance) are intentionally
|
|
17
|
+
* untouched: the template edit cannot affect them.
|
|
18
|
+
*
|
|
19
|
+
* Verifies by resolving every layout before and after: unchanged footprint
|
|
20
|
+
* vertices must not move, the two shortened leg vertices must move by exactly
|
|
21
|
+
* 0.5″, and every piece not referencing the templates must be bit-identical.
|
|
22
|
+
*
|
|
23
|
+
* Usage: `npx tsx src/fix-corner-ruin.ts` (run from `tools/`).
|
|
24
|
+
*/
|
|
25
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import { resolveLayout, polygonCentroid, footprintVertices, } from "./terrain/resolve.js";
|
|
28
|
+
const REPO_ROOT = join(new URL("../..", import.meta.url).pathname);
|
|
29
|
+
const CATALOG_PATH = join(REPO_ROOT, "data", "core", "terrain-templates.json");
|
|
30
|
+
const LAYOUTS_PATH = join(REPO_ROOT, "data", "core", "terrain-layouts.json");
|
|
31
|
+
const OLD_LEG = 7;
|
|
32
|
+
const NEW_LEG = 6.5;
|
|
33
|
+
const TARGETS = new Set(["corner-ruin-left", "corner-ruin-right"]);
|
|
34
|
+
/** Resolver output is rounded to 1e-4; position rounding adds ≤5e-5 per axis. */
|
|
35
|
+
const TOL = 2e-4;
|
|
36
|
+
// Mirror → rotate, matching the resolver's orientation math (y-down, CW).
|
|
37
|
+
function applyMirror(v, m) {
|
|
38
|
+
if (m === "horizontal")
|
|
39
|
+
return { x: -v.x, y: v.y };
|
|
40
|
+
if (m === "vertical")
|
|
41
|
+
return { x: v.x, y: -v.y };
|
|
42
|
+
return v;
|
|
43
|
+
}
|
|
44
|
+
function rotateCw(v, deg) {
|
|
45
|
+
const r = (deg * Math.PI) / 180;
|
|
46
|
+
const c = Math.cos(r);
|
|
47
|
+
const s = Math.sin(r);
|
|
48
|
+
return { x: c * v.x - s * v.y, y: s * v.x + c * v.y };
|
|
49
|
+
}
|
|
50
|
+
const round4 = (n) => Math.round(n * 1e4) / 1e4;
|
|
51
|
+
function main() {
|
|
52
|
+
const catalog = JSON.parse(readFileSync(CATALOG_PATH, "utf8"));
|
|
53
|
+
const layouts = JSON.parse(readFileSync(LAYOUTS_PATH, "utf8"));
|
|
54
|
+
// ---- snapshot ground truth with the OLD catalog --------------------------
|
|
55
|
+
const before = new Map(layouts.map((l) => [l.id, resolveLayout(l, catalog)]));
|
|
56
|
+
// ---- patch the two templates, recording centroid deltas ------------------
|
|
57
|
+
const deltas = new Map();
|
|
58
|
+
for (const t of catalog) {
|
|
59
|
+
if (!TARGETS.has(t.id))
|
|
60
|
+
continue;
|
|
61
|
+
if (t.footprint.type !== "polygon")
|
|
62
|
+
throw new Error(`${t.id}: expected polygon footprint`);
|
|
63
|
+
const oldVerts = footprintVertices(t.footprint);
|
|
64
|
+
const movedIdx = [];
|
|
65
|
+
t.footprint.points.forEach((p, i) => {
|
|
66
|
+
if (p.y === OLD_LEG) {
|
|
67
|
+
p.y = NEW_LEG;
|
|
68
|
+
movedIdx.push(i);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
if (movedIdx.length !== 2) {
|
|
72
|
+
throw new Error(`${t.id}: expected exactly 2 leg vertices at y=${OLD_LEG}, found ${movedIdx.length}`);
|
|
73
|
+
}
|
|
74
|
+
const cOld = polygonCentroid(oldVerts);
|
|
75
|
+
const cNew = polygonCentroid(footprintVertices(t.footprint));
|
|
76
|
+
const delta = { x: cNew.x - cOld.x, y: cNew.y - cOld.y };
|
|
77
|
+
deltas.set(t.id, { delta, movedIdx });
|
|
78
|
+
console.log(`${t.id}: centroid (${round4(cOld.x)}, ${round4(cOld.y)}) → (${round4(cNew.x)}, ${round4(cNew.y)})`);
|
|
79
|
+
}
|
|
80
|
+
if (deltas.size !== 2)
|
|
81
|
+
throw new Error(`expected both templates in catalog, found ${deltas.size}`);
|
|
82
|
+
const touched = [];
|
|
83
|
+
let skippedInline = 0;
|
|
84
|
+
for (const layout of layouts) {
|
|
85
|
+
for (const piece of layout.pieces ?? []) {
|
|
86
|
+
if (!piece.template || !TARGETS.has(piece.template))
|
|
87
|
+
continue;
|
|
88
|
+
if (piece.footprint) {
|
|
89
|
+
skippedInline++; // baked geometry: template id is provenance only
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const { delta, movedIdx } = deltas.get(piece.template);
|
|
93
|
+
const d = rotateCw(applyMirror(delta, piece.mirror ?? "none"), piece.rotation_degrees ?? 0);
|
|
94
|
+
piece.position = { x: round4(piece.position.x + d.x), y: round4(piece.position.y + d.y) };
|
|
95
|
+
touched.push({
|
|
96
|
+
layoutId: layout.id,
|
|
97
|
+
pieceId: piece.id ?? "<anon>",
|
|
98
|
+
template: piece.template,
|
|
99
|
+
movedIdx,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// ---- verify against the snapshot ------------------------------------------
|
|
104
|
+
const after = new Map(layouts.map((l) => [l.id, resolveLayout(l, catalog)]));
|
|
105
|
+
const touchedKey = new Set(touched.map((t) => `${t.layoutId}/${t.pieceId}`));
|
|
106
|
+
let failures = 0;
|
|
107
|
+
console.log("\nlayout/piece fixed-corner Δ leg Δ");
|
|
108
|
+
for (const t of touched) {
|
|
109
|
+
const b = before.get(t.layoutId).find((r) => r.id === t.pieceId);
|
|
110
|
+
const a = after.get(t.layoutId).find((r) => r.id === t.pieceId);
|
|
111
|
+
if (!b || !a || b.vertices.length !== a.vertices.length) {
|
|
112
|
+
failures++;
|
|
113
|
+
console.error(` ✗ ${t.layoutId}/${t.pieceId}: piece missing or vertex count changed`);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
let fixedMax = 0;
|
|
117
|
+
let legMin = Infinity;
|
|
118
|
+
let legMax = 0;
|
|
119
|
+
b.vertices.forEach((bv, i) => {
|
|
120
|
+
const av = a.vertices[i];
|
|
121
|
+
const moved = Math.hypot(av.x - bv.x, av.y - bv.y);
|
|
122
|
+
if (t.movedIdx.includes(i)) {
|
|
123
|
+
legMin = Math.min(legMin, moved);
|
|
124
|
+
legMax = Math.max(legMax, moved);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
fixedMax = Math.max(fixedMax, moved);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
const ok = fixedMax <= TOL && Math.abs(legMin - 0.5) <= TOL && Math.abs(legMax - 0.5) <= TOL;
|
|
131
|
+
if (!ok)
|
|
132
|
+
failures++;
|
|
133
|
+
console.log(` ${ok ? "✓" : "✗"} ${`${t.layoutId}/${t.pieceId}`.padEnd(52)} ${fixedMax.toExponential(1).padStart(8)} ${legMin.toFixed(4)}–${legMax.toFixed(4)}`);
|
|
134
|
+
}
|
|
135
|
+
// Everything NOT touched must be bit-identical.
|
|
136
|
+
for (const layout of layouts) {
|
|
137
|
+
const b = before.get(layout.id);
|
|
138
|
+
const a = after.get(layout.id);
|
|
139
|
+
for (let i = 0; i < b.length; i++) {
|
|
140
|
+
const key = `${layout.id}/${b[i].id ?? ""}`;
|
|
141
|
+
if (touchedKey.has(key))
|
|
142
|
+
continue;
|
|
143
|
+
const same = b[i].vertices.length === a[i].vertices.length &&
|
|
144
|
+
b[i].vertices.every((v, j) => v.x === a[i].vertices[j].x && v.y === a[i].vertices[j].y);
|
|
145
|
+
if (!same) {
|
|
146
|
+
failures++;
|
|
147
|
+
console.error(` ✗ ${key}: untouched piece geometry changed`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (failures > 0) {
|
|
152
|
+
console.error(`\nFIX FAILED: ${failures} verification failures; files NOT written.`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
writeFileSync(CATALOG_PATH, `${JSON.stringify(catalog, null, 2)}\n`);
|
|
156
|
+
writeFileSync(LAYOUTS_PATH, `${JSON.stringify(layouts, null, 2)}\n`);
|
|
157
|
+
console.log(`\n✓ ${touched.length} placements compensated (${skippedInline} inline-baked pieces left untouched). Wrote both data files.`);
|
|
158
|
+
}
|
|
159
|
+
main();
|
|
160
|
+
//# sourceMappingURL=fix-corner-ruin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fix-corner-ruin.js","sourceRoot":"","sources":["../src/fix-corner-ruin.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * One-shot correction (refuses to re-run once applied) of the `corner-ruin-left` /\n * `corner-ruin-right` catalog templates: the long leg is 7″ but the physical\n * piece is 6.5″. Shrinks the leg in `data/core/terrain-templates.json` and\n * compensates every layout piece referencing those templates in\n * `data/core/terrain-layouts.json` so the resolved outer corner stays exactly\n * where it was placed.\n *\n * Pieces are centroid-anchored (`board = position + R·M·(v − c)`), so changing\n * the footprint moves the centroid `c` and would shift the whole piece. Keeping\n * any unchanged vertex fixed requires `position += R·M·(c_new − c_old)` — the\n * same correction in board space and in a parent area's centroid-local frame\n * (the area transform is rigid and applied after).\n *\n * Pieces with an inline baked `footprint` (the migrated gw-11e-crucible corner\n * segments, which carry the template id only as provenance) are intentionally\n * untouched: the template edit cannot affect them.\n *\n * Verifies by resolving every layout before and after: unchanged footprint\n * vertices must not move, the two shortened leg vertices must move by exactly\n * 0.5″, and every piece not referencing the templates must be bit-identical.\n *\n * Usage: `npx tsx src/fix-corner-ruin.ts` (run from `tools/`).\n */\nimport { readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nimport {\n resolveLayout,\n polygonCentroid,\n footprintVertices,\n type Vec2,\n type Mirror,\n type TerrainTemplate,\n type TerrainLayout,\n} from \"./terrain/resolve.js\";\n\nconst REPO_ROOT = join(new URL(\"../..\", import.meta.url).pathname);\nconst CATALOG_PATH = join(REPO_ROOT, \"data\", \"core\", \"terrain-templates.json\");\nconst LAYOUTS_PATH = join(REPO_ROOT, \"data\", \"core\", \"terrain-layouts.json\");\n\nconst OLD_LEG = 7;\nconst NEW_LEG = 6.5;\nconst TARGETS = new Set([\"corner-ruin-left\", \"corner-ruin-right\"]);\n/** Resolver output is rounded to 1e-4; position rounding adds ≤5e-5 per axis. */\nconst TOL = 2e-4;\n\n// Mirror → rotate, matching the resolver's orientation math (y-down, CW).\nfunction applyMirror(v: Vec2, m: Mirror): Vec2 {\n if (m === \"horizontal\") return { x: -v.x, y: v.y };\n if (m === \"vertical\") return { x: v.x, y: -v.y };\n return v;\n}\nfunction rotateCw(v: Vec2, deg: number): Vec2 {\n const r = (deg * Math.PI) / 180;\n const c = Math.cos(r);\n const s = Math.sin(r);\n return { x: c * v.x - s * v.y, y: s * v.x + c * v.y };\n}\nconst round4 = (n: number): number => Math.round(n * 1e4) / 1e4;\n\nfunction main(): void {\n const catalog = JSON.parse(readFileSync(CATALOG_PATH, \"utf8\")) as TerrainTemplate[];\n const layouts = JSON.parse(readFileSync(LAYOUTS_PATH, \"utf8\")) as TerrainLayout[];\n\n // ---- snapshot ground truth with the OLD catalog --------------------------\n const before = new Map(layouts.map((l) => [l.id, resolveLayout(l, catalog)]));\n\n // ---- patch the two templates, recording centroid deltas ------------------\n const deltas = new Map<string, { delta: Vec2; movedIdx: number[] }>();\n for (const t of catalog) {\n if (!TARGETS.has(t.id)) continue;\n if (t.footprint.type !== \"polygon\") throw new Error(`${t.id}: expected polygon footprint`);\n const oldVerts = footprintVertices(t.footprint);\n const movedIdx: number[] = [];\n t.footprint.points.forEach((p, i) => {\n if (p.y === OLD_LEG) {\n p.y = NEW_LEG;\n movedIdx.push(i);\n }\n });\n if (movedIdx.length !== 2) {\n throw new Error(`${t.id}: expected exactly 2 leg vertices at y=${OLD_LEG}, found ${movedIdx.length}`);\n }\n const cOld = polygonCentroid(oldVerts);\n const cNew = polygonCentroid(footprintVertices(t.footprint));\n const delta = { x: cNew.x - cOld.x, y: cNew.y - cOld.y };\n deltas.set(t.id, { delta, movedIdx });\n console.log(\n `${t.id}: centroid (${round4(cOld.x)}, ${round4(cOld.y)}) → (${round4(cNew.x)}, ${round4(cNew.y)})`,\n );\n }\n if (deltas.size !== 2) throw new Error(`expected both templates in catalog, found ${deltas.size}`);\n\n // ---- compensate every template-referencing placement ---------------------\n interface Touched {\n layoutId: string;\n pieceId: string;\n template: string;\n movedIdx: number[];\n }\n const touched: Touched[] = [];\n let skippedInline = 0;\n for (const layout of layouts) {\n for (const piece of layout.pieces ?? []) {\n if (!piece.template || !TARGETS.has(piece.template)) continue;\n if (piece.footprint) {\n skippedInline++; // baked geometry: template id is provenance only\n continue;\n }\n const { delta, movedIdx } = deltas.get(piece.template)!;\n const d = rotateCw(applyMirror(delta, piece.mirror ?? \"none\"), piece.rotation_degrees ?? 0);\n piece.position = { x: round4(piece.position.x + d.x), y: round4(piece.position.y + d.y) };\n touched.push({\n layoutId: layout.id,\n pieceId: piece.id ?? \"<anon>\",\n template: piece.template,\n movedIdx,\n });\n }\n }\n\n // ---- verify against the snapshot ------------------------------------------\n const after = new Map(layouts.map((l) => [l.id, resolveLayout(l, catalog)]));\n const touchedKey = new Set(touched.map((t) => `${t.layoutId}/${t.pieceId}`));\n let failures = 0;\n\n console.log(\"\\nlayout/piece fixed-corner Δ leg Δ\");\n for (const t of touched) {\n const b = before.get(t.layoutId)!.find((r) => r.id === t.pieceId);\n const a = after.get(t.layoutId)!.find((r) => r.id === t.pieceId);\n if (!b || !a || b.vertices.length !== a.vertices.length) {\n failures++;\n console.error(` ✗ ${t.layoutId}/${t.pieceId}: piece missing or vertex count changed`);\n continue;\n }\n let fixedMax = 0;\n let legMin = Infinity;\n let legMax = 0;\n b.vertices.forEach((bv, i) => {\n const av = a.vertices[i];\n const moved = Math.hypot(av.x - bv.x, av.y - bv.y);\n if (t.movedIdx.includes(i)) {\n legMin = Math.min(legMin, moved);\n legMax = Math.max(legMax, moved);\n } else {\n fixedMax = Math.max(fixedMax, moved);\n }\n });\n const ok = fixedMax <= TOL && Math.abs(legMin - 0.5) <= TOL && Math.abs(legMax - 0.5) <= TOL;\n if (!ok) failures++;\n console.log(\n ` ${ok ? \"✓\" : \"✗\"} ${`${t.layoutId}/${t.pieceId}`.padEnd(52)} ${fixedMax.toExponential(1).padStart(8)} ${legMin.toFixed(4)}–${legMax.toFixed(4)}`,\n );\n }\n\n // Everything NOT touched must be bit-identical.\n for (const layout of layouts) {\n const b = before.get(layout.id)!;\n const a = after.get(layout.id)!;\n for (let i = 0; i < b.length; i++) {\n const key = `${layout.id}/${b[i].id ?? \"\"}`;\n if (touchedKey.has(key)) continue;\n const same =\n b[i].vertices.length === a[i].vertices.length &&\n b[i].vertices.every((v, j) => v.x === a[i].vertices[j].x && v.y === a[i].vertices[j].y);\n if (!same) {\n failures++;\n console.error(` ✗ ${key}: untouched piece geometry changed`);\n }\n }\n }\n\n if (failures > 0) {\n console.error(`\\nFIX FAILED: ${failures} verification failures; files NOT written.`);\n process.exit(1);\n }\n\n writeFileSync(CATALOG_PATH, `${JSON.stringify(catalog, null, 2)}\\n`);\n writeFileSync(LAYOUTS_PATH, `${JSON.stringify(layouts, null, 2)}\\n`);\n console.log(\n `\\n✓ ${touched.length} placements compensated (${skippedInline} inline-baked pieces left untouched). Wrote both data files.`,\n );\n}\n\nmain();\n"]}
|
package/dist/gen-conformance.js
CHANGED
|
@@ -25,6 +25,8 @@ import { fileURLToPath } from "node:url";
|
|
|
25
25
|
import { Dataset } from "./data/dataset.js";
|
|
26
26
|
import { normalizeName } from "./data/normalize.js";
|
|
27
27
|
import { describeScoringCard } from "./translate/index.js";
|
|
28
|
+
import { awardsOf } from "./scoring/index.js";
|
|
29
|
+
import { createRunnerState, dispatch } from "./runner.js";
|
|
28
30
|
import { exportRoster } from "./export/index.js";
|
|
29
31
|
import { importRoster, REGISTERED_ADAPTERS } from "./import/import-roster.js";
|
|
30
32
|
import { selectAdapter } from "./import/adapter.js";
|
|
@@ -369,6 +371,154 @@ function genScoringTranslation() {
|
|
|
369
371
|
writeJson(join(CONFORMANCE, "scoring-translation", "cases.json"), cases);
|
|
370
372
|
console.log(`scoring-translation/cases.json: ${cases.length} cases`);
|
|
371
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* Scoring-engine corpus: pin the pure VP arithmetic of the scoring engine
|
|
376
|
+
* (`tools/src/scoring/` — the oracle) so the Rust `wh40kdc::scoring` port
|
|
377
|
+
* reproduces it. Three ops, each case `{ name, op, args, expected }`:
|
|
378
|
+
*
|
|
379
|
+
* - `score_event` — per card and approach, assert every award matching the
|
|
380
|
+
* approach (by its full-`awards`-array index). Pins `scoreAward`, `scoreTurn`
|
|
381
|
+
* (exclusive-group "highest only", `vp_per × count` clamped to `per_max`,
|
|
382
|
+
* cumulative sums), `scoreCap` (tactical 5 vs fixed `vp_max`/uncapped), and
|
|
383
|
+
* `scoreSecondaryEvent`; primary cards also carry a `roundCap` to pin
|
|
384
|
+
* `scorePrimaryEvent`. `cap: null` means uncapped (Infinity has no JSON form).
|
|
385
|
+
* - `score_state` — replay scenarios over a `PlayerGame`, pinning the per-round
|
|
386
|
+
* cap (15), per-game primary cap (45), grand-total cap (100), score+discard,
|
|
387
|
+
* and undo.
|
|
388
|
+
* - `wtc_result` — the 20-point band mapping across its boundaries.
|
|
389
|
+
*
|
|
390
|
+
* Goldens are produced by driving the TS runner's own `dispatch`, so the corpus
|
|
391
|
+
* and the runner agree by construction; the cross-impl contract is the Rust
|
|
392
|
+
* runner reproducing them. Integers are compared exactly (no tolerance).
|
|
393
|
+
*/
|
|
394
|
+
function genScoring() {
|
|
395
|
+
const ds = Dataset.embedded();
|
|
396
|
+
mkdirSync(join(CONFORMANCE, "scoring"), { recursive: true });
|
|
397
|
+
// One initialized runner state, reused across cases (the ops don't mutate it).
|
|
398
|
+
const specVersion = Number.parseInt(readFileSync(join(CONFORMANCE, "SPEC_VERSION"), "utf8").trim(), 10);
|
|
399
|
+
const state = createRunnerState();
|
|
400
|
+
const init = dispatch(state, {
|
|
401
|
+
op: "init",
|
|
402
|
+
args: { spec_version: specVersion, locale: "C", tz: "UTC", seed: 0 },
|
|
403
|
+
});
|
|
404
|
+
if (!init.ok)
|
|
405
|
+
throw new Error(`gen scoring: init failed: ${JSON.stringify(init)}`);
|
|
406
|
+
const run = (op, args) => {
|
|
407
|
+
const r = dispatch(state, { op, args });
|
|
408
|
+
if (!r.ok)
|
|
409
|
+
throw new Error(`gen scoring: ${op} failed: ${JSON.stringify(r)} for ${JSON.stringify(args)}`);
|
|
410
|
+
return r.value;
|
|
411
|
+
};
|
|
412
|
+
const cases = [];
|
|
413
|
+
// score_event: every mission card, both approaches. Assert the approach's
|
|
414
|
+
// awards by their full-array index; count vp_per awards to their per_max
|
|
415
|
+
// (else 2) so the cap logic actually bites.
|
|
416
|
+
const cards = ds.missionCards.all
|
|
417
|
+
.slice()
|
|
418
|
+
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
419
|
+
for (const card of cards) {
|
|
420
|
+
for (const approach of ["fixed", "tactical"]) {
|
|
421
|
+
const asserted = awardsOf(card)
|
|
422
|
+
.map((aw, index) => ({ aw, index }))
|
|
423
|
+
.filter(({ aw }) => aw.mode == null || aw.mode === approach)
|
|
424
|
+
.map(({ aw, index }) => aw.vp_per != null ? { index, count: aw.per_max ?? 2 } : { index });
|
|
425
|
+
const args = { cardId: card.id, approach, asserted };
|
|
426
|
+
if (card.card_type === "primary")
|
|
427
|
+
args.roundCap = 15;
|
|
428
|
+
cases.push({
|
|
429
|
+
name: `score_event/${card.id}/${approach}`,
|
|
430
|
+
op: "score_event",
|
|
431
|
+
args,
|
|
432
|
+
expected: run("score_event", args),
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
// score_state: hand-authored replay scenarios. Card ids are real deck/mission
|
|
437
|
+
// cards; expected state is whatever the engine produces.
|
|
438
|
+
const stateScenarios = [
|
|
439
|
+
{
|
|
440
|
+
name: "primary-round-and-game-caps",
|
|
441
|
+
args: {
|
|
442
|
+
approach: "tactical",
|
|
443
|
+
ops: [
|
|
444
|
+
{ kind: "set-primary", round: 1, vp: 30, roundCap: 15, gameCap: 45 },
|
|
445
|
+
{ kind: "set-primary", round: 2, vp: 30, roundCap: 15, gameCap: 45 },
|
|
446
|
+
{ kind: "set-primary", round: 3, vp: 30, roundCap: 15, gameCap: 45 },
|
|
447
|
+
{ kind: "set-primary", round: 4, vp: 30, roundCap: 15, gameCap: 45 },
|
|
448
|
+
],
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
// The full primary path: a card's raw round total, clamped to the round
|
|
453
|
+
// cap on store, then cleared back to 0 by a set-primary 0.
|
|
454
|
+
name: "score-primary-then-clear",
|
|
455
|
+
args: {
|
|
456
|
+
approach: "tactical",
|
|
457
|
+
ops: [
|
|
458
|
+
{
|
|
459
|
+
kind: "score-primary",
|
|
460
|
+
cardId: "ground-control",
|
|
461
|
+
round: 2,
|
|
462
|
+
asserted: awardsOf(ds.missionCards.get("ground-control")).map((aw, index) => aw.vp_per != null ? { index, count: aw.per_max ?? 3 } : { index }),
|
|
463
|
+
roundCap: 15,
|
|
464
|
+
gameCap: 45,
|
|
465
|
+
},
|
|
466
|
+
{ kind: "set-primary", round: 3, vp: 99, roundCap: 15, gameCap: 45 },
|
|
467
|
+
{ kind: "set-primary", round: 2, vp: 0, roundCap: 15, gameCap: 45 },
|
|
468
|
+
],
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
name: "secondary-score-and-undo",
|
|
473
|
+
args: {
|
|
474
|
+
approach: "tactical",
|
|
475
|
+
ops: [
|
|
476
|
+
{ kind: "draw", cardId: "no-prisoners" },
|
|
477
|
+
{ kind: "score-secondary", cardId: "no-prisoners", round: 2, asserted: [{ index: 0, count: 3 }] },
|
|
478
|
+
{ kind: "remove-score", index: 0 },
|
|
479
|
+
],
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
// Uncapped set-primary (no caps) overshoots so the 100 grand-total cap bites.
|
|
484
|
+
name: "grand-total-cap-at-100",
|
|
485
|
+
args: {
|
|
486
|
+
approach: "tactical",
|
|
487
|
+
ops: [
|
|
488
|
+
{ kind: "set-primary", round: 1, vp: 30 },
|
|
489
|
+
{ kind: "set-primary", round: 2, vp: 30 },
|
|
490
|
+
{ kind: "set-primary", round: 3, vp: 30 },
|
|
491
|
+
{ kind: "set-primary", round: 4, vp: 30 },
|
|
492
|
+
{ kind: "set-primary", round: 5, vp: 30 },
|
|
493
|
+
{ kind: "draw", cardId: "no-prisoners" },
|
|
494
|
+
{ kind: "score-secondary", cardId: "no-prisoners", round: 5, asserted: [{ index: 0, count: 99 }] },
|
|
495
|
+
],
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
];
|
|
499
|
+
for (const s of stateScenarios) {
|
|
500
|
+
cases.push({ name: `score_state/${s.name}`, op: "score_state", args: s.args, expected: run("score_state", s.args) });
|
|
501
|
+
}
|
|
502
|
+
// wtc_result: band boundaries and symmetry.
|
|
503
|
+
const wtcPairs = [
|
|
504
|
+
[50, 50],
|
|
505
|
+
[48, 45],
|
|
506
|
+
[45, 50],
|
|
507
|
+
[56, 50],
|
|
508
|
+
[50, 61],
|
|
509
|
+
[100, 50],
|
|
510
|
+
[100, 49],
|
|
511
|
+
[0, 100],
|
|
512
|
+
[60, 40],
|
|
513
|
+
[55, 50],
|
|
514
|
+
];
|
|
515
|
+
for (const [a, b] of wtcPairs) {
|
|
516
|
+
const args = { a, b };
|
|
517
|
+
cases.push({ name: `wtc_result/${a}-${b}`, op: "wtc_result", args, expected: run("wtc_result", args) });
|
|
518
|
+
}
|
|
519
|
+
writeJson(join(CONFORMANCE, "scoring", "cases.json"), cases);
|
|
520
|
+
console.log(`scoring/cases.json: ${cases.length} cases`);
|
|
521
|
+
}
|
|
372
522
|
/**
|
|
373
523
|
* Terrain-resolver corpus: resolve template-anchored layouts to absolute
|
|
374
524
|
* board-space vertices (y-down inches). The TS resolver is the oracle; the Rust
|
|
@@ -530,5 +680,6 @@ genRosters();
|
|
|
530
680
|
genLinkedApi();
|
|
531
681
|
genAttribution();
|
|
532
682
|
genScoringTranslation();
|
|
683
|
+
genScoring();
|
|
533
684
|
genTerrainResolver();
|
|
534
685
|
//# sourceMappingURL=gen-conformance.js.map
|