Commit graph

3683 commits

Author SHA1 Message Date
Natalie
fa93e59425 feat(@projects/@magic-civilization): 🔥 p3-26 gap 2 — wildfire effect (apply_wildfire) ported
The first per-category event EFFECT, pure + testable. apply_wildfire burns forest tiles in
a hex disk around a center (offset coords via offset_to_axial/hex_spiral/axial_to_offset):
transforms matching terrain to `becomes`, drops quality (min 1) + moisture (min 0); returns
tiles burned. Mirrors GDScript process_wildfire's per-tile effect.

Test: a forest patch + apply_wildfire(radius 2, becomes grassland, quality_loss 2) → forest
→ grassland, moisture/quality dropped, non-forest tiles untouched. mc-climate events 6/6.

Next: the dispatch (process_events — category_fires gate + roll_severity + deterministic
center pick → apply_wildfire) + config-on-GameState + wire into the mc-turn climate phase.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:42:29 -04:00
Natalie
4e82b322cb feat(@projects/@magic-civilization): 📋 p3-26 gap 2 — event-config loader (mc-climate::events)
Next brick of the events port: load per-category configs from the canonical JSON
(Rail-2, nothing hardcoded).

- EventCategoryConfig { base_frequency, severity_weights, raw } — typed dispatch inputs +
  the full JSON kept in `raw` so each per-category handler reads its own fields (tiers,
  target_terrain, becomes, aerosol_strength) without modeling all 12 shapes up front.
- load_event_configs(dir) reads public/resources/events/<category>.json (category =
  filename stem; skips *.schema / cross_triggers / events). Test parses the real
  wildfire.json (base_frequency 0.04, severity_weights, target_terrain ∋ "forest").

mc-climate events 5/5. Next: dispatch (process_events using category_fires + roll_severity)
+ per-category handlers (wildfire first — burn forest in radius, transform biome) + wire
into the mc-turn climate phase. Tile-picking will use a Rust-deterministic RNG (the
headless sim needs internal determinism, not byte-match with the live game's Godot RNG;
the dispatch GATE already matches GDScript bit-for-bit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:39:04 -04:00
Natalie
9ccc7e10ff feat(@projects/@magic-civilization): 🎲 p3-26 gap 2 (start) — deterministic event core ported to mc-climate::events
First slice of the natural/"apocalyptic" events port (M3). The deterministic primitives
every category depends on, ported from GDScript ecological_event_utils:

- hash_noise(x,y,seed) = frac(sin(x*127.1+y*311.7+seed*74.3)*43758.5453), f64 — verified
  to match the LIVE GDScript game bit-for-bit (ran it: hash_noise(10,0,1000) =
  0.67791910066535). The headless sim must match the game, NOT the TS web guide (whose
  Math.sin diverges on these large arguments — a pre-existing game-vs-guide gap, not a
  port bug; the old comment's "0.1270 from TS" golden was misleading).
- roll_severity(weights, turn_seed, channel, max_tier) — weighted tier roll with era cap.
- category_fires(base_frequency, channel, turn_seed) — the per-category dispatch gate.

4 cargo tests (GDScript-golden determinism, channel separation, severity bounds + cap,
fire gate). Source corrected: .messy is gone — the port source is the live
ecological_events.gd + handlers_a/b + public/resources/events/*.json. Next: event-config
structs/loading + dispatch + per-category handlers (wildfire first) + turn wiring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:35:14 -04:00
Natalie
1bdad8e497 feat(@projects/@magic-civilization): 🌪️ p3-26 gap 1 (cont.) — weather + climate effects (unit HP) in the headless turn
Extends the headless climate phase from physics-only to the full per-turn chain mirroring
the live game's _process_climate (climate → weather → effects):

- process_climate_phase now: ClimatePhysics::process_step → weather::derive_events
  (storms/heat-waves/blizzards, default thresholds = live GdWeatherPhysics) →
  apply_climate_effects.
- apply_climate_effects (extracted, testable): runs climate_effects::apply (tile effects +
  per-unit hp_loss) then fans hp_loss onto MapUnit.hp as max(0, hp - hp_loss) — exactly
  climate_effects.gd. movement_penalty surfaced but not applied to units (matches live).

Tests: apply_climate_effects_fans_hp_loss_onto_units (deterministic — unit in heat-wave
radius loses HP, unit outside unharmed) + the determinism test; mc-turn 337/0, no
regression. Gap 1 remaining: marine_harvest (ocean_dead_fraction → climate).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:25:33 -04:00
Natalie
e9409f22cd feat(@projects/@magic-civilization): 🌡️ p3-26 gap 1 (start) — climate physics ticks in the headless turn
The headless mc-turn ran no climate (live-game GDScript only). Now it does:
- mc-turn deps mc-climate (no cycle; mc-climate is lower-level).
- TurnProcessor::process_climate_phase ticks mc_climate::physics::ClimatePhysics once per
  round on state.grid (process_step(grid, turn, map_seed, dt=1.0)). Default config
  ("{}"/"[]"/"{}") matches the ecology bench; the grid carries climate state across turns;
  fresh-processor-per-turn is safe (physics is the operator, grid is the state). No-op
  without a grid.
- Called in step() right after fauna (world-level end-of-round phase).

First slice of gap 1 — temperature/aerosol/precipitation now evolve on the live grid in
self-play. Still to come: the weather + climate_effects (unit HP) + marine_harvest chain.
Verified: climate_phase_ticks_grid_deterministically (determinism + no-grid no-op);
mc-turn 336/0 (no regression — climate phase runs in every step() with a grid).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 08:25:01 -04:00
Natalie
f9593c4d29 docs(@projects/@magic-civilization): 🗺️ p3-26 — roadmap to complete the headless simulator (loop done-criterion)
Owner directive: the /loop isn't finished until the SIMULATOR is complete — the headless
Rust sim must play full self-play games with ALL systems, not the reduced subset.

p3-26 enumerates the verified live-vs-headless gaps + sequenced plan:
- Gap 1: climate/environment runtime (port the marine→climate→weather→effects chain into
  mc-turn; physics already in mc-climate).
- Gap 2: natural/"apocalyptic" events (M3 milestone — port .messy ecological_events.gd,
  12 categories, deterministic per EVENT_FREQUENCY_SPEC).
- Gap 3: equipment/crafting (recipes exist; no headless Craft action).
- Gap 4: per-building build queues (dual city-model; bench has a single queue).

Corrects my earlier "apocalyptic events don't exist" — they're specced (m3-natural-events)
with a .messy reference impl, just unimplemented in Rust.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 08:25:01 -04:00
Natalie
4fe3bc6091 feat(@projects/@magic-civilization): 🧪 p3-25 headless hardening — wire resource categories into mc-sim benches
Owner chose headless-only hardening over the live-game refactor (step 6 deferred).

- mc_sim::load_deposit_categories(deposits_dir) reads public/resources/deposits/*.json →
  id→category map (handles single-object or array files; skips bad files).
- dominion_bench + tournament_bench now set state.resource_categories from it after
  building GameState. These benches run the Rust TurnProcessor (process_trade_phase) but
  never loaded categories, so step-4's real sourcing had left their inter-player trades
  inert (sourcing from empty categories → no luxuries/strategics → no trades). Now bench
  trade dynamics (trade_willingness axis, gold-from-sales) form again.

Also recorded: real-game confirmation that the headless pipeline is live — the magic-civ
MCP view_json returns cities[].owned_tiles populated (step-2 territory projection running
in a real headless game on the rebuilt dylib).

Verified: mc-sim load_deposit_categories_reads_real_deposits passes; dominion_bench +
tournament_bench compile. solo_dominion (single-player, no trade partners) intentionally
not wired.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 07:15:15 -04:00
Natalie
828b4171c6 docs(@projects/@magic-civilization): 📐 p3-25 step 6 — sequence the live-game trade-unification plan + risk
Step 6 fully scoped: making the live game Rust-authoritative for trades is one
interlocking change (sync dual city model → run process_trade_phase in the live turn →
panel reads Rust deals FFI → retire the shipped GDScript Diplomacy.process_turn) with no
safe isolated brick. It modifies the working, screenshot-proven p3-23 live trade feature
for Rail-1 purity (not a functionality gap). Plan + risk recorded; awaiting go on the
approach before touching the live trade system.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 02:40:49 -04:00
Natalie
3f7a4e5442 feat(@projects/@magic-civilization): 🔌 p3-25 — wire headless harness to load resource categories (trade pipeline now LIVE)
The headless trade pipeline was unit-proven but inert in real runs: nothing called
set_resource_categories_json, so process_trade_phase saw empty categories and sourced
nothing. Wire it in.

- scenes/headless/player_api_main.gd::_apply_resource_categories builds the resource
  id→category map from DataLoader.get_all_resources() and stamps it onto GdPlayerApi via
  set_resource_categories_json, AFTER load_state_json (same #[serde(skip)] re-stamp
  pattern as units_runtime_catalog + tech_web). Now a real headless game classifies
  owned-tile collectibles → sources luxury/strategic surpluses → forms trades → view_json
  carries them. End-to-end LIVE.

Verified: unit+integration GUT 750 (737 pass / 13 pending / 0 fail); the headless
projection-roundtrip boot path (which exercises _apply_resource_categories) is green.
GDScript-only change calling an existing FFI — no dylib rebuild needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 02:32:42 -04:00
Natalie
91c0b79ad1 docs(@projects/@magic-civilization): 📋 p3-25 — record e2e proof + no-regression verification (canonical GUT 607/0)
p3-25 steps 1-5 verification recorded in the objective:
- End-to-end: process_trade_phase forms+persists a real StrategicSwap → projected into
  view_json (steps 2-5 chain proven).
- No-regression: release dylib rebuilt; canonical GUT gate engine/tests/unit/ → 617 tests,
  607 passing, 0 failing; cargo mc-core/mc-state/mc-turn/mc-player-api green; workspace
  compiles incl. api-gdext dylib.
- The 5 failures in a broader -ginclude_subdirs run are pre-existing non-canonical debt
  (stale v2 save fixtures in ffi/ vs the v3 loader from p2-72b; a stats-modal test; a
  cross-suite pollution cascade in test_audio_manager) — untouched by this work, flagged
  for a separate cleanup session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 02:26:53 -04:00
Natalie
7e93dcef42 test(@projects/@magic-civilization): p3-25 — end-to-end headless trade pipeline proof
process_trade_phase_forms_and_persists_strategic_swap: a crafted 2-player state with
complementary owned-tile strategics (p0 rainforest→hardwood, p1 mountains→iron_ore,
biomes guarantee neither has the other's) → process_trade_phase forms a StrategicSwap,
persists it to state.trade_ledger, and fans it onto both players' traded_strategics.

Proves the full chain end-to-end: owned-tile territory (step 2) → resource-category
classification (step 3) → real sourcing + evaluate_trades + persistence (step 4) →
which DiplomacyView.trade_deals then projects (step 5, separately tested). mc-turn green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 02:09:20 -04:00
Natalie
507a87104f feat(@projects/@magic-civilization): 👁️ p3-25 step 5 — project real trade deals into view_json (DiplomacyView.trade_deals)
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>
2026-06-26 02:04:57 -04:00
Natalie
1a7fd849c0 feat(@projects/@magic-civilization): 💱 p3-25 step 4 — real trade sourcing + ledger persistence in the headless turn
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>
2026-06-26 01:57:32 -04:00
Natalie
e376920766 feat(@projects/@magic-civilization): 📇 p3-25 step 3 — resource-category catalog into Rust state
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>
2026-06-26 01:37:59 -04:00
Natalie
37fbb6153d feat(@projects/@magic-civilization): 🗺️ p3-25 step 2 — real city territory in the bench sim (culture border expansion) + projected
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>
2026-06-26 01:18:24 -04:00
Natalie
922c18fb0c feat(@projects/@magic-civilization): 🛤️ p3-25 step 1 — projection reads real agreement state (de-stub DiplomacyView)
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>
2026-06-26 00:43:46 -04:00
Natalie
e6b7c9b2ce feat(@projects/@magic-civilization): p3-23 DONE — trade richness complete; deal-UI screenshot-proven
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>
2026-06-26 00:07:43 -04:00
Natalie
99e0a4447f feat(@projects/@magic-civilization): 🤝 p3-23 revival step 5 — deal UI: active trade deals in the diplomacy panel
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>
2026-06-25 23:38:21 -04:00
Natalie
916dcda55d test(@projects/@magic-civilization): p3-23 revival step 4 — verify trade tile-sourcing; full pipeline end-to-end proven
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>
2026-06-25 23:08:26 -04:00
Natalie
f7959e11a0 feat(@projects/@magic-civilization): ⚔️ p3-23 revival step 3 — unit-gating honors traded strategics
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>
2026-06-25 22:37:05 -04:00
Natalie
32dd6d2723 feat(@projects/@magic-civilization): 🔌 p3-23 revival step 2 — enable Diplomacy.process_turn in the turn loop (loop-survival proven)
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>
2026-06-25 22:08:37 -04:00
Natalie
e926345ad2 feat(@projects/@magic-civilization): 🔧 p3-23 revival step 1 — reconcile diplomacy↔process_trades contract (safe, isolation-proven)
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>
2026-06-25 21:53:35 -04:00
Natalie
de983fac54 docs(@projects/@magic-civilization): 🔎 p3-23 — discovery: inter-player trade turn-integration is DISABLED in-game
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>
2026-06-25 20:41:24 -04:00
Natalie
a8c01cb5e1 feat(@projects/@magic-civilization): 💰 p3-23 part A — inter-player gold sales hit the treasury in-game
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>
2026-06-25 20:12:40 -04:00
Natalie
e00c0477ab docs(@projects/@magic-civilization): p3-24 phase 3 — climate HP-loss verified Rust-owned (bullet 3 done)
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>
2026-06-25 19:39:51 -04:00
Natalie
417c8d195b refactor(@projects/@magic-civilization): 🏛️ p3-24 phase 2 — port happiness aggregation GDScript→Rust (Rail-1)
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>
2026-06-25 19:15:35 -04:00
Natalie
a330704fe4 refactor(@projects/@magic-civilization): 🏛️ p3-24 phase 1 — port gold aggregation GDScript→Rust (Rail-1)
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>
2026-06-25 18:48:57 -04:00
Natalie
d45ba32a3a feat(@projects/@magic-civilization): 💰 p3-23 (part 2) — gold-for-resource sales in mc-trade
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>
2026-06-25 18:18:25 -04:00
Natalie
4a97ab1d02 feat(@projects/@magic-civilization): 💱 p3-23 (part 1) — strategic-resource swaps in mc-trade
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>
2026-06-25 17:50:45 -04:00
Natalie
9555f934a2 feat(@projects/@magic-civilization): 🦬 p3-21 DONE — drought drives fauna migration
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>
2026-06-25 17:19:16 -04:00
Natalie
e77149f8fb feat(@projects/@magic-civilization): 🌫️ p3-20 DONE — weather reduces scouting in the rendered game
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>
2026-06-25 16:45:04 -04:00
Natalie
c30b74523b feat(@projects/@magic-civilization): 🌫️ p3-20 (consumer) — compute_vision shrinks sight under weather
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>
2026-06-25 16:11:16 -04:00
Natalie
923af9c0ec feat(@projects/@magic-civilization): 🌫️ p3-20 (core) — WeatherEvent.vision_penalty (weather-producer side)
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>
2026-06-25 15:45:10 -04:00
Natalie
bb38d5db0e feat(@projects/@magic-civilization): 🌲 p3-19 DONE — flora half: deforestation depletes live flora populations
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>
2026-06-25 15:16:18 -04:00
Natalie
d6eaa79838 feat(@projects/@magic-civilization): 🦌 p3-19 (fauna) — over-hunting depletes live populations → local extinction
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>
2026-06-25 14:47:23 -04:00
Natalie
eb2cf18c2d feat(@projects/@magic-civilization): 🦌 p3-19 (core) — ecology population depletion API for player pressure
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>
2026-06-25 14:06:31 -04:00
Natalie
d01d72082d feat(@projects/@magic-civilization): 🔭 p3-22 — AI builds dedicated scouts for exploration
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>
2026-06-25 13:57:20 -04:00
Natalie
ed3c836e2f docs(@projects/@magic-civilization): 📋 reopen Game-1 scope — 6 verified gap objectives (p3-19..24)
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>
2026-06-25 13:45:20 -04:00
Natalie
58b608804a docs(@projects/@magic-civilization): 📊 regenerate objectives dashboard — p3-18 done (290→291)
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>
2026-06-25 07:29:08 -04:00
Natalie
e67c3d7265 docs(@projects/@magic-civilization): p3-18 DONE — water crossing complete (embark + transport, sim + UI, verified)
All core acceptance bullets ✓: embark (pathfinding gate, data-driven config,
auto-embark + combat penalty, AI water-pathing, single-source after deleting the
Civ-V model), transport (board/carry/disembark + cargo-loss), UI embark (Rust
embark_level #[func] + pathfinder.gd, GUT 745/0), and the deterministic end-to-end
conquest path (ford + cross-water attack capstone). Verified across cargo /
integration / GUT. Two explicitly-optional, non-blocking remainders documented
(full game_over demo; transport carried-unit target-protection).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 07:28:05 -04:00
Natalie
22d0379a2c test(@projects/@magic-civilization): p3-18 P6 — capstone: army fords water then attacks enemy on the far landmass
teched_army_fords_water_then_attacks_enemy_on_far_landmass: end-to-end proof of
the conquest payoff that motivated p3-18. Over an ocean-wall map, a player-0 army
with ocean_navigation embark fords the ocean column to landmass B (process_move_requests),
then strikes a player-1 unit waiting there (queued AttackRequest → process_pvp_combat):
a cross-water battle resolves and the enemy takes damage. Embark turns an
otherwise-unreachable rival on another landmass into an attackable one.

Deterministic, cargo-verifiable; complements P6-core (the ford proof). The full
headless 1v1-to-game_over demo is the only remaining (confirmatory) P6 item.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 07:26:11 -04:00
Natalie
1f3f535e10 refactor(@projects/@magic-civilization): 🧹 p3-18 P5b — trim vestigial embark inputs from the action FFI
Completes single-source on the action bridge. With auto-embark (no explicit
Embark action), the embark/adjacency inputs to legal_actions_for / can_invoke were
dead:
- api-gdext: drop is_embarked / adjacent_water / adjacent_land params from both
  #[func]s (+ the consume lines + doc).
- GDScript callers updated to the new arity: unit.gd (get_legal_actions +
  can_invoke_action), unit_panel.gd (legal_actions_for + its dead arg vars and the
  three dead _get_* helpers), test_unit_actions.gd (4 calls).
- unit.gd: remove the now-dead has_adjacent_water/has_adjacent_land fields (never
  set — their _refresh_unit_terrain_context setter is long gone) + their serialize/
  deserialize; test_unit_serialize.gd updated. is_embarked stays (real state).

Verified: cargo check green; dylib rebuilt + deployed; canonical GUT suite 745
tests / 732 passing / 0 failing (13 pending). No arg-mismatch errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 07:19:26 -04:00
Natalie
c141a86b77 docs(@projects/@magic-civilization): 📝 p3-18 — P5b done (UI embark, GUT-verified); feature complete in sim + UI
Embark + transport now work in both the headless sim (authoritative) and the
rendered UI, verified at unit / integration / GUT levels (732 GUT passing).
Remaining: P6-full headless 1v1-to-game_over demo (confirmatory over P6-core),
vestigial-FFI trim (cosmetic), transport refinements (deferred, documented).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 07:03:10 -04:00
Natalie
b02274f920 test(@projects/@magic-civilization): p3-18 P5b — GUT proof the rendered pathfinder embark gate works
test_pathfinder_embark.gd: over an ocean-column map, a land unit finds no path
without embark (embark_level 0), fords the column with embark (level > 0) and
reaches the far landmass; naval movement is unaffected by the embark arg.

Verified headless against the rebuilt dylib: GUT 732 passing / 0 failed (+3 from
729 — these tests; no regression). UI embark now matches the authoritative
headless sim.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 07:02:03 -04:00
Natalie
7d40c2ce86 feat(@projects/@magic-civilization): p3-18 P5b(2) — rendered movement is embark-aware (UI)
Wires the rendered game's GDScript movement to the data-driven embark capability,
so a human playing the UI can cross water when teched (previously embark worked
headless only):
- pathfinder.gd: find_path / movement_range / find_path_with_fog gain a defaulted
  embark_level param (non-breaking for all existing callers); _is_passable lets a
  land unit enter water when embark_level > 0. Single-tier UI gate — the precise
  coast-vs-ocean tier stays authoritative in the Rust sim; the gate avoids
  hardcoding the biome→tier map in GDScript (Rail 1).
- KnowledgeWeb + TechWeb: embark_level(researched) passthroughs to the Rust
  GdTechWeb.embark_level (the tech→level mapping stays in Rust, data-driven).
- world_map.gd: _current_embark_level() (defensive) feeds the active player's
  level into all four pathfinder call sites (move, range overlay, fog previews).

gdlint clean. Verifies via dylib rebuild + GUT (next).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 06:54:42 -04:00
Natalie
04a6120fe5 feat(@projects/@magic-civilization): p3-18 P5b(1) — GdTechWeb.embark_level #[func] (single-source UI embark)
The rendered game moves via GDScript pathfinder.gd (GDScript-authoritative; no
Rust round-trip) over GDScript-canonical tech. To make UI movement embark-aware
WITHOUT re-deriving the tech→level mapping in GDScript (Rail 1: logic stays in
Rust), GdTechWeb gains embark_level(researched: PackedStringArray) -> i64 (0 none
/ 1 coast / 2 ocean) backed by the data-driven TechWeb::embark_level. The GDScript
pathfinder will call this and carry only a thin water-passability gate.

cargo check -p magic-civ-physics-gdext green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 06:43:19 -04:00
Natalie
307219e039 docs(@projects/@magic-civilization): 📝 p3-18 — P5a done, P6-core done; scope P5b (rendered-movement embark, single-source via bridge)
P5a (dead GDScript embark UI) and P6-core (ford integration proof) landed. P5b
(rendered UI embark) scoped: the rendered state is hybrid (GDScript GameState/Player
+ Rust GdGameState bridge); the single-source path is a GdGameState #[func]
exposing the Rust-computed EmbarkLevel, threaded into pathfinder.gd — dylib-gated
(build-gdext.sh works) + GUT. P6-full demo is confirmatory over the integration proof.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 06:36:19 -04:00
Natalie
d3b5b12612 refactor(@projects/@magic-civilization): 🧹 p3-18 P5a — remove dead GDScript explicit-embark UI (single source)
P3b deleted the Civ-V explicit Embark/Disembark actions on the Rust side, leaving
the GDScript UI plumbing dangling (it dispatched the now-removed "embark"/
"disembark" actions; gated on the amphibious keyword no unit carries, so it never
rendered). Removed it: unit_panel.gd embark_pressed/disembark_pressed signals,
their ACTION_SIGNAL_MAP entries + emit cases; world_map.gd signal connections +
_on_embark/_on_disembark handlers. No dangling refs remain; no GUT test referenced
them. Auto-embark (move onto water) remains the only embark model, now consistent
across Rust and the GDScript UI.

Remaining P5: the rendered game's movement (pathfinder.gd, GDScript logic) is not
yet embark-aware — entangled with the broader Rail-1 debt that rendered movement
runs GDScript rather than the Rust pathfinder. Tracked in the objective.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 06:33:20 -04:00
Natalie
11988d60a6 test(@projects/@magic-civilization): p3-18 P6-core — end-to-end proof an army fords open ocean to a far landmass
The scenario that motivated p3-18: an army on landmass A reaching landmass B
across an ocean strip. teched_army_fords_open_ocean_to_far_landmass asserts the
full chain through process_move_requests — without naval tech the ocean rejects
the move (army stays put); with ocean_navigation (Ocean embark) the army crosses
the strip onto the far landmass and auto-disembarks on arrival.

Deterministic, cargo-verifiable evidence (the project's preferred proof form), no
dylib/Godot needed. The full headless 1v1-to-game_over demo (P6-full) is the
heavier dylib follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 06:26:01 -04:00
Natalie
5aefbff8a2 docs(@projects/@magic-civilization): 📝 p3-18 — correct P5/P6 blocker: gdext builds locally (crate is magic-civ-physics-gdext)
The earlier "cargo resolver panic blocks the gdext dylib" claim was wrong: I was
running `cargo -p gdextension-api`, which is a crates.io godot dependency, not our
crate. The local crate is `magic-civ-physics-gdext`; `cargo check -p
magic-civ-physics-gdext` is green in ~38s. P5 (UI embark) and P6 (demo) are
feasible locally via build-gdext.sh + GUT — not build-host-gated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 06:23:38 -04:00