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:
parent
d01d72082d
commit
eb2cf18c2d
5 changed files with 71 additions and 7 deletions
|
|
@ -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'>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue