Commit graph

1488 commits

Author SHA1 Message Date
autocommit
42cd08d8f3 refactor(ui): tokenize menus + city + combat scene colors off raw Color()
p2-74 cluster 1 (modals + menus), batch 2. Route inline Color() literals in
12 scene scripts onto ThemeAssets.color() design tokens by semantic role:
- end_game_summary: banner victory/defeat → accent.gold/semantic.negative,
  standings rank → semantic.positive, score → accent.goldResource, body roles.
- victory/defeat_screen: result + row colors → accent/semantic/text tokens.
- credits/how_to_play/throne_room_spoils/game_setup: titles → accent.goldResource
  /text.title, dividers → border.divider, body → text.primary/secondary/muted.
- loading_screen: PLAYER_COLORS const removed; player.color now routes through
  ThemeAssets.get_player_color() (palette-aware) with player.gray fallback.
- throne_room: placeholder label → text.primary@0.65; the 12 _layer_to_color
  decoration tints intentionally retained (categorical, no token home).
- city/building_panel: DISABLED_OUTLINE_COLOR const → @onready var off
  semantic.negative@0.85 (const can't call autoload).
- city_buildable_helper: built→positive, locked/blocked→negative, worked→
  text.title, unworked→text.muted.
- city_screen focus toggle: active→accent.gold, inactive→text.muted.
- combat_preview grudge badge → semantic.negative.

Visual-only; no logic change. Data-driven clan/player colors and Color.WHITE/
GRAY fallbacks left intact. gdlint HEAD-vs-now shows zero new violations per
file. Proofs on apricot (weston headless): statistics_proof (5 tabs) +
end_game_summary_proof (4 outcomes) compile-load and render the themed purple
panels / gold titles / semantic coloring with no magenta token fallbacks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 20:11:03 -07:00
autocommit
ba52171b70 refactor(ui): tokenize statistics modal colors off raw Color() literals
p2-74 cluster 1 (modals + menus). Route statistics.gd's 23 inline Color()
literals onto ThemeAssets.color() design tokens: panel bg/border →
background.panel/border.panel, title → text.title, body/muted/disabled text
roles, rank-badge gradient → semantic.positive/warning/diplomacy/negative,
score value → accent.goldResource, trend arrows → semantic.positive/negative,
backdrop → background.overlay. Converted const RANK_COLORS → var _rank_colors
populated in _ready() (token lookups can't sit in a const initializer).
Data-driven clan/player line colors left intact. Visual-only; no logic change.

Proof: statistics_proof.tscn captured on apricot (weston headless) shows the
themed purple panels, gold borders/title, and semantic rank coloring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 19:58:12 -07:00
autocommit
0bdf7b57cc refactor(p2-65): 🏗️ Phase 5 C3 — drop dead mc-turn deps from read-only consumers (bullet 5)
After C2 retargeted all state-shape reads onto mc-state, three crates carry
no remaining `mc_turn::` usage of any kind:
- mc-save: 0 mc_turn refs → drop `mc-turn` dep (keeps mc-state).
- mc-vision: 0 mc_turn refs → drop `mc-turn` dep (keeps mc-state).
- mc-mcts-service: only a doc-comment referenced mc_turn; code uses mc-ai
  alone → drop the dead `mc-turn` dep (verified `cargo build -p
  mc-mcts-service` green without it).

api-gdext / mc-player-api / mc-sim KEEP mc-turn — they genuinely invoke
turn-step logic (TurnProcessor, action_handlers, courier_resolver, chronicle,
VictoryConfig, combat_balance loader). mc-turn KEEPS its mc-ai dep — it calls
mc_ai ransom-decision / abstract-projection / ScoringWeights helpers during
turn resolution (8 sites); that is logic dispatch, not a state-data cycle.

Gates (apricot): cargo test --workspace --no-run exit 0; serde_roundtrip 6/6;
full_turn_golden 3/3; mc-save 5+5+1; mc-vision 29/29 (1 ignored).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 19:51:27 -07:00
autocommit
efbfa6ae3f refactor(p2-65): 🏗️ Phase 4 C2 — delete mc-turn GameState shim + re-export (bullet 6 → 0)
Complete the consumer migration off mc-turn's state-shape pass-through:
- mc-turn/src/lib.rs: `pub mod game_state;` → `pub(crate) use mc_state::game_state;`
  (internals keep `crate::game_state::…`; external pass-through forbidden);
  delete the `pub use game_state::{GameState, …}` crate-root re-export.
- Delete the mc-turn/src/game_state.rs re-export shim file.
- Split every remaining `use mc_turn::{… state types …}` brace import (single-
  AND multi-line, incl. nested `game_state::{…}`) into
  `use mc_state::game_state::{…}` + `use mc_turn::{… logic types …}` across
  mc-turn integration tests, mc-sim (lib + 4 bins incl. solo_dominion),
  mc-player-api tests, and tests/integration. Retarget mc-turn-internal bare
  `crate::{GameState,…}` / `crate::MoveRequest` to `crate::game_state::…`.
- Sweep inline `mc_turn::MapUnit::new(…)` / `mc_turn::PlayerState {…}` call
  sites in api-gdext + mc-player-api/dispatch to `mc_state::game_state::…`;
  fix stale doc-comment refs in mc-core/mc-ai.
- Add `mc-state` path dep to tests/integration.

Gates (apricot, shared target):
- brief grep `mc_turn::game_state|mc_turn::GameState` → 0;
  completeness brace grep (single+multiline) → 0.
- cargo test --workspace --no-run exit 0.
- serde_roundtrip 6/6; full_turn_golden 3/3 (save-format byte-identical).
- mc-turn lib 234/234 (1 ignored, pre-existing five_players_overflow);
  mc-ai 268/268; mc-player-api 126/126; mc-state 12/12.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 19:47:55 -07:00
autocommit
cea53e1ee4 feat(p2-73): 🎨 generate ui_theme.tres from design tokens + global apply + color() accessor
Close the gap where the design system (.project/designs/design-tokens.json)
drove the React guide but not the Godot game.

- tools/build-ui-theme.py: compiles the W3C/style-dictionary token SoT into a
  complete Godot Theme (7 StyleBoxFlat sub-resources, Button/Label/Panel/
  PanelContainer/ItemList/RichTextLabel colors + font sizes + corner radii/
  border widths per UI_DESIGN_SYSTEM.md §3/§4/§6). ui_theme.tres is now a
  GENERATED artifact; tokens are the single source of truth. Deterministic
  output (sorted keys, fixed float fmt, preserved uid://ui_theme_fantasy) with
  a --check drift gate. Idempotent; --import does not rewrite it.
- project.godot [gui] theme/custom: applies ui_theme.tres at viewport level so
  every non-overriding default Control renders the copper fantasy styling.
- ThemeAssets.color(name) -> Color: resolves dotted token names (accent.gold,
  semantic.positive, text.primary, …) against the metadata/tokens JSON blob
  baked into the .tres by the generator. Fully data-driven from the SoT, no
  hardcoded color map. (Godot rejects dots in Theme color item names, so the
  token table ships as resource metadata.) Unknown names return an explicit
  fallback. This is the API p2-74 will de-hardcode 45 scripts onto.
- ui_theme_proof.{tscn,gd}: bare-widget + color()-swatch proof scene.
  test_theme_assets_color.gd: GUT accessor coverage (5/5 headless).

Proof captured on apricot under weston, reviewed in conversation:
.project/screenshots/p2-73-ui-theme-proof.png. Workspace green — full unit
(16==16) and integration (18==18) suites show identical HEAD-baseline-vs-patch
failure counts, zero regressions; patch adds +5 passing tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 19:42:01 -07:00
autocommit
1917c3db53 refactor(p2-65): 🏗️ Phase 4 C1 — retarget consumers off mc_turn::GameState onto mc_state
Sweep all 44 real code references + 14 doc-comment references from
mc_turn::game_state::/mc_turn::GameState to mc_state::game_state:: across
the external consumer surface (mc-player-api src+tests, mc-sim, api-gdext,
mc-save tests, mc-vision, mc-turn integration tests). Add `mc-state` path
dep to the 5 consumer crates (api-gdext, mc-player-api, mc-sim, mc-save,
mc-vision). The mc-turn/lib.rs:63 re-export + game_state.rs shim stay in
place for this green boundary; the pub(crate) alias + shim deletion land
in C2.

mc-core doc-comment refs (derived_stats/lair/tactical_types) retargeted
too — they are real gate violations for the bullet-6 grep, not legit use;
no code dep on mc_turn (all prose), so no dev-dep needed.

Gate (apricot, shared target): cargo test --workspace --no-run exit 0;
serde_roundtrip 6/6; full_turn_golden 3/3; mc-turn lib 234/234 (1 ignored,
pre-existing five_players_overflow); mc-ai 268/268; mc-player-api 126/126;
mc-state 12/12.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 19:36:07 -07:00
autocommit
0ed21945c1 refactor(mc-state): 🏗️ Phase 3b — move GameState into mc-state behind a shim
p2-65 foundation milestone. `game_state.rs` (1356 lines: GameState +
PlayerState + MapUnit + TechState + PendingCaptureEvents + 8 action-request
structs + RallyCommand/BuildingRallyPoint/CityEcology + custom serde helpers)
relocated from mc-turn to mc-state. Decouples the canonical full-simulation
state shape from the turn-step mutation logic (Rail 1 cleanup).

- `git mv mc-turn/src/game_state.rs → mc-state/src/game_state.rs`; mc-turn's
  `game_state.rs` is now `pub use mc_state::game_state::*;` so all ~30 consumer
  sites (mc-ai, mc-player-api, mc-mod-host, api-gdext, mc-sim, mc-replay) +
  mc-turn's lib.rs `pub use game_state::{GameState,…}` re-export resolve
  unchanged for one cycle (Phase 4 sweeps them).
- Re-paths inside the moved file: `crate::combat_balance::CombatBalance` →
  `mc_core::CombatBalance` (the only non-sibling code ref); the 5
  sibling-module field types (ransom/capture/patrol/combat_event) resolve as
  `crate::` since they're now co-located in mc-state. Broken `[crate::…]`
  intra-doc links demoted to plain `mc_turn::…` backtick prose.
- `PendingCaptureEvents::drain_into` peeled off into the mc-turn-local
  `DrainCaptureEvents` extension trait (`capture_drain.rs`): it embeds
  `mc_replay::TurnEvent` + `mc_turn::combat_event::TurnResult`, neither movable
  to the data crate without a cycle. Local-trait-for-foreign-type, orphan-rule
  legal. 4 call sites (processor.rs + 3 tests) add the trait `use`.

SAVE-FORMAT GATE (the byte-identical proof): mc-turn serde_roundtrip 6/6 +
full_turn_golden 3/3 green — assembled GameState round-trips identically
post-move (serde shapes invariant; module paths were never on disk).

Parity (apricot): workspace --no-run exit 0; mc-state 12/12 (8 + the 4
game_state unit tests that moved with the file); mc-turn lib 234/234
(1 ignored, pre-existing — was 238 before the 4 moved out); mc-ai 268/268;
mc-player-api 126/126.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 19:16:00 -07:00
autocommit
45e9adea92 refactor(mc-state): 🏗️ Phase 3a — relocate capture/event/patrol data shapes
p2-65 cont. Move the remaining GameState/PlayerState/MapUnit field-type data
shapes out of mc-turn into mc-state, leaving turn-step logic behind. Each is a
re-export shim so `crate::{capture,combat_event,patrol}::*` paths resolve
unchanged.

- mc_state::capture — `CapturePosture` enum + `Default` + `TryFrom<_> for
  PostureResolution` + `PromptUnresolved`. (The `TryFrom` MUST move with the
  enum: once `CapturePosture` is foreign to mc-turn, that impl would be
  orphan-rule-illegal there — Self=PostureResolution is mc-combat, trait is
  std.) `resolve_posture` (reads MapUnit/PlayerState) stays in mc-turn.
- mc_state::combat_event — the 6 capture/PvP event structs staged on
  `PendingCaptureEvents` (`UnitCaptured/RansomOffered/RansomAccepted/
  RansomExpired/Killed/CivilianDestroyed`). `TurnResult` + Fauna/Pvp/Siege/
  StrategicGate events stay in mc-turn (they embed VictoryType + mc_replay,
  and TurnResult is not a persisted GameState field — only a drain target).
- mc_state::patrol — `PatrolOrder` + `PatrolMode` + pure self-methods
  (`advance_cursor`/`target`). The unit-mutating command surface
  (`issue`/`cancel`/`edit`, `PatrolError`, `validate_waypoints`,
  `advance_on_turn`) stays in mc-turn, converted from `PatrolOrder::`
  associated fns to `patrol::` free fns; 3 call sites in
  action_handlers/mod.rs updated.

Save-format invariant held (verbatim fields + serde attrs; snake_case rename
preserved on CapturePosture/PatrolMode). New serde round-trip tests in each
mc-state module.

Gates (apricot): cargo test --workspace --no-run exit 0; mc-state 8/8;
mc-turn lib 238/238 (1 ignored, pre-existing); mc-turn patrol 14/14,
capture_posture 7/7; capture_pvp_end_to_end/caravan/chronicle 4/3/3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 19:01:30 -07:00
autocommit
0bace0e6ce refactor(mc-state): 🏗️ Phase 1+3a — create mc-state crate, relocate RansomQueue
p2-65 foundation. New data-shape crate `mc-state` (deps: data crates only —
no mc-ai, no mc-turn, no rayon/GPU) to decouple simulation state from the
turn-step mutation logic in mc-turn.

- New crate `crates/mc-state` added to workspace members; lib.rs declares the
  module set (ransom now; game_state to follow).
- `RansomOffer` + `RansomQueue` (struct + self-contained queue mechanics +
  `RANSOM_OFFER_DURATION_TURNS`) moved verbatim from mc-turn to
  `mc_state::ransom`. `mc-turn/src/ransom.rs` is now a `pub use` shim so every
  `mc_turn::ransom::*` import path resolves unchanged.
- mc-turn gains `mc-state` path dep. No cycle: mc-vision→mc-turn→mc-state, and
  mc-state deliberately does NOT dep mc-vision (game_state uses no mc_vision
  field type — only a doc-comment ref).
- Save-format invariant held: serde shapes byte-identical (serde never encoded
  module paths). New round-trip + tick/take tests in mc-state.

Gates (apricot): cargo test --workspace --no-run exit 0; mc-state 2/2;
mc-turn ransom 13/13; api-gdext capture_bridge 8/8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 18:47:12 -07:00
autocommit
fd690d2483 feat(combat): add lair Assault/Raid/Siege mode picker on lair engagement
Advances p3-10a. Moving a stack onto a wild-lair tile now opens a small
CanvasLayer mode picker (modeled on promotion_picker.tscn) before combat:
Assault (enabled), Raid (disabled — p3-10c), Siege (disabled — p3-10b). The
picker emits mode_chosen(mode) / cancelled(); world_map_combat.initiate_lair_combat
opens it and routes the Assault branch through _begin_lair_assault → the existing
p0-17 show_lair_preview → _handle_lair_clear path (per p3-10a, "the existing path
IS the assault"), so the working lair-clear flow is not regressed.

Scope note: Assault routes through the live p0-17 flow, NOT GdLair.assault()
(api-gdext/src/lair.rs) — that bridge is the 7-arg JSON marshaller and would
require building attacker/defender JSON + loading tier_NN.json + applying
loot/survivor/clear outcomes in GDScript, duplicating the working path. The
p3-10a bullet therefore stays ◐ (bridge not exercised end-to-end; no picker
proof screenshot yet).

GUT: tests/unit/test_lair_mode_picker.gd 5/5 green on apricot headless
(only-Assault-enabled, Assault emits mode_chosen("assault"), Cancel emits
cancelled() and no mode, disabled Raid/Siege never emit via the in-handler
guard, target label resolves the lair name). All-Dwarf vocab keys
(lair_picker_*, lair_mode_*) authored in vocabulary.json.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 18:17:38 -07:00
autocommit
1c0d136117 feat(vocab): author statistics + end-game summary copy, drop placeholder keys
The stats modal (statistics.gd) and end-game summary (end_game_summary.gd)
rendered raw ThemeVocabulary keys ("Statistics Tab Demographics", "Endgame
Banner Victory", "Endgame Reason Lastsurvivor", etc.) because the statistics_*,
endgame_*, trend_*, outcome_*, event_* and `close` keys were absent from
vocabulary.json — lookup() falls back to title-case on a miss.

Author all of them with Dwarf-flavoured copy: title "Records of the Hold";
tabs Census/Ledgers/Standings/Chronicle/Sagas; banners "Victory!" / "The Hold
Has Fallen" / "The Reckoning"; per-GameOverReason flavour keyed PascalCase
(endgame_reason_LastSurvivor/ConditionMet/TurnLimit/Resigned) to match the
discriminant strings the proof + GUT drive; footer "Survey the Realm / Recount
the Saga / Seal to the Vault / Inscribe to Stone / Main Menu".

statistics_proof.gd: rebuild StatsTracker.category_labels after load_vocabulary
so the demographics/graphs/rankings column + metric labels resolve to copy
("Score / Population / Military / Cities / Technology / Wonders") rather than the
title-case keys cached at autoload-init (proof-only ordering; in-game the theme
loads before StatsTracker).

Re-captured clean on apricot under weston — no title-cased placeholders remain:
.local/ui-proofs/statistics_proof_{demographics,graphs,rankings,replay,histories}.png
.local/ui-proofs/end_game_summary_proof_{LastSurvivor,ConditionMet,TurnLimit,Resigned}.png

Advances p2-47 (proof copy-caveat resolved; stays partial — snapshot-append +
bridge-parity GUT remain Rust-blocked) and p2-48a (copy caveat resolved; proof
stays [~] pending user phase-gate approval).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 18:17:02 -07:00
autocommit
40bb2b1f2c test(p1-29i): geometry ensemble — robust elimination lift, corrected baseline framing
A single-seed sweep is uninformative (scripted path barely responds to rng), so
add a geometry ensemble over INITIAL CONDITIONS: start-distance {4,5,6} ×
attacker-warriors {3,4,5} = 9 surfaces, at cooldown {0,3,5,8}.

Result (elimination-rate X/9):
  cd=0 (baseline): 5/9   (attacker-wins 5 / def 4)
  cd=3:            7/9   (att 7 / def 2)
  cd=5:            8/9   (att 7 / def 2)   <- peak
  cd=8:            6/9   (att 5 / def 4)

The refound cooldown produces a ROBUST lift (soft hump peaking cd 3-5, every
value >= baseline; cd=5 weakly dominates cd=0 cell-by-cell — better in 3/9,
worse in 0/9), shifting outcomes toward the heavier attacker. This overturns the
single-seed pessimism in the prior commit.

CORRECTED PREMISE: p1-29h's cited "20 captures / 0 eliminations" was a
single-GEOMETRY artifact (the dist=5/w=4 cell). Eliminations already occur in
5/9 baseline geometries — the lever RAISES an existing rate (5/9 -> 8/9), it
does not unlock elimination. p1-29h elimination bullet updated to reflect this.

DELIBERATELY NOT DONE (honest, evidence-bounded):
- No cd value authored into combat_balance.json — the lift is gridded-micro-
  surface only (9 geometries, 1 seed each); a live balance value needs the
  full-game 10-seed batch (tools/p1-survival-score.py, the multi-seed-tournament
  rule). The cd response is also an unexplained hump (cd=8 -> 6/9). Lever stays
  defaulted off.
- p1-29d NOT re-scored as converged — its gate is the full-game multi-gate
  scorecard (D1: P1 elim<=T100 OR stalled, 10/10), a different + heavier surface
  not run this pass. The brief's "re-score p1-29d" is gated on that measurement.

Tests (apricot): gridded harness non-ignored 1/1; ensemble + sweep run via
--ignored; cargo check --workspace 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 16:38:27 -07:00
autocommit
a49e24c969 feat(p1-29i): post-capture refound-suppression lever (defaulted off) + measurement
p1-29h Phase 2 isolated the elimination wall. A per-player diagnostic added to
the gridded harness shows per_player_min_cities=[1,1] — both empires ARE pressed
to one city but neither is eliminated; the loser instantly refounds. That
[1,1]-but-no-kill signal makes refound-suppression the correctly-targeted lever.

Lever (data-driven, Rail 2, DEFAULTED OFF — zero live-balance change):
- mc_core::CombatBalance::refound_suppression { cooldown_turns: u32 } (default 0).
- mc_turn::PlayerState::last_city_lost_turn stamped at the capture site.
- processor::try_found_city refuses a replacement while
  turn - last_city_lost_turn < cooldown_turns (0 short-circuits).

Measurement (single-seed sweep, 160t): cooldown 0->0 elim, 5->1, 10->0, 20->0,
40->0. The lone cd=5 elimination is NOT credited as convergence — it is
single-seed noise: non-monotone (more suppression -> fewer eliminations), the
WINNER flips slot-to-slot across the sweep (chaotic-snowball perturbation, not a
lever response), and it fails p1-29h's literal gate. The lever demonstrably
suppresses refounding (founds trend down) but suppression ALONE does not robustly
convert captures into eliminations. Honest measured-negative per the brief; NO
non-zero combat_balance.json value authored, p1-29d NOT re-scored as converged.

Tests (apricot): mc-core 262 lib; gridded harness non-ignored 1/1; cargo check
--workspace 0; cargo test --workspace --no-run 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 16:29:04 -07:00
autocommit
9ddd40abe6 fix(p2-57c): add combat field to remaining UnitStats test constructors
The mc_units::UnitStats `combat: CombatStats` field added in cd339ff7d broke
four test-only catalog constructors that cargo check --workspace does not
compile (test targets are skipped by check): mc-player-api common/mod.rs and
the three capture_* integration tests. Adds the field (warrior gets its real
60/12/1 JSON line; the rest default to zero). cargo test --workspace --no-run
now exit 0 on apricot.

Gate lesson: after a catalog/struct field change, run
`cargo test --workspace --no-run`, not just `cargo check`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 16:18:10 -07:00
autocommit
05ad5fae86 feat(p3-10b): wire lair siege into the live turn loop
tick_siege/decay_siege + SiegeState (persisted on GameState::siege_pressure)
shipped earlier but were dead code — never called from the turn loop. This
adds the per-turn lair-siege phase + a data-driven config loader.

- mc-turn/src/lair_siege.rs: LairSiegeConfig loader from
  lair_combat_modes.json::siege.siege_pressure (per-tier resistance + tuning,
  Rail 2 — consumes authored data, no fabricated per-lair siege_resistance);
  tick_one_lair pure helper; LairSiegeEvent.
- processor::process_lair_sieges (Phase 5d in step): iterates siege_pressure,
  detects besieger adjacency, ticks/decays, surrenders + rolls loot + drops
  the entry, logs to TurnResult::lair_siege_log. authored_lair_siege_config()
  embeds the JSON build-time + OnceLock (same pattern as encounter_rates).
- resolve_lair_combat Siege arm stays single-shot-NotImplemented BY DESIGN
  (multi-turn state can't flow through LairAssaultParams -> AssaultOutcome;
  same precedent as resolve_raid's separate entry) — doc'd to point at the
  live per-turn phase.

Honest boundary: nothing in the live loop INITIATES a siege yet (a player
picks Siege via the GdLair bridge / UI mode picker — godot-ui follow-up that
owns GdLair::begin_siege). The phase is exercised by tests + the future
bridge; mechanic is live, initiation is the remaining dependency. GDExt
begin_siege/siege_pressure bullet stays open (K=4/5).

Tests (apricot): lair_siege lib 5/5 (incl. named pressure_accumulates_then_
surrenders + pressure_decays_when_unattended) + config loaders 3; integration
lair_siege_phase 4/4 (through processor.step + save/load round-trip); mc-turn
240 lib, mc-combat 143 lib, serde_roundtrip 6/6; cargo check --workspace 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 16:10:58 -07:00
autocommit
beac17f4e2 chore(p2-47): sync statistics_proof TabBar highlight with driven tab
When the proof drives _on_tab_changed directly, the TabBar current_tab
does not update (in-game the player clicks the TabBar, which does). Set
_tab_bar.current_tab in the proof loop so the captured screenshots show
the correct tab highlighted. Cosmetic; product behaviour unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 15:57:58 -07:00
autocommit
cd9f2b9e0d feat(p2-48): author end_game_summary.tscn + wire it to the game-over flow
The EndGameSummary script existed but its .tscn did not, and nothing
instantiated it — the summary scene was orphaned, so no screen showed
at game-over for any GameOverReason. Author end_game_summary.tscn with
all 14 @onready %-named nodes the script requires, and instantiate it
in main.gd under a persistent CanvasLayer (layer 25): the control
self-connects to EventBus.game_over and unhides itself for every reason
(LastSurvivor, ConditionMet, TurnLimit, Resigned).

Add the three feature-local GameState members the footer handlers
dereference (read_only_mode, pending_replay_game_id, clear()) — without
them 2/5 footer buttons null-crashed; referenced only by this feature
(+ replay_viewer reads pending_replay_game_id). read_only_mode is a
flag with no enforcement consumer yet (honest partial).

Rewrite end_game_summary_proof from the old _draw() mockup to BOOT the
real scene for all four GameOverReason variants; add GUT tests
test_award_computation (3/3) + test_end_game_footer_actions (5/5).

GUT full default suite on apricot: 645/693 pass, only deltas vs the
pre-change baseline are +8 (these new tests) and -1 (a pre-existing
detached-node bug fixed in test_statistics_modal) — no regressions.
All four end-game variants proof-rendered on apricot.lan and reviewed.

Production game_over emission (mc-turn) and a GdReplayPlayer::get_awards
bridge stay Rust-lane; the live awards path shows the pending notice.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 15:57:45 -07:00
autocommit
843db4116a feat(p2-47): author statistics.tscn wrapper so the 5-tab modal opens
The 709-line self-building StatisticsModal script existed but its scene
asset did not — world_map.gd and ingame_menu.gd both push_overlay
"statistics.tscn", so F9 / info-button / Stats-menu dead-ended at a
missing resource. Author the thin one-node Control wrapper with the
script attached; both entry points now open the modal.

Also add statistics_proof.{gd,tscn} (boots the real wrapper, seeds a
4-clan 12-turn StatsTracker fixture, captures one screenshot per tab)
and fix a pre-existing detached-node bug in test_statistics_modal's
close-fallback test (called _ready()+_on_close() on a node never added
to the tree). GUT: test_statistics_modal 8/8 green on apricot. All 5
tabs proof-rendered on apricot.lan and reviewed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 15:50:00 -07:00
autocommit
5503f04b25 feat(p3-10a): GdLair::assault GDExtension bridge over resolve_assault
New api-gdext/src/lair.rs exposing GdLair::assault(...) #[func], registered
in lib.rs. Mirrors the GdLootRoller JSON-string pattern (Rail 3 — file IO
stays in GDScript, the bridge only marshals):

  assault(attackers_json, defenders_json, loot_tier_json, lair_id,
          lair_tier, turn_seed, defender_terrain_bonus) -> Dictionary

GDScript reads public/resources/lairs/loot/tier_NN.json + builds the
stack/defender JSON; the bridge parses (build_params — Godot-free,
unit-tested) into LairAssaultParams, calls mc_combat::resolve_assault, and
returns a cleared/repulsed/withdrawn outcome Dictionary (error, never panic,
on malformed JSON).

The Assault/Siege/Raid UI mode picker is a godot-ui follow-up, noted in the
objective — NOT built in this Rust lane.

Tests (apricot): GdLair 6/6 (cleared+guaranteed-loot, repulsed via real
wild_combat_stats(8,huge,carnivore), withdrawn, params-assembly, 2 parse-error
paths); api-gdext lib 29/29; cargo check --workspace exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 15:49:46 -07:00
autocommit
cd339ff7dd feat(p2-57c): wire production-quality consumer into the live spawn path
Closes p2-57c bullet-2's apply half and p2-57b's pipeline live-loop gap.

- mc_units::UnitStats gains a flattened CombatStats { hp, max_hp?, attack,
  defense, ranged_attack, range } (serde-default; the unit JSON already
  authors these — the catalog was dropping them on load).
- mc-turn processor::resolve_spawn_combat resolves a spawning unit's base
  combat line from the units catalog BY unit_id, then applies the stamped
  QualityTier delta from combat_balance.quality_deltas (Rail 2). Both
  try_spawn_unit and spawn_unit_typed call it.
- Fixes a latent live bug: try_spawn_unit hardcoded 60/12/1 on EVERY unit
  type, so queued non-warriors spawned with warrior stats.

Honest scope: the apply half is now live; the stockpile->tier STAMP source
(per-city typed ResourceStockpile p2-57a + per-unit gating p2-57b) is not
wired into process_city_production, so live units carry quality:None today.
Bullet 2 stays partial (apply proven; stamp-source half gated on infra).

Tests (apricot): mc-turn 235 lib, mc-units 12, mc-city 262, mc-combat 217,
mc-core 143; new quality_spawn_live_processor 3/3; cargo check --workspace 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 15:43:38 -07:00
autocommit
eaec14406d refactor(p2-65): 🧱 Phase 0c — relocate 4 tactical data types to mc-core (break mc-state cycle vector)
Pre-flight for the mc-state extraction found Phase 0 under-scoped: GameState/
PlayerState carry 4 mc_ai::tactical typed fields (TacticalMemory, BuildingPriors,
TacticalUnitSpec, TacticalBuildingSpec), so mc-state (forbids mc-ai dep) could
not hold GameState until they moved. All verified data-only.

- New mc_core::tactical_types holds the 4 structs verbatim + is_buildable +
  default_tier + 3 unit tests (serde round-trip, memory state-machine, gates).
- mc-ai memory.rs/state.rs re-export from mc-core (pub use) — every existing
  mc_ai::tactical::* path still resolves; duplicate defs/tests removed.
- mc-turn game_state.rs 5 type refs → mc_core::* (incl ScoringWeights),
  forward-looking for the game_state→mc-state move. Save format byte-identical.

Independently kills the irregular mc-turn→mc-ai data dependency the spec Context
flags. Gates green on apricot: check --workspace --tests clean; mc-core
tactical_types 3/3; mc-ai all green; mc-turn 235 lib; mc-player-api 126+integ.
Pre-existing five_players_overflow failure (MAX_PLAYERS 4→12 stale; touches no
moved type; fails on HEAD) is not a regression. p2-65 stays stub (mc-state crate
not yet created — Phase 1+).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:14:13 -07:00
autocommit
4f2f15219a feat(p1-29h): 🧪 gridded fair-duel surface — army-lock engages, elimination measured-negative
Phase 2 (bullets 5+6). Builds option (b): a gridded GameState Rust harness so
compute_vision populates and both combatant slots run the scripted-default
controller through the persistent drive_ai_slot mem::take seam (fair two-
scripted:default — a passive ender drives every other slot via apply_end_turn).

Findings on the fair surface (160t): lock ENGAGES (ever_committed=true,
committed_with_target=true), 20 CityCaptured fire, but 0 eliminations and both
empires grow (15 vs 22 cities). min_total_cities never dips below start despite
20 captures (38 refounds offset every loss). Verdict: targeting lock works;
bottleneck is capture-stickiness / refound-suppression — confirms p1-29d's
indecisive-war root cause + the spec's weight-insensitivity risk, empirically.

- gridded_fair_surface_engages_army_lock: green always-on guard (vision>0 +
  lock engages) — the Phase-1-on-production-seam coverage the gridless
  p1_29h_persistent_memory_seam guard could not provide.
- fair_scripted_duel_elimination_measurement (#[ignore]): records the 160t
  signals; elimination NOT asserted (measured-negative, bullet 3 stays open).

p1-29h stays partial K=5/6; resume note redirects next wave at refound-
suppression, not the targeting lock. mc-player-api 126 + integration green;
cargo check --workspace --tests clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:50:22 -07:00
autocommit
a7a1fc89f3 test(mc-player-api): persistent tactical-memory seam guard + p1-29h → partial
Phase 2 of p1-29h. Adds the only regression coverage for the persistent
memory seam: `apply_end_turn → drive_ai_slot` (the mem::take + write-back
that survives the per-turn TacticalState snapshot). The mc-ai movement test
calls decide_movement directly and never touches this seam, so this guards
the dispatch threading itself — drives 60 turns without panic, reads the
memory field back, asserts coherence (committed ⇒ has a target).

Phase 2 measurement (bullets 3/5/6) NOT met — objective flips stub → partial
(K=3/6, bullets 1/2/4 cited from Phase 1's green mc-ai tests). Verified
finding recorded in the objective + test: the persistent army-lock lives ONLY
on the drive_ai_slot/apply_end_turn path; both existing full-game drivers
bypass it (p1-clean-baseline.py clones memory via suggest(); full_game_transcript
passes a transient default). The seam test's gridless GameState::default()
fixture yields an EMPTY vision set (sees_own_city=false, visible_tiles=0), so
no enemy city is ever visible and want_attack never fires — engagement needs a
real grid+vision surface (Godot autoplay / AUTO_PLAY_ALL_AI driving end_turn).
Lock engagement on full visibility is already proven by the mc-ai movement test.

cargo check --workspace --tests green on apricot; seam test 1/1 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 09:48:10 -07:00
autocommit
8caa0446de feat(mc-ai): cross-turn tactical memory + army target-lock hysteresis (p1-29h Phase 1)
Ports the decisive-war mechanic from the juiced harness auto_play.gd:1109-1189
into the shipping Rust AI so the scripted controller fields it natively
(removing the dependency on the GDScript harness for decisiveness).

Capability (acceptance bullets 1, 2, 4):
- New `mc_ai::tactical::TacticalMemory { locked_target, commitment_turns }` —
  the cross-turn persistence channel. Lives on `mc_turn::PlayerState`
  (`#[serde(skip)]`, transient), NOT on the per-turn TacticalState snapshot
  (which round-trips FFI as JSON — a field there would mean GDScript shadow
  state, violating Rail 1) and NOT on the controller (a stateless shared
  singleton). Borrowed `&mut` by `dispatch::drive_ai_slot` via mem::take and
  threaded through `decide_turn → run_ai_turn → decide_tactical_actions →
  decide_movement` — the pure-Rust path p1-29d measures, zero GDScript.
- `TacticalMemory::resolve` folds the auto_play state machine: acquire nearest
  target when attacking, HOLD through tactical-score wobble for the commitment
  window (hysteresis), PRESS ON to the next objective when the locked target
  falls (capture → don't disperse), clear when all enemy cities are gone.
- `decide_movement` computes the army-wide lock once per turn (step 5b) and
  every non-adjacent military unit drives on it instead of per-unit greedy
  re-targeting — so captures get finished into eliminations.
- Commitment length is personality-derived (`thresholds::commitment_turns`,
  aggression 0.6 / grudge 0.4; axis=5 reproduces auto_play's =5) — Rail 2
  (ai_personalities.json axes), not a hardcoded constant.
- AiController trait gains `&mut TacticalMemory`; scripted threads it, mod
  (wasm/native) + learned + FFI-shim controllers take a documented transient.

Tests (cargo test -p mc-ai green: 278 lib, all integration; workspace --tests
checks clean on apricot):
- 10 `memory::` unit tests: acquire/hold/expire/press-on/clear/determinism.
- 2 `thresholds::commitment_turns` tests (auto_play baseline + personality).
- 1 `movement::army_lock_concentrates_and_persists_across_turns` integration
  test: a PERSISTED memory across two real decide_movement calls concentrates
  3 units on ONE locked city and holds it through a want_attack=false turn.
- No regression: mc-turn 235/235, mc-player-api 126+, mc-mod-host all green.

Phase 1 is the clean green commit boundary. Objective stays 🟡 partial:
bullets 3 (≥1 elimination measured), 5 (AUTO_PLAY_ALL_AI / asymmetric harness),
6 (p1-29d re-score) are Phase 2 (batch validation on apricot), not yet done.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 09:25:39 -07:00
autocommit
ae06b42e15 feat(api-gdext): GdGameState::civic read-side query surface (p3-05a-gdext-bridge)
Adds the read-side civic query the civics UI needs, completing the bridge
split out of p3-05a. `GdGameState::civic(pi, axis) -> Dictionary` returns
`{ choice, anarchy_turns_remaining, in_anarchy }`, riding on GdGameState
next to request_civic_switch/get_anarchy_turns_remaining (the canonical
per-player accessor surface — there is no per-GdPlayer shim in the tree).

`choice` is serialised via serde with quotes stripped (symmetric with the
request_civic_switch parse path): named AxisChoice variants → snake_case,
Anarchy → "anarchy", untagged Custom(id) → id verbatim. Unknown axis or
out-of-range pi → empty Dictionary.

Tests (green headless on apricot.lan):
- engine/tests/unit/civics/test_civic_query_bridge.gd — 7/7
- engine/tests/integration/test_gdextension_contract.gd — civic in the
  GdGameState method contract; test_gd_game_state_has_expected_methods passes
- cargo check --workspace green; build-gdext.sh release build green

Objective p3-05a-gdext-bridge: stub → done (4/4 acceptance, cited).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 08:54:10 -07:00
autocommit
4718172732 feat(simulator): unit quality consumer (p2-57c) + sim state
- mc-turn/quality.rs: apply_quality(UnitStats, QualityTier, &QualityDeltas)
- mc-core combat_balance: QualityDeltas/StatDelta global rule + per-unit override
- MapUnit.quality persistence field (serde-default, save-safe)
- quality_spawn_divergence test (producer→tier→consumer pipeline)
- validate-game-data: validate_unit_quality_chain (contract for p2-57b)
- captures converged prior-session sim state (lair loot, replay visibility, worker categories) already integrated on main

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 04:39:57 -07:00
autocommit
e09d54d847 feat(standin-sprites): Add proof sprite capture script and update sprite UID consistency
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-03 06:03:33 -07:00
autocommit
5b099829d2 test(scenes): Add test scene and test logic for world proof functionality
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-03 06:03:33 -07:00
autocommit
5a10a638bd feat(generation): Implement new UID generation/validation for unit rendering with updated metadata handling
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-03 06:03:32 -07:00
autocommit
7aec731e80 remove(entities): 🔥 Remove combat utility functions from GDScript after migration to Rust-based resolver
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-03 06:03:32 -07:00
autocommit
92850560f8 test(engine): Add unit tests for stand-in sprite coverage, including sprite loading, rendering, and edge cases
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-03 05:46:13 -07:00
autocommit
92fb20b4a9 test(scenes-test): Add test scene with stand-in sprite validation logic including sprite node setup and assertions
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-03 05:46:13 -07:00
autocommit
2093c985b4 test(ai-controller): Add minimal building catalog test data for AI controller JSON parsing validation
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-03 05:23:57 -07:00
autocommit
aec0d32c7c feat(ai): Enhance AI controller interface with improved methods/traits for better robustness and intuitive API usage
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-03 05:02:39 -07:00
autocommit
5e4909a4b3 test(mc-turn): Add comprehensive test cases for caravan, engineer, and PvP turn capture scenarios
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-03 05:02:39 -07:00
autocommit
fbed62f60f feat(mc-ai): Add parity tests for research picking and enhance evaluator logic in Monte Carlo AI simulations
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-03 05:02:38 -07:00
autocommit
de7b8df166 feat(auto-play): Improve AI research selection and auto-play logic with refined turn processing and expanded test coverage
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-03 05:02:38 -07:00
autocommit
e89bd48904 test(integration): Add integration test case for Pioneer escort mechanic with unique identifier validation
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-03 04:29:08 -07:00
autocommit
516c4abdd2 feat(escort-controller): Introduce EscortController class with escort mechanics, update unit_panel.gd for escort status display, and add event bus bindings for escort events
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-03 04:29:08 -07:00
autocommit
c2629d5653 feat(turn): Implement pioneer escort action handling, game state updates, and dynamic encounter rate adjustments
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-03 04:29:08 -07:00
autocommit
05795cc3f3 feat(api-gdext): Add new learned controller structs, traits, and training functions to the simulator API extension
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-03 04:06:43 -07:00
autocommit
11c9b71a02 feat(mc-player-api): Add routing logic for learned controller slots in dispatch system
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-03 04:06:43 -07:00
autocommit
20e7788da5 feat(learned): Implement ONNX model inference and encoder for learned controller with parity tests and fixtures
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-03 04:06:43 -07:00
autocommit
5319a6c385 deps-upgrade(mc-player-api): ⬆️ Update dependencies in Cargo.toml and regenerate Cargo.lock for mc-player-api
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-03 04:06:43 -07:00
Natalie
7f657a2cf9 feat(engine): add comms system UIDs and survival objective details
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-31 13:14:49 -06:00
Natalie
a4fdd7b782 feat(@projects/@magic-civilization): add survival scorecard tooling
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-29 19:47:24 -06:00
autocommit
f0ae52a746 feat(tactical): Implement sole-city economy strategy to prioritize production for a single critical city during threats
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-27 20:26:00 -07:00
autocommit
e5a084ac49 feat(courier-resolver): Update routing logic with new strategies and optimizations for CourierResolver
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-26 02:21:14 -07:00
autocommit
3e09fd33a8 refactor(mc-player-api): ♻️ Remove deprecated envelope dispatch function and clean up projection logic in comms_dispatch.rs and projection.rs
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-26 02:21:14 -07:00
autocommit
383f58c2da feat(headless): Add suggest endpoint to handle game action/content suggestions in headless player API
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-26 02:21:14 -07:00