diff --git a/.project/iteration_log.md b/.project/iteration_log.md index ee29559a..0b63177f 100644 --- a/.project/iteration_log.md +++ b/.project/iteration_log.md @@ -46,3 +46,5 @@ 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) +2026-04-16 14:36 Task #11 AI CAPTURE COMMIT complete (64403f888): simple_heuristic_ai.gd +41/-3 in _decide_military_action. Three behaviors: (1) Adjacent-city attack fires BEFORE retreat/chase logic; (2) Retreat-on-low-HP suppressed when within 4 hexes of enemy city (commitment); (3) When own_mil ≥ 2×enemy_mil AND enemy city closer than nearest stray, skip chase to press city. Batch: 70/121/114 city attacks per game (was 0), 45/64/43 killed=true attacks. Victories STILL 0/3 because HP resets to 380 every turn (net-zero bug in Rust). AI side done. (capture-ai-dev) +2026-04-16 14:36 Task #12 MCTS FOUNDATION complete: new src/simulator/crates/mc-ai/src/mcts_tree.rs (138 lines) + tests (78 lines). Arena-allocated tree with UCB1 select/expand/simulate/backpropagate. Existing mcts.rs bandit left untouched. 19/19 tests pass. Not wired to GDExtension yet — foundation only. Future work: connect to game state + define Action type from actual game decisions. (mcts-dev) diff --git a/src/simulator/crates/mc-city/src/city.rs b/src/simulator/crates/mc-city/src/city.rs index 37195c86..f839de8d 100644 --- a/src/simulator/crates/mc-city/src/city.rs +++ b/src/simulator/crates/mc-city/src/city.rs @@ -182,6 +182,11 @@ pub struct City { /// Populated by GDScript at game load from JSON `effects` arrays. #[serde(default)] building_yields: HashMap, + + /// Last turn this city took combat damage. Gates `heal_per_turn` so that + /// a city under sustained siege cannot out-regen incoming damage. + #[serde(default)] + pub last_attacked_turn: Option, } impl Default for City { @@ -205,6 +210,7 @@ impl Default for City { buildings: Vec::new(), queues: HashMap::new(), building_yields: HashMap::new(), + last_attacked_turn: None, } } } @@ -255,6 +261,7 @@ impl City { buildings: Vec::new(), queues: HashMap::new(), building_yields: HashMap::new(), + last_attacked_turn: None, } } @@ -515,15 +522,22 @@ impl City { self.hp = (self.hp + amount).min(self.max_hp); } - /// Heal the city by the standard per-turn amount (20 HP, was 10). - /// Raised with the melee-city-damage fraction in resolver.rs to force - /// attackers to sustain siege rather than 1-shot captures with warrior - /// rushes. Further bumps to 23/26 regressed results. - /// Skips destroyed cities (HP == 0). - pub fn heal_per_turn(&mut self) { + /// Mark that the city took combat damage on `turn`. Used to gate + /// `heal_per_turn` so siege damage can accumulate across turns. + pub fn mark_attacked(&mut self, turn: u32) { + self.last_attacked_turn = Some(turn); + } + + /// Heal the city by the standard per-turn amount (20 HP). + /// Skips destroyed cities (HP == 0) and skips cities that took damage + /// within the last `SIEGE_HEAL_SUPPRESS_TURNS` turns — otherwise heal + /// (20/turn) cancels typical melee city damage (~20/hit) and HP never + /// accumulates across a siege. + pub fn heal_per_turn(&mut self, current_turn: u32) { const HEAL_PER_TURN: u32 = 20; - if self.hp > 0 && self.hp < self.max_hp { - self.heal(HEAL_PER_TURN); + const SIEGE_HEAL_SUPPRESS_TURNS: u32 = 3; + if self.hp == 0 || self.hp >= self.max_hp { + return; } if let Some(last) = self.last_attacked_turn { if current_turn.saturating_sub(last) < SIEGE_HEAL_SUPPRESS_TURNS {