Commit graph

3839 commits

Author SHA1 Message Date
Natalie
e24c1a03d2 feat(turn): consume promotion picks + inject promotion modifiers into combat
Add consume_pending_promotions phase to the end-turn step (after combat XP):
validates each unit's pending_promotion against its applied promotions
(requires-chain, no-dupes, existence), records it, folds hp_bonus into max_hp,
applies heal_on_promote (clamped), clears the pick. Illegal picks are dropped.

Inject per-unit promotion combat modifiers at both PvP combat sites, mirroring
equip_combat_bonus: attacker offence keys off its tile biome + defender flags,
defender defence off its own tile + whether the attack is ranged. Percentages
fold into flat atk/def/ranged against base stats. Projection now gauges
promotion_available against the real level (promotions.len()).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 15:46:54 -04:00
Natalie
66cf5b7e45 feat(combat): promotion effect registry + per-unit combat modifiers
Add MapUnit.promotions (applied promo ids; len = promotion level) and a
mc-combat registry parsed from the embedded promotions.json trees. Replace
the stale PromotionEffect {stat,value} model with {type,value,condition}
matching the JSON, and add promotion_combat_modifiers(applied, ctx) — a pure
aggregator that evaluates effect conditions (open/rough terrain, vs_ranged,
vs_city, vs_fortified, in_city) against a combat context and sums atk/def/
ranged/range/hp/movement/wall/xp modifiers. validate_promotion_pick enforces
existence, no-dupes, and requires-chains.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 15:40:26 -04:00
Natalie
1a4588279e docs(agents): document dist:image (incremental rebuild) + dist:prune in cloud-dx-do
Adds the two new verbs to the table and rewrites the iteration section as a
cost-tiered ladder (dist:sync seconds / dist:image ~8min / --cold ~20min) so agents
reach for the incremental rebuild, not a cold packer build per change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 15:14:24 -04:00
Natalie
82dcffce4d docs(objective): p3-25 Phase-0 culture_stored landed; clarify unit-XP deferral
Mark CityView.culture_stored done (04763a387) — the last clean Phase-0 projection
gap. Note UnitView.experience already projects bench MapUnit XP; the deferred item
is the live-unit XP store (Phase 1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 15:08:15 -04:00
Natalie
04763a3870 feat(view): project CityView.culture_stored from the per-city CulturePool (Rail-1 Phase 0)
The live city_screen reads a per-city culture meter off the GDScript CityScript;
view_json had no equivalent, so the UI-pure-view migration couldn't render it from
getState(). project_cities now surfaces culture_stored from
PlayerState.culture_pool.city(c_idx) (the same accumulator mc-turn::process_culture
ticks for border expansion); 0.0 for a city with no pool entry.

Closes the last genuine Phase-0 projection gap (UnitView equipped/experience/movement/
posture + ResourceView golden_age were already projected — design-doc table was stale).

Test: projection_surfaces_city_culture_stored. mc-player-api lib 142/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 15:05:34 -04:00
Natalie
0c50c04b4c feat(infra): dist:prune to delete superseded golden snapshots
Incremental rebuilds accumulate snapshots (~$0.40/mo each). dist:prune keeps
the newest N (default 2: current + one rollback); dist:image reminds you to run it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 14:51:06 -04:00
Natalie
d9588f8c80 perf(infra): incremental golden-image rebuilds (layer on the last snapshot)
Packer base image is now a var; ./run dist:image builds FROM the newest
mc-golden snapshot by default, so the idempotent provision.sh only redoes changed
work (~3-8 min vs ~20 cold). --cold rebuilds from stock Ubuntu to reset layer
cruft. Made the clone step idempotent (clone-or-fetch) so it works on a
pre-provisioned base. Directly addresses 'avoid unnecessary rebuilds'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 14:41:01 -04:00
Natalie
68099051b8 docs(agents): add 'avoid per-fix image rebuilds' iteration discipline to cloud-dx-do
Validate provision.sh on a live box first; packer -on-error=ask; batch fixes;
check size/forge prereqs before building; code via dist:sync (image rebuild only
for toolchain/accelerator changes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 14:37:07 -04:00
Natalie
d4c4a5aa1b fix(test): add missing fields to stale struct literals across workspace tests
Four E0063 compile errors broke `cargo test --workspace --no-run`, blocking
`./run dist:test` on the DO fleet. Each is a stale struct literal in test/test-cfg
code that drifted from its current definition:

- mc-worldsim event_dispatch low_bio_thresholds: BiologicalThresholds missing
  migration_drought_factor / migration_drought_max (p3-21 drought coupling) —
  set to 0.0 / 1.0 to keep the helper's migration-suppression intent.
- mc-mod-host wasm_controller_{noop,limits}: TacticalState missing embark_level —
  Default::default() (EmbarkLevel::None) to match the empty-state intent.
- api-gdext ai.rs tile_with + ai_controller test: TacticalTile missing explored /
  TacticalState missing embark_level — explored:true (pre-field default = seen),
  embark_level default.

Mirrors the sibling fix 04fabbc1c. `cargo test --workspace --no-run` now compiles
clean; full suite passes except 3 pre-existing GPU-parity tests (Metal fp drift,
unrelated to these changes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 14:35:44 -04:00
Natalie
153f430c48 perf(infra): bake mold linker + sccache into the golden image
Worker-only ~/.cargo/config.toml (Linux, never plum's macOS): mold via
-fuse-ld=mold (fast cdylib linking) + sccache rustc-wrapper (compiled-crate cache,
warm in the baked image). mold installs via apt or GitHub static-binary fallback;
both gated on install success so a miss never breaks cargo. Verified mold=true
sccache=true in the build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 14:07:36 -04:00
Natalie
e9e8a8220c docs(agents): teach specialists the DigitalOcean fleet is the RUN host
New cloud-dx-do.md (dist:*/forge:* verbs, setup state, gotchas: size tier,
exfil autoMode gate, always dist:down, linux-only .so). Wired into the CLAUDE.md
router, specialist-preamble (all specialists), canonical-commands banner, and the
instructions README index/tree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 13:55:03 -04:00
Natalie
04fabbc1c2 fix(test): add is_ranged field to stale AttackRequest in pvp_combat_determinism
The RangedAttack dispatch work (e8dd4a85b) added `is_ranged` to
`AttackRequest`, but the mc-golden-tests pvp_combat_determinism test still
constructed the struct without it, breaking `cargo test --workspace` (and
the cloud fleet) with E0063. Set `false` for this melee-combat test,
matching the mc-turn PvP tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 12:49:19 -04:00
Natalie
6332d47011 fix(infra): make the DO fleet actually work on real hardware + render host
Real-DO testing surfaced bugs the mocked tests couldn't:
- ssh key: reference shared 'mc-fleet' key via data source, not a duplicate (DO 422s on dup pubkeys).
- cmd_dist_up: fail loudly on failed apply; dist:up waits for cloud-init readiness.
- snapshot cloud-init skips runcmd -> bake authorized_keys (FLEET_PUBKEY) + 'cloud-init clean' before snapshot.
- build user passwordless sudo; apt dpkg-lock race fixed (cloud-init --wait + Lock::Timeout).
- size s-8vcpu-16gb-amd (tier max); creds via PKR_VAR env not argv.
- render host: weston+Mesa baked; ./run dist:render proven (Godot->PNG on DO, no GPU). forge:dns shortcut.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 12:45:29 -04:00
Natalie
a5d66ce477 feat(infra): make DO workers render-capable (weston + Mesa) + dist:render
Golden image now installs the software-render stack (weston, libgl1-mesa-dri
llvmpipe, mesa-vulkan-drivers, vulkan-tools) so any worker renders proof scenes
via gl_compatibility/opengl3 with no GPU. New ./run dist:render <scene> <out.png>
wraps tools/capture-proof.sh against a worker (replaces the apricot SCREENSHOT_HOST).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 09:56:56 -04:00
Natalie
9ee33f49ed chore(@projects/@magic-civilization): 📇 regen objectives dashboard (timestamp)
Auto-regenerated objectives.json; totals unchanged (298 done).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 09:47:49 -04:00
Natalie
655d25e2c1 docs(@projects/@magic-civilization): 🛡️ Rail-2 — document the two-path content divergence + track an enforcement gate
rust-source-of-truth.md: add the "two-path divergence" rule to the canonical
content store section. Content reaches the sim two ways — in-game (GDScript
DataLoader reads JSON at runtime, projection.rs:41) and headless (Rust falls back
to a compile-time include_str!/hardcode copy, dispatch.rs:410). A balance constant
hardcoded in a crate is both a Rail-2 violation and a silent second copy that
drifts from the JSON — and headless is where the AI trains. Rule: grep
public/resources + public/games/**/data for a JSON home before adding a numeric
balance const; if it exists, LOAD it (OnceLock+include_str!, never std::fs in
shared sim code). References the p3-28 ContentRegistry endgame.

p3-28: add the matching "Rail-2 verify gate (enforcement)" acceptance bullet —
tools/check-no-rust-hardcoded-content.py + a verify step to catch the next
hardcode, best landed alongside the ContentRegistry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 09:47:43 -04:00
Natalie
24c0e0c24c test(@projects/@magic-civilization): 🛤️ Rail-1 Phase-1 — end-to-end live-unit-store loop test
Proves the spawn → command → view contract the GdGameState bridge exposes for
the render-gated live flip, at the mc_player_api layer its shims call: a MapUnit
pushed onto inner (as spawn_unit_into_inner produces) appears in project_view;
a Fortify via apply_action is reflected in the next view; a command on a stale
unit id is a typed error, not a panic. Existing integration tests load pre-built
states — none exercised the spawn-then-act-then-view triple a freshly-spawned
live unit goes through. De-risks the foundation before the GDScript flip depends
on it. mc-player-api 3/3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 09:44:39 -04:00
Natalie
b4c402e766 docs(@projects/@magic-civilization): p3-26 Gap 3 DONE (equipment/crafting verified headless) + Gap 4 scope assessment
Gap 3 — Equipment/crafting: verified the full craft→equip→combat path runs
headless and Rust-authoritative (orig bullet was stale at [ ]):
  - PlayerAction::CraftEquipment → craft_equipment dispatch (materials gate +
    consume strategic_ledger + equip), 2 tests
  - recipe_phase ("recipe_refine") in END_OF_TURN_PHASES — passive crafting
    economy refines raw→quality-tiered product every self-play turn, 1 test
  - equip_combat_bonus reads boot-loaded item_combat at every combat site, 2 tests
  - boot path: set_item_combat_json FFI ← headless harness _apply_item_combat
  - MCTS AI not electing to craft = deliberate 9-kind GPU-rollout constraint,
    not a missing system
  Verified green: mc-turn + mc-player-api 557/0.

Gap 4 — Per-building queues: recorded verified assessment. Bench single-slot +
per-turn AI reselection is functionally equivalent to a FIFO build queue for the
self-play SIMULATION outcome; the multi-item queue is a live-game UI affordance
belonging to the p3-25/p3-29 projection arc. Owner scope call pending: does p3-26
require simulating a multi-item queue, or reclassify Gap 4 out of the headless bar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 09:44:11 -04:00
Natalie
22f7fa1116 feat(infra): DO compute-offload verbs + forge on/off lifecycle
Offload heavy compute from plum (M2 Air) to on-demand DO workers:
- dist:test  — cargo test --workspace (nextest) on a worker (the main DX win)
- dist:build — cargo build + WASM on a worker; rsync the platform-independent
  WASM back (native .so is linux-only, stays on the worker)
- dist:sync  — git pull <ref> + rebuild gdext on live workers (no image rebuild)
- forge:down/up — snapshot+destroy / restore-from-snapshot (DO bills powered-off
  droplets; only destroy stops it). ~$6/mo -> ~$0.30/mo idle; refreshes the
  forge IP in ~/.vault/mc_forge_creds on restore.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 09:24:30 -04:00
Natalie
e8dd4a85b4 feat(@projects/@magic-civilization): 🛤️ Rail-1 Phase-1 — RangedAttack dispatch (completes unit input for the live store)
The live unit store (GdGameState.apply_action_json → inner) handled melee but
RangedAttack was NotYetImplemented. Wire it by reusing the melee resolver:
split resolve_single_pvp_attack into resolve_single_pvp_attack_typed(.., is_ranged);
ranged sets CombatType::Ranged → sources ranged_attack/range from units_catalog
and the resolver's prevents_retaliation(combat_is_ranged=true) suppresses the
counter-attack. Did NOT reuse the crude pending_volley AoE (separate Volley
action); verified live parity is immediate-resolve (combat_resolver.gd:87-104),
so a direct resolve mirroring melee is correct.

- AttackRequest gains is_ranged (serde-default); process_pvp_combat threads it.
- dispatch apply_ranged_attack: owner + enemy + within-range gate, then resolve.
- tests: ranged_pvp_no_retaliation (resolver: damage, attacker untouched, 0
  retaliation), ranged_attack_no_retaliation (dispatch: range gate + rejections).
Deferred (parity, cited): no movement-spend on attack — melee doesn't spend it
either; a "ranged is in-scope; verify gate mc-combat+mc-turn+mc-player-api 0 failed.

Dispatched combat-dev; verify gate green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 09:21:36 -04:00
Natalie
b689f52ccc feat(@projects/@magic-civilization): 🛤️ Rail-1 Phase-1 — GdGameState act/view/spawn bridge (live unit store foundation)
Gives the live GdGameState the same Rust-driven surface the headless GdPlayerApi
has, on its own inner GameState, so inner.players[].units (rich MapUnit) can
become the live unit store:
- apply_action_json(player, action_json) → mc_player_api::apply_action(&mut inner)
- inner_view_json(player) → mc_player_api::project_view(&inner)
- spawn_unit_into_inner(player, unit_type_id, col, row) → MapUnit::new + push,
  monotonic next_unit_id (same idiom as the AI-faction spawn).
Thin shims over the SAME mc_player_api fns GdPlayerApi calls (no dispatch/
projection duplication; 4 envelope helpers made pub(crate) for reuse). No
GDScript touched; GdPlayerApi + bench path untouched.

Contract for the later (render-gated) live caller: stamp inner.units_catalog
(+ action configs) via the existing set_*_catalog_json setters before relying on
the view — documented inline (lib.rs:4297). cdylib links with all 3 #[func]s
registered (distinct symbols from GdPlayerApi); mc-player-api 0 failed.

Dispatched simulator-infra; verify gate green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 08:57:17 -04:00
Natalie
f5c5d1a410 feat(infra): distributed test/train fleet on DigitalOcean (Terraform + Packer + dispatch)
Ephemeral CPU Droplet fleet that horizontally scales the iteration loop:
- infra/terraform/test-fleet: cattle Droplets from a golden image (auto-discovered
  by name via digitalocean_images), grouped under the mc:dev DO project, with a
  mocked-provider test suite (no token/spend).
- infra/packer: golden-image builder reusing scripts/dev-setup/linux.sh.
- scripts/run/dist.sh: ./run dist:{check,up,sim,train,down} — shard sim/test
  batches across workers via autoplay-batch AUTOPLAY_HOST+SEED_OFFSET.
GPU intentionally absent (workload is CPU-bound per docs/ai-production.md).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 08:51:09 -04:00
Natalie
bd186b162a feat(@projects/@magic-civilization): 🛤️ Rail-1 Phase-1 — bench unit XP/veterancy in the Rust turn
Units gained no XP in the headless/bench turn (only GDScript UnitScript tracked
it). The XP amounts were already Rust-authoritative (mc-combat: BASE_COMBAT_XP=5
× xp_from_combat strength scaling; resolver zeroes dead-defender XP / suppresses
capture XP). This wires the award into the bench turn so the unified game has
veterancy:
- MapUnit.experience: i32 (#[serde(default)]; all 110 literals use ..default()).
- resolve_single_pvp_attack accumulates attacker_xp/defender_xp onto survivors,
  survival-gated exactly like combat_resolver.gd:215-223.
- project_units surfaces UnitView.experience + promotion_available from XP
  threshold eligibility (mc_combat::check_promotion), replacing the 0 stub.
- new test pvp_combat_awards_xp_to_survivors (queued-attack path, no kills →
  both survivors gain XP).

Deferred (cited, out of scope): the veteran_level/promotion stat-growth pick
subsystem (bench uses flat UnitStats, not the D20 path) and the pre-existing
Rust↔JSON promotion-threshold divergence (promotions.json [15,30,45,60] vs Rust
[10,30,60,100]) — a Rail-2 content/code gap tracked separately.

Dispatched combat-dev; verify gate: mc-combat+mc-turn+mc-player-api 0 failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 08:48:35 -04:00
Natalie
081cddcab3 docs(@projects/@magic-civilization): 🛤️ Rail-1 design — narrow the dual-model fork (cities ~done, units are the hold-out)
Verified the live city-projection path: api-gdext/city_slot.rs is a full ops
module over presentation_cities (rich mc_city::City) + GdCity wraps it; CityScript
is a hybrid proxy already routing to the Rust slot. So cities are largely
Rust-authoritative — the GDScript residue is just the city-centre queue +
placed_buildings. UNITS are the real Phase-1 hold-out (UnitScript fully
GDScript-authoritative, no Rust slot). Rails: bench CityState/MapUnit and the live
game are sanctioned-separate contexts (code-layering #3), so the live view must
project the RICH city / a new live-unit store — NOT the bench types. Refines the
Phase-1 plan accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 08:29:44 -04:00
Natalie
8c3e7b8a27 feat(@projects/@magic-civilization): 🛤️ Rail-1 Phase-0 — project equipped items to UnitView
EquippedItemView (item_id, category, charges_remaining, triggers_in_combat —
the exact mc_items::EquippedItem fields, cited) + UnitView.equipped, projected
from MapUnit.equipped for OWN units only, omitted from the wire when empty.
Surfaces the unit_panel.gd:789 entity read via view_json.

happiness_breakdown DEFERRED (verified, not fabricated): the per-contributor
breakdown is a transient calculate_happiness return (mc-happiness/pool.rs:170),
not persisted PlayerState — only the scalar happiness pool is stored
(game_state.rs:1295), already surfaced as ResourceView.happiness_pool. A
Phase-1 SOT-flip widening, like XP/culture_stored.

Dispatched simulator-infra; verify gate: mc-player-api green incl. new equipped
round-trip/omit test. Additive (serde defaults).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 08:19:07 -04:00
Natalie
76b3e48ae3 docs(@projects/@magic-civilization): 🛤️ p3-25 — record Phase-0 projection increments + blueprint link
Re-scope p3-25's view-completeness as Phase 0 of the owner's full UI-pure-view
migration (blueprint: designs/p3-rail1-ui-pure-view-migration-design.md). Log
the two landed increments (unit movement+posture 568e43084, golden age 0d501a3d7)
and the deferred bench-model gaps (XP, culture_stored, building_queues,
placed_buildings → Phase-1 SOT flip).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 08:03:51 -04:00
Natalie
0d501a3d72 feat(@projects/@magic-civilization): 🛤️ Rail-1 Phase-0 — project Golden Age state to the HUD
ResourceView gains golden_age_active + golden_age_turns, projected from the
PlayerState GA fields. These are the top_bar.gd:162/169 entity reads now
available via view_json (HUD badge driven by Rust, not the Player entity).
Omitted from the wire when inactive. Per-city culture_stored (city_screen.gd:287)
is DEFERRED: it has no bench CityState backing (culture is a player-level pool in
the reduced model) — a Phase-1 SOT-flip widening, not fabricated here.

Additive (serde defaults). mc-player-api 140/0 incl. a GA round-trip/omission test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 08:03:07 -04:00
Natalie
568e43084b feat(@projects/@magic-civilization): 🛤️ Rail-1 Phase-0 — project real unit movement + tactical posture
First projection-completeness increment toward UI-driven-by-Rust. project_units
stubbed movement_left/max=0, sentry=false, promotion_available=false despite the
bench MapUnit carrying the real data — fix to read movement_remaining/base_moves/
is_sentrying/pending_promotion (same fields the move dispatch + legal-move gate
use). Add UnitPostureView (embarked/deployed/stealthed/ambushing/field_aura/
fire_arrows/pursuing/shield_wall/braced/rage/war_cry) + formation_id, projected
for OWN units only (no stealth/ambush leak); omitted from the wire when resting.
These are the unit_panel.gd entity reads (audit Group B) now available via
view_json. XP stays 0 (not yet on MapUnit — Phase-1 SOT-flip gap, noted).

Additive (serde defaults; no other UnitView constructors in the workspace).
mc-player-api 139/0 incl. 2 new posture round-trip/omission tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 08:00:41 -04:00
Natalie
d78152388a docs(@projects/@magic-civilization): 🛤️ Rail-1 endgame — UI-pure-view migration blueprint
Owner directive: remove all sim logic from GDScript; UI becomes a pure view of
the Rust game server. Phased design grounded in a verified 3-surface audit +
view.rs/projection.rs/game_state.rs/turn_*.gd ground-truth. Key finding: the
live game holds TWO unsynced authoritative stores (GDScript entities = live SOT;
Rust GdGameState = parallel copy), so this is a spine rewrite — the live game
must converge onto the headless GdPlayerApi shape. Phases: 0 projection
completeness (headless, first), 1 SOT flip, 2 live turn = end_turn(), 3 delete
GDScript sim layer, 4 render-proof.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 07:51:19 -04:00
Natalie
edc7e31b12 docs(@projects/@magic-civilization): 📊 regen objectives dashboard (p3-27 flora, p3-30 bridge)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 07:36:07 -04:00
Natalie
acf57fd05f docs(@projects/@magic-civilization): 🐺 p3-30 — record owner ruling (bridge) + bridge landed
Owner ruled the GdWildAiController bridge over an in-step substrate. Mark
acceptance #2 [~] (Rust bridge done 8696a48aa; GDScript rewire render-gated)
and spell out the remaining live-game rewire steps + the render/live-run host
needed for the parity proof.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 07:35:17 -04:00
Natalie
8696a48aa0 feat(@projects/@magic-civilization): 🐺 p3-30 — GdWildAiController bridge (owner-chosen drive path)
Owner chose the bridge over a headless wild-unit substrate. Adds the
JSON contract + a pure mc_ai::wild::decide_wild_actions_json(json, seed)
helper (parses a WildContextDto — wilds, player_units, lairs, cities, config,
passable set — runs the decision core, returns per-action JSON strings, the
GdAiController envelope), and a thin GdWildAiController GDExtension shim
(set_rng_seed + decide_actions → PackedStringArray) over it.

The live game keeps its roaming owner==-1 units; GDScript projects them into
the DTO and dispatches the returned move/attack Actions via the existing
AI-action path — so the wild DECISION logic is fully Rust (Rail-1), no
duplicated headless model. 16 wild tests (4 new JSON-bridge: chase/attack/
passable-roam/malformed), mc-ai lib 305/0; gdext cdylib links with the class
registered. Remaining (render-gated): GDScript rewire of _process_wild_creatures
+ wild_creature_ai.gd deletion + render-proof.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 07:34:02 -04:00
Natalie
ca31834db0 docs(@projects/@magic-civilization): 🌿 p3-27 — flora succession confirmed subsumed by process_step
Close the flora-succession [~] bullet. Verified in engine code (not the
comment): process_step → run_tier_advancement advances tiers in-place
(par_iter_mut → tick_tiers_capped mutates slots) and returns FloraTransitions
only as a chronicle report. The headless ecology_phase applies succession via
process_step and buffers transitions for the p3-29 FloraSuccession event — no
separate mc-flora::FloraEngine pass needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 06:54:35 -04:00
Natalie
e477784731 docs(@projects/@magic-civilization): 🐺 p3-30 — decision core done; integration is a verified fork
stub → partial. Acceptance #1 (decide_wild_actions) + #4 (determinism) ✓ with
cited evidence (95a2e580b, mc-ai 301/0). Record the verified premise that the
headless GameState has no roaming wild-unit substrate (units implicit-owned,
wilds = lairs + encounters), making the in-step path a substrate-build that
duplicates encounters vs. the allowed GdWildAiController bridge. Recommend the
bridge; integration + .gd deletion stay render/decision-gated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 06:53:39 -04:00
Natalie
95a2e580bc feat(@projects/@magic-civilization): 🐺 p3-30 — Rust wild-creature decision AI core
Port wild_creature_ai.gd's decision logic to a pure, deterministic Rust
module (mc-ai::wild). decide_wild_actions(ctx, rng) -> Vec<Action> mirrors
process_wild_turn → _act: chase+attack a player unit in detection range,
drive home when leashed out, drift toward the nearest city, else roam a
leashed neighbour. One action per creature (the player-tactical convention:
attack iff adjacent, else move). Reuses mc_core hex helpers + XorShift64 +
the existing Action taxonomy; combat resolution stays in mc_combat::wilds.

Fork-neutral: WildContext is a flat projection, identical whether the
integration drives it inside mc_turn::step or via a GdWildAiController bridge
(p3-30 leaves that drive-site to infra). 12 unit tests: target-select,
chase, attack-iff-adjacent, leash return, leashed roam, city drift, passable
gating, no-movement skip, determinism, wilds.json config parse. mc-ai lib
301/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 06:50:44 -04:00
Natalie
cbc68a68c1 docs(@projects/@magic-civilization): 🔎 p3-26 Gap-2 — era max_tier cap is non-parity; fired-event surfacing is observability-only
Verified file:line: the live GDScript events modules have NO era-based max_tier
cap (0 hits) — headless flat max_tier=10 is correct parity; an era cap would
invent a rule the game lacks (gold-plating, dropped). And natural events already
fire + apply terrain effects headless; only the fired list surfacing to
TurnResult is missing (processor.rs:1117 `let _fired =`), an observability nicety
not a system gap. Confirms the headless natural-events system is functionally
complete; narrows Gap-2's real remainder.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 06:29:41 -04:00
Natalie
ac5efa4bec docs(@projects/@magic-civilization): 🌊 p3-26/27 — close marine gap (Rust-authoritative); drop ocean-collapse as gold-plating
Verified file:line that the marine→climate feed is already complete headless:
process_climate_phase → ClimatePhysics::process_step → compute_global_stats
writes grid.ocean_dead_fraction (reef-based, physics.rs:800) and step_evaporation
consumes it (physics.rs:460), every turn. Gap-1's "marine_harvest remaining"
is CLOSED.

Correction: mc_ecology:🌊:tick_ocean_state (4-phase trophic cascade) is
wired in NEITHER the live GDExt bridge NOR the live GDScript — the live game
runs a simple fish-stock ocean_dead_fraction (marine_harvest.gd), not the
cascade. Wiring tick_ocean_state headless would build a system the live game
doesn't run (parity ≠ gap). Marked OUT/gold-plating with citations so a future
session doesn't port it. The Rust reef-based formula vs the live fish-stock
formula is a divergence; Rail-1 → Rust drives, no reconciliation owed.

Also recorded D1 ruling (distinct ItemProduced) in p3-29.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 06:27:29 -04:00
Natalie
a0ace92c23 docs(@projects/@magic-civilization): p3-29 — T2/T3 done; correct T5 scope + T6 keystone status
§A event emission now DONE except ItemProduced (T4, blocked on D1):
- T2 UnitHealed (158ef4d1b), T3 GoldenAgeStarted/Ended (a87ea9f4d).
Corrected T5: the buffer pattern alone is insufficient — EcologyEngine::
process_step returns only flora transitions; fauna births/deaths + biome
changes are unreported by the headless engine (live signals come from
fauna.gd). T5 needs a new mc-ecology report surface + an owner ruling on
creature-event granularity. Also marked T6 keystone DONE (was pre-landing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 06:20:13 -04:00
Natalie
a87ea9f4d4 feat(@projects/@magic-civilization): 🌅 p3-29 T3 — Rust turn emits GoldenAgeStarted/Ended
The live GDScript turn emitted `golden_age_started`/`golden_age_ended` inline;
the headless happiness phase flipped `golden_age_active` silently. Detect the
false→true / true→false edge in `process_happiness_phase`, buffer it into a new
transient `GameState.pending_golden_age_events` (registry-has-no-event-sink
pattern), and drain it in `step()` into `GoldenAgeStarted`/`GoldenAgeEnded`.
Both edges emitted since the live UI consumes both signals (event_bus.gd). No
wire surface — dispatch drops them; live UI reads the kind-tagged event_to_dict.

Verified headless: mc-replay 20/0 (golden_age_events_serde), mc-turn 291/0
(golden_age_start_edge_buffers_started_event +
golden_age_end_edge_buffers_ended_event + event_collector_wiring).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 06:18:26 -04:00
Natalie
158ef4d1bd feat(@projects/@magic-civilization): 🩹 p3-29 T2 — Rust turn emits UnitHealed
The live GDScript turn emitted `unit_healed` inline; the headless healing
phase recovered HP silently. The healing phase runs in the end-of-turn
`fn(&mut GameState)` registry (no event sink), so follow the FloraSuccession
buffer pattern: stash `(player, unit_id, applied_amount, col, row)` into a new
transient `GameState.pending_heal_events`, drain it in `step()` into
`TurnEvent::UnitHealed`. The buffered amount is the CLAMPED delta actually
applied (not the nominal heal rate). No wire surface — dispatch drops it; the
live UI consumes it via the kind-tagged `event_to_dict` dict.

Verified headless: mc-replay 19/0 (unit_healed_serde), mc-turn 289/0
(healing_buffers_unit_heal_event_with_applied_amount +
healing_buffers_clamped_amount_near_full_hp + event_collector_wiring).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 06:12:07 -04:00
Natalie
236a5058e5 feat(@projects/@magic-civilization): 📣 orchestration transparency — announce specialist start/finish
Adds a narration convention so the user SEES the orchestration (and can verify parallelism at a
glance): whoever orchestrates emits a "▶ Dispatching [parallel|sequential] (N): agent(task)…" start
line and a "✓/✗ agent — outcome · proof" finish line per specialist. "parallel" must match behavior
(one message, multiple Agent calls). Milestone/decision/blocker also go out-of-band via TTS(ravdess02)
/ PushNotification; per-dispatch stays text-only (TTS every spawn = noise). Wired into the playbook
(agents-task-map.md), the team-lead agent, and the finish-game-1 skill's reporting section.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 06:09:18 -04:00
Natalie
7681def5b5 docs(@projects/@magic-civilization): p3-29 — T1 CultureResearched done; correct §B keystone (already landed)
T1 (CultureResearched event) landed in 74844f74d. Also corrected the §B
keystone status: the generic events[] in turn_result_to_dict was marked
"NOT STARTED" but actually landed with step 2 (a9b92df51, lib.rs:6573) —
verified file:line. Code wins over the drifted note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 06:05:22 -04:00
Natalie
74844f74d3 feat(@projects/@magic-civilization): 🎭 p3-29 T1 — Rust turn emits CultureResearched
The live GDScript turn emitted `culture_researched` inline; the headless
Rust turn dropped tradition completions. Emit a `TurnEvent::CultureResearched`
at the completion site in `process_culture_research` (single-source per Rail-1),
translate it to the existing `wire::Event::CultureResearched` in dispatch (not
dropped), and surface it as a kind-tagged dict in `event_to_dict` so the live
turn_manager will receive it at the Rail-1 swap. Threaded the events sink into
the phase + both call sites.

Verified headless: mc-replay 18/0 (culture_researched_serde), mc-turn 287/0
(event_collector_wiring), mc-player-api 138/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 06:03:47 -04:00
Natalie
6e3d9b2fd2 feat(@projects/@magic-civilization): 🔎 session bootloader leaves a verifiable trace
The hook left no trace, so 'did the bootloader fire this session?' was unverifiable. Now every fire
(a) stamps the orientation header with 'bootloader fired <UTC>' (visible in-context) and (b) appends
a line to .local/last-session-orient (gitignored breadcrumb) — so verification is one command:
cat .local/last-session-orient. Answers the 'how do I know it bootloaded?' question deterministically.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 06:00:28 -04:00
Natalie
f1fda3c18a feat(@projects/@magic-civilization): 🎮 add /finish-game-1 skill — autonomous Game-1 completion driver
Replaces the ad-hoc "/loop finish game 1" with a durable skill that captures both the mission and the
method. Encodes: the 3-part definition of done (scope complete + headless sim complete + Rail-1
getState unification), the per-iteration loop (orient → load rails → pick → classify → implement →
verify-by-type → commit → continue), the stop-and-ask conditions (balance/scope/architecture/render-
host = owner's call), and the guardrails this session paid for (verify premises, Rust drives, eliminate
don't fix the orchestrator, no stubs, don't gold-plate). Chains the tooling built this session
(session-orient → specialist-preamble → code-layering → orchestration).

First project skill; creates tooling/claude/dot-claude/skills/. Available next session (skills load at
session start).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 05:41:57 -04:00
Natalie
54d7f8f630 docs(@projects/@magic-civilization): 🧭 tooling — encode getState/never-reconcile + verify-premises lessons
Final agent-tooling pass folding in this session's hardest-won lessons (previously only in my memory):

code-layering.md:
- anti-pattern #4 "fixing the orchestrator instead of eliminating it" (the swap→extract→FFI drift;
  the fix is usually DELETE the GDScript path, let Rust compute + UI render getState()).
- anti-pattern #5 "UI holding state / calling logic instead of reading getState()".
- principles rewritten: (1) Rust drives everything — divergence is a bug to DELETE, never reconcile;
  (2) the UI is a pure view of getState() (render/act/end_turn, GdPlayerApi is the reference);
  (3) single-source LOGIC not necessarily STATE — but never two state copies in ONE running game.

specialist-preamble.md (always-loaded core):
- verify rule gains "verify architectural PREMISES before committing to a plan" (the 3-turn drift,
  collapsed by one grep of view.rs).
- layering rule gains the Rust-drives / UI-is-a-view-of-getState one-liner.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 05:38:43 -04:00
Natalie
b93328cd11 docs(@projects/@magic-civilization): p3-29 — correct false claim: getState() is ALREADY a rich projection
Self-correction (the prior commit's "view_json carries almost nothing renderable" was wrong — an
over-grep that only caught the yields line). Verified PlayerView (view.rs:318): CityView carries
position/owner/population/production_queue/owned_tiles(territory)/hp/focus; UnitView carries
position/hp/movement/xp/fortified/sentry; TileView carries position/biome/improvement/river/
explored+visible(fog)/owner_city; plus resources/research/culture/civics/diplomacy/score.

So the projection foundation largely EXISTS — the gap to "UI calls getState()" is the GDScript side
(renderer reads CityScript/Player entities, not PlayerView). Recast target updated: the bulk is the
GDScript rewire to consume view_json + switch the turn to end_turn(); the only genuine projection
additions are render-only extras (animation deltas/UnitMoved, VFX, player colors, minimap).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 05:33:58 -04:00
Natalie
84790caf74 docs(@projects/@magic-civilization): 🎯 p3-29 corrected (v2) — UI is a pure view of getState(); folds into p3-25
Owner: "shouldn't the UI just call getState()?" — re-asserting the p3-25 directive. Both prior
framings were wrong: v1 (swap orchestrators) and v1.5 (extract each formula into an FFI the GDScript
turn calls) BOTH keep GDScript calling logic / holding state. Correct architecture (already proven by
the headless GdPlayerApi): Rust owns ALL state + runs the whole turn (end_turn); view_json = getState
is the complete render projection; UI renders it + sends act(). The dual GameState is THE bug, not a
constraint. The 4 inlined modifiers vanish when turn_processor.gd::_process_* is deleted — no
per-formula FFI extraction. Folds into p3-25 (complete the projection). First step: projection-gap
audit (what the renderer reads from entities vs what view_json carries — today: almost nothing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 05:31:48 -04:00
Natalie
6b1d92b2af docs(@projects/@magic-civilization): 🔍 p3-29 recast — audit live turn; swap→logic-extraction (drop B7)
Owner reconsidered the swap. Audit (verified file:line) shows the live turn_processor.gd already
delegates the TICK to Rust in every phase but inlines 4 MODIFIER formulas with hardcoded constants —
_process_growth (CONFIRMED divergence from mc_happiness::get_growth_modifier), _process_production
(0.75/1.2), _process_culture (1.2), _catchup_research_mult (1.5×). Recast from a big-bang state swap
to incremental logic extraction: each phase keeps its state + FFI tick; only the inlined formula
moves to its owning crate. No state migration; former B7 city-model convergence DROPPED (dual state
is legitimate — single-source is LOGIC not STATE). Worklist added; swap steps 3-5 marked superseded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 05:21:37 -04:00
Natalie
8628ea7d88 feat(@projects/@magic-civilization): 🧭 add SessionStart bootloader — live project orientation for fresh sessions
No project bootloader existed: a new session/agent booted with only the static CLAUDE.md router and
had to manually dig for current state. Adds a SessionStart hook (session-orient.sh) that injects a
LIVE orientation every session — the dynamic counterpart to the static router:

- In-flight objectives (partial/stub from objectives.json) — where to resume
- Blocked count + last 5 commits + unpushed-commit warning (94 right now; forge down)
- Verify-before-trusting reminder + tooling entry-points (preamble / orchestration / code-layering)

State is read live every run (objectives.json + git) — never embedded, so it can't go stale
(the same anti-drift principle the agent tooling enforces). Read-only, <2s, never breaks the
session (any error → exits 0). Dual-mode: hook JSON by default, `--human` prints markdown for
manual mid-session re-orientation (`bash .claude/hooks/session-orient.sh --human`).

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