diff --git a/public/games/age-of-dwarves/data/objectives.json b/public/games/age-of-dwarves/data/objectives.json index e71cb7b6..e9448ef0 100644 --- a/public/games/age-of-dwarves/data/objectives.json +++ b/public/games/age-of-dwarves/data/objectives.json @@ -1,12 +1,12 @@ { - "generated_at": "2026-04-18T00:30:12Z", + "generated_at": "2026-04-18T04:55:13Z", "totals": { - "oos": 17, - "stub": 3, - "partial": 14, - "done": 44, + "partial": 17, "missing": 8, - "total": 86 + "done": 43, + "oos": 17, + "stub": 6, + "total": 91 }, "objectives": [ { @@ -23,11 +23,11 @@ "id": "p0-02", "title": "Five AI clan personalities drive distinct playstyles", "priority": "p0", - "status": "done", + "status": "partial", "scope": "game1", "owner": "warcouncil", "updated_at": "2026-04-17", - "summary": "`ai_personalities.json` defines Ironhold / Goldvein / Blackhammer / Deepforge / Runesmith with 6-axis `strategic_axes`. `ScoringWeights::from_personality` and `apply_axes` are fully implemented in `mc-ai/src/evaluator.rs`.\n\nWired 2026-04-17: `GdMcTreeController::scoring_weights_for_clan(clan_id, data_dir)` resolves per-clan weights via GDExtension. `ai_turn_bridge.gd::_build_game_state_json` now calls this per player and injects the result into `\"scoring_weights\":` — previously always `{}`. `AI_PIN_PERSONALITY` env var added to `personality_assigner.gd` for per-clan batch testing. Smoke run confirms `player_clans: {\"1\": \"blackhammer\"}` in meta.json, EXIT_CODE=0.\n\n**5 × 10-seed batch results (2026-04-17, .local/iter/p0-02-clans/):**\n\n| Clan | Wins | TTV_med | p1_gold | p1_mil | p1_techs |\n|---|---|---|---|---|---|\n| ironhold | 10/10 | T185.5 | 266 | 3.0 | 27.5 |\n| goldvein | 10/10 | T155.5 | **543** | 3.5 | 25.5 |\n| blackhammer | 9/9 | T189 | 327 | 3.0 | 28 |\n| deepforge | 10/10 | T185.5 | 266 | 3.0 | 27.5 |\n| runesmith | 10/10 | T155.5 | 543 | 3.5 | 25.5 |\n\nBalance: 49 total games, each clan 3 AI-wins, max 33% — passes. Gold axis: goldvein 2× ironhold (wealth=9 vs 3) — passes. TTV: goldvein/runesmith finish 30 turns faster than ironhold/deepforge — passes. First-combat: identical at T9 across all clans (map-forced, not AI-driven). Deepforge/ironhold and goldvein/runesmith pairs are metric-identical — overlapping weight profiles and same 10 seeds converge.\n\n**B5 re-run (2026-04-17, `.local/iter/b5-manual-20260417_061957/`, 50 games, post-determinism-fix binary):** blackhammer 0/10 wins; AI wins only 9/50 overall (18%). Win-rate balance bullet fails. See \"Remaining to done\" for tuning plan.\n\n**Axis ablation sweep (2026-04-17, `.local/iter/ablate__20260417_072921/`, 10 seeds T300 per axis):** Each axis neutralized to 5 for all clans in sequence. All 6 axes show ≥10% delta on correlated metric vs pooled baseline (TTV=185, gold=379, mil=3):\n\n| Axis | Correlated metric | Baseline | Ablated | Delta |\n|---|---|---|---|---|\n| aggression | mil_med | 3.0 | 2.5 | -16.7% |\n| expansion | ttv_med | 185 | 134 | -27.6% |\n| grudge_persistence | ttv_med | 185 | 131.5 | -28.9% |\n| production | ttv_med | 185 | 139 | -24.9% |\n| trade_willingness | gold_med | 379 | 193.5 | -48.9% |\n| wealth | gold_med | 379 | 227.5 | -40.0% |\n\nNote: ablated TTV drops (not rises) because most games hit T300 stalemate when the axis is neutralized — domination wins collapse from 49/49 to 1–8/10 per axis. The TTV delta reflects game degradation, not faster play. All axes confirmed live." + "summary": "`ai_personalities.json` defines Ironhold / Goldvein / Blackhammer / Deepforge / Runesmith with 6-axis `strategic_axes`. `ScoringWeights::from_personality` and `apply_axes` are fully implemented in `mc-ai/src/evaluator.rs`.\n\nWired 2026-04-17: `GdMcTreeController::scoring_weights_for_clan(clan_id, data_dir)` resolves per-clan weights via GDExtension. `ai_turn_bridge.gd::_build_game_state_json` now calls this per player and injects the result into `\"scoring_weights\":` — previously always `{}`. `AI_PIN_PERSONALITY` env var added to `personality_assigner.gd` for per-clan batch testing. Smoke run confirms `player_clans: {\"1\": \"blackhammer\"}` in meta.json, EXIT_CODE=0.\n\n**5 × 10-seed batch results (2026-04-17, `.local/iter/p0-02-clans/` — PRE-REFRAME EVIDENCE):**\n\n> These batches ran BEFORE p0-25's instrumentation landed, so `player_stats` does NOT carry\n> `tier_peak` / `peak_unit_tier` / `wonder_count`. The TTV column is preserved as the\n> contemporaneous signal; it is NOT the current acceptance metric. Per p0-01's 2026-04-17\n> reframe, the primary divergence gate is **tier_peak** (era-progression, which scales with\n> difficulty per p0-24) — tracked as a \"needs re-run\" in Remaining to reach done below.\n\n| Clan | Wins | TTV_med (legacy) | p1_gold | p1_mil | p1_techs |\n|---|---|---|---|---|---|\n| ironhold | 10/10 | T185.5 | 266 | 3.0 | 27.5 |\n| goldvein | 10/10 | T155.5 | **543** | 3.5 | 25.5 |\n| blackhammer | 9/9 | T189 | 327 | 3.0 | 28 |\n| deepforge | 10/10 | T185.5 | 266 | 3.0 | 27.5 |\n| runesmith | 10/10 | T155.5 | 543 | 3.5 | 25.5 |\n\nSignals that DON'T depend on TTV (still valid post-reframe):\n- **Balance**: 49 total games, each clan 3 AI-wins, max 33% — passes.\n- **Gold axis**: goldvein 2× ironhold (wealth=9 vs 3) — passes.\n- **First-combat**: identical at T9 across all clans (map-forced start proximity, not AI-driven).\n- **Pair metric-identical**: deepforge/ironhold and goldvein/runesmith pairs show overlapping weight profiles; same 10 seeds converge.\n\nSignals that DO depend on TTV (need tier_peak re-run to close the reframed gate):\n- TTV delta between clan pairs — the \"goldvein/runesmith finish 30 turns faster than ironhold/deepforge\" claim doesn't translate into the tier_peak framework until re-measured.\n\n**B5 re-run (2026-04-17, `.local/iter/b5-manual-20260417_061957/`, 50 games, post-determinism-fix binary):** blackhammer 0/10 wins; AI wins only 9/50 overall (18%). Win-rate balance bullet fails. See \"Remaining to done\" for tuning plan.\n\n**Axis ablation sweep (2026-04-17, `.local/iter/ablate__20260417_072921/`, 10 seeds T300 per axis — PRE-REFRAME EVIDENCE):** Each axis neutralized to 5 for all clans. Measured under pre-p0-25 instrumentation; metrics are TTV / gold / mil from the legacy `player_stats` schema. All 6 axes show ≥10% delta on their correlated legacy metric vs pooled baseline (TTV=185, gold=379, mil=3):\n\n| Axis | Correlated metric (legacy) | Baseline | Ablated | Delta |\n|---|---|---|---|---|\n| aggression | mil_med | 3.0 | 2.5 | -16.7% |\n| expansion | ttv_med | 185 | 134 | -27.6% |\n| grudge_persistence | ttv_med | 185 | 131.5 | -28.9% |\n| production | ttv_med | 185 | 139 | -24.9% |\n| trade_willingness | gold_med | 379 | 193.5 | -48.9% |\n| wealth | gold_med | 379 | 227.5 | -40.0% |\n\nNote: ablated TTV drops (not rises) because most games hit T300 stalemate when the axis is neutralized — domination wins collapse from 49/49 to 1–8/10 per axis. The TTV delta reflects game degradation, not faster play. All axes CONFIRMED LIVE under the legacy metric set. Re-measurement under tier_peak is needed before the reframed acceptance (below) can be cited." }, { "id": "p0-03", @@ -329,6 +329,36 @@ "updated_at": "2026-04-17", "summary": "Forwarding stub. p0-31 restored the Rust ecology tick via `ClimateScript.process_turn`\nbut left the two sibling `process_turn` calls in `turn_processor.gd::_process_climate`\ncommented out (see turn_processor.gd `_process_climate` trailing comment). Both classes\nare empty stubs: calling their `process_turn` aborts `next_player` and kills the arena\nturn loop.\n\nThis objective fully implements:\n\n- `WeatherScript.process_turn(game_map)` — per-turn weather rolls (rain, storms,\n seasonal shifts) feeding `game_map.tiles[*].temperature` / `moisture` deltas.\n- `ClimateEffectsScript.process_turn(game_map, weather, players)` — apply weather\n consequences to unit movement, city production, tile yields.\n\nScope is the bridge from the empty-stub classes to fully-working per-turn calls\nthat slot into the climate sequencing order\n(`marine_harvest → weather → climate → climate_effects`)." }, + { + "id": "p0-33", + "title": "World-map input wiring — unit selection panel, city click, ESC/F10 menu, panel close", + "priority": "p0", + "status": "partial", + "scope": "game1", + "owner": "wireguard", + "updated_at": "2026-04-17", + "summary": "The world-map is unplayable because basic interaction is broken: clicking a unit does nothing visible, clicking a city does nothing, and there is no way to exit the game (ESC and F10 are both dead). Five discrete wiring gaps are responsible:\n\n1. **Unit selection produces no feedback** — `_select_unit()` in `world_map.gd` calls `_hud.show_unit_panel()` (the slim programmatic panel in `world_map_hud.gd`) and emits `EventBus.unit_selected`, but the panel is either invisible or off-screen. The richer `unit_panel.tscn` (which listens on `EventBus.unit_selected` and renders full stats + action buttons) is never instantiated in the scene tree.\n\n2. **City single-click enters bombard mode, not city screen** — `_handle_hex_click()` (lines ~350-358 of `world_map.gd`) checks `not city_ref.has_bombarded` and sets `_bombard_city`, consuming the click without opening the city screen. The city screen only opens via double-click through `_unhandled_input`. Single-click on a city should open the city screen; bombard should require an explicit secondary action (right-click or dedicated button).\n\n3. **F10 is unbound** — no `KEY_F10` handler anywhere in the project.\n\n4. **ESC does not open the in-game menu** — `main.gd._unhandled_key_input` has the correct logic (`push_overlay(\"res://engine/scenes/ui/ingame_menu.tscn\")`) but it may be racing with `world_map.gd._unhandled_input` or simply not firing when expected. Needs verification and a reliable binding for both ESC (when no panel is open) and F10 (always).\n\n5. **ESC does not close open panels** — `city_screen.gd` has no `_unhandled_input` / `_unhandled_key_input` handler; closing requires clicking the close button. ESC should close the top-most open panel (city screen, tech tree, chronicle) and bubble up to the in-game menu only when no panel is open." + }, + { + "id": "p0-34", + "title": "Freepeople tribe-founding cinematic — turn -1 / 0 / 1 start sequence and Dwarf Tribe founder unit", + "priority": "p0", + "status": "partial", + "scope": "game1", + "owner": "shipwright", + "updated_at": "2026-04-17", + "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." + }, + { + "id": "p0-35", + "title": "Movement mode UX — Move button, path preview, right-click confirm, fog-aware pathing", + "priority": "p0", + "status": "stub", + "scope": "game1", + "owner": "wireguard", + "updated_at": "2026-04-17", + "summary": "Movement is currently a silent left-click on a reachable hex — no path shown, no\nconfirmation step. Players expect the Civ-style flow: enter movement mode (M key\nor Move button), see a path preview, right-click to confirm. This objective\nadds the full movement-mode state machine, path rendering, fog-of-war-aware\npathing, and the Move button on the unit action panel with disabled-state\ntooltips for all action buttons.\n\nDepends on **p0-33** (unit panel must be in the scene tree before the Move\nbutton can be wired)." + }, { "id": "p1-01", "title": "Diplomacy-lite — peace/war toggle plus one trade action", @@ -479,6 +509,26 @@ "updated_at": "2026-04-17", "summary": "With p1-15 landed, `./run deploy:guide:next` can be invoked manually from\nplum. The next step is zero-touch redeploy on every push to main so\ncontributors without plum access still see their work at\n`https://mc.next.black.local` within minutes of merging.\n\nDepends on p1-15 (infra + command must exist; they do)." }, + { + "id": "p1-18", + "title": "Village discovery — world-map feedback (notification, reward popup, minimap ping)", + "priority": "p1", + "status": "stub", + "scope": "game1", + "owner": "wireguard", + "updated_at": "2026-04-17", + "summary": "`EventBus.village_discovered(tile_pos, reward)` fires when a unit walks onto a village\ntile, but `_on_village_discovered` in `world_map.gd` is a no-op stub. The player receives\ngold silently with no visual feedback." + }, + { + "id": "p1-19", + "title": "Tutorial opt-in — HUD button, disappears after turn 5, starts from Step 1", + "priority": "p1", + "status": "stub", + "scope": "game1", + "owner": "wireguard", + "updated_at": "2026-04-17", + "summary": "The first-run tutorial currently auto-shows on game start (gated by\n`TutorialOverlay.should_show_on_first_run()`). This is hostile to returning\nplayers and to playtesting — the tutorial interrupts real gameplay every fresh\nboot. Assume the player doesn't need a tutorial by default. Offer it as a\nbutton on the world-map HUD that disappears after turn 5.\n\nAdditionally, when the tutorial IS started, it begins at **Step 1** (camera\npan), not Step 2. This is already the authoritative step order in\n`tutorial_overlay.gd:_STEPS` — the fix is only to make sure `_current_step`\ninitializes to `1` (it does) and that no code skips ahead." + }, { "id": "p2-01", "title": "Minimap — fog reflection and unit markers",