diff --git a/src/game/engine/scenes/tests/auto_play.gd b/src/game/engine/scenes/tests/auto_play.gd index 82024346..feb5537c 100644 --- a/src/game/engine/scenes/tests/auto_play.gd +++ b/src/game/engine/scenes/tests/auto_play.gd @@ -1075,6 +1075,13 @@ func _next_building(city: Variant, player: Variant, city_count: int, has_founder _score_add(scores, "worker", 7.0) if unimproved_food > 0 and own_workers < city_count and int(city.population) >= 2: _score_add(scores, "worker", 4.0) + # First worker is a strong priority once pop can spare the food — + # tile improvements are the primary long-term yield multiplier. + if own_workers == 0 and int(city.population) >= 2: + _score_add(scores, "worker", 10.0) + # Keep worker supply replenished: one worker per city after first. + if own_workers < city_count and int(city.population) >= 3: + _score_add(scores, "worker", 3.0) if gold_now < 20 and gpt < 0: _score_add(scores, "marketplace", 7.0) if not city.has_building("forge"): 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 e2b75571..ac5d86c3 100644 --- a/src/game/engine/src/modules/ai/simple_heuristic_ai.gd +++ b/src/game/engine/src/modules/ai/simple_heuristic_ai.gd @@ -25,8 +25,8 @@ const FOUND_MIN_DIST_OWN: int = 4 ## deadlock founders that spawned near each other (observed in arena ## smoke tests where start placement put both players on tile 0,0). const FOUND_MIN_DIST_ENEMY: int = 1 -const RETREAT_HP_FRACTION: float = 0.4 -const DEFENSIVE_CHASE_RANGE: int = 12 +const RETREAT_HP_FRACTION: float = 0.15 +const DEFENSIVE_CHASE_RANGE: int = 8 const MILITARY_COMBAT_TYPES: Array[String] = [ "melee", "ranged", "cavalry", "siege", ] @@ -574,8 +574,14 @@ static func _decide_production( ): return _prod_unit(city_index, "founder") - # Priority 4: Military — maintain 2 warriors per city - var want_military: bool = military_count < maxi(2, city_count * 2) + # Priority 4: Military — maintain 2 warriors per city, scaling up to + # match enemy's FULL army when they're closing on us so we don't lose + # on parity once reserves arrive. + var enemy_mil: int = enemy_total if threatened else 0 + var mil_target: int = maxi(4, city_count * 3) + if enemy_mil > 0: + mil_target = maxi(mil_target, enemy_mil + 1) + var want_military: bool = military_count < mil_target if want_military: var unit_id: String = _pick_buildable_military_unit_id(city, player) if not unit_id.is_empty(): diff --git a/src/simulator/crates/mc-city/src/city.rs b/src/simulator/crates/mc-city/src/city.rs index 438efbec..282c665d 100644 --- a/src/simulator/crates/mc-city/src/city.rs +++ b/src/simulator/crates/mc-city/src/city.rs @@ -99,11 +99,12 @@ impl TileYield { } } -/// Food consumed per citizen per turn. Lowered from 1.2 → 1.0 so late-game -/// cities don't stall against the exponential threshold curve. At pop 20, -/// consumption drops 24 → 20, freeing ~4 food/turn for growth. Target: -/// median p0_pop_peak ≥ 30 at T300 (Civ5-like capital size). -pub const FOOD_PER_POP: f64 = 1.0; +/// Food consumed per citizen per turn. Tuned to 1.2 (well below Civ5's 2.0) +/// so small cities can climb past the pop 2↔3 oscillation observed in iter10. +/// With 4-food center: pop 3 needs 3.6, breakeven on center alone (+0.4 +/// surplus); pop 4 = 4.8, covered by one 1-food tile; pop 6 = 7.2, reachable +/// with two decent food tiles. Target: median p0_pop_peak ≥ 7 at T150. +pub const FOOD_PER_POP: f64 = 1.2; /// Fraction of the previous growth threshold retained as stored food on /// growth (Civ5-style always-on granary effect). Each new pop starts with a @@ -724,10 +725,10 @@ mod tests { fn food_surplus_calculation() { let mut city = City::found("Ironhold", (5, 5), true, 1); city.population = 1; - // With no tile yields, city center gives 3 food. - // Surplus = 3.0 - 2.0*1 = 1.0 + // With no tile yields, city center gives 4 food. + // Surplus = 4.0 - 1.2*1 = 2.8 let surplus = city.get_food_surplus(&[]); - assert!((surplus - 1.0).abs() < f64::EPSILON); + assert!((surplus - 2.8).abs() < 1e-9); } #[test] @@ -740,27 +741,24 @@ mod tests { let ty = vec![ TileYield { coord: (6, 5), food: 5.0, ..TileYield::default() }, ]; - // Surplus per turn: (3 + 5) - 2*1 = 6.0 + // Surplus per turn: (4 + 5) - 1.2*1 = 7.8 // Threshold at pop 1: 15.0 - // Turn 1: food_stored = 6 + // Turn 1: food_stored = 7.8 assert_eq!(city.process_growth(&ty), 0); - assert_eq!(city.food_stored, 6.0); - // Turn 2: food_stored = 12 - assert_eq!(city.process_growth(&ty), 0); - assert_eq!(city.food_stored, 12.0); - // Turn 3: food_stored = 18 >= 15 → grow, carry 3.0 + assert!((city.food_stored - 7.8).abs() < 1e-9); + // Turn 2: food_stored = 15.6 >= 15 → grow, carry 0.6 assert_eq!(city.process_growth(&ty), 1); assert_eq!(city.population, 2); - assert_eq!(city.food_stored, 3.0); // 18 - 15 = 3 + assert!((city.food_stored - 0.6).abs() < 1e-9); } #[test] fn process_growth_starvation() { let mut city = City::found("Ironhold", (5, 5), true, 1); - city.population = 3; - // No worked tiles beyond center → yields = 3 food - // Consumption = 2*3 = 6. Surplus = 3 - 6 = -3 - // food_stored starts at 0, then 0 + (-3) = -3 < 0, pop > 1 → starve + city.population = 5; + // No worked tiles beyond center → yields = 4 food + // Consumption = 1.2*5 = 6.0. Surplus = 4 - 6.0 = -2.0 + // food_stored: 0 + (-2.0) = -2.0 < 0, pop > 1 → starve let ty: Vec = vec![]; assert_eq!(city.process_growth(&ty), -1); assert_eq!(city.population, 4);