feat(@projects/@magic-civilization): 🦌 p3-19 (core) — ecology population depletion API for player pressure

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) <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-06-25 14:06:31 -04:00
parent d01d72082d
commit eb2cf18c2d
5 changed files with 71 additions and 7 deletions

View file

@ -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** |
</td><td valign='top' style='padding-left:2em'>

View file

@ -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

View file

@ -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",

View file

@ -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);

View file

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