Skip to content

feat(sim,render,save): finite food piles with runtime respawn (closes #112)#113

Merged
LightAxe merged 1 commit into
mainfrom
feat/issue-112-food-pile-depletion
May 10, 2026
Merged

feat(sim,render,save): finite food piles with runtime respawn (closes #112)#113
LightAxe merged 1 commit into
mainfrom
feat/issue-112-food-pile-depletion

Conversation

@LightAxe
Copy link
Copy Markdown
Owner

@LightAxe LightAxe commented May 9, 2026

Summary

Closes #112. Food piles previously existed as infinite static markers seeded at scenario start. They now drain by pickup-charges and a runtime spawner places fresh piles on a time-based cadence — turning food into a strategic survival mechanic where colonies must redirect foraging when nearby sources deplete.

  • Sim: `FoodPile` gains `pickupsRemaining`/`pickupsInitial` (charge counter, decoupled from carry quantity); `antPickupFood` drains charges; `tickForagerActions` splices depleted piles + records depletion; new tick step 16d `tickFoodPileSpawn` runs every 1800 ticks (~90s) with rejection-sampled grass-weighted placement that avoids colonies, entrances, rally points, existing piles, and recently-depleted neighbourhoods.
  • Render: 4-bucket shrink keyed on `pickupsRemaining/pickupsInitial` so both small ephemeral piles and large strategic anchors visibly drain across their lifetime.
  • Save format: bumped to v3 (`SAVE_KEY = 'subterrans:save:v3'`); v2 saves rejected via the existing `SaveVersionMismatchError` flow per the "intentional for beta" policy. New validator coverage on charge fields and the new `recentlyDepletedFood` array.

Design lock and rationale lives at #112 (planning thread, not duplicated here).

Determinism / boundary

  • Scenario seeding's per-pile pickup-charge draw uses a hash of `(terrainSeed, foodPileId)` instead of advancing the world RNG — preserving byte-identical replay for every existing test that calls `createScenario(seed)`.
  • Runtime spawn uses the seeded world RNG with unconditional draws so the rejection-sampling sequence is replay-stable across re-runs.
  • No `Math.random` / `Date` / `performance.now` / floats / cross-boundary imports introduced. ESLint pre-existing baseline (6 errors) is unchanged.

Defaults flagged for retune

Per the issue planning thread, these defaults are best-guesses pending playtest:

  • `FOOD_PILE_INITIAL_PICKUPS_MIN/MAX = 20 / 150` (pile-size variation)
  • `FOOD_PILE_PICKUP_DRAIN = 1` (one charge per pickup)
  • `FOOD_PILE_SPAWN_INTERVAL_TICKS = 1800` (~90s at 20Hz)
  • `FOOD_PILE_SOFT_CEILING = 30` (= 2× FOOD_PILE_COUNT, well under the v109 hard cap of 60)
  • `FOOD_PILE_TERRAIN_GRASS_WEIGHT = 4` vs `OTHER = 1`

Test plan

  • 1936 vitest tests pass (1901 → 1936; +35 new). Includes new `food-system.test.ts` (spawn gating, placement constraints, determinism, entity-ID exhaustion, tick-dispatcher integration), `pheromone/food-trail-decays-after-depletion.test.ts` (verifies the issue's "self-balancing pheromone decay" claim end-to-end), updated `save.test.ts` (direct `parseSaveFile` v2-rejection assertion + validator strictness), and `types.test.ts` `copyWorldState` round-trip coverage for the new field.
  • Three internal code-review passes — final pass clean (only one NIT: off-cycle gating test uses literal tick values that would need updating if `FOOD_PILE_SPAWN_INTERVAL_TICKS` is retuned to certain values; not blocking).
  • `npm run typecheck` clean. `npm run lint` shows only pre-existing baseline errors (none introduced by this PR). `scripts/check-sim-boundary.sh` and `scripts/check-asset-paths.sh` clean.
  • UAT focus — this is a sim-behavior-changing PR. Worth playtesting:
    • Does the depletion + respawn cadence feel right? Initial defaults may need retuning per playtest feel.
    • Verify the visible shrink-buckets read clearly at the current TILE_SIZE_PX.
    • Verify v2 saves boot fresh (intentional rejection — surface in any UI message you have for "no save found").

Sim version

No `LATEST_SIM_VERSION` bump. Save-format bump rejects all v2 saves outright, so every save the engine ever loads is post-spawn-mechanic — no surface where old data needs to replay under "no spawn" behaviour gates.

🤖 Generated with Claude Code

…112)

Food piles previously existed as infinite static markers seeded at scenario
start. They now drain by pickup-charges and a runtime spawner places fresh
piles on a time-based cadence — turning food into a strategic survival
mechanic where colonies must redirect foraging when nearby sources deplete.

Sim changes:
- FoodPile gains pickupsRemaining + pickupsInitial (charge counter; distinct
  from the fixed-point food quantity transferred per pickup, which stays at
  FOOD_PICKUP_AMOUNT). antPickupFood drains FOOD_PILE_PICKUP_DRAIN charges
  per successful transfer; tickForagerActions splices the pile and records
  the depletion when charges hit zero.
- New tick step 16d (tickFoodPileSpawn) runs after tickNurseActions, before
  combat. Time-gated (every FOOD_PILE_SPAWN_INTERVAL_TICKS = 1800 ticks /
  90s at 20Hz), soft-ceiling at FOOD_PILE_COUNT * 2 = 30. Placement uses
  rejection sampling with grass-weighted RNG and rejects tiles near
  colonies, entrances, rally points, existing piles, and recently-depleted
  tiles (anti-teleport guard).
- WorldState gains recentlyDepletedFood: bounded ring-buffer-style array
  (cap = soft ceiling, append-time shift) to prevent jarring "pile vanished
  here, instantly reappeared next door" placements while pheromone trails
  decay naturally.
- Scenario seeding's pickup-charge draw uses a deterministic hash of
  (terrainSeed, foodPileId) instead of advancing the world RNG, preserving
  byte-identical replay for every existing test.

Render: surface piles now visibly shrink across 4 size buckets keyed on
pickupsRemaining/pickupsInitial — both small ephemeral piles and large
strategic anchors visibly drain across their lifetime.

Save format: bumped to v3 (SAVE_KEY = 'subterrans:save:v3'). v2 saves are
rejected on load via the existing SaveVersionMismatchError flow per the
"intentional for beta" policy. Validators check pickup-charge fields
(pickupsInitial in [MIN, MAX], pickupsRemaining in [1, pickupsInitial])
and the new recentlyDepletedFood array.

Tests: 1936 passing (1901 → 1936). New coverage in food-system.test.ts
(spawn gating, placement constraints, determinism, entity-ID exhaustion,
tick-dispatcher integration), pheromone/food-trail-decays-after-depletion
test (verifies the design's "self-balancing" claim), save.test.ts updates
(v2 reject via parseSaveFile, validator strictness), and types.test.ts
copyWorldState round-trip coverage for the new field.

Default balance numbers (initial pickups 20-150, drain 1/pickup, spawn
~90s, grass weight 4:1) are best-guesses pending playtest retune.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@LightAxe
Copy link
Copy Markdown
Owner Author

LightAxe commented May 9, 2026

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@LightAxe LightAxe merged commit 4245c06 into main May 10, 2026
1 check passed
@LightAxe LightAxe deleted the feat/issue-112-food-pile-depletion branch May 10, 2026 15:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] Food piles deplete with foraging and respawn over time (strategic survival mechanic)

1 participant