view_json now carries real inter-player trades — the headless "simulator provides
everything" goal is met. A player-like the headless adapter sees territory (step 2) AND
trades (this step) from the projected view, no GDScript re-derivation.
- view.rs: DiplomacyView gains trade_deals: Vec<TradeDealView> ({kind, you_receive,
you_give, gold_per_turn}, described from the viewer's perspective; serde skip-if-empty
for wire stability).
- projection.rs build_diplomacy: populates trade_deals from the persisted
state.trade_ledger swap/sale agreements (LuxurySwap/StrategicSwap/ResourceSale) for the
viewer↔counterpart pair, via swap_deal_view/sale_deal_view helpers (correct give/receive
direction; sale gold signed + for seller, − for buyer).
Verified: projection_surfaces_trade_deals_from_ledger (luxury swap direction + sale
buyer/gold); mc-player-api 171/0. (Disk filled mid-step from cargo target — cargo clean
reclaimed 9.5GiB; tests re-run from a clean build.)
p3-25 steps 1-5 DONE: view_json now carries territory + real trades, sourced fully in
Rust. Step 6 (live game adopts the unified PlayerView) reframed as a large separate
follow-on — the headless view-completeness this objective targets is achieved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The core of the rail-1 trade port: inter-player trades now form in the Rust headless
sim from REAL owned-tile resources (no proxy), persist to state, and apply.
- mc-turn::process_trade_phase: source_tradeable_resources sources each player's tradeable
luxuries + strategics from its cities' owned tiles → deterministic tile_collectibles
rolls (seed = map_seed ^ coord, stable across turns) → classified via
GameState.resource_categories (dups kept for MIN_COPIES_TO_TRADE). Replaces the old
proxy (tile_strategics: Vec::new(), tile_luxuries from traded_luxuries).
- Persists the re-derived swap/sale agreements into state.trade_ledger (retaining the
persistent OpenBorders/SharedMap), so the projection/view can carry real trades.
- Writes PlayerState.traded_strategics (new serde-default field) + applies net per-turn
gold flow (gold_flow_for: seller +, buyer −).
Verified: mc-turn source_tradeable_resources_classifies_owned_tile_collectibles
(determinism + classification purity + uncategorized-filtered + empty-categories no-op);
mc-turn+mc-state+mc-player-api 517/0; workspace cargo check clean (new PlayerState field
broke no literals). p3-25 steps 1-4 done; 5-6 remain (project trade deals into the view,
then GDScript view-only).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rail-1 city-model unification, step 3: give the headless sim the luxury/strategic
categories it needs to classify owned-tile resources for trade sourcing — currently
GDScript-only (DataLoader). No content hardcoded in Rust (Rail-2): loaded from JSON.
- GameState.resource_categories: BTreeMap<String,String> (id → "luxury"/"strategic"/
"bonus"), #[serde(skip)] boot-loaded exactly like units_catalog/civic_catalog (not
save-persisted; empty Default → nothing tradeable, a safe no-op).
- GameState::load_resource_categories_json parses the flat {id:category} object GDScript's
DataLoader emits; no-clobber on malformed input.
- GdPlayerApi.set_resource_categories_json FFI loads it onto the held state (call after
load_state_json, since the field is serde-skip).
Verified: mc-state load_resource_categories_parses_flat_map + suite 13/0; workspace
cargo check clean (GameState field addition broke no literals — all use ..Default).
Rust-only; live game unaffected. Unblocks step 4 (process_trade_phase classification).
p3-25 steps 1-3 done; 4-6 remain.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rail-1 city-model unification, step 2: give the headless/bench simulation real,
growing territory so view_json carries it (toward "simulator provides everything").
- mc_city::CityState gains owned_tiles: Vec<(i32,i32)> (serde default; backward-compat).
- mc-turn::process_culture: the culture-ready list from CulturePool::tick_all was
previously DROPPED (let _ready = ...). Now each ready city claims one contiguous,
in-bounds frontier tile per turn into owned_tiles — real border expansion in Rust.
Deterministic pick (lowest col,row among the unclaimed frontier); city centre owned
implicitly via city_positions, materialised on first expansion; consume_expansion
advances the threshold. Grid dims read before the &mut player borrow.
- mc-player-api projection: CityView.owned_tiles (schema field that existed but was
stubbed Vec::new()) now projects CityState.owned_tiles, with a centre fallback so
every city reports at least the tile it sits on.
- Fixed a pre-existing broken test (serde_roundtrip HappinessInput literal missing the
building_happiness_effects/happiness_per_city_effects fields p3-24 added).
Verified: cargo test mc-city + mc-turn + mc-player-api 725/0, incl. new
culture_expansion_claims_frontier_tiles + projection_surfaces_city_owned_tiles. Rust-only
headless-path change; live game (presentation_cities) unaffected. Unblocks step 4
(trade sourcing from owned-tile resources). p3-25 steps 1-2 done; 3-6 remain.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Owner directive: "gd should only be UI view of simulation / simulator provides
everything / no stubs". Root cause (verified): the sim holds two parallel city models —
authoritative mc_city::City (presentation_cities, has owned_tiles) vs bench
mc_state::CityState (GameState.players[].cities, no territory) — and project_view reads
the bench one, so view_json is structurally blind to territory + trades. New objective
p3-25 captures the full sequenced unification plan.
Step 1 (this commit) de-stubs what real bench state already carries, no fabrication:
- projection.rs build_diplomacy: DiplomacyView.{open_borders,shared_map,agreements_active}
now read the real OpenBorders/SharedMap entries from state.trade_ledger (the entries
dispatch writes on signing), replacing hardcoded false/false/empty stubs.
- CityView.owned_tiles left honestly TODO (center-only would mislead; fills in step 2 when
bench territory + border expansion are ported).
Verified: cargo test -p mc-player-api 169/0 (incl. new projection_surfaces_open_borders_
from_ledger). Rust-only headless-view change; no GDScript touched, live game path
unaffected. Swap/sale trade deals NOT yet in the view — they need the sourcing+persistence
port (p3-25 steps 2-5); not faked here per "no stubs".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase-gate proof for the deal UI (revival step 5 verification): diplomacy_deal_proof.tscn
instantiates the REAL diplomacy_panel.tscn with a crafted GameState (human + 2 AI rivals,
ledger holding one LuxurySwap + one StrategicSwap + one ResourceSale) and self-captures.
Screenshot reviewed in-conversation — the panel renders, in the correct per-rival rows:
- AI 1 (Ironhold): "Luxury Trade: Receiving Silk for Furs" + "Resource Sale: Buying
Horses (−2 gold/turn)"
- AI 2 (Goldvein): "Strategic Trade: Receiving Coal Seam for Iron Ore"
All three deal types render with correct direction, resources, and gold flow.
p3-23 status partial → done. Every acceptance bullet now met with evidence: gold↔resource
+ strategic swaps + luxury swaps (mc-trade) · AI evaluation · in-game pipeline revived
end-to-end (steps 1-4, GUT 750/0 + 25-turn arena exit 0) · deal UI (step 5, screenshot).
tribute.rs stays Game-2 deferred.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Active inter-player trade deals now surface in the diplomacy panel's per-rival agreement
section, alongside open-borders / shared-map rows.
- diplomacy.gd get_active_agreements parses the serde-tagged LuxurySwap / StrategicSwap /
ResourceSale entries straight out of GameState.trade_ledger_json (_append_trade_deals +
_swap_entry + _sale_entry — pure GDScript, no new FFI). Each deal becomes a display dict
{type, partner, you_receive/you_give | role/resource/gold_per_turn}.
- diplomacy_panel._make_agreement_section renders luxury_swap/strategic_swap (receiving X
for Y) + resource_sale (buying/selling X, ±gold/turn). 6 diplomacy_* vocab keys added.
- GUT test_get_active_agreements_surfaces_trade_deals: all three deal types + partner/
direction/resource fields. Panel script compiles + its tests pass. Full suite 750/0.
p3-23 implementation + logic now COMPLETE and GUT-proven across steps 1-5. The only item
left before status:done is a phase-gate proof screenshot of the trade rows (needs a
crafted live state with a human-held ledger deal; not reproducible in the all-AI arena).
Stays partial per objective-integrity.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Added the last missing link test: _collect_tradeable_resources against a real GameMap.
- test_collect_tradeable_resources_classifies_owned_tiles: builds a GameMap with
iron_ore×2 (strategic) + silk (luxury) deposit tiles owned by a player, asserts
_collect_tradeable_resources returns strategics=[iron_ore,iron_ore] (dups kept for
the MIN_COPIES_TO_TRADE surplus rule) + luxuries=[silk]. Proves _serialize_players'
real DataLoader-category tile sourcing. NB: DataLoader maps the "resources" category
to the deposits/ dir — served strategic ids are iron_ore/horses, not "iron".
- before_all loads the theme (category lookups need DataLoader). GUT 749/0.
Full inter-player trade pipeline now GUT-proven link-by-link AND headless-proven live:
real tiles → _collect_tradeable_resources (step 4) → process_trades → ledger →
traded_luxuries/strategics (step 1) → strategic build access (step 3); gold sale →
gold_flow_for → net gold (pre-existing); integration runs every round in a live 25-turn
arena without aborting the loop (step 2). Gold flow no longer inert — process_turn now
populates GameState.trade_ledger_json each round.
Only remaining for done: the deal UI (diplomacy panel + wire trade_agreed). Stays partial.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A strategic resource gained via an active inter-player trade now grants unit-build
access exactly like a tile-owned copy (Civ-style "access", not stockpile).
- turn_processor._player_owns_resource (the live production-COMPLETION gate behind
EventBus.strategic_gate_rejected) now short-circuits true when resource_id is in
player.traded_strategics, before the game_map tile scan. The Rust
GdCitySlot.enqueue_item gate has no live GDScript caller (unused FFI surface), so
this completion gate is the only live unit-gating path.
- GUT: test_player_owns_resource_via_traded_strategic (traded → access) +
_false_without_tile_or_trade (neither → no access). Full suite 748/0.
Acceptance chain now GUT-proven link-by-link: process_trades→ledger→traded_strategics
(step 1) · traded_strategics→build access (step 3) · gold_flow_for→net gold
(test_trade_gold_flows_into_net_gold, pre-existing).
Next (step 4): end-to-end in-game proof (a trade demonstrably forms in a played game,
gold flows >0, a gated unit becomes buildable) + deal UI. p3-23 stays partial.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Step 1 made diplomacy.gd's contract correct + isolation-proven. Step 2 wires it into
the turn loop, carefully.
- turn_manager.gd now calls (diplomacy as DiplomacyScript).process_turn(GameState.players,
GameState.turn_number, GameState.get_game_map()) once per full round, where the old
empty-stub call sat disabled. The original abort risk was a MISSING method (the stub
had none, so the call killed next_player + the arena loop). process_turn now exists and
is internally defensive (null game_map, missing GdTrade extension, unknown resources all
handled) so it cannot abort the round loop.
Verified (carefully, per owner): 25-turn headless AUTO_PLAY arena (seed 7) →
exit 0, 0 SCRIPT ERRORs, 0 process_trades errors, 26 turn_stats rows, clean score victory.
The trade-eval runs every round against real players/cities/map without crashing.
NOT yet shown: in-game trade FORMATION evidence (auto_play has no trade logging; 25 turns
is sparse for complementary surpluses). Mechanism itself is GUT round-trip-proven (step 1).
Next (step 3): in-game trade visibility — GdTradeLedger agreements-enumeration #[func] +
wire EventBus.trade_agreed (dangling, no listener) into the chronicle, then a longer arena
to confirm deals form in play. p3-23 stays partial.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Owner greenlit "revive carefully". First safe step: make diplomacy.gd's contract
correct and prove it works in isolation, WITHOUT enabling the turn-loop call.
- diplomacy.gd now matches the current GdTrade.process_trades {ledger} contract:
_serialize_players emits the PlayerTradeInput shape (player_index, tile_luxuries,
tile_strategics, trade_willingness), sourcing each player's controlled luxuries +
strategics from owned tiles classified by resource `category`; process_turn reads
result["ledger"], stores it, and _apply_ledger_resources fans the ledger's
incoming_luxuries/incoming_strategics onto each player (buyer gains the resource).
- Removed the dead _apply_trade_changes/_apply_relation_changes (they matched an old
contract that returned new_trades/relation_changes; process_trades returns {ledger}).
- player.gd gains traded_strategics (field + serialize/deserialize); _clear_pair_luxuries
clears it on war. GdTradeLedger.incoming_luxuries #[func] added (mirrors incoming_strategics).
- test_diplomacy.gd: replaced the 4 stale _apply_trade_changes tests with ledger-based
tests, incl. a full round-trip (PlayerTradeInput JSON → process_trades → ledger →
_apply_ledger_resources → buyers gain wine/horses/silk/iron).
Verified: cargo check gdext; dylib rebuilt; canonical GUT 746/0 (both new tests pass).
Turn-loop call REMAINS disabled (next step enables it carefully). p3-23 stays partial.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Verify-first while scoping part B (strategic gating) surfaced that p3-23's premise
was wrong. The inter-player trade evaluation does NOT run in the played game:
- turn_manager.gd:287 has Diplomacy.process_turn() commented out under a stale
"empty stub module" note (diplomacy.gd was rebuilt but never re-enabled).
- The only writer of GameState.trade_ledger_json is diplomacy.gd:32 inside that
disabled call → the ledger is never populated → NO inter-player trades run
(luxury, strategic, or gold).
- The diplomacy.gd <-> GdTrade.process_trades contract has drifted in 3 places:
input shape ({index,traded_luxuries,personality} vs Vec<PlayerTradeInput>),
return keys ({ledger} vs trade_ledger_json/relation_changes/new_trades), and
relations advancement. So enabling is not a one-line uncomment.
Consequence: the part-A gold-flow wiring (last pass) is correct + GUT-tested but
INERT in-game until the integration is revived (it reads an always-empty ledger).
The mc-trade simulation logic remains complete + cargo-tested (66/0). p3-23's real
remaining work is now scoped: revive the diplomacy trade turn-integration
(reconcile the 3 contract drifts, re-enable carefully, add PlayerState.traded_strategics
+ unit-gating, verify headless+GUT) then the deal UI. Status stays partial.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires the ResourceSale gold flow into the live economy (leveraging the p3-24
phase-1 economy port). GdTradeLedger gains gold_flow_for + incoming_strategics
#[func]s; GdEconomy gains a trade_gold param added to net gold AFTER the yield /
golden-age multipliers (a trade transfer must not be amplified by difficulty
handicap); economy.gd._player_trade_gold reads GameState.trade_ledger_json via
GdTradeLedger and passes the player's net flow. A seller now gains gold and a
buyer pays it each turn a sale is active.
Verified: GUT test_trade_gold_flows_into_net_gold (seller +3 → net 8, buyer −2 →
net 3, trade_gold echoed); dylib rebuilt + canonical GUT 748/0.
p3-23 stays partial — gold-trade flow now live (part A); remaining part B is the
strategic-resource gating (FFI sources tile_strategics, PlayerState.traded_strategics,
unit-gating reads incoming_strategics) + the deal UI.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Verify-first (per the never-infer rule): the objective flagged climate_effects.gd:125
(unit.hp -= hp_loss) as a GDScript simulation-logic violation, but verification shows
the hp_loss COMPUTATION already lives in mc-climate::climate_effects::apply
(hp_loss = unit_damage × severity_scale; climate_effects.rs:113, 6 cargo tests).
GdClimateEffectsPhysics.apply delegates to it; climate_effects.gd is a thin marshaler
that fans the Rust-computed value onto GDScript Unit entities — the same sanctioned
pattern as economy.gd's disbanded_units fan-out (the file's own doc says so). No
GDScript simulation arithmetic remained, so bullet 3 is marked done with evidence
rather than churning already-compliant code.
All three named GDScript violations (gold, happiness, climate) now resolved. p3-24
stays partial only on the explicit (Stretch) bullet 4 (per-turn orchestration → Rust
turn driver), deferred to the broader pathfinder/turn port. cargo + GUT 747/0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
happiness.gd summed building happiness effects and applied the
happiness_per_city × city_count multiply IN GDScript before the GdHappiness
call. Moved both into mc-happiness:
- HappinessInput gains building_happiness_effects + happiness_per_city_effects
(#[serde(default)]); building_happiness_total() does the sum + per-city multiply.
calculate_happiness uses it. Legacy building_happiness kept as a back-compat
default field.
- happiness.gd passes the raw effect lists (no arithmetic); turn_processor_helpers
sum_building_effects → collect_building_effects (pure per-building extraction,
its only caller was happiness.gd). The luxury-map assembly stays GDScript (tile/
DataLoader extraction; mc-happiness is pure).
Verified: 2 new mc-happiness cargo tests (aggregates effects+per-city; back-compat
legacy field); mc-happiness 23/0; dylib rebuilt + canonical GUT 747/0 (full
happiness.gd path test_happiness_turn -6/-4 unchanged).
p3-24 bullet 2 done; stays partial — remaining: climate HP-loss→Rust, orchestration.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
economy.gd:65-79 computed gold IN GDScript (building-effect sum, gold-per-pop
multiply, gold-from-mines loop, percent sum) before the GdEconomy call —
violating "GDScript is presentation only". Moved all of it into mc-economy:
- New CityGoldRaw (per-building effect VALUES + population + mine_count) and
aggregate_city_gold() that does the building-sum + per-pop×pop + per-mine×mines
+ percent composition. Pure arithmetic, cargo-tested.
- GdEconomy FFI now deserializes the raw shape and aggregates before process_gold.
- economy.gd reduced to data extraction: _collect_effect_ints/_floats (no summing)
+ mine count; zero gold arithmetic. gdlint clean.
Verified: 3 new mc-economy cargo tests (sums/per-pop+per-mine/percent+e2e);
GdEconomy bridge GUT tests migrated to the raw shape; mc-economy green; dylib
rebuilt + canonical GUT 747/0.
p3-24 bullet 1 done; stays partial — remaining phases: happiness assembly→
mc-happiness, climate HP-loss→Rust, orchestration (stretch).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the trade-richness simulation logic. New DiplomaticAgreement::ResourceSale
forms as a barter FALLBACK: when a pair can't swap (one side has a surplus the
other lacks but not vice-versa), the surplus holder SELLS the resource to the
buyer for SALE_GOLD_PER_TURN. evaluate_trades produces it after the swap passes
(sale_candidate probes pa-then-pb, luxuries-then-strategics, deterministic).
TradeLedger.gold_flow_for(player) exposes the per-turn flow (seller +, buyer −);
incoming_luxuries/incoming_strategics route the bought resource to the right pool;
has_resource_sale + breaks-on-war. Variant grouped with the swap arms across the
renewal/courier matches; break_trades_on_war gets its own ResourceSale arm.
Verified: 4 new cargo tests (forms-as-fallback, no-sale-when-swap, strategic-
routing, breaks-on-war); existing no-surplus test updated for the new fallback;
mc-trade 66/0; api-gdext + mc-turn compile.
p3-23 stays partial — both trade-logic halves (swaps + sales) now done + tested;
remaining is the in-game application (mc-economy gold flow + traded-resource
gating/happiness + GDScript deal UI), which lands with p3-24's economy port.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds strategic-resource trading to the inter-player evaluator (iron for horses),
the high-value half of trade richness. New DiplomaticAgreement::StrategicSwap;
evaluate_trades forms one per pair independently of the luxury swap via a shared
swap_candidates helper; PlayerTradeInput gains tile_strategics (#[serde(default)],
forward-compatible). TradeLedger.incoming_strategics / has_strategic_agreement
expose the buyer's gained access (unit-gating); swaps break on war. Keeps the
"keep your last copy" surplus rule (MIN_COPIES_TO_TRADE). The variant is grouped
with LuxurySwap in the renewal/courier matches (non-renewable, re-derived each turn).
Verified: 4 new cargo tests (forms-from-complementary-surplus, needs-surplus-and-
complementarity, luxury+strategic coexist, breaks-on-war); mc-trade 62/0;
api-gdext compiles.
p3-23 stays partial — remaining: gold↔resource deals (mc-economy gold flow) +
in-game wiring (FFI sources tile_strategics, PlayerState.traded_strategics,
unit-gating reads incoming_strategics).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migration now responds to climate, not just population pressure. In the per-turn
population mover (generation.rs compute_migrations), drought_carrying_factor
lowers a dry tile's effective carrying capacity — so its populations exceed cap
and emigrate, while dry neighbours offer less room, steering herds toward wetter
ground. The MigrationPulse events path (biological.rs) gets a matching
data-driven migration_climate_stress trigger bias. Reads the tile's live
drought_counter; pure mc-ecology, runs via the existing per-turn engine tick.
Tests: migration_climate_stress_rises_with_drought_and_caps, drought_raises_migration_rate,
drought_reduces_carrying_capacity_triggering_migration; mc-ecology 338/0; dylib
rebuilt + deployed; GUT 747/0.
p3-21 → done. Next: p3-23 (trade richness).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes weather→scouting. weather.gd gains vision_penalty_at(col,row) (worst
penalty from active events covering the tile, hex-distance footprint);
world_map_vision.recalculate_vision cuts each unit's sight radius by it (floored
at 1) before computing visible_hexes — so a unit standing in a storm/blizzard/dust
storm reveals fewer tiles. The penalty VALUE is Rust-derived (WeatherEvent.vision_penalty,
data); GDScript only reads + applies it. Headless path has compute_vision_with_penalties.
Verified: mc-climate 45/0, mc-vision 30/0, GUT vision_penalty_at (within/outside/
worst-overlap); dylib rebuilt + deployed; canonical GUT 747/0.
p3-20 → done. Next: p3-21 (weather-driven migration).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The vision-consumer half of weather→scouting. compute_vision_with_penalties takes
a per-tile vision-penalty map and reduces each unit's effective sight by the
penalty at its tile (floored at WEATHER_MIN_VISION=1, so a unit always sees its
own tile + immediate ring). compute_vision delegates with an empty map, so all
existing callers are unchanged (no churn). refresh_for_player +
compute_player_visible_set thread the penalty.
Test: weather_vision_penalty_shrinks_unit_sight (radius-2 disk 19 → radius-1 disk
7 under penalty 1; floored under penalty 99); mc-vision 30/0.
p3-20 stays partial — bridge/GDScript wiring (weather events → penalty map →
compute_vision_with_penalties) + dylib/GUT next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The producer side of weather→scouting: WeatherEvent gains vision_penalty: i32
(serde-default + flexible deser), and derive_events sets it per kind — storm 1,
blizzard 2, dust_storm 2; heat_wave/drought/flood 0 (clear-weather events don't
impair sight). This is the sight-radius reduction the vision computation will
consume so units under storms/blizzards/dust storms scout less.
Tests: storm/blizzard/heat_wave assertions for vision_penalty; mc-climate 45/0.
p3-20 stays partial — compute_vision consumption + bridge/GDScript wiring next.
(Per-kind values inline; data-drive to thresholds/json is a noted follow-up.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes player→ecology feedback. EcologyEngine::deplete_flora_at(col,row,amount)
depletes a tile's Producer-diet (flora) populations (registry-identified);
GdFaunaEcology.deplete_flora_at exposes it; EcologyState._on_tile_improved fires
it when a flora-clearing improvement (deforestation) completes — so clear-cutting
a forest removes its flora (the terrain→grassland change also drives the gradual
die-off). With the fauna half (over-hunting → extinction), the living world now
reacts to player pressure both ways. Logic stays in mc_ecology (Rail 1).
Test: deplete_flora_at_targets_producer_species_only (mc-ecology green); dylib
rebuilt + deployed; canonical GUT 745/0 (wiring loads, no regression).
p3-19 → done. Next: p3-20 (weather→scouting).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires the player→ecology coupling for fauna (the USP: the living world reacts to
the player). GdFaunaEcology.deplete_species #[func] resolves the string species id
→ numeric via the species library and calls EcologyEngine::deplete_population.
combat_utils._roll_wild_creature_loot now passes the slain creature's tile into
item_system.roll_fauna_drops, which calls EcologyState.fauna_ecology.deplete_species
on every fauna kill — so sustained hunting drives a species to local extinction
(is_extinct), and the engine's growth/emergence recover it once pressure eases.
Logic stays in mc_ecology (Rail 1); GDScript only triggers + passes the tile.
Verified: mc-ecology cargo green; dylib rebuilt + deployed; canonical GUT 745/0
(new roll_fauna_drops signature + caller load cleanly, deplete_species callable).
p3-19 stays partial — flora-harvest half (chop/intensive → flora population) next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Rust foundation for player→ecology feedback: PopulationSlot::deplete(amount)
(floors at 0, can cross the is_extinct threshold) + EcologyEngine::deplete_population
(col,row,species_id,amount) → post-depletion population, safe 0.0 no-op for a
missing tile/species. This is the hook hunting/harvesting will call to make
over-harvest drive local extinction; the engine's existing growth/emergence then
recovers abundance once pressure eases.
Tests: deplete_reduces_floors_at_zero_and_can_extinct (PopulationSlot) +
deplete_population_reduces_tile_species_and_can_extinct (engine); mc-ecology green.
p3-19 → partial. Remaining: GdEcologyEngine #[func] + GDScript kill/harvest wiring
+ dylib/GUT (loop continues).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pick_for_city gains a scout branch (after the early-military floor, before
expansion): when dwarf_scout is buildable (its tech/race/resource/building gates
met — mirrors pick_best_unit_of_type) and the capital owns no scout, the capital
queues one. Single scout, capital-only; the existing frontier-seek + scout-sweep
maneuvers (movement.rs) already drive dwarf_scout, so the AI stops diverting
combat units to scouting.
Tests: ai_builds_scout_when_buildable_and_none_owned +
ai_does_not_build_scout_without_its_tech; mc-ai 289/0 green. p3-22 → done.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Game 1 was NOT actually complete; the green dashboard overstated it. Created
objectives for the verified, untracked gaps (all confirmed 2026-06-25 against the
code, both Rust + GDScript paths):
- p3-19 player→ecology feedback (over-harvest/hunt deplete live populations; extinction)
- p3-20 weather affects scouting (vision penalty, not just movement)
- p3-21 weather-driven migration (migration ignores weather today)
- p3-22 AI builds dedicated scouts (only frontier-seeks with idle military)
- p3-23 trade richness (luxury swaps only; no gold/strategic trades)
- p3-24 Rail-1 port of economy/happiness/climate per-turn glue logic from GDScript
Dashboard regenerated. (Systems I'd wrongly doubted — ecology engine ticking, AI
worker improvements, naval harbor-gating — are confirmed working; not reopened.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tools/objectives-report.py after marking p3-18 (water crossing) done. Keeps the
dashboard fresh for verify step 2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
P1 hardcoded the tech ids ("shipbuilding"/"ocean_navigation") in Rust — a Rail-2
violation (JSON is the canonical content store) and not single-source. Per owner
direction ("should be a player-level config setting"), the embark grant now lives
in data and is cached per player:
- mc_core::EmbarkLevel moves to the shared base crate (was mc-pathfinding) so
PlayerState can hold it; mc-pathfinding re-exports it. Adds from_mechanic_key
(the ONLY place the embark_* mechanic-key strings live).
- The naval techs carry the grant in JSON via unlocks.mechanics: shipbuilding →
embark_coast, ocean_navigation → embark_ocean. Which tech grants embark is now
authored data, not Rust.
- TechWeb::embark_level(researched) derives the strongest grant across a player's
researched techs (None < Coast < Ocean).
- PlayerState gains a cached embark_level field; process_science recomputes it
each turn from the researched set (idempotent → save-load / tech injection
covered). The move handler reads the cache (no per-move tech parsing).
Tests: mc-core EmbarkLevel ordering + mapping; mc-tech embark_level method
(inline web) + a real-data guard that authored naval.json carries the mechanics;
mc-pathfinding 9/9 unchanged. All green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`tools/objectives-report.py --check` was failing (README/objectives.json stale vs
per-objective frontmatter after recent objective edits). Regenerated both.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the TechWeb fix: research advanced to 109 techs but every city
still spawned tier-1 dwarf_warrior. Five stacked defects severed the
tech→unit→production chain; all are fixed here. Proven in a 599-turn duel
self-play (seed 42): slot 0 fields a 222-strong tier-8 dwarf_adamantine_champion
army; both clans now CHOOSE tier-2..8 units by tech (was tier-1 only).
1. units_catalog (#[serde(skip)]) was never stamped onto the dispatch
GdPlayerApi — only GdGameState. The harness comment claimed 'both must be
re-stamped at boot' but only did GdGameState. Added
GdPlayerApi::set_units_runtime_catalog_json + a post-load harness re-stamp.
Without it apply_queue_production/try_spawn_unit ran catalog-blind.
2. apply_queue_production classified queued ids as unit-vs-building by a
starts_with("dwarf_") prefix. Replaced with an authoritative
units_catalog.get() lookup (prefix kept only as empty-catalog fallback).
The prefix leaked dwarf_-prefixed BUILDINGS (dwarf_deep_forge) onto the map
as units and misfiled every non-dwarf unit.
3. project_tactical_player hardcoded race_id: None, filtering out every
race_required:dwarf unit. Added PlayerState.race_id (race gates production →
it is sim state, not pure presentation per p2-72a), stamped it in
set_player_presentation_json + the headless harness (sourced from
setup.json::default_race), and projected it.
4. build_unit_catalog loaded faction:"wild" monsters (ancient_hydra, …) and
freepeople into the AI production catalog. With race_required:null they
passed every race filter and, being high-tier, the picker preferred them.
Excluded non-player factions from the production catalog (runtime catalog
still carries them for encounters).
5. Generic warrior/spearmen/archer carried clan_affinity to ALL FIVE clans at
tier 1, so clan_affinity_score=2 dominated tier and every named clan locked
onto tier-1 'warrior'. Cleared their affinity (they are neutral baselines,
not clan signatures).
mc-player-api 132 + mc-state 12 tests green. Known next layer (not this fix):
production VOLUME — try_spawn_unit's empty-queue dwarf_warrior auto-spawn still
floods tier-1, and the loser snowballs; the picker is correct, army composition
tuning is separate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tech unlocks + a unit tech-gate referenced ids absent from the loaded set:
- improvement renames (techs used stale ids): irrigation_channel→irrigation,
stone_road→road, fortress→fort
- improvement refs with no existing target removed: orchard, aqueduct_channel,
bridge, fishing_boats (no such improvement is authored)
- flight units (dwarf_gyrocopter/iron_hawk/mithril_hawk) exist + their unlock
techs are in-scope, but weren't in the manifest's units subscription → added
- dwarf_master_engineer.tech_required deep_engineering (nonexistent) → total_war
(its grand_engineer upgrade-from's gate, which is subscribed)
GUT: test_data_integrity 0 dangling refs (724 passing / 2 failing).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two diplomacy models contradicted each other in the written record: p1-01
diplomacy-lite ('all pairs start at war, missing key → war') vs the newer
courier-diplomacy (COMMUNICATIONS.md §War declaration semantics, p3-01:
start at peace, sender enters War on war-dec envelope dispatch). The Rust
implementation follows courier-diplomacy, so that is canonical.
- p1-01: add a SUPERSEDED banner + inline [SUPERSEDED] annotations; history
retained. Canonical rule is start-at-peace, war via dispatched war-dec.
- COMMUNICATIONS.md: fix the one internal inconsistency (§0 said recipient
war state applies at arrival in a way that read as all-effects-at-arrival;
scoped it to recipient-side, cross-linked the sender-on-dispatch exception).
- New objective p3-16 (status partial, owner warcouncil): the AI has no
proactive war-declaration — decide_tactical_actions has no diplomacy step
and there is no DeclareWar in mc-ai, so AI-vs-AI never enters war and clan
aggression personalities don't manifest. Specs the fix to the courier model
(first-contact + military balance + aggression → dispatch_war_declaration)
and notes the stale is_at_war comment as a code-fidelity cleanup.
- Register p3-16 under warcouncil; regen objectives dashboard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Setup gains a name field per slot (humans editable; AI slots named after their
clan via PersonalityAssigner, deduped); player_names flows payload → loading_screen
→ player_name. GameState.randomize_turn_order() adds seeded Fisher-Yates ordering
that next_player() rotates through. Minimap now fogs to the local player's
knowledge. Adds hotseat_view_proof (same game, two fogged worlds) + a turn-order
unit test; refreshes the p3-15 acceptance evidence.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two+ humans sharing one device get a pass-the-device hand-off (hotseat_handoff)
gated by GameState.is_hotseat(). Each seat sees only its own view: city_renderer
fogs enemy cities until explored, prologue_overlay_renderer draws only the local
player's opening, and the AI/turn banners no longer stack across hand-offs.
Documents the flow in TURN_SEQUENCE.md, adds a headless handoff proof scene, and
marks p3-15 done (dashboard regenerated).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Game opening becomes a moddable JSON script driven by mc_worldsim::StartScriptRunner
and exposed to Godot via GdStartScript. Start scripts + dwarf tribe/wanderer units
live in public/resources/start_scripts; START_SCRIPTS.md documents the contract.
Adds tools/validate-start-scripts.py + wires it into CI (stage 3b) and verify.sh
(step 0b). Marks p3-14 done and regenerates the objectives dashboard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire the static .tscn label/button text in 5 scenes (end_game_summary section
titles + footer, merge_panel, city_screen merge button, game_setup section
headers, past_games) through ThemeVocabulary.lookup() in their controllers'
_ready, and remove the hardcoded .tscn text. Add the corresponding vocab keys;
drop 3 redundant endgame_* keys (footer buttons already use endgame_footer_*).
validate-i18n now passes: 145 scenes scanned, 0 hardcoded UI strings.
This clears the i18n verify gate with no bypass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace hardcoded user-visible strings in 12 scene scripts (great_person_modal,
merge_panel, specialists_drag_panel, throne_room_great_works,
intelligence_log_panel, knowledge_tree tier badge, credits, game_setup,
past_games, replay_viewer, lens_switcher/ransom_offers tooltips) with
ThemeVocabulary.lookup() + add the corresponding keys (incl. fmt_* for format
strings). Part of clearing validate-i18n with no bypass. (48 → 23 remaining.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
replay_viewer.tscn hardcoded its title/placeholder/turn/play/step/speed-label
text (pre-existing since May). Route the word labels through ThemeVocabulary
(replay_back/title/placeholder/step keys) set in _ready; runtime-set labels
(turn/play-pause/speed) lose their static .tscn text. Symbolic speed buttons
(0.5×/1×/2×) left as-is (not flagged). Clears validate-i18n for this scene.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tools register after restart (confirmed). First in-session call timed out (slow
throttled MCP-spawned windowed Godot); fixed via connect-first + 150s grace +
respawn (6310433cc), re-verified through the built client (NODE_EXIT=0, real
3.85MB PNG). Running MCP needs to reload the post-fix dist (one restart) to use
it in-session.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
world_map lair POI overlay, tile_info_panel tooltip wiring, lair standin
sprites + build_demo_lairs.py, tooltip unit test, lair proof scenes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Foundation for collapsing the ~188 add_theme_color_override calls onto Godot
Theme inheritance. Adds a `typeVariations` section to design-tokens.json and
emits each as a Godot type variation in ui_theme.tres.
9 Label variations covering the high-count font_color override patterns:
LabelTitle/Muted/Secondary/Disabled/Positive/Negative/Warning/Gold/Science
(→ text.title/muted/secondary/disabled, semantic.positive/negative/warning,
accent.gold/science). Widgets set `theme_type_variation = "LabelMuted"` to
inherit instead of `add_theme_color_override("font_color", ...)`.
Additive — nothing consumes them yet, zero visual change. Verified: variations
baked into ui_theme.tres, theme --check clean, headless load exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Establish ONE data source for biome render colour, lifting hex_renderer.gd's
authoritative 69-entry palette (value-preserving, biome_id -> [r,g,b] 0-255).
- public/games/age-of-dwarves/data/biome_colors.json — the single source.
- DataLoader: _load_biome_colors() at theme load + get_biome_color(biome_id)
with '_default' fallback then magenta sentinel.
Additive only — no consumer rerouted yet (next phases: hex_renderer, minimap,
proof scenes all read this + delete their hardcoded TERRAIN_COLORS dicts).
Verified: headless load clean, biome_colors.json parses, 0 script errors.
(data_loader.gd max-file-lines is pre-existing, tracked by p2-10k.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>