diff --git a/game/__init__.py b/game/__init__.py new file mode 100644 index 0000000..808560a --- /dev/null +++ b/game/__init__.py @@ -0,0 +1 @@ +"""Pitchwell game package (sim stubs; no Godot required).""" diff --git a/game/sim/__init__.py b/game/sim/__init__.py new file mode 100644 index 0000000..a74850a --- /dev/null +++ b/game/sim/__init__.py @@ -0,0 +1,13 @@ +"""Godot-less Pitchwell sim stubs.""" + +from .pitch_economy import Economy, SumpWell, Worker, START_PITCH, WORKER_LOAD, WELL_CAPACITY, SEEP_PER_SEC + +__all__ = [ + "Economy", + "SumpWell", + "Worker", + "START_PITCH", + "WORKER_LOAD", + "WELL_CAPACITY", + "SEEP_PER_SEC", +] diff --git a/game/sim/pitch_economy.py b/game/sim/pitch_economy.py new file mode 100644 index 0000000..ff784f7 --- /dev/null +++ b/game/sim/pitch_economy.py @@ -0,0 +1,67 @@ +"""Pitch economy stub — numbers from docs/GAMEPLAY.md (Sump Wells / Start / Carry).""" + +from __future__ import annotations + +from dataclasses import dataclass, field + + +# GAMEPLAY.md: Start 500 Pitch +START_PITCH: float = 500.0 +# GAMEPLAY.md: Worker load 100 +WORKER_LOAD: int = 100 +# GAMEPLAY.md: Sump Wells finite 2500, seep +0.4/s while claimed +WELL_CAPACITY: float = 2500.0 +SEEP_PER_SEC: float = 0.4 + + +@dataclass +class SumpWell: + """Finite Pitch node. Claim via finished refinery in radius (GAMEPLAY.md).""" + + remaining: float = WELL_CAPACITY + claimed: bool = False + has_refinery: bool = False + + def claim_via_refinery(self) -> None: + """Claim well by finishing a refinery flag (GAMEPLAY.md).""" + self.has_refinery = True + self.claimed = True + + def tick_seep(self, dt: float) -> float: + """Seep +0.4/s while claimed; depletes remaining. Returns Pitch extracted.""" + if not self.claimed or self.remaining <= 0.0 or dt <= 0.0: + return 0.0 + extracted = min(self.remaining, SEEP_PER_SEC * dt) + self.remaining -= extracted + return extracted + + +@dataclass +class Worker: + """Carry load 100 (GAMEPLAY.md). Stub — load cap only for now.""" + + load: int = 0 + capacity: int = WORKER_LOAD + + def can_pick(self, amount: int) -> int: + return max(0, min(amount, self.capacity - self.load)) + + +@dataclass +class Economy: + """Faction Pitch pool + claimed wells.""" + + pitch: float = START_PITCH + wells: list[SumpWell] = field(default_factory=list) + + def add_well(self, well: SumpWell | None = None) -> SumpWell: + w = well if well is not None else SumpWell() + self.wells.append(w) + return w + + def tick(self, dt: float) -> float: + gained = 0.0 + for w in self.wells: + gained += w.tick_seep(dt) + self.pitch += gained + return gained diff --git a/kitchen/patches/D3.md b/kitchen/patches/D3.md new file mode 100644 index 0000000..0957e73 --- /dev/null +++ b/kitchen/patches/D3.md @@ -0,0 +1,21 @@ +# D3 — `@deltoid-loop` Pitch economy stub + +**When:** 2026-09-05 ~22:10:20Z +**Scope:** Godot-less sim matching `docs/GAMEPLAY.md` Pitch numbers. No Nintendo/KKnD assets. No push. + +## Files + +| Path | Role | +|------|------| +| `game/sim/pitch_economy.py` | SumpWell (2500, seep +0.4/s claimed), Worker load 100, start Pitch 500, claim via refinery | +| `game/sim/__init__.py` | exports | +| `game/__init__.py` | package | +| `scripts/smoke/economy_smoke.py` | 500 → claim → 10s seep → assert gain; exit 0/1 | +| `scripts/smoke/run_all.sh` | runs economy_smoke (+ hooks if other smokes appear); no `D3-HOOK` in missing headless | + +## Verify + +```bash +python3 scripts/smoke/economy_smoke.py +# expect PASS, exit 0 +```