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);
+ }
}
|