From 91d7516049187a1546ed7d4e70abf9531061959e Mon Sep 17 00:00:00 2001 From: autocommit Date: Mon, 27 Apr 2026 02:56:12 -0700 Subject: [PATCH] =?UTF-8?q?feat(simulator):=20=E2=9C=A8=20Add=20fauna=20pr?= =?UTF-8?q?oduct=20simulation=20module=20with=20fauna=5Fproduct.rs=20logic?= =?UTF-8?q?=20and=20lib.rs=20module=20declaration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../crates/mc-ecology/src/fauna_product.rs | 149 ++++++++++++++++++ src/simulator/crates/mc-ecology/src/lib.rs | 2 + 2 files changed, 151 insertions(+) create mode 100644 src/simulator/crates/mc-ecology/src/fauna_product.rs diff --git a/src/simulator/crates/mc-ecology/src/fauna_product.rs b/src/simulator/crates/mc-ecology/src/fauna_product.rs new file mode 100644 index 00000000..69f2623b --- /dev/null +++ b/src/simulator/crates/mc-ecology/src/fauna_product.rs @@ -0,0 +1,149 @@ +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; + +use crate::engine::EcologyEngine; + +/// Mirrors the fauna_product JSON shape for a single product entry. +/// Source files are single-element arrays; callers iterate and pass slices here. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FaunaProduct { + pub id: String, + pub source_fauna: Vec, + pub min_population: i32, + pub harvest_rate: f64, +} + +/// Compute how many units of each fauna product the player can harvest +/// from their owned tiles this turn. +/// +/// For each product, the total live population of all listed source_fauna +/// species across player_owned_tiles is summed. If the total strictly +/// exceeds min_population, `floor(total * harvest_rate)` units are produced. +/// Products below threshold are absent from the returned map. +pub fn fauna_product_supply( + player_owned_tiles: &[(i32, i32)], + engine: &EcologyEngine, + products: &[FaunaProduct], +) -> BTreeMap { + if player_owned_tiles.is_empty() || products.is_empty() { + return BTreeMap::new(); + } + + // Build a reverse map: species_key → total population across owned tiles. + // We accumulate f64 for precision before applying harvest_rate. + let mut species_totals: BTreeMap = BTreeMap::new(); + + for &(col, row) in player_owned_tiles { + let slots = match engine.tile_populations.get(&(col, row)) { + Some(s) => s, + None => continue, + }; + for slot in slots { + let species = match engine.species_registry.get(&slot.species_id) { + Some(sp) => sp, + None => continue, + }; + if !species.species_key.is_empty() { + *species_totals.entry(species.species_key.clone()).or_insert(0.0) += + slot.population as f64; + } + } + } + + let mut result = BTreeMap::new(); + for product in products { + let total: f64 = product + .source_fauna + .iter() + .map(|key| species_totals.get(key).copied().unwrap_or(0.0)) + .sum(); + + if total > product.min_population as f64 { + let units = (total * product.harvest_rate).floor() as i32; + if units > 0 { + result.insert(product.id.clone(), units); + } + } + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::engine::EcologyEngine; + use crate::population::PopulationSlot; + use crate::species::Species; + use crate::traits::{ + Diet, Habitat, Locomotion, Reproduction, Size, Social, Thermal, TraitSet, + }; + + fn make_species(id: u32, key: &str) -> Species { + let mut sp = Species::derive_from_traits( + id, + key.to_string(), + TraitSet { + size: Size::Large, + diet: Diet::Omnivore, + habitat: Habitat::Terrestrial, + locomotion: Locomotion::Walking, + reproduction: Reproduction::KStrategy, + thermal: Thermal::WarmBlooded, + social: Social::Solitary, + }, + ); + sp.species_key = key.to_string(); + sp + } + + fn bear_product() -> FaunaProduct { + FaunaProduct { + id: "bear_pelt".to_string(), + source_fauna: vec!["cave_bear".to_string()], + min_population: 50, + harvest_rate: 0.05, + } + } + + #[test] + fn fauna_product_supply_empty_tiles_returns_empty_map() { + let engine = EcologyEngine::new(); + let products = vec![bear_product()]; + let result = fauna_product_supply(&[], &engine, &products); + assert!(result.is_empty()); + } + + #[test] + fn fauna_product_supply_population_below_threshold_yields_zero() { + let mut engine = EcologyEngine::new(); + let bear = make_species(1, "cave_bear"); + engine.register_species(bear); + // Seed exactly at threshold — must NOT produce (strict >) + engine.seed_population(0, 0, PopulationSlot::new(1, 50.0)); + + let products = vec![bear_product()]; + let result = fauna_product_supply(&[(0, 0)], &engine, &products); + assert!( + result.get("bear_pelt").copied().unwrap_or(0) == 0, + "At-threshold population should not produce any supply" + ); + } + + #[test] + fn fauna_product_supply_population_above_threshold_applies_harvest_rate() { + let mut engine = EcologyEngine::new(); + let bear = make_species(1, "cave_bear"); + engine.register_species(bear); + // 100 bears, threshold 50, harvest_rate 0.05 → floor(100 * 0.05) = 5 + engine.seed_population(0, 0, PopulationSlot::new(1, 100.0)); + + let products = vec![bear_product()]; + let result = fauna_product_supply(&[(0, 0)], &engine, &products); + assert_eq!( + result.get("bear_pelt").copied().unwrap_or(0), + 5, + "100 bears × 0.05 harvest_rate = 5 pelts" + ); + } +} diff --git a/src/simulator/crates/mc-ecology/src/lib.rs b/src/simulator/crates/mc-ecology/src/lib.rs index 50228ec4..a91b5520 100644 --- a/src/simulator/crates/mc-ecology/src/lib.rs +++ b/src/simulator/crates/mc-ecology/src/lib.rs @@ -7,6 +7,7 @@ pub mod behavior; pub mod classification; +pub mod fauna_product; pub mod combat; pub mod config; pub mod dynamics; @@ -34,6 +35,7 @@ pub use events::{EventCategory, EventTierData, load_event_categories}; pub use species::load_species_library; pub use evolution::{run_evolution, ClimateStep, EvolutionResult, EventConfig, WorldAgeConfig}; pub use combat::CombatStatsConfig; +pub use fauna_product::{FaunaProduct, fauna_product_supply}; pub use wilds::{generate_lairs, check_lair_formation, check_lair_abandonment, check_lair_state_transitions, lair_inheritable, LairConfig, locomotion_str_from_u8, size_str_from_u8, LairType, LairState,