test(simulator): Add comprehensive test cases for simulator edge cases and new functionality

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
autocommit 2026-04-16 22:07:52 -07:00
parent aee31e286f
commit 05ead5077c
5 changed files with 215 additions and 0 deletions

View file

@ -0,0 +1,14 @@
[package]
name = "mc-golden-tests"
version = "0.1.0"
edition = "2021"
# Integration-level golden tests spanning mc-core → mc-city → mc-happiness → mc-combat.
# Each test file is a Rust integration test (in tests/) — requires the crates to be
# compiled as libraries, which they are.
[dev-dependencies]
mc-core = { path = "../../crates/mc-core" }
mc-city = { path = "../../crates/mc-city" }
mc-happiness = { path = "../../crates/mc-happiness" }
mc-combat = { path = "../../crates/mc-combat" }

View file

@ -0,0 +1 @@
// Stub — tests live in tests/*.rs integration test files.

View file

@ -0,0 +1,78 @@
//! Golden test: biome×quality → tile_collectibles → city get_yields
//!
//! Frozen vector (seed=7, biome="temperate_forest", quality=3):
//! hardwood qty=4 (roll=0.3898 < 0.70 → hit)
//! wild_game qty=1 (roll=0.0168 < 0.50 → hit)
//! mushrooms (roll=0.9008 < 0.35 → miss)
//!
//! If this test breaks, a change to the collectibles table or SplitMix64
//! sequence altered deterministic output — update the frozen vector and
//! document why in the commit message.
use mc_city::city::{City, TileYield, CITY_CENTER_BASELINE_FOOD, CITY_CENTER_BASELINE_PRODUCTION};
use mc_core::collectibles::{tile_collectibles, CollectibleRoll, SplitMix64};
const SEED: u64 = 7;
const BIOME: &str = "temperate_forest";
const QUALITY: u8 = 3;
fn frozen_rolls() -> Vec<CollectibleRoll> {
vec![
CollectibleRoll { resource_id: "hardwood".into(), quantity: 4, quality: QUALITY },
CollectibleRoll { resource_id: "wild_game".into(), quantity: 1, quality: QUALITY },
]
}
#[test]
fn tile_collectibles_matches_frozen_vector() {
let mut rng = SplitMix64::new(SEED);
let rolls = tile_collectibles(BIOME, QUALITY, &mut rng);
assert_eq!(
rolls, frozen_rolls(),
"seed={SEED} biome={BIOME} quality={QUALITY}: roll vector changed — update frozen_rolls() if intentional"
);
}
#[test]
fn city_get_yields_folds_collectibles_correctly() {
// quality_mult = 1 + (3-1)*0.2 = 1.4
// hardwood → Production: 4 * 1.4 = 5.6
// wild_game → Food: 1 * 1.4 = 1.4
let tile = TileYield {
coord: (0, 0),
food: 0.0,
production: 0.0,
gold: 0.0,
culture: 0.0,
science: 0.0,
collectibles: frozen_rolls(),
};
let mut city = City::new("khazad");
city.worked_tiles = vec![(0, 0)];
let yields = city.get_yields(&[tile]);
let expected_food = CITY_CENTER_BASELINE_FOOD + 1.4;
let expected_prod = CITY_CENTER_BASELINE_PRODUCTION + 5.6;
assert!(
(yields.food - expected_food).abs() < 1e-9,
"food: expected {expected_food}, got {}", yields.food
);
assert!(
(yields.production - expected_prod).abs() < 1e-9,
"production: expected {expected_prod}, got {}", yields.production
);
}
#[test]
fn determinism_same_seed_same_rolls() {
let mut rng_a = SplitMix64::new(SEED);
let mut rng_b = SplitMix64::new(SEED);
assert_eq!(
tile_collectibles(BIOME, QUALITY, &mut rng_a),
tile_collectibles(BIOME, QUALITY, &mut rng_b)
);
}

View file

@ -0,0 +1,60 @@
//! Golden test: luxury happiness calculation.
//!
//! Frozen expectation:
//! owned_luxuries = {"furs", "salt", "wine"} (3 unique entries)
//! LUXURY_HAPPINESS = 4 per unique luxury
//! expected = 3 * 4 = 12
//!
//! Note: these IDs are deposit-level IDs as referenced by biome collectible
//! tables (furs from boreal_forest, salt from desert). Concept-resolution
//! (deposit_id → resource concept) is pending a future concept_resource field
//! in resources.json — for now happiness lookups use deposit IDs directly.
use mc_happiness::pool::{happiness_from_luxuries, HappinessConfig, LUXURY_HAPPINESS};
use std::collections::BTreeSet;
fn owned() -> BTreeSet<String> {
BTreeSet::from(["furs".to_string(), "salt".to_string(), "wine".to_string()])
}
#[test]
fn three_luxuries_yield_twelve_happiness() {
let config = HappinessConfig::default();
// Frozen: 3 unique * LUXURY_HAPPINESS(4) = 12
assert_eq!(LUXURY_HAPPINESS, 4, "LUXURY_HAPPINESS constant changed — update frozen expectation");
let result = happiness_from_luxuries(&owned(), &config);
assert_eq!(result, 12, "happiness_from_luxuries({{furs, salt, wine}}) must equal 12");
}
#[test]
fn empty_luxuries_yield_zero() {
let config = HappinessConfig::default();
assert_eq!(happiness_from_luxuries(&BTreeSet::new(), &config), 0);
}
#[test]
fn duplicate_luxury_counted_once() {
let config = HappinessConfig::default();
// BTreeSet deduplicates — inserting "furs" twice still counts as 1
let mut luxuries = BTreeSet::new();
luxuries.insert("furs".to_string());
luxuries.insert("furs".to_string());
luxuries.insert("salt".to_string());
// 2 unique * 4 = 8
assert_eq!(happiness_from_luxuries(&luxuries, &config), 8);
}
#[test]
fn happiness_scales_linearly_with_unique_count() {
let config = HappinessConfig::default();
for n in 0_usize..=6 {
let luxuries: BTreeSet<String> =
(0..n).map(|i| format!("luxury_{i}")).collect();
let expected = (n as i32) * config.luxury_happiness;
assert_eq!(
happiness_from_luxuries(&luxuries, &config),
expected,
"n={n} luxuries should give {expected} happiness"
);
}
}

View file

@ -0,0 +1,62 @@
//! Golden test: strategic resource gate — build cavalry (requires iron),
//! debit, second build fails, unit dies → iron credited → build succeeds.
//!
//! Tests check_strategic_reqs / debit_resources / credit_resources from
//! mc-combat::requirements across the full lifecycle.
use mc_combat::requirements::{check_strategic_reqs, credit_resources, debit_resources, MissingResource};
use std::collections::BTreeMap;
fn ledger(pairs: &[(&str, u32)]) -> BTreeMap<String, u32> {
pairs.iter().map(|(k, v)| (k.to_string(), *v)).collect()
}
const IRON: &str = "iron_ore";
#[test]
fn cavalry_build_debit_block_kill_credit_cycle() {
let reqs = vec![IRON.to_string()];
let mut ld = ledger(&[(IRON, 1)]);
// Check: iron available → build allowed
assert!(check_strategic_reqs(&reqs, &ld).is_ok(), "iron present, build should be allowed");
// Debit: consume iron on build
debit_resources(&reqs, &mut ld);
assert_eq!(ld[IRON], 0, "iron debited to 0 after build");
// Second build blocked: ledger at zero
assert_eq!(
check_strategic_reqs(&reqs, &ld),
Err(MissingResource(IRON.to_string())),
"second cavalry build must be blocked while first is alive"
);
// Unit dies: iron returned
credit_resources(&reqs, &mut ld);
assert_eq!(ld[IRON], 1, "iron credited back on unit death");
// Build allowed again
assert!(check_strategic_reqs(&reqs, &ld).is_ok(), "iron restored, build should succeed again");
}
#[test]
fn no_reqs_never_blocks() {
let ld = BTreeMap::new();
assert!(check_strategic_reqs(&[], &ld).is_ok());
}
#[test]
fn missing_resource_error_names_resource() {
let reqs = vec!["horses".to_string()];
let err = check_strategic_reqs(&reqs, &BTreeMap::new()).unwrap_err();
assert_eq!(err, MissingResource("horses".to_string()));
}
#[test]
fn debit_saturates_at_zero() {
let reqs = vec![IRON.to_string()];
let mut ld = ledger(&[(IRON, 0)]);
debit_resources(&reqs, &mut ld);
assert_eq!(ld[IRON], 0, "debit must not underflow past zero");
}