diff --git a/TODO.md b/TODO.md index 5e3f42c..136f2ac 100644 --- a/TODO.md +++ b/TODO.md @@ -511,6 +511,13 @@ not obvious from any one of them. from the document, because by the time it is a model the evidence is gone. + Three more since: six wounds is a dead MekWarrior rather than a hurt one, + a slot marked as an empty magazine that holds a laser is a file + describing something that cannot be, and a slot shot out that was empty + to begin with is one written against a different design. The magazine one + matters most because it is silent - the reader drops the slot either way, + so the design comes out missing a weapon nobody shot. + A complaint says whether it is about the file or about us. A force of tanks is a good file helm cannot score yet, and refusing it would be charging a player for our own gap - so only the file's own faults fail diff --git a/crates/helm-bv/src/check.rs b/crates/helm-bv/src/check.rs index 2dbe9c9..afe9610 100644 --- a/crates/helm-bv/src/check.rs +++ b/crates/helm-bv/src/check.rs @@ -44,7 +44,8 @@ pub enum Trouble { /// A crew skill outside 0-8. MegaMek's own range, and the battle value /// table has no row for anything else. Skill { what: &'static str, value: u8 }, - /// More wounds than a MekWarrior can take. Six is dead. + /// More wounds than a MekWarrior can take, or exactly enough. Six is dead, + /// and a dead crew is not a unit somebody is fielding. PilotHits(u8), /// The file describes a location this design's shape does not have - a /// quad with an arm, a biped with a fourth leg. @@ -80,6 +81,19 @@ pub enum Trouble { slot: usize, slots: usize, }, + /// A slot marked as an empty magazine that holds no magazine. + /// + /// `shots="0"` on a laser is a file describing something that cannot be, + /// and it is silent: the reader takes the slot out of the loadout, so the + /// design comes out missing a weapon nobody shot. + NotAMagazine { + location: String, + slot: usize, + what: String, + }, + /// A slot shot out that has nothing in it. Harmless and a sign the file + /// was written against a different design. + NothingInThatSlot { location: String, slot: usize }, /// The design was found and cannot be scored, which is a limit here rather /// than a fault in the file. Unscoreable(String), @@ -101,6 +115,18 @@ impl std::fmt::Display for Trouble { Trouble::UnknownDesign => write!(f, "no design in the library answers to this name"), Trouble::Skill { what, value } => write!(f, "{what} {value} is outside 0-8"), Trouble::PilotHits(hits) => write!(f, "{hits} pilot hits, and six is dead"), + Trouble::NotAMagazine { + location, + slot, + what, + } => write!( + f, + "{location} slot {} is marked empty and holds {what}", + slot + 1 + ), + Trouble::NothingInThatSlot { location, slot } => { + write!(f, "{location} slot {} was shot out and is empty", slot + 1) + } Trouble::NoSuchLocation { index } => { write!(f, "location {index}, which this shape does not have") } @@ -144,7 +170,7 @@ pub fn check(design: Option<&Unit>, catalogue: &Catalogue, entity: &MulUnit) -> found.push(Trouble::Skill { what, value }); } } - if entity.pilot_hits > 6 { + if entity.pilot_hits >= 6 { found.push(Trouble::PilotHits(entity.pilot_hits)); } @@ -214,6 +240,46 @@ pub fn check(design: Option<&Unit>, catalogue: &Catalogue, entity: &MulUnit) -> } } + // A magazine that is not a magazine. The reader drops the slot from the + // loadout either way, so a design whose laser is marked empty comes out + // missing a laser and says nothing about it. + for (location, slots) in &entity.empty_ammo { + let Some(criticals) = design.criticals.get(location) else { + continue; + }; + for slot in slots { + let Some(what) = criticals.get(*slot) else { + continue; + }; + if !what.to_ascii_lowercase().contains("ammo") { + found.push(Trouble::NotAMagazine { + location: location.clone(), + slot: *slot, + what: what.clone(), + }); + } + } + } + + // Shot out and empty to begin with, which is a file written against a + // different design. + for (location, slots) in &entity.destroyed { + let Some(criticals) = design.criticals.get(location) else { + continue; + }; + for slot in slots { + if criticals + .get(*slot) + .is_some_and(|what| what.trim() == "-Empty-" || what.trim().is_empty()) + { + found.push(Trouble::NothingInThatSlot { + location: location.clone(), + slot: *slot, + }); + } + } + } + // A slot can be both shot out and empty, and that is one bad slot rather // than two, so the same complaint is not made twice. for named in [&entity.destroyed, &entity.empty_ammo] { @@ -270,9 +336,25 @@ mod tests { armor_locations: [("CT".to_string(), 47), ("RTC".to_string(), 14)] .into_iter() .collect(), - criticals: [("Right Arm".to_string(), vec![String::new(); 12])] - .into_iter() - .collect(), + criticals: [( + "Right Arm".to_string(), + vec![ + "Shoulder".to_string(), + "Upper Arm Actuator".to_string(), + "PPC".to_string(), + "IS Ammo LRM-10".to_string(), + "-Empty-".to_string(), + "-Empty-".to_string(), + "-Empty-".to_string(), + "-Empty-".to_string(), + "-Empty-".to_string(), + "-Empty-".to_string(), + "-Empty-".to_string(), + "-Empty-".to_string(), + ], + )] + .into_iter() + .collect(), ..Default::default() } } @@ -311,6 +393,51 @@ mod tests { assert_eq!(faults(None, &broken), vec![Trouble::UnknownDesign]); } + /// Six wounds is a dead MekWarrior, so a file that says six is not + /// describing a unit anybody is fielding. + #[test] + fn six_wounds_is_a_dead_crew_rather_than_a_hurt_one() { + let mut wrong = entity(); + wrong.pilot_hits = 6; + assert_eq!(faults(Some(&design()), &wrong), vec![Trouble::PilotHits(6)]); + + let mut alive = entity(); + alive.pilot_hits = 5; + assert_eq!(faults(Some(&design()), &alive), vec![]); + } + + /// `shots="0"` on a laser is silent damage: the reader takes the slot out + /// of the loadout either way, so the design comes out missing a weapon + /// nobody shot. + #[test] + fn a_magazine_that_is_not_a_magazine_is_reported() { + let mut wrong = entity(); + wrong.empty_ammo.insert("Right Arm".into(), [2].into()); + assert_eq!( + faults(Some(&design()), &wrong), + vec![Trouble::NotAMagazine { + location: "Right Arm".into(), + slot: 2, + what: "PPC".into() + }] + ); + } + + /// Shot out and empty to begin with: harmless, and a sign the file was + /// written against a different design. + #[test] + fn a_slot_with_nothing_in_it_cannot_have_been_shot_out() { + let mut wrong = entity(); + wrong.destroyed.insert("Right Arm".into(), [4].into()); + assert_eq!( + faults(Some(&design()), &wrong), + vec![Trouble::NothingInThatSlot { + location: "Right Arm".into(), + slot: 4 + }] + ); + } + #[test] fn a_crew_outside_the_table_is_reported_by_name() { let mut wrong = entity();