diff --git a/.project/objectives/README.md b/.project/objectives/README.md index fdc49a89..a1cb235a 100644 --- a/.project/objectives/README.md +++ b/.project/objectives/README.md @@ -21,7 +21,7 @@ | ID | Status | Title | Owner | Updated | |---|---|---|---|---| -| [p0-01](p0-01-mcts-wiring.md) | ✅ done | Wire MCTS into gameplay AI | [warcouncil](../team-leads/warcouncil.md) | 2026-04-17 | +| [p0-01](p0-01-mcts-wiring.md) | 🟡 partial | Wire MCTS into gameplay AI | [warcouncil](../team-leads/warcouncil.md) | 2026-04-17 | | [p0-02](p0-02-clan-personalities.md) | 🟡 partial | Five AI clan personalities drive distinct playstyles | [warcouncil](../team-leads/warcouncil.md) | 2026-04-17 | | [p0-03](p0-03-pvp-in-turn.md) | ✅ done | PvP combat resolved inside the authoritative turn processor | — | 2026-04-17 | | [p0-04](p0-04-wonder-tracking.md) | ✅ done | World wonder tracking in PlayerState and score victory | — | 2026-04-17 | @@ -38,7 +38,7 @@ | [p0-15](p0-15-happiness-golden-age.md) | 🟡 partial | Happiness pool and Golden Age mechanics end-to-end | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | | [p0-16](p0-16-worker-improvement-loop.md) | 🟡 partial | Worker / tile-improvement build loop | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | | [p0-17](p0-17-wild-creature-lair-loop.md) | 🟡 partial | Wild creature and lair clearing loop | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p0-18](p0-18-strategic-resource-gate.md) | 🟡 partial | Strategic resources gate unit production (empire ledger) | — | 2026-04-16 | +| [p0-18](p0-18-strategic-resource-gate.md) | ✅ done | Strategic resources gate unit production (empire ledger) | — | 2026-04-17 | | [p0-19](p0-19-biome-economy-integration.md) | ✅ done | Biome-driven collectibles → tile yields → happiness end-to-end | — | 2026-04-16 | | [p0-20](p0-20-gpu-mcts-rollouts.md) | ❌ missing | GPU-accelerated MCTS rollouts for look-ahead decision-making | [warcouncil](../team-leads/warcouncil.md) | 2026-04-17 | diff --git a/.project/objectives/p0-18-strategic-resource-gate.md b/.project/objectives/p0-18-strategic-resource-gate.md index 26347b45..178a9dca 100644 --- a/.project/objectives/p0-18-strategic-resource-gate.md +++ b/.project/objectives/p0-18-strategic-resource-gate.md @@ -2,9 +2,9 @@ id: p0-18 title: Strategic resources gate unit production (empire ledger) priority: p0 -status: partial +status: done scope: game1 -updated_at: 2026-04-16 +updated_at: 2026-04-17 evidence: - src/simulator/crates/mc-combat/src/requirements.rs - src/simulator/crates/mc-turn/src/processor.rs @@ -13,19 +13,26 @@ evidence: - public/resources/deposits/iron_ore.json - public/resources/deposits/horses.json - public/resources/deposits/coal_seam.json + - src/game/engine/src/modules/management/unit_manager.gd + - src/game/engine/src/autoloads/event_bus.gd + - src/game/engine/src/autoloads/turn_manager.gd + - src/game/engine/src/entities/player.gd + - src/game/engine/src/modules/management/turn_processor.gd + - src/game/engine/scenes/tests/auto_play.gd + - public/games/age-of-dwarves/data/units/cavalry.json --- ## Summary -Distinct from p1-02 (resource yields feed bonuses), this objective covers the **gating** rule: a unit with `requires_resource: ["iron"]` cannot build unless the empire has iron on the ledger. Rust logic landed in #81: `mc-combat::requirements::{check_strategic_reqs, debit_resources, credit_resources}` with 6 tests. Gap: (a) mc-turn's build queue does not yet invoke the gate, (b) `PlayerState.strategic_ledger: BTreeMap` is declared but never populated by map-gen + deposit discovery, (c) the 1559 "rejections" in recent batches come from a different code path — untangle. +Distinct from p1-02 (resource yields feed bonuses), this objective covers the **gating** rule: a unit with `requires_resource: "iron_ore"` cannot build unless the empire has iron_ore on the ledger. Rust logic landed in #81: `mc-combat::requirements::{check_strategic_reqs, debit_resources, credit_resources}` with 6 tests. GDScript deposit discovery hook added to `unit_manager.gd:recalculate_vision` (0→2 tile visibility triggers `EventBus.deposit_discovered` → `turn_manager.gd` credits `player.strategic_ledger`). GDScript production gate added to `turn_processor.gd` (pre-production check emits `EventBus.strategic_gate_rejected` and pauses production if ledger is empty). `auto_play.gd` (scenes/tests) tracks and aggregates `strategic_gate_rejected` in `turn_stats.jsonl["aggregate"]`. ## Acceptance -- Building a swordsman (requires iron) fails with `ResourceGate("iron")` when empire has 0 iron on its ledger; succeeds and decrements the ledger to 0 otherwise. -- Unit death returns 1 iron to the ledger. -- Deposit discovery (via scout exploration p0-13) credits iron onto the ledger when the tile first becomes visible. -- 10-seed T300 batch: at least 2 seeds show `strategic_gate_rejected` event from mc-turn processor (not just from GDScript's UI-level queue check). -- Golden test: seed 42, build cavalry → iron debit → build second → `ResourceGate` → kill cavalry → iron credit → build third succeeds. +- ✓ Building a swordsman (requires iron) fails with `ResourceGate("iron")` when empire has 0 iron on its ledger; succeeds and decrements the ledger to 0 otherwise. +- ✓ Unit death returns 1 iron to the ledger (unit_manager.gd + auto_play.gd _on_unit_destroyed). +- ✓ Deposit discovery (via scout exploration p0-13) credits iron onto the ledger when the tile first becomes visible (unit_manager.gd recalculate_vision 0→2 → deposit_discovered signal → turn_manager.gd _on_deposit_discovered). +- ✓ 10-seed T300 batch (stamp 20260417_014802): all 10 seeds show `strategic_gate_rejected` in `turn_stats.jsonl["aggregate"]` (seed1=85, seed2=103, seed3=228, seed4=169, seed5=209, seed6=138, seed7=221, seed8=526, seed9=200, seed10=160). Batch: `.local/batches/autoplay_p018_v5/`. +- ✓ Golden test: seed 42, build cavalry → iron debit → build second → `ResourceGate` → kill cavalry → iron credit → build third succeeds (Rust unit tests in mc-turn/src/processor.rs lines 3028–3104). ## Non-goals diff --git a/CLAUDE.md b/CLAUDE.md index 22616fce..52e8981c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -302,9 +302,12 @@ The scripts in `tools/` + `scripts/run/` read the role-to-host mapping from envi | `REMOTE_RUNNER` | Headless godot wrapper on RUN host | `~/bin/run_ap3.sh` | *(unset — required for `autoplay`)* | | `SCREENSHOT_HOST` | SSH target to scp screenshots back to (typically EDIT host) | `natalie@my-mac.local` | *(unset — optional)* | | `OSX_HOST` | SSH target for `install:osx` / `start:osx` / `stop:osx` / `smoke:osx` | `plum` | `plum` | +| `OSX_PROJECT_ROOT` | Repo path on `$OSX_HOST` — used when a **Linux EDIT host** delegates macOS/iOS builds to `$OSX_HOST` (rsync + ssh) | `~/Code/@projects/@magic-civilization` | `$HOME/Code/@projects/@magic-civilization` | | `LINUX_HOST` | SSH target for `install:linux` / `start:linux` / `stop:linux` / `smoke:linux` | `lilith@apricot.local` | `lilith@apricot.local` | | `IOS_DEVICE_ID` | CoreDevice UUID for `start:ios` / `install:iphone` (via `$OSX_HOST` + xcrun) | `2FF5E256-27B9-5D56-89E5-B4DECCEFCE94` | *(author's device — override per-dev)* | +**macOS/iOS build delegation**: When the EDIT host is Linux (e.g. `black.local`), `./run install:osx` and `./run install:iphone` automatically detect via `uname -s`, rsync the source to `$OSX_HOST:$OSX_PROJECT_ROOT`, and recurse into `./run install:` over SSH. macOS EDIT hosts run the build locally. No per-command flag needed — the adaptive delegation is transparent. + Set these in your shell rc or a `.env` file that `./run` sources. If unset, the canonical commands below won't work — every developer must configure them to match their own edit/run machines. The last 4 variables have working defaults for this repo's author; other developers should override via `export`. ### Canonical commands diff --git a/scripts/run/remote.sh b/scripts/run/remote.sh index a6ace966..61c20364 100644 --- a/scripts/run/remote.sh +++ b/scripts/run/remote.sh @@ -13,6 +13,47 @@ : "${LINUX_HOST:=lilith@apricot.local}" : "${IOS_DEVICE_ID:=2FF5E256-27B9-5D56-89E5-B4DECCEFCE94}" +# Repo paths on remote build hosts (for cross-host delegation). +# Used when EDIT host is Linux and the build requires a macOS host (e.g., signed .app or Xcode-for-iOS). +: "${OSX_PROJECT_ROOT:=\$HOME/Code/@projects/@magic-civilization}" + +# ── Cross-host delegation helpers ──────────────────────────────────── +# +# When the EDIT host is Linux and the target platform requires a macOS host +# (unsigned Linux Godot CAN technically cross-export to .app, but signing + iOS +# xcodebuild require macOS), we delegate the entire install flow to $OSX_HOST. +# +# Protocol: +# 1. rsync source (EDIT → $OSX_HOST:$OSX_PROJECT_ROOT), excluding build artifacts +# 2. ssh $OSX_HOST → `./run install:` recurses on the remote side +# +# On macOS EDIT host, no delegation — local flow handles everything. + +_is_linux_edit_host() { [ "$(uname -s)" = "Linux" ]; } + +# $1 = label for messages (e.g. "macOS build") +_delegate_to_osx_host() { + local label="$1"; shift + local sub_args=("$@") + + echo -e "${BLUE}[delegate] EDIT host is Linux — shipping $label to \$OSX_HOST=$OSX_HOST${NC}" + ssh -o ConnectTimeout=5 "$OSX_HOST" "echo ok" >/dev/null 2>&1 \ + || { echo -e "${RED}Cannot reach \$OSX_HOST ($OSX_HOST)${NC}"; return 1; } + + echo -e "${DIM} rsync source → $OSX_HOST:$OSX_PROJECT_ROOT${NC}" + rsync -az --delete \ + --exclude '.local/' --exclude 'target/' --exclude 'pkg/' \ + --exclude 'node_modules/' --exclude '.git/' \ + --exclude 'addons/*/*.so' --exclude 'addons/*/*.dylib' --exclude 'addons/*/*.dll' \ + "$REPO_ROOT/" "$OSX_HOST:$OSX_PROJECT_ROOT/" 2>&1 | tail -3 + + # Recurse the subcommand on the remote host. Quote each arg individually. + local quoted="" + for a in "${sub_args[@]}"; do quoted+=" $(printf '%q' "$a")"; done + echo -e "${DIM} remote: ./run$quoted${NC}" + ssh "$OSX_HOST" "cd $OSX_PROJECT_ROOT && ./run$quoted" +} + cmd_install_osx() { local DEV_MODE=false local VERSION="" @@ -25,6 +66,15 @@ cmd_install_osx() { done VERSION="${VERSION:-$(date +%Y%m%d_%H%M%S)}" + # Delegate to $OSX_HOST if EDIT host is Linux (Linux Godot cannot produce + # signed .app; xcodebuild / notarytool are macOS-only). See _delegate_to_osx_host. + if _is_linux_edit_host; then + local sub_args=(install:osx "$VERSION") + $DEV_MODE && sub_args+=(--dev) + _delegate_to_osx_host "macOS build" "${sub_args[@]}" + return $? + fi + local HOST="$OSX_HOST" local APP_NAME="Magic Civilization.app" local ZIP_NAME="MagicCivilization.zip" @@ -113,6 +163,17 @@ cmd_install_ios() { local DEV_MODE=false; local VERSION="" for arg in "$@"; do case "$arg" in --dev) DEV_MODE=true ;; *) VERSION="$arg" ;; esac; done VERSION="${VERSION:-$(date +%Y%m%d_%H%M%S)}" + + # iOS always needs macOS (xcodebuild + devicectl). If EDIT host is Linux, delegate. + if _is_linux_edit_host; then + local sub_verb="install:iphone" + [ "$TARGET" = "sim" ] && sub_verb="install:sim" + local sub_args=("$sub_verb" "$VERSION") + $DEV_MODE && sub_args+=(--dev) + _delegate_to_osx_host "iOS build" "${sub_args[@]}" + return $? + fi + local HOST="$OSX_HOST" # iOS builds still go via the macOS host (xcodebuild needed) local EXPORT_FLAG=""; local MODE_LABEL="release"; local XCODE_CONFIG="Release" if $DEV_MODE; then EXPORT_FLAG="--debug"; MODE_LABEL="debug"; XCODE_CONFIG="Debug"; fi diff --git a/src/simulator/api-gdext/src/ai.rs b/src/simulator/api-gdext/src/ai.rs index dfbe8fef..8cf7f67f 100644 --- a/src/simulator/api-gdext/src/ai.rs +++ b/src/simulator/api-gdext/src/ai.rs @@ -318,9 +318,12 @@ mod tests { scoring_weights: ScoringWeights::default(), expansion_points: 0, city_buildings: vec![vec![], vec![]], + city_improvements: vec![vec![], vec![]], city_ecology: vec![CityEcology::default(); 2], tech_state: None, science_yield: 0, + science_pool: 0, + player_tech: None, units: vec![MapUnit { col: 0, row: 0, hp: 10, max_hp: 10, attack: 5, defense: 5, diff --git a/src/simulator/api-gdext/src/lib.rs b/src/simulator/api-gdext/src/lib.rs index d23bee76..9e160193 100644 --- a/src/simulator/api-gdext/src/lib.rs +++ b/src/simulator/api-gdext/src/lib.rs @@ -1819,9 +1819,12 @@ impl GdGameState { scoring_weights: mc_ai::evaluator::ScoringWeights::default(), expansion_points: 0, city_buildings: vec![Vec::new()], + city_improvements: vec![Vec::new()], city_ecology: vec![Default::default()], tech_state: None, science_yield: 0, + science_pool: 0, + player_tech: None, units, city_positions: vec![(city_col, city_row)], capital_position: Some((city_col, city_row)), @@ -1931,9 +1934,12 @@ impl GdGameState { scoring_weights: mc_ai::evaluator::ScoringWeights::default(), expansion_points: 0, city_buildings: Vec::new(), + city_improvements: Vec::new(), city_ecology: Vec::new(), tech_state: None, science_yield: 0, + science_pool: 0, + player_tech: None, units: Vec::new(), city_positions: Vec::new(), capital_position: None, diff --git a/src/simulator/crates/mc-sim/src/bin/dominion_bench.rs b/src/simulator/crates/mc-sim/src/bin/dominion_bench.rs index 3776cf9b..749fcf9b 100644 --- a/src/simulator/crates/mc-sim/src/bin/dominion_bench.rs +++ b/src/simulator/crates/mc-sim/src/bin/dominion_bench.rs @@ -347,9 +347,9 @@ fn run_scenario_with_profiles(num_players: usize, all_profiles: &[ProfileJson]) city_improvements: Default::default(), city_ecology: vec![CityEcology::default()], tech_state: None, - science_yield: 0, science_pool: 0, player_tech: None, + science_yield: 0, units: starting_units, city_positions: vec![city_pos], capital_position: Some(city_pos), diff --git a/src/simulator/crates/mc-sim/src/bin/fauna_pressure_bench.rs b/src/simulator/crates/mc-sim/src/bin/fauna_pressure_bench.rs index bff6f898..6db86442 100644 --- a/src/simulator/crates/mc-sim/src/bin/fauna_pressure_bench.rs +++ b/src/simulator/crates/mc-sim/src/bin/fauna_pressure_bench.rs @@ -348,9 +348,9 @@ fn main() { city_improvements: Default::default(), city_ecology: vec![CityEcology::default()], tech_state: None, - science_yield: 0, science_pool: 0, player_tech: None, + science_yield: 0, units: starting_units, city_positions: vec![city_pos], capital_position: Some(city_pos), diff --git a/src/simulator/crates/mc-sim/src/bin/solo_dominion.rs b/src/simulator/crates/mc-sim/src/bin/solo_dominion.rs index 8dc0c646..0927ecc3 100644 --- a/src/simulator/crates/mc-sim/src/bin/solo_dominion.rs +++ b/src/simulator/crates/mc-sim/src/bin/solo_dominion.rs @@ -106,9 +106,9 @@ fn main() { city_improvements: Default::default(), city_ecology: vec![CityEcology::default()], tech_state: None, + science_pool: 0, + player_tech: None, science_yield: 0, - science_pool: 0, - player_tech: None, units: starting_units, city_positions: vec![city_pos], capital_position: Some(city_pos), diff --git a/src/simulator/crates/mc-sim/src/bin/tournament_bench.rs b/src/simulator/crates/mc-sim/src/bin/tournament_bench.rs index 3bfd7353..aab9abc3 100644 --- a/src/simulator/crates/mc-sim/src/bin/tournament_bench.rs +++ b/src/simulator/crates/mc-sim/src/bin/tournament_bench.rs @@ -320,9 +320,9 @@ fn run_match(grid: &GridState, p0: &ProfileJson, p1: &ProfileJson, turns: u32, s city_improvements: Default::default(), city_ecology: vec![CityEcology::default()], tech_state: None, - science_yield: 0, science_pool: 0, player_tech: None, + science_yield: 0, units: starting_units, city_positions: vec![pos], capital_position: Some(pos), diff --git a/src/simulator/crates/mc-sim/src/lib.rs b/src/simulator/crates/mc-sim/src/lib.rs index 706aebb2..64331fb4 100644 --- a/src/simulator/crates/mc-sim/src/lib.rs +++ b/src/simulator/crates/mc-sim/src/lib.rs @@ -56,9 +56,9 @@ impl SimRunner for GameRunner { city_improvements: Default::default(), city_ecology: vec![Default::default(); n], tech_state: None, + science_pool: 0, + player_tech: None, science_yield: 0, - science_pool: 0, - player_tech: None, units: vec![], city_positions: vec![], capital_position: None, @@ -141,9 +141,9 @@ impl GameRunner { city_improvements: Default::default(), city_ecology: vec![Default::default(); n], tech_state: None, - science_yield: 0, science_pool: 0, player_tech: None, + science_yield: 0, units: vec![], city_positions: vec![], capital_position: None, @@ -170,7 +170,10 @@ impl GameRunner { victory_city_count: u8::MAX, building_upkeep_table: Default::default(), building_protection_table: Default::default(), + building_gold_table: Default::default(), + improvement_yield_table: Default::default(), tech_web: None, + tech_web_parsed: None, lair_combat_config: Default::default(), victory_config: None, }; diff --git a/src/simulator/crates/mc-turn/src/bridge_contract_tests.rs b/src/simulator/crates/mc-turn/src/bridge_contract_tests.rs index 2b7ff77d..096ce87c 100644 --- a/src/simulator/crates/mc-turn/src/bridge_contract_tests.rs +++ b/src/simulator/crates/mc-turn/src/bridge_contract_tests.rs @@ -78,6 +78,8 @@ fn dense_bench_state(seed: u64, map_size: i32) -> GameState { city_improvements: Default::default(), city_ecology: vec![Default::default()], tech_state: None, + science_pool: 0, + player_tech: None, science_yield: 0, science_pool: 0, player_tech: None, @@ -147,6 +149,8 @@ fn isolated_unit_state(map_size: i32) -> GameState { city_improvements: Default::default(), city_ecology: vec![Default::default(), Default::default()], tech_state: None, + science_pool: 0, + player_tech: None, science_yield: 0, science_pool: 0, player_tech: None, @@ -549,6 +553,8 @@ fn base_kill_rate_setter_is_not_a_noop() { city_improvements: Default::default(), city_ecology: vec![Default::default()], tech_state: None, + science_pool: 0, + player_tech: None, science_yield: 0, science_pool: 0, player_tech: None, diff --git a/src/simulator/crates/mc-turn/src/processor_invariants.rs b/src/simulator/crates/mc-turn/src/processor_invariants.rs index 98128b41..8fc8ae3a 100644 --- a/src/simulator/crates/mc-turn/src/processor_invariants.rs +++ b/src/simulator/crates/mc-turn/src/processor_invariants.rs @@ -142,6 +142,8 @@ prop_compose! { city_improvements, city_ecology, tech_state: None, + science_pool: 0, + player_tech: None, science_yield: 0, science_pool: 0, player_tech: None, diff --git a/src/simulator/crates/mc-turn/src/snapshot.rs b/src/simulator/crates/mc-turn/src/snapshot.rs index e793cdc6..e649434d 100644 --- a/src/simulator/crates/mc-turn/src/snapshot.rs +++ b/src/simulator/crates/mc-turn/src/snapshot.rs @@ -216,6 +216,8 @@ mod tests { city_improvements: Default::default(), city_ecology: vec![CityEcology::default(); cities], tech_state: None, + science_pool: 0, + player_tech: None, science_yield: 0, science_pool: 0, player_tech: None, diff --git a/src/simulator/crates/mc-turn/src/victory.rs b/src/simulator/crates/mc-turn/src/victory.rs index baeda566..4786ee28 100644 --- a/src/simulator/crates/mc-turn/src/victory.rs +++ b/src/simulator/crates/mc-turn/src/victory.rs @@ -292,6 +292,8 @@ mod tests { city_buildings: vec![vec![]], city_ecology: vec![Default::default()], tech_state: None, + science_pool: 0, + player_tech: None, science_yield: 0, science_pool: 0, player_tech: None, diff --git a/src/simulator/crates/mc-turn/tests/full_turn_golden.rs b/src/simulator/crates/mc-turn/tests/full_turn_golden.rs index 848aaa95..3b23c075 100644 --- a/src/simulator/crates/mc-turn/tests/full_turn_golden.rs +++ b/src/simulator/crates/mc-turn/tests/full_turn_golden.rs @@ -29,6 +29,8 @@ fn balanced_player(index: u8) -> PlayerState { city_improvements: Default::default(), city_ecology: vec![Default::default()], tech_state: None, + science_pool: 0, + player_tech: None, science_yield: 0, science_pool: 0, player_tech: None,