diff --git a/prek.toml b/prek.toml index 7d4799f..6e4697d 100644 --- a/prek.toml +++ b/prek.toml @@ -92,6 +92,22 @@ files = '^plan/.*\.md$' # touching plan/ is a human writing prose in these same files, and a hook that # edited the register underneath them would be changing the thing being # written rather than reporting on it. +# The unit table against the reports it is collected from. Both are committed +# and neither regenerates the other, so a report refreshed without re-running +# the script leaves every weight on a force card describing a machine that is +# no longer in the fight - and a stale figure renders exactly as well as a +# current one. +# +# Hermetic and instant: the inputs are the two report.json files in this repo, +# so unlike boards.json or megamek-text.ts this needs no MegaMek to check. +[[repos.hooks]] +id = "unit-stats" +name = "unit stats" +language = "system" +entry = "scripts/unit-stats.py --out web/src/content/unit-stats.json --check" +pass_filenames = false +files = '^(scripts/unit-stats\.py|web/src/content/unit-stats\.json|services/api/src/matches/(daily|scenarios)/report\.json)$' + [[repos.hooks]] id = "plan-register" name = "plan register" diff --git a/web/scripts/daily.test.mjs b/web/scripts/daily.test.mjs index 8339ce1..2bf54a1 100644 --- a/web/scripts/daily.test.mjs +++ b/web/scripts/daily.test.mjs @@ -304,6 +304,58 @@ test("every rule a challenge switches has MegaMek's own description", async () = } }); +test("each side's battle value is the sum of the units in it", () => { + // daily.json is generated from the scenarios and arena's report of what they + // are worth, and the per-unit figures and the side total come from different + // parts of that report. If they ever disagree, one of them is describing a + // fight nobody is playing - and a wrong battle value renders exactly as well + // as a right one, so nothing else would notice. + // + // This is also what says the positional join in `mms index` still lines up: + // the report lists a faction in MegaMek's own order, and if that stopped + // matching the file's order the units would keep their names and swap their + // values, which changes no total but every row. Two of Bug Company's + // Stingers differ by 124 BV on skills alone. + for (const challenge of challenges) { + for (const side of challenge.sides) { + const known = side.units.filter((unit) => unit.bv !== undefined); + if (known.length === 0) continue; + assert.equal( + known.length, + side.units.length, + `${challenge.slug} ${side.name}: some units carry a battle value and ` + + `some do not, so the total cannot be checked against them`, + ); + const summed = known.reduce((total, unit) => total + unit.bv, 0); + assert.equal( + summed, + side.bv, + `${challenge.slug} ${side.name}: the units add up to ${summed} but ` + + `the side says ${side.bv}`, + ); + } + } +}); + +test("a daily's crews are carried, because the battle values depend on them", () => { + // BV 2.0 multiplies by gunnery and piloting, so a figure without the skills + // that produced it cannot be checked by anything, here or by a reader. + for (const challenge of challenges) { + for (const side of challenge.sides) { + for (const unit of side.units) { + if (unit.bv === undefined) continue; + assert.equal( + typeof unit.gunnery, + "number", + `${challenge.slug} ${side.name} ${unit.name}: a battle value with ` + + `no gunnery behind it`, + ); + assert.equal(typeof unit.piloting, "number", `${unit.name} piloting`); + } + } + } +}); + test("a rule the card shows is one somebody chose to show", async () => { // Disclosure is a property of the lock, not a second kind of lock: a daily // pins its map and its planetary conditions too, and those are equally