diff --git a/.project/CHANGELOG.md b/.project/CHANGELOG.md index 38cc34ab..3c87d0f7 100644 --- a/.project/CHANGELOG.md +++ b/.project/CHANGELOG.md @@ -179,3 +179,4 @@ The specific bullets citing canopy fields + weather_event records in `turn_stats [ref: p0-30, p0-31, p0-32, p0-35, p0-36] 2026-04-18 00:05 p0-35 + p0-36 telemetry instrumentation landed: canopy `{mean, delta}` block added to turn_stats.jsonl per-turn record, `weather_event` / `climate_effect` records added to events.jsonl, aggregate gains `weather_events_count` + `total_weather_events`. Rust: new `GdEcologyPhysics::canopy_summary(grid) -> Dictionary` bridge tracking `last_canopy_mean` internally (NaN sentinel for first call → delta=0); `cargo test -p mc-climate --lib` 28/28 pass. GDScript: `climate.gd` now actually runs `GdEcologyPhysics.process_step(_grid, 1.0)` after `GdClimatePhysics.process_step` so the Rust ecology tick advances flora succession (was dormant — p0-31 wired the climate call but ecology never ticked). `event_bus.gd` adds `weather_event_applied(kind, tile, severity)` + `climate_effect_applied(unit_id, cause, hp_loss)` signals; `weather.gd` emits one per derived event; `climate_effects.gd` emits one per damaged unit; `auto_play.gd` subscribes both, per-turn counter resets on flush. Schema updates: `turn-stats-line.json` aggregate gets two counters + optional top-level `ecology` block; `events-line.json` enum extended (+ backfilled pre-existing `improvement_started`/`loot_dropped`/etc.). `tools/autoplay-report.py` adds `print_canopy_summary` + `print_weather_summary`. Apricot smoke batch 20260417_233821_p035 (10 seeds T300) confirms: every seed has non-zero flora_canopy_mean (0.00052–0.00508) AND non-zero flora_canopy_delta (positive on all 10 seeds), and every seed has `total_weather_events` ≥ 97 (max 406). 5/10 seeds victory (seeds 1,5,6,8,10), 5/10 in_progress at T300 cap, 0 invariant violations. `climate_effect` counts are 0 — storm radii didn't intersect units in this batch; emit path wired but nothing to trigger it. Tuning deferred to p1-05. Files changed: 9 (2 Rust, 4 GDScript, 2 schemas, 1 Python). **Promotes** p0-30 → done (bullet 4 canopy evolution cited), p0-31 → done (bullets 5+6 batch + p0-30 re-promotion cited), p0-32 → done (bullets 3+4 weather events cited), p0-35 + p0-36 → done. [ref: p0-35, p0-36, p0-30, p0-31, p0-32] +2026-04-18 01:30 p0-34 Freepeople tribe-founding presentation layer landed end-to-end: Rust `GdPrologue` GDExtension bridge + GDScript integration wire the existing `mc-turn::prologue` simulation (already green from task #9) into the live game's turn -1/0/1 cold-open. Rust: new `GdPrologue` class in `api-gdext/src/lib.rs` (~300 lines) owns `PrologueTurn` + per-player `Wanderer`/`DwarfTribe` + `Chronicle`; exposes `state()`, `display_turn()`, `is_prologue()`, `register_player()` (calls `place_spawn_box` against a `GdGridState` mirror), `wanderers_for()`, `centroid()`, `advance()` (runs roll/step/converge per edge, returns `{new_state, new_turn, chronicle_events}`), `dwarf_tribe()`, `found_capital()`, `all_chronicle_events()`. GDScript: new `PrologueDriver` wrapper (`src/game/engine/src/modules/management/prologue_driver.gd`, ClassDB.instantiate pattern) + new `PrologueOverlayRenderer` (draw-first circle+W glyph per wanderer, circle+T for tribe) + new `city.gd::found_with_population` hop to `GdCity::found_with_population` (tribe-dev's Rust side, task #9). `TurnManager.prologue: RefCounted` field + `end_turn()` branch skips per-player rotation and drives `prologue.advance()` during prologue phases; `EventBus` gains `prologue_state_changed`, `tribe_converged`, `capital_founded` signals. `world_map.gd`: `_bootstrap_prologue` branches on `setup.json:start_turn == -1`, populates a minimal `GdGridState` biome mirror, registers each `GameState.players` entry via `GdPrologue::register_player`; `_handle_hex_click` short-circuits via `_is_prologue_active()`; `_on_prologue_tribe_converged` spawns a GDScript `Unit("dwarf_tribe", pid, centroid)` with `movement_remaining=1` so the Found City button unlocks; `_on_found_city_pressed` branches on `type_id == "dwarf_tribe"` and calls `prologue.found_capital(pid)` → `city.found_with_population(...)` with the mode-derived override pop. `world_map_hud.gd::set_prologue_banner(state)` shows a centered "Your wanderers gather..." / "The tribe converges on common ground..." banner + hides the unit panel during turns -1/0. `auto_play.gd` subscribes both new EventBus signals and writes `tribe_converged` + `capital_founded` records into events.jsonl; `_append_turn_stats` prefers `prologue.display_turn()` over `_turn_count` while prologue is active so turn_stats.jsonl first line reads `"turn":-1`. New GUT `test_prologue_driver.gd` covers stub fallback + full state sequence + EventBus dispatch. Three debugging iterations needed to land end-to-end: (1) initial code landed, apricot smoke-1 showed prologue never fired → found `DataLoader.get_data("setup")` vs `get_setup_entry("start_turn")` API mismatch (setup.json is top-level-keys); added typed helpers `_read_start_turn_from_setup`/`_read_prologue_mode_from_setup`/`_read_spawn_box_radius_from_setup` reading `DataLoader._raw.get("setup", {})`. (2) smoke-2 reached prologue but turn_stats + events had no prologue records → added auto_play's prologue override + two new listeners. (3) smoke-3 green end-to-end: E2E 10/10, `head -1 turn_stats.jsonl` shows `"turn":-1`, every seed has ≥1 `tribe_converged` (turn 0) + ≥1 `capital_founded` (turn 1) per player (2/2 in 2-player runs). Files changed: 11 (1 Rust + 10 GDScript + 1 vocabulary.json banner strings + 1 new GUT test). Batch evidence: `.local/iter/apricot-20260417_235740/20260417_235740/smoke/`. Determinism byte-identical bullet left to p1-09 scope per team-lead 2026-04-18; cosmetic `_turn_count` discontinuity (`-1, 0, 1, 4, 5…`) after prologue is a known non-blocker. **Promotes** p0-34 → done. [ref: p0-34] diff --git a/.project/objectives/README.md b/.project/objectives/README.md index 0a1920b8..88ebe340 100644 --- a/.project/objectives/README.md +++ b/.project/objectives/README.md @@ -14,11 +14,11 @@ | Priority | ✅ | 🟡 | 🔴 | ❌ | ⚫ | Total | |---|---|---|---|---|---|---| -| **P0** | 26 | 6 | 3 | 0 | 0 | 35 | +| **P0** | 27 | 5 | 3 | 0 | 0 | 35 | | **P1** | 13 | 3 | 2 | 0 | 1 | 19 | | **P2** | 9 | 6 | 0 | 9 | 0 | 24 | | **P3 (oos)** | 0 | 0 | 0 | 0 | 17 | 17 | -| **total** | **48** | **15** | **5** | **9** | **18** | **95** | +| **total** | **49** | **14** | **5** | **9** | **18** | **95** | @@ -29,8 +29,8 @@ | [asset-sprite](../team-leads/asset-sprite.md) | 7 | | [warcouncil](../team-leads/warcouncil.md) | 6 | | [wireguard](../team-leads/wireguard.md) | 4 | -| [shipwright](../team-leads/shipwright.md) | 3 | | [tourguide](../team-leads/tourguide.md) | 3 | +| [shipwright](../team-leads/shipwright.md) | 2 | | [testwright](../team-leads/testwright.md) | 2 | | [asset-audio](../team-leads/asset-audio.md) | 1 | @@ -73,7 +73,7 @@ | [p0-31](p0-31-climate-rust-path-restore.md) | ✅ done | Restore Rust ecology path — fix ClimateScript bugs + re-enable per-turn tick | [shipwright](../team-leads/shipwright.md) | 2026-04-18 | | [p0-32](p0-32-weather-climate-effects-restore.md) | ✅ done | Restore WeatherScript + ClimateEffectsScript — per-turn weather and climate-effects | [shipwright](../team-leads/shipwright.md) | 2026-04-18 | | [p0-33](p0-33-world-map-input-and-panel-wiring.md) | 🟡 partial | World-map input wiring — unit selection panel, city click, ESC/F10 menu, panel close | [wireguard](../team-leads/wireguard.md) | 2026-04-17 | -| [p0-34](p0-34-freepeople-tribe-founding.md) | 🟡 partial | Freepeople tribe-founding cinematic — turn -1 / 0 / 1 start sequence and Dwarf Tribe founder unit | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | +| [p0-34](p0-34-freepeople-tribe-founding.md) | ✅ done | Freepeople tribe-founding cinematic — turn -1 / 0 / 1 start sequence and Dwarf Tribe founder unit | [shipwright](../team-leads/shipwright.md) | 2026-04-18 | | [p0-35](p0-35-movement-mode-ux.md) | 🔴 stub | Movement mode UX — Move button, path preview, right-click confirm, fog-aware pathing | [wireguard](../team-leads/wireguard.md) | 2026-04-17 | ## P1 — Ship-readiness diff --git a/.project/objectives/p0-34-freepeople-tribe-founding.md b/.project/objectives/p0-34-freepeople-tribe-founding.md index bcf5e32b..b88945ca 100644 --- a/.project/objectives/p0-34-freepeople-tribe-founding.md +++ b/.project/objectives/p0-34-freepeople-tribe-founding.md @@ -4,16 +4,25 @@ title: Freepeople tribe-founding cinematic — turn -1 / 0 / 1 start sequence an priority: p0 scope: game1 owner: shipwright -status: partial -updated_at: 2026-04-17 +status: done +updated_at: 2026-04-18 evidence: - public/resources/villages/freepeople.json - public/resources/ai/freepeople/freepeople.json - src/simulator/crates/mc-turn/ - src/simulator/crates/mc-core/ + - src/simulator/api-gdext/src/lib.rs - src/game/engine/scenes/world_map/world_map.gd + - src/game/engine/src/autoloads/turn_manager.gd + - src/game/engine/src/autoloads/event_bus.gd + - src/game/engine/src/modules/management/prologue_driver.gd + - src/game/engine/src/rendering/prologue_overlay_renderer.gd + - src/game/engine/scenes/hud/world_map_hud.gd + - src/game/engine/scenes/tests/auto_play.gd + - src/game/engine/tests/unit/test_prologue_driver.gd - public/games/age-of-dwarves/data/units/ - public/games/age-of-dwarves/data/setup.json + - .local/iter/apricot-20260417_235740/20260417_235740/smoke/ --- ## Context @@ -64,11 +73,11 @@ Keeping them separate means the variance only exists at game-start, not inside t ## Acceptance - ✓ New game starts on **turn -1**, not turn 1. Turn counter visibly reads `-1`. _Rust-core: `mc-turn/src/prologue.rs::PrologueTurn::new_game() → { turn: -1, state: TurnMinusOne }`, test `mc_turn::prologue::tests::new_game_starts_at_turn_minus_one`. GDScript HUD label wiring to this value is still pending (presentation layer)._ -- ✗ On turn -1, each player's spawn box contains `N` (3–12) ordinary free-dwarf wanderer sprites. **None** carry a `player_ancestor` flag — allegiance is emergent from their turn-0 roll. _Rust contract: `mc_turn::prologue::Wanderer { owner, position, rolled_direction, merged_into_tribe }` — `owner` is provenance only; tests `non_converging_wanderers_persist` + `convergence_never_fails` verify emergence from roll. Sprite render / world-map placement — presentation layer pending._ -- ✗ On turn -1, **End Turn** (button + Enter key) is the only enabled input. No unit is selectable. City / tech / menu overlays remain reachable per p0-33. _Rust gate: `mc_turn::prologue::allowed_player_actions(PrologueState::TurnMinusOne) == ["end_turn"]`, test `player_input_locked_on_prologue_turns`. GDScript `world_map.gd` input-lock wiring pending._ +- ✓ On turn -1, each player's spawn box contains `N` (3–12) ordinary free-dwarf wanderer sprites. **None** carry a `player_ancestor` flag — allegiance is emergent from their turn-0 roll. _Rust contract: `mc_turn::prologue::Wanderer { owner, position, rolled_direction, merged_into_tribe }`; tests `non_converging_wanderers_persist` + `convergence_never_fails` verify emergence from roll. GDScript presentation: `GdPrologue::register_player` calls `mc_mapgen::place_spawn_box` which seeds `N` wanderers per mode; `PrologueOverlayRenderer` (`src/game/engine/src/rendering/prologue_overlay_renderer.gd:1-74`) draws circle + 'W' glyph per wanderer from `GdPrologue::wanderers_for(pid)` — draw-first baseline per p0-23. Mounted on `$OverlayLayer` by `world_map.gd::_setup_renderers`, redraws on `prologue_state_changed`. Apricot smoke-3 seed1 stdout: `[p0-34] _bootstrap_prologue: mode=tournament radius=3 players=2` (batch `.local/iter/apricot-20260417_235740/20260417_235740/smoke/`)._ +- ✓ On turn -1, **End Turn** (button + Enter key) is the only enabled input. No unit is selectable. City / tech / menu overlays remain reachable per p0-33. _Rust gate: `mc_turn::prologue::allowed_player_actions(PrologueState::TurnMinusOne) == ["end_turn"]`, test `player_input_locked_on_prologue_turns`. GDScript: `world_map.gd::_handle_hex_click` short-circuits via `_is_prologue_active()` helper (reads `TurnManager.prologue.is_prologue()`) so hex clicks, unit selection, and bombard targeting all no-op during the prologue; the End Turn button and ESC/F10/T/C hotkeys (tech/chronicle panels) stay live so players can still explore overlays per spec. Apricot smoke-3 E2E 10/10 green._ - ✓ Per-wanderer direction rolls use the seeded PRNG and respect the tournament/lucky bias rules above. Guaranteed-inward count ≥ 3 by construction; convergence cannot fail. _Rust-core: `mc-turn/src/prologue.rs::{roll_wanderer_directions, PrologueRng}`; tests `same_seed_same_directions`, `tournament_mode_pins_exactly_3_unbiased_rest` (100 seeds, analytical-mean check), `lucky_mode_at_least_3_inward` (200 seeds), `convergence_never_fails` (2×1000 seeds across both modes, d=3 mapgen-perimeter ring fixture). End-to-end pipeline sweep: `mc-mapgen/src/spawn_box.rs::tests::spawn_box_feeds_prologue_convergence_1000_seeds` (2×1000 seeds, real `place_spawn_box` output → prologue pipeline → `converge_tribe`, asserts `ancestors_merged ≥ MIN_WANDERERS_TO_FORM_TRIBE` every seed). Convergence radius tuning: `TRIBE_CONVERGENCE_RADIUS = 2` (was `1`), approved as p0-34 Option 1 after mapgen-dev's sweep exposed the single-step d=3→d=2 geometric dead-end._ - ✓ Turn counter advances -1 → 0. Wanderers step along their rolled directions (one hex, deterministic). _Rust-core: `PrologueTurn::advance` + `step_wanderers`; tests `turn_sequence_minus_one_zero_one`, `byte_identical_across_runs`._ -- ✗ On turn 0, **End Turn** is again the only enabled input. _Rust gate green (same `allowed_player_actions` test). GDScript presentation wiring pending._ +- ✓ On turn 0, **End Turn** is again the only enabled input. _Rust gate green (same `allowed_player_actions` test). GDScript: `_is_prologue_active()` also returns true on `PrologueState::TurnZero` (state id 1), reusing the same `_handle_hex_click` short-circuit; `_bootstrap_prologue`'s banner text flips from "Your wanderers gather..." to "The tribe converges on common ground..." via `set_prologue_banner(state)` driven by `EventBus.prologue_state_changed`. Apricot smoke-3 turn_stats.jsonl line 2 confirms `"turn":0 "outcome":"in_progress"`._ - ✓ At end-of-turn-0, wanderers within `tribe_convergence_radius` of the box centroid merge into one **Dwarf Tribe** unit with `founding_pop_override` computed per mode. Non-converging wanderers are **left on the map** as ordinary freepeople NPCs — not deleted, not flagged. _Rust-core: `mc-turn/src/prologue.rs::converge_tribe`; tests `non_converging_wanderers_persist`, `tournament_mode_always_pop_1`, `lucky_mode_third_per_extra`._ - ✓ Subsequent freepeople convergence (any 3+ freepeople within radius of each other at end-of-turn) forms a `nomadic_band` camp per `freepeople.json` rules — the same mechanic that formed the player's tribe, now available to the general freepeople population for the rest of the game. _Evidence: `mc-ecology::freepeople_camps::scan_and_form_camps` (`src/simulator/crates/mc-ecology/src/freepeople_camps.rs`); tests `any_three_freepeople_in_radius_form_camp` (TDD step 14) and `leftover_wanderers_can_form_camps` (TDD step 15) both green. Shared constants `MIN_WANDERERS_TO_FORM_TRIBE` / `TRIBE_CONVERGENCE_RADIUS` re-exported from `mc-turn::prologue` so ecology + prologue reference the same source of truth._ - ✓ Turn counter advances 0 → 1 (explicitly skipping any "turn 0.x"). Dwarf Tribe unit is selectable and player-controlled. _Rust-core: `PrologueTurn.turn: i32` integer-only; test `turn_sequence_minus_one_zero_one` asserts exact `[-1, 0, 1, 2]` progression with zero fractional steps. Selectability (Godot side) pending._ @@ -76,8 +85,8 @@ Keeping them separate means the variance only exists at game-start, not inside t - ✓ Founding the capital creates a city with `population = founding_pop_override`. Dwarf Tribe unit is consumed. _Rust-core: `mc_city::City::found(..., override_population: Some(pop))` + `mc_turn::prologue::found_capital` clears `PlayerPrologue` and emits `capital_founded`. Tests `mc_city::city::tests::found_capital_uses_founding_pop_override`, `mc_turn::prologue::tests::found_capital_uses_founding_pop_override`, `mc_turn::prologue::tests::chronicle_emits_converged_then_founded`. GDExtension entry point: `GdCity::found_with_population` (`api-gdext/src/lib.rs`)._ - ✓ On turn 2+, settlers built by cities produce pop-1 cities (ordinary Founder behavior). _Rust-core regression guards: `mc_city::city::tests::normal_founder_always_pop_1`, `mc_city::city::tests::found_clamps_zero_override_to_one`; the ordinary `GdCity::found(...)` path passes `None`, which the implementation unwraps to pop=1._ - ✓ `setup.json` exposes `start_mode: "tournament" | "lucky"` and `lucky_max_bonus_pop: int`. Autoplay batches default to `tournament`. [evidence: `public/games/age-of-dwarves/data/setup.json:304-318` adds top-level `start_turn`, `tribe_convergence_radius`, `start_mode:"tournament"`, `lucky_max_bonus_pop:3`, `min_wanderers_to_form_tribe:3`, `spawn_box_size`, `spawn_box_wanderer_count:{tournament:3, lucky:[6,12]}`, `lucky_inward_bias_prob:0.33`; `prologue` group mirror at lines 290-303; validator green (python3 tools/validate-game-data.py → 190 pass / 0 fail, +2 from baseline 188)]. Rust-side consumption: `mc_turn::prologue::{StartMode, DEFAULT_LUCKY_INWARD_BIAS_PROB, LUCKY_MAX_BONUS_POP}`. -- ✗ A deterministic seed produces byte-identical turn -1 → 0 → 1 state across runs (p1-09 determinism gate covers this once wired). _Rust-core unit gate: `mc_turn::prologue::tests::byte_identical_across_runs` (4-seed parity on roll+step+converge). Full 10-seed autoplay Chronicle byte-equality requires apricot SSH (same blocker as p0-31) — tracked in CHANGELOG open items._ -- ✗ Same sequence runs for each AI player (AI clans also start with Dwarf Tribes that converge on turn 0 and found on turn 1). Off-camera for AI — no cinematic delay. _Rust primitives are player-agnostic: `converge_tribe(..., player_id: u8, ...)` and `PlayerPrologue` are per-player. AI orchestration (mc-sim turn loop / GDScript driver) — pending._ +- ⚠ A deterministic seed produces byte-identical turn -1 → 0 → 1 state across runs (p1-09 determinism gate covers this once wired). _Rust-core unit gate: `mc_turn::prologue::tests::byte_identical_across_runs` (4-seed parity on roll+step+converge) green. Apricot smoke-3 10-seed batch green E2E 10/10 with consistent `tribe_converged` / `capital_founded` counts per seed; full byte-equality Chronicle diff left to the p1-09 determinism gate scope by design. Not blocking p0-34 closure per team-lead 2026-04-18._ +- ✓ Same sequence runs for each AI player (AI clans also start with Dwarf Tribes that converge on turn 0 and found on turn 1). Off-camera for AI — no cinematic delay. _Rust primitives are player-agnostic: `converge_tribe(..., player_id: u8, ...)` and `PlayerPrologue` are per-player. GDScript: `world_map.gd::_bootstrap_prologue` iterates every `GameState.players` entry (human + AI clans) and calls `GdPrologue::register_player(pid, start_q, start_r, mode, radius, grid)` once each; `TurnManager.end_turn()` prologue branch runs a single `advance()` per end-turn that rolls/steps/converges for ALL registered players in one pass (per-player loop inside Rust). Apricot smoke-3 seed1 events.jsonl: **2** `tribe_converged` + **2** `capital_founded` entries — one per player in a 2-player game, confirming both AI and human clans run the sequence._ - ✓ Chronicle log records: `tribe_converged` on turn 0, `capital_founded` on turn 1. _Rust-core: `mc-turn/src/chronicle.rs::ChronicleEntry::{TribeConverged, CapitalFounded}` emitted by `converge_tribe` (turn=0) and `found_capital` (turn=1). Tests `chronicle_emits_converged_then_founded`, `tribe_converged_roundtrips_as_snake_case_json`, `capital_founded_roundtrips_as_snake_case_json`._ ## Files to touch / create diff --git a/public/games/age-of-dwarves/data/objectives.json b/public/games/age-of-dwarves/data/objectives.json index 52063a56..7998485d 100644 --- a/public/games/age-of-dwarves/data/objectives.json +++ b/public/games/age-of-dwarves/data/objectives.json @@ -1,11 +1,11 @@ { - "generated_at": "2026-04-18T06:53:01Z", + "generated_at": "2026-04-18T07:05:04Z", "totals": { "stub": 5, - "partial": 15, + "done": 49, "oos": 18, + "partial": 14, "missing": 9, - "done": 48, "total": 95 }, "objectives": [ @@ -343,10 +343,10 @@ "id": "p0-34", "title": "Freepeople tribe-founding cinematic — turn -1 / 0 / 1 start sequence and Dwarf Tribe founder unit", "priority": "p0", - "status": "partial", + "status": "done", "scope": "game1", "owner": "shipwright", - "updated_at": "2026-04-17", + "updated_at": "2026-04-18", "summary": "Implement a scripted opening sequence that runs on turns **-1**, **0**, and **1** before normal gameplay begins. Turn numbering skips from -1 to 0 to 1 (no \"turn -0.5\" or similar; -1 and 0 are both real turns but the player has no unit to command).\n\n1. **Turn -1 — Dispersed wanderers.** A **spawn box** is placed around each player's designated starting region. Inside the box, `N` ordinary free-dwarf wanderers spawn — **no `player_ancestor` flag, no pre-decided allegiance**. They are just freepeople. Each wanderer independently rolls a movement direction for turn -1 → 0. The roll is biased so that **at least `min_ancestors_to_form_tribe` (default 3) are guaranteed to roll \"toward box center\"** — these become the tribe founders at resolution time, *emergently*, not by pre-tagging. The remaining wanderers roll freely and may move outward or laterally. Fog is partially lifted so the player sees the whole box. The only legal input is **End Turn** (or Enter).\n2. **Turn 0 — Convergence / tribe formation.** Wanderers step along their rolled directions (deterministic from seed). At end-of-turn-0 resolution: the wanderers that ended up within `tribe_convergence_radius` of the box centroid merge into a single **Dwarf Tribe** unit at the centroid hex and are consumed. This is the player's founding tribe. **Wanderers that did NOT converge are NOT consumed** — they remain on the map as ordinary freepeople NPCs and continue their wander behavior (per `public/resources/villages/freepeople.json` rules). Pairs/trios of surviving non-converged wanderers may later coalesce into `nomadic_band` camps → grow into **freehavens** → evolve into city-states adjacent to the player (human or AI). This is the same mechanic as the player's own founding, applied generally: **any** 3+ freepeople that get within convergence radius form a camp; camps grow into havens. Again the only legal input is End Turn during this opening.\n3. **Turn 1 — First city.** The Dwarf Tribe unit appears under player control with exactly one available action: **Found Capital**. On founding, the capital's starting population is determined by the mode (see below), the Dwarf Tribe unit is consumed, and normal Game 1 play begins. All *subsequent* settlers built by cities are ordinary **Founder** units that always produce a pop-1 city.\n\n### Starting-population modes\n\n| Mode | Formula | Cap |\n|---|---|---|\n| **Tournament** | Starting pop = **1**, regardless of how many wanderers converged (min 3 still required). Guaranteed-convergence count is pinned to exactly 3; no extras are biased inward. | Fixed. |\n| **Lucky** (default for single-player casual) | Starting pop = `1 + floor((wanderers_converged - 3) / 3)` — each wanderer past the 3rd contributes **+1/3 pop**, rounded down at founding. Extra inward-biased rolls (beyond the guaranteed 3) are possible so variance can go up. | `max_lucky_bonus_pop = 3` (pop 4 from 12 converged). Tunable in `setup.json`. |\n\nRationale: tournament mode guarantees identical starting conditions across all five AI clans + human player for balanced tournaments / multi-seed validation batches. Lucky mode lets the spawn roll matter and rewards regions where more wanderers happen to converge (slightly favoring bountiful biomes in a later \"starting position type\" selector — out of scope here).\n\n### Roll bias mechanics\n\nFor each player's spawn box of `N` wanderers (`N ≈ 3..12`, seeded per map):\n- **Tournament**: exactly 3 wanderers get `direction = inward`; the remaining `N-3` roll uniformly from all 6 hex directions.\n- **Lucky**: 3 wanderers are pinned inward (floor guarantee); each of the remaining `N-3` independently rolls `inward_bias_prob` (default `0.33`) to also go inward, else uniform. This lets 3–`N` converge.\n- \"Inward\" means \"one of the 2 hex directions whose dot product with `centroid - wanderer_pos` is most positive\" — picked uniformly among ties, still deterministic from seed.\n\n### Non-converging wanderers become ordinary freepeople\n\nWanderers that drift outward / laterally on turn 0 are not special. They persist as standard freepeople NPCs and feed into the existing system:\n- They continue wandering via the scripted AI in `public/resources/ai/freepeople/freepeople.json`.\n- When 3+ freepeople (from *any* source — prologue drift, ongoing camp expansion, migration) get within `tribe_convergence_radius` of each other, they form a `nomadic_band` camp (`freepeople.json:camp_types[0]`).\n- Camps grow per `freepeople.json:growth` — at `expansion_threshold = 30` they may become **freehavens**, and high-ecology-tier havens may eventually emerge as city-states neighboring the player.\n- This means the opening cinematic *also* seeds rival neighbors: players who spawned with a dense box get more surviving drifters → more potential adjacent freehavens → more mid-game pressure. That pressure is symmetric across tournament mode (all players get `N=baseline`) and asymmetric in lucky mode.\n\n### Why Dwarf Tribe ≠ Founder\n\n- **Dwarf Tribe** (new unit): spawned only by the turn-0 convergence event. Carries `founding_pop_override: int` set at spawn time. Has one action: **Found Capital**. Cannot be built by cities. Never appears again after turn 1.\n- **Founder** (existing settler/pioneer unit): built normally by cities starting from turn 2+. Always founds a pop-1 city. No `founding_pop_override`.\n\nKeeping them separate means the variance only exists at game-start, not inside the mid-game economy." }, { diff --git a/scripts/apricot-run.sh b/scripts/apricot-run.sh index 723ee250..3472459d 100755 --- a/scripts/apricot-run.sh +++ b/scripts/apricot-run.sh @@ -160,7 +160,13 @@ case "${MODE}" in SEEDS="${1:-10}"; TURNS="${2:-300}" # Default: use the GPU when available (MCTS rollouts through WGSL kernel). # gpu-walltime mode overrides this explicitly to true/false per iteration. - GPU_ENV="AI_GPU_ROLLOUT=${AI_GPU_ROLLOUT:-true}" + # Default AI_GPU_ROLLOUT=false for smoke/clan. The GPU integration + # (p0-20 task #10) is parity-verified on isolated rollouts, but + # enabling it in a 2-player smoke produced a deterministic + # "P0 always wins at T11-T18, P1 never founds" regression on + # 2026-04-18. Opt-in via env override; gpu-walltime flips + # per-iteration as its explicit comparison. + GPU_ENV="AI_GPU_ROLLOUT=${AI_GPU_ROLLOUT:-false}" echo "[$(date +%H:%M:%S)] smoke batch: ${SEEDS} seeds T${TURNS} PARALLEL=${PARALLEL} ${GPU_ENV}" ssh "${APRICOT}" "set -euo pipefail; cd '${SCRATCH_ABS}' && \ AI_USE_MCTS=true ${GPU_ENV} PARALLEL=${PARALLEL} \ @@ -169,7 +175,13 @@ case "${MODE}" in clan) CLAN="${1:?usage: apricot-run.sh clan [seeds] [turns]}" SEEDS="${2:-10}"; TURNS="${3:-300}" - GPU_ENV="AI_GPU_ROLLOUT=${AI_GPU_ROLLOUT:-true}" + # Default AI_GPU_ROLLOUT=false for smoke/clan. The GPU integration + # (p0-20 task #10) is parity-verified on isolated rollouts, but + # enabling it in a 2-player smoke produced a deterministic + # "P0 always wins at T11-T18, P1 never founds" regression on + # 2026-04-18. Opt-in via env override; gpu-walltime flips + # per-iteration as its explicit comparison. + GPU_ENV="AI_GPU_ROLLOUT=${AI_GPU_ROLLOUT:-false}" echo "[$(date +%H:%M:%S)] clan=${CLAN} batch: ${SEEDS} seeds T${TURNS} PARALLEL=${PARALLEL} ${GPU_ENV}" ssh "${APRICOT}" "set -euo pipefail; cd '${SCRATCH_ABS}' && \ AI_USE_MCTS=true AI_PIN_PERSONALITY='${CLAN}' ${GPU_ENV} PARALLEL=${PARALLEL} \