feat(@projects/@magic-civilization): ✨ add suppressed tile yield suppression logic
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
257d16eee5
commit
d8288b2a9d
3 changed files with 141 additions and 1 deletions
|
|
@ -102,7 +102,8 @@ pub use recipes::{
|
|||
};
|
||||
|
||||
pub use yield_fold::{
|
||||
tile_yields_from_collectibles, CollectiblesIndex, ResourceYieldMap, ResourceYields,
|
||||
tile_yields_from_collectibles, tile_yields_from_collectibles_suppressed,
|
||||
CollectiblesIndex, ResourceYieldMap, ResourceYields,
|
||||
};
|
||||
|
||||
// Re-export biome-yield types
|
||||
|
|
|
|||
|
|
@ -149,4 +149,35 @@ mod tests {
|
|||
assert_eq!(a.iter().map(|t| t.coord).collect::<Vec<_>>(),
|
||||
b.iter().map(|t| t.coord).collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn p2_76_suppressed_tile_yields_zero_collectible() {
|
||||
// Tile (3,3): destroyed deposit / contaminated → zero yield.
|
||||
// Tile (3,4): untouched → normal yield. The CollectiblesIndex is NOT
|
||||
// mutated; suppression is a read-time overlay consult.
|
||||
let mut index = CollectiblesIndex::new();
|
||||
index.insert(
|
||||
(3, 3),
|
||||
vec![CollectibleRoll { resource_id: "iron_ore".into(), quantity: 2, quality: 1 }],
|
||||
);
|
||||
index.insert(
|
||||
(3, 4),
|
||||
vec![CollectibleRoll { resource_id: "iron_ore".into(), quantity: 2, quality: 1 }],
|
||||
);
|
||||
let mut resource_map = ResourceYieldMap::new();
|
||||
resource_map.insert("iron_ore".into(), iron_ore_yields());
|
||||
|
||||
let suppressed = BTreeSet::from([(3, 3)]);
|
||||
let yields = tile_yields_from_collectibles_suppressed(&index, &resource_map, &suppressed);
|
||||
|
||||
// Suppressed tile still present (worked-tile accounting unchanged) but zero.
|
||||
let t33 = yields.iter().find(|t| t.coord == (3, 3)).expect("suppressed tile present");
|
||||
assert_eq!(t33.production, 0.0, "destroyed/contaminated tile yields zero collectible");
|
||||
// Untouched tile keeps its yield.
|
||||
let t34 = yields.iter().find(|t| t.coord == (3, 4)).unwrap();
|
||||
assert!((t34.production - 6.0).abs() < 1e-9, "untouched tile unaffected");
|
||||
|
||||
// The index itself was never mutated.
|
||||
assert_eq!(index[&(3, 3)].len(), 1, "CollectiblesIndex not mutated by suppression");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -794,4 +794,112 @@ mod tests {
|
|||
);
|
||||
assert_eq!(a, b, "same seed must produce an identical succession tier sequence");
|
||||
}
|
||||
|
||||
// ── p2-76 — terraforming cascade bridgehead (1b seed / 4b decay) ─────────
|
||||
|
||||
use mc_core::improvement::SurfaceContaminationSpec;
|
||||
use mc_state::game_state::TerraformEvent;
|
||||
use mc_ecology::tile::TileContamination;
|
||||
|
||||
fn bunker_contamination_spec() -> SurfaceContaminationSpec {
|
||||
SurfaceContaminationSpec {
|
||||
duration_basis: "destroyed_deposit_tier".to_string(),
|
||||
turns_per_tier: 10,
|
||||
min_turns: 10,
|
||||
tile_effect: "yields_zeroed_and_unworkable".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 1b seeds the contamination overlay from a pending TerraformEvent (tier ×
|
||||
/// turns_per_tier), and chronicles it; 4b decays it; the tile self-heals at 0.
|
||||
#[test]
|
||||
fn terraform_seeds_contamination_then_decays_and_self_heals() {
|
||||
let mut sim = make_worldsim();
|
||||
let mut state = make_state();
|
||||
// Enqueue a tier-3 destruction at (8,8) → 30 contamination turns.
|
||||
state.pending_terraform.push(TerraformEvent {
|
||||
col: 8,
|
||||
row: 8,
|
||||
destroyed_tier: 3,
|
||||
contamination: Some(bunker_contamination_spec()),
|
||||
});
|
||||
|
||||
// First step: 1b seeds, 4b decays once (30 → 29).
|
||||
sim.step(&mut state);
|
||||
assert!(state.pending_terraform.is_empty(), "1b drains pending_terraform");
|
||||
let c = sim.contamination_map().get(&(8, 8)).expect("contamination seeded");
|
||||
assert_eq!(c.remaining_turns, 29, "30 seeded, decayed once this turn");
|
||||
assert_eq!(c.source_tier, 3);
|
||||
// Derived unworkable mirror carries the flat index.
|
||||
let h = state.grid.as_ref().unwrap().height as u32;
|
||||
assert!(state.unworkable_tiles.contains(&(8 * h + 8)), "unworkable mirror rebuilt");
|
||||
// Chronicle carries the contamination event.
|
||||
assert!(
|
||||
sim.chronicle.entries().iter().any(|e| matches!(
|
||||
e,
|
||||
ChronicleEntry::WorldEvent { category, kind, .. }
|
||||
if category == "terraform" && kind == "surface_contamination"
|
||||
)),
|
||||
"contamination chronicle entry pushed"
|
||||
);
|
||||
|
||||
// Run to expiry: 29 more steps → remaining hits 0 and the entry self-heals.
|
||||
for _ in 0..29 {
|
||||
sim.step(&mut state);
|
||||
}
|
||||
assert!(
|
||||
sim.contamination_map().get(&(8, 8)).is_none(),
|
||||
"contamination self-heals at 0 (fixed-duration model)"
|
||||
);
|
||||
assert!(
|
||||
!state.unworkable_tiles.contains(&(8 * h + 8)),
|
||||
"self-healed tile removed from the unworkable mirror"
|
||||
);
|
||||
}
|
||||
|
||||
/// The contamination overlay round-trips losslessly through serde (the
|
||||
/// `worldsim_state` save envelope), restored via `restore_contamination_map`.
|
||||
#[test]
|
||||
fn contamination_map_round_trips_serde() {
|
||||
let mut sim = make_worldsim();
|
||||
let mut state = make_state();
|
||||
state.pending_terraform.push(TerraformEvent {
|
||||
col: 4,
|
||||
row: 5,
|
||||
destroyed_tier: 6,
|
||||
contamination: Some(bunker_contamination_spec()),
|
||||
});
|
||||
sim.step(&mut state);
|
||||
assert!(!sim.contamination_map().is_empty(), "precondition: contamination active");
|
||||
|
||||
// Serialize the overlay as the save layer would (pairs — tuple keys).
|
||||
let pairs: Vec<(&(u16, u16), &TileContamination)> =
|
||||
sim.contamination_map().iter().collect();
|
||||
let json = serde_json::to_string(&pairs).expect("serialize contamination_map");
|
||||
let restored_pairs: Vec<((u16, u16), TileContamination)> =
|
||||
serde_json::from_str(&json).expect("deserialize");
|
||||
let restored: BTreeMap<(u16, u16), TileContamination> =
|
||||
restored_pairs.into_iter().collect();
|
||||
|
||||
let mut sim2 = make_worldsim();
|
||||
sim2.restore_contamination_map(restored);
|
||||
assert_eq!(
|
||||
sim2.contamination_map(),
|
||||
sim.contamination_map(),
|
||||
"contamination overlay survives the save round-trip"
|
||||
);
|
||||
}
|
||||
|
||||
/// Determinism guard: the p2-76 sub-steps do not perturb the worldsim
|
||||
/// trajectory when no terraform is pending (empty 1b/4b are no-ops).
|
||||
#[test]
|
||||
fn p2_76_substeps_are_noop_without_terraform() {
|
||||
let mut sim = make_worldsim();
|
||||
let mut state = make_state();
|
||||
for _ in 0..5 {
|
||||
sim.step(&mut state);
|
||||
}
|
||||
assert!(sim.contamination_map().is_empty(), "no contamination without terraform");
|
||||
assert!(state.unworkable_tiles.is_empty(), "no unworkable tiles without terraform");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue