feat(simulator): ✨ Add fauna product simulation module with fauna_product.rs logic and lib.rs module declaration
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
a62ab721de
commit
91d7516049
2 changed files with 151 additions and 0 deletions
149
src/simulator/crates/mc-ecology/src/fauna_product.rs
Normal file
149
src/simulator/crates/mc-ecology/src/fauna_product.rs
Normal file
|
|
@ -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<String>,
|
||||
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<String, i32> {
|
||||
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<String, f64> = 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue