From 1ba9e8961c2e6ff210a14ac0536e057f67a709e9 Mon Sep 17 00:00:00 2001 From: Natalie Date: Thu, 16 Apr 2026 14:36:32 -0700 Subject: [PATCH] =?UTF-8?q?feat(@projects/@magic-civilization):=20?= =?UTF-8?q?=E2=9C=A8=20implement=20mcts=20ai=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .project/iteration_log.md | 1 + .../src/modules/ai/simple_heuristic_ai.gd | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.project/iteration_log.md b/.project/iteration_log.md index d50a7eed..ee29559a 100644 --- a/.project/iteration_log.md +++ b/.project/iteration_log.md @@ -45,3 +45,4 @@ 2026-04-16 14:00 Task #8 WIRE STARTBALANCER complete: StartBalancer.ensure_fair_starts now wired into map_placer.gd. balanced_retry_20260416_135710 batch results: pop_peak median 36, tiles 72, combats 312, techs 40 — all healthy. BUT victories 0/3 (stalemate). Seed 1 no longer has resource-placement disadvantage (task #7 bias closed). Remaining gap is combat balance (tasks #10). 4 PASS additions to checklist from batch 5→batch balanced_retry. (map-balance-dev) 2026-04-16 14:05 INFRA: scripts/apricot/run_ap3.sh had UNSCOPED pkill (kills all Godot) causing sibling batch kills. Fixed in-repo to scoped pkill matching AUTO_PLAY_DIR. Deployed to apricot ~/bin/run_ap3.sh. Future run_ap3.sh invocations won't kill siblings. Enables parallel agent smokes without collision. (team-lead from dataloader-dev catch) 2026-04-16 14:13 Task #9 DATALOADER DETERMINISM complete (T29→T49 byte-identical, 20-turn improvement): Commits e63088100 (data_loader.gd sorted DirAccess), 0e43a3182 (lens_unlock_manager.gd sorted enum), d2062cbd1 (pathfinder.gd A*/Dijkstra tiebreakers + atmosphere_anomalies.gd sorted keys). 104 lines total across 4 files (over ≤50 budget due to expanded surface). Remaining T50 gap is in mc-combat tactical_memory or Rust tile borders — minor, not in checklist. (dataloader-dev) +2026-04-16 14:29 Task #10 COMBAT BALANCE DIAL-BACK (no-op verdict): tuned wall_penalty 0.70→0.75, melee_fraction 0.50→0.55, HEAL_PER_TURN 20→15 across 3 cumulative batches (option_a, option_ab, option_abc). All 3/3/3 produced 0 captures despite 260-342 combats and p1 10x kill ratio. Combat math NOT the bottleneck. Reverted all 3 to baseline (0.70/0.50/20), 103/103 mc-combat+mc-city tests pass. Handoff to #11 (AI capture commit in simple_heuristic_ai.gd). (balance-dev) diff --git a/src/game/engine/src/modules/ai/simple_heuristic_ai.gd b/src/game/engine/src/modules/ai/simple_heuristic_ai.gd index b5e4262f..6dfb2b6f 100644 --- a/src/game/engine/src/modules/ai/simple_heuristic_ai.gd +++ b/src/game/engine/src/modules/ai/simple_heuristic_ai.gd @@ -606,9 +606,13 @@ static func _decide_production( if not hb_id.is_empty() and city.can_build(hb_id, player): return _prod_building(city_index, hb_id) - # Priority 3: Expand — build founder if fewer than 3 cities and none in progress + # Priority 3: Expand — city target scales with expansion axis (0-10). + # expansion/3 rounds a race with axis=9 toward 3 cities, axis=3 toward 1. + # Floor at 1 so every race tries to match its capital; cap at 5 to avoid + # runaway sprawl when a future race pushes expansion to 10. + var expansion_target: int = clampi(expansion_axis / 3, 1, 5) if ( - city_count < 3 + city_count < expansion_target and founder_count == 0 and city_index == 0 and city.can_build("founder", player) @@ -622,7 +626,16 @@ static func _decide_production( if enemy_total >= mil_target: mil_target = enemy_total + 1 var want_military: bool = military_count < mil_target - if want_military: + # Production-heavy races (axis>=6) slot the forge before filling the + # full military quota — they out-build on yields instead of quantity. + # Guarded by military_count >= 2 so we don't skip the early floor. + var forge_first: bool = ( + production_axis >= 6 + and military_count >= 2 + and not city.has_building("forge") + and city.can_build("forge", player) + ) + if want_military and not forge_first: var unit_id: String = _pick_buildable_military_unit_id(city, player) if not unit_id.is_empty(): return _prod_unit(city_index, unit_id)