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:
parent
aee31e286f
commit
05ead5077c
5 changed files with 215 additions and 0 deletions
14
src/simulator/tests/integration/Cargo.toml
Normal file
14
src/simulator/tests/integration/Cargo.toml
Normal 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" }
|
||||
1
src/simulator/tests/integration/src/lib.rs
Normal file
1
src/simulator/tests/integration/src/lib.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
// Stub — tests live in tests/*.rs integration test files.
|
||||
78
src/simulator/tests/integration/tests/biome_yield_golden.rs
Normal file
78
src/simulator/tests/integration/tests/biome_yield_golden.rs
Normal 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)
|
||||
);
|
||||
}
|
||||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue