From 208c2957d992e724f673b0b075f6342af46061bb Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Wed, 19 Aug 2026 18:47:50 -0400 Subject: [PATCH] test(unit-rules): a stripped design needs everything it was built with The exact property that catches a mis-keyed location, over all 4,294 Meks - the keys differ by shape, and only three locations have rear plate. --- TODO.md | 24 ++++++++++- crates/helm-bv/tests/conformance.rs | 67 +++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/TODO.md b/TODO.md index 2fac33f..dd8f6f4 100644 --- a/TODO.md +++ b/TODO.md @@ -442,9 +442,29 @@ not obvious from any one of them. conditions rather than a new rule: score, compare, damage further. Worth knowing that armour is most of the defensive rating and that emptying ammunition can *raise* a figure, so the search is not monotonic. -- [ ] **Turn a post-match `.mul` into a repair list.** The difference between +- [x] **Turn a post-match `.mul` into a repair list.** The difference between the design and its condition, per location: armour points, structure, - the slots that were shot out. helm holds both sides already. + the slots that were shot out, and the magazines to fill. + `helm_bv::repairs` reports it and `helm repairs ` prints it, + with `--why` adding what the damage cost in battle value. A slot names + what was in it, because "slot 3" is not a repair order and "PPC" is. + + Nothing is costed. What a repair takes in time, parts and C-bills is a + campaign's question; what is missing is a fact about two files. + + Writing it found a real bug in the `.mul` writer. The key for rear plate + was built as `RT` plus the location's first letter, which is right for + the three torsos and sends a left *arm* to `RTL` - the left torso's rear. + Reading was harmless, since MegaMek writes no rear plate on a limb. + Writing was not: a force with a damaged torso came back with rear armour + tags on two limbs that never had any, and `Bot_North.mul` gained a whole + `` it did not have. The round trip over the shipped + files missed it because none of them has rear damage. + + The test that holds the mapping is exact: strip a design to nothing and + the repair list has to come to the armour and structure it was built + with. Over all 4,294 Meks, every one does - which is what says the keys + are right for a quad's four legs as well as a biped's arms. - [x] **Say where a unit's value went.** Which term lost it - plate, frame, a weapon, cooling. `helm_bv::attribute` takes two conditions and reports the terms that moved between them, worst loss first, in battle value diff --git a/crates/helm-bv/tests/conformance.rs b/crates/helm-bv/tests/conformance.rs index f949b44..9f704ed 100644 --- a/crates/helm-bv/tests/conformance.rs +++ b/crates/helm-bv/tests/conformance.rs @@ -589,6 +589,73 @@ fn nothing_megamek_wrote_is_called_wrong() { ); } +/// Strip a design to nothing, and the repair list is the design. +/// +/// The property is exact and it is the one that catches a mis-keyed location: +/// a machine with no plate and no frame left needs every point it was built +/// with, so the totals have to come to the design's own armour and its own +/// structure. Reading a location under the wrong key shows up here as a +/// figure that is too small - the plate is not asked for - or too large, +/// where two locations answer to one key. +/// +/// It is checked over the whole library because the keys differ by shape: +/// a quad has four legs where a biped has two arms, and only three locations +/// have rear plate. +#[test] +#[ignore = "needs a MegaMek install and a bridge dump; set HELM_MEGAMEK and HELM_BRIDGE"] +fn a_stripped_design_needs_everything_it_was_built_with() { + let Some(inputs) = inputs() else { + panic!("set HELM_MEGAMEK to a MegaMek install and HELM_BRIDGE to a bridge dump"); + }; + + let mut checked = 0; + let mut wrong: Vec = Vec::new(); + for unit in &inputs.library.units { + if unit.format_str != "mtf" || unit.armor_locations.is_empty() { + continue; + } + let Some(tons) = unit.mass else { + continue; + }; + let shape = helm_bv::Shape::from_config(unit.config.as_deref()); + let Some(frame) = helm_bv::structure_per_location(tons, shape) else { + continue; + }; + // Nothing left anywhere: every location the design has, front, rear + // and frame, at nought. + let mut stripped = helm_bv::Condition::undamaged(); + for location in unit.armor_locations.keys() { + stripped.armor.insert(location.clone(), 0); + } + for (location, _) in &frame { + stripped.structure.insert((*location).to_string(), 0); + } + + checked += 1; + let total = helm_bv::Total::of(&helm_bv::repairs(unit, &stripped)); + let built_armor: i64 = unit.armor_locations.values().sum(); + let built_frame: i64 = frame.iter().map(|(_, points)| *points).sum(); + if total.armor != built_armor || total.structure != built_frame { + wrong.push(format!( + "{}: needs {} armour of {built_armor} and {} structure of {built_frame}", + unit.display_name(), + total.armor, + total.structure + )); + } + } + + println!("{checked} designs stripped"); + for line in wrong.iter().take(20) { + println!(" {line}"); + } + assert!( + wrong.is_empty(), + "{} designs do not ask for what they were built with", + wrong.len() + ); +} + /// Every `.mul` the MegaMek install ships, read, written back, and read again. /// /// The structural round trip is checked in helm-unitfile against files written -- 2.51.2