/** * CLEAN GROUND's declared cast, read from the document rather than copied out of it. * * Two guards now measure that scenario's own encounter — check-firstblood and * check-attackers — and both must be measuring the people the document actually casts. * A cast written out by hand in either of them would go on measuring the old six after a * re-cast and report success, which is the defect this project keeps finding in itself. * R-267 committed exactly that inside the guard built to prevent a weaker version of it. * * One reader rather than two, because two copies of this would be the duplication * check-rules exists to forbid, and would let the two guards disagree about who is in the * fight. check-rollable established the `` marker; this reads the same * one and asks nothing further of the document. * * NOT named scenario-something: check-scenarios sweeps `tools/scenario-*` into the * scenario corpus, so a file with that prefix has its prose read as a scenario's and the * marker quoted below read as a real cast declaration. It was, and check-rollable said so. */ import { existsSync, readFileSync } from "node:fs"; import path from "node:path"; const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1")), ".."); export const SCENARIO = path.join(ROOT, "docs", "scenarios", "CLEAN_GROUND.md"); /* The scaling note drops Pollard at five and Okonkwo as well at four, so the cut is the declared cast less those two. Named rather than sliced: "the first four" would survive a reordering of the cast list and quietly measure different people. */ const DROPPED_AT_FOUR = ["pollard", "okonkwo"]; /** The six the document declares, without their `pc_` prefixes. Fatal if it declares none. */ export function declaredCast(who = "scenario-cast") { if (!existsSync(SCENARIO)) { console.error(`${who}: ${path.relative(ROOT, SCENARIO)} is missing, and the cast is read ` + `from it. Nothing here can be measured against a scenario that is not there.`); process.exit(1); } const m = readFileSync(SCENARIO, "utf8").match(//i); if (!m) { console.error(`${who}: ${path.relative(ROOT, SCENARIO)} declares no cast. Add the ` + ` marker check-rollable already reads, or this guard is measuring ` + `a fight the scenario no longer casts.`); process.exit(1); } return m[1].split(",").map(x => x.trim().replace(/^pc_/, "")).filter(Boolean); } /** The declared cast and its four-player cut, checked for the shape the guards recorded. */ export function castAndCut(who = "scenario-cast") { const line = declaredCast(who); const cut = line.filter(k => !DROPPED_AT_FOUR.includes(k)); if (cut.length !== 4 || line.length !== 6) { console.error(`${who}: CLEAN GROUND now casts ${line.length} and its four-player cut works ` + `out at ${cut.length}. The recorded fights are six against six and four against three; ` + `re-read the scaling note and re-record with --update rather than reverting.`); process.exit(1); } return { line, cut }; }