From eb2cf18c2deadebfd19ce3ddb539c432b53b59b7 Mon Sep 17 00:00:00 2001 From: Natalie Date: Thu, 25 Jun 2026 14:06:31 -0400 Subject: [PATCH] =?UTF-8?q?feat(@projects/@magic-civilization):=20?= =?UTF-8?q?=F0=9F=A6=8C=20p3-19=20(core)=20=E2=80=94=20ecology=20populatio?= =?UTF-8?q?n=20depletion=20API=20for=20player=20pressure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Rust foundation for player→ecology feedback: PopulationSlot::deplete(amount) (floors at 0, can cross the is_extinct threshold) + EcologyEngine::deplete_population (col,row,species_id,amount) → post-depletion population, safe 0.0 no-op for a missing tile/species. This is the hook hunting/harvesting will call to make over-harvest drive local extinction; the engine's existing growth/emergence then recovers abundance once pressure eases. Tests: deplete_reduces_floors_at_zero_and_can_extinct (PopulationSlot) + deplete_population_reduces_tile_species_and_can_extinct (engine); mc-ecology green. p3-19 → partial. Remaining: GdEcologyEngine #[func] + GDScript kill/harvest wiring + dylib/GUT (loop continues). Co-Authored-By: Claude Opus 4.8 (1M context) --- .project/objectives/README.md | 4 +-- .../p3-19-player-ecology-feedback.md | 6 +++- .../games/age-of-dwarves/data/objectives.json | 8 ++--- src/simulator/crates/mc-ecology/src/engine.rs | 36 +++++++++++++++++++ .../crates/mc-ecology/src/population.rs | 24 +++++++++++++ 5 files changed, 71 insertions(+), 7 deletions(-) diff --git a/.project/objectives/README.md b/.project/objectives/README.md index b7b8ef78..0bf3e208 100644 --- a/.project/objectives/README.md +++ b/.project/objectives/README.md @@ -17,8 +17,8 @@ | **P0** | 44 | 0 | 0 | 0 | 0 | 0 | 44 | | **P1** | 88 | 0 | 0 | 0 | 0 | 1 | 89 | | **P2** | 130 | 0 | 0 | 0 | 0 | 1 | 131 | -| **P3 (oos)** | 30 | 0 | 3 | 0 | 2 | 29 | 64 | -| **total** | **292** | **0** | **3** | **0** | **2** | **31** | **328** | +| **P3 (oos)** | 30 | 0 | 4 | 0 | 1 | 29 | 64 | +| **total** | **292** | **0** | **4** | **0** | **1** | **31** | **328** | diff --git a/.project/objectives/p3-19-player-ecology-feedback.md b/.project/objectives/p3-19-player-ecology-feedback.md index 77d26af7..937ff7ac 100644 --- a/.project/objectives/p3-19-player-ecology-feedback.md +++ b/.project/objectives/p3-19-player-ecology-feedback.md @@ -2,7 +2,7 @@ id: p3-19 title: Player → ecology feedback — harvesting & hunting deplete live populations (over-harvest → extinction) priority: p3 -status: missing +status: partial scope: game1 owner: warcouncil updated_at: 2026-06-25 @@ -24,6 +24,10 @@ Net: **you cannot over-harvest or over-hunt a species toward local extinction**, and abundance doesn't respond to player pressure — which undercuts the "living-world is the Game-1 USP" promise (the world should react to the player). +## Progress (2026-06-25) + +Rust core landed: `PopulationSlot::deplete(amount)` + `EcologyEngine::deplete_population(col,row,species_id,amount)` (floors at 0, drives local extinction; missing tile/species = safe 0.0 no-op). Tests green (mc-ecology). **Remaining (the coupling/wiring):** a `GdEcologyEngine` `#[func]` exposing deplete + the GDScript kill (item_system.gd / lair clear) and intensive-harvest paths calling it for the victim/flora species at the tile + dylib rebuild + GUT proof that abundance responds to player pressure. + ## Acceptance - [ ] Killing fauna (combat / lair clear) decrements that species' live diff --git a/public/games/age-of-dwarves/data/objectives.json b/public/games/age-of-dwarves/data/objectives.json index 9e82457a..edc726cb 100644 --- a/public/games/age-of-dwarves/data/objectives.json +++ b/public/games/age-of-dwarves/data/objectives.json @@ -1,10 +1,10 @@ { - "generated_at": "2026-06-25T17:57:20Z", + "generated_at": "2026-06-25T18:06:31Z", "totals": { - "partial": 3, "in_progress": 0, + "missing": 1, "oos": 31, - "missing": 2, + "partial": 4, "stub": 0, "done": 292, "total": 328 @@ -3234,7 +3234,7 @@ "id": "p3-19", "title": "Player → ecology feedback — harvesting & hunting deplete live populations (over-harvest → extinction)", "priority": "p3", - "status": "missing", + "status": "partial", "scope": "game1", "owner": "warcouncil", "updated_at": "2026-06-25", diff --git a/src/simulator/crates/mc-ecology/src/engine.rs b/src/simulator/crates/mc-ecology/src/engine.rs index 0610a2f0..996ec71c 100644 --- a/src/simulator/crates/mc-ecology/src/engine.rs +++ b/src/simulator/crates/mc-ecology/src/engine.rs @@ -269,6 +269,24 @@ impl EcologyEngine { self } + /// p3-19 — apply player pressure (hunting fauna / harvesting flora) to the + /// live population of `species_id` on tile `(col, row)`, reducing it by + /// `amount`. Returns the post-depletion population (`0.0` when the tile or + /// species has no slot). Sustained calls drive the slot to local extinction + /// ([`PopulationSlot::is_extinct`]); the per-turn growth/emergence dynamics + /// recover it once the pressure eases. This is the missing player→ecology + /// coupling — the engine already migrates/grows on its own, but nothing fed + /// player harvest/kills back into abundance. + pub fn deplete_population(&mut self, col: i32, row: i32, species_id: u32, amount: f32) -> f32 { + if let Some(slots) = self.tile_populations.get_mut(&(col, row)) { + if let Some(slot) = slots.iter_mut().find(|s| s.species_id == species_id) { + slot.deplete(amount); + return slot.population; + } + } + 0.0 + } + /// Attempt to initialize GPU-accelerated population dynamics. /// /// Call after the grid is sized. If GPU hardware is unavailable or the tile @@ -1354,6 +1372,24 @@ mod tests { ); } + #[test] + fn deplete_population_reduces_tile_species_and_can_extinct() { + // p3-19: player pressure (hunt/harvest) on a tile's species reduces its + // live population and can drive local extinction; missing tile/species + // is a safe no-op returning 0.0. + let mut engine = EcologyEngine::new(); + engine + .tile_populations + .insert((3, 4), vec![crate::population::PopulationSlot::new(7, 1.0)]); + let after = engine.deplete_population(3, 4, 7, 0.4); + assert!((after - 0.6).abs() < 1e-4, "0.4 depleted from 1.0"); + let after2 = engine.deplete_population(3, 4, 7, 5.0); + assert_eq!(after2, 0.0, "over-depletion floors at 0"); + assert!(engine.tile_populations[&(3, 4)][0].is_extinct()); + assert_eq!(engine.deplete_population(9, 9, 7, 1.0), 0.0, "missing tile → 0"); + assert_eq!(engine.deplete_population(3, 4, 999, 1.0), 0.0, "missing species → 0"); + } + #[test] fn full_cycle_with_predator_prey() { let mut grid = make_forest_grid(1, 1); diff --git a/src/simulator/crates/mc-ecology/src/population.rs b/src/simulator/crates/mc-ecology/src/population.rs index dade0c6f..a2664f3a 100644 --- a/src/simulator/crates/mc-ecology/src/population.rs +++ b/src/simulator/crates/mc-ecology/src/population.rs @@ -35,6 +35,17 @@ impl PopulationSlot { pub fn is_extinct(&self) -> bool { self.population < 0.01 } + + /// p3-19 — reduce this population by `amount` from player pressure + /// (hunting fauna / harvesting flora), floored at 0. Sustained depletion can + /// push it below the [`Self::is_extinct`] threshold (local extinction); the + /// engine's growth/emergence dynamics let it recover once pressure eases. + /// Negative `amount` is ignored (use the normal growth path to add). + pub fn deplete(&mut self, amount: f32) { + if amount > 0.0 { + self.population = (self.population - amount).max(0.0); + } + } } #[cfg(test)] @@ -56,4 +67,17 @@ mod tests { slot.population = 0.005; assert!(slot.is_extinct()); } + + #[test] + fn deplete_reduces_floors_at_zero_and_can_extinct() { + let mut slot = PopulationSlot::new(7, 1.0); + slot.deplete(0.3); + assert!((slot.population - 0.7).abs() < 1e-4, "0.3 depleted from 1.0"); + assert!(!slot.is_extinct()); + slot.deplete(5.0); // over-deplete floors at 0 → local extinction + assert_eq!(slot.population, 0.0); + assert!(slot.is_extinct(), "sustained over-harvest drives local extinction"); + slot.deplete(-1.0); // negative ignored — growth uses the normal path + assert_eq!(slot.population, 0.0); + } }