diff --git a/src/simulator/crates/mc-combat/src/lib.rs b/src/simulator/crates/mc-combat/src/lib.rs index bff35acd..301ff731 100644 --- a/src/simulator/crates/mc-combat/src/lib.rs +++ b/src/simulator/crates/mc-combat/src/lib.rs @@ -2,6 +2,7 @@ pub mod bonuses; pub mod keywords; pub mod loot; pub mod promotions; +pub mod requirements; pub mod resolver; pub mod siege; pub mod wilds; @@ -20,6 +21,9 @@ pub use resolver::{ CombatOutcome, CombatParams, CombatResolver, CombatResult, CombatType, UnitAttributes, UnitStats, }; +pub use requirements::{ + check_strategic_reqs, credit_resources, debit_resources, MissingResource, +}; pub use siege::{melee_wall_penalty, siege_city_bonus, split_ranged_damage_vs_city}; pub use wilds::wild_combat_stats; diff --git a/src/simulator/crates/mc-combat/src/requirements.rs b/src/simulator/crates/mc-combat/src/requirements.rs new file mode 100644 index 00000000..e6296c8d --- /dev/null +++ b/src/simulator/crates/mc-combat/src/requirements.rs @@ -0,0 +1,111 @@ +use std::collections::BTreeMap; + +/// A required strategic resource was not available in the empire ledger. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MissingResource(pub String); + +impl std::fmt::Display for MissingResource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "missing strategic resource: {}", self.0) + } +} + +/// Returns `Ok(())` if every required resource has at least one unit in the +/// ledger, otherwise `Err(MissingResource)` for the first missing entry. +/// +/// `unit_reqs` is the `requires_resource` list from unit JSON (may be empty). +/// `ledger` maps resource ID → stockpile count (decremented on build, +/// credited on unit death — see `debit_resource` / `credit_resource`). +pub fn check_strategic_reqs( + unit_reqs: &[String], + ledger: &BTreeMap, +) -> Result<(), MissingResource> { + for req in unit_reqs { + let count = ledger.get(req.as_str()).copied().unwrap_or(0); + if count == 0 { + return Err(MissingResource(req.clone())); + } + } + Ok(()) +} + +/// Deduct one unit of each required resource from the ledger on successful +/// build. Saturates at zero (cannot go negative). +pub fn debit_resources(unit_reqs: &[String], ledger: &mut BTreeMap) { + for req in unit_reqs { + let entry = ledger.entry(req.clone()).or_insert(0); + *entry = entry.saturating_sub(1); + } +} + +/// Return one unit of each required resource to the ledger when the unit dies. +pub fn credit_resources(unit_reqs: &[String], ledger: &mut BTreeMap) { + for req in unit_reqs { + *ledger.entry(req.clone()).or_insert(0) += 1; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn ledger(pairs: &[(&str, u32)]) -> BTreeMap { + pairs.iter().map(|(k, v)| (k.to_string(), *v)).collect() + } + + #[test] + fn no_reqs_always_passes() { + assert!(check_strategic_reqs(&[], &BTreeMap::new()).is_ok()); + } + + #[test] + fn missing_resource_returns_err() { + let reqs = vec!["iron_ore".to_string()]; + let result = check_strategic_reqs(&reqs, &BTreeMap::new()); + assert_eq!(result, Err(MissingResource("iron_ore".to_string()))); + } + + #[test] + fn present_resource_returns_ok() { + let reqs = vec!["iron_ore".to_string()]; + let ld = ledger(&[("iron_ore", 2)]); + assert!(check_strategic_reqs(&reqs, &ld).is_ok()); + } + + #[test] + fn debit_decrements_ledger() { + let reqs = vec!["iron_ore".to_string()]; + let mut ld = ledger(&[("iron_ore", 3)]); + debit_resources(&reqs, &mut ld); + assert_eq!(ld["iron_ore"], 2); + } + + #[test] + fn debit_saturates_at_zero() { + let reqs = vec!["iron_ore".to_string()]; + let mut ld = ledger(&[("iron_ore", 0)]); + debit_resources(&reqs, &mut ld); + assert_eq!(ld["iron_ore"], 0); + } + + #[test] + fn golden_build_then_kill_restores_iron() { + let reqs = vec!["iron_ore".to_string()]; + let mut ld = ledger(&[("iron_ore", 1)]); + + // Build cavalry: check then debit + assert!(check_strategic_reqs(&reqs, &ld).is_ok()); + debit_resources(&reqs, &mut ld); + assert_eq!(ld["iron_ore"], 0); + + // Unit is now alive, ledger at 0 — another build blocked + assert!(check_strategic_reqs(&reqs, &ld).is_err()); + + // Unit dies: credit returns the resource + credit_resources(&reqs, &mut ld); + assert_eq!(ld["iron_ore"], 1); + + // Can build again + assert!(check_strategic_reqs(&reqs, &ld).is_ok()); + } +}