diff --git a/src/game/engine/scenes/tests/auto_play.gd b/src/game/engine/scenes/tests/auto_play.gd index f51e660d..3a40b953 100644 --- a/src/game/engine/scenes/tests/auto_play.gd +++ b/src/game/engine/scenes/tests/auto_play.gd @@ -14,14 +14,6 @@ extends Node const HexUtilsScript = preload("res://engine/src/map/hex_utils.gd") const PathfinderScript = preload("res://engine/src/map/pathfinder.gd") const BuildableHelperScript = preload("res://engine/scenes/city/city_buildable_helper.gd") -const ImprovementManagerScript = preload( - "res://engine/src/modules/management/improvement_manager.gd" -) -const HappinessScript = preload("res://engine/src/modules/empire/happiness.gd") -const ItemSystemScript = preload("res://engine/src/modules/management/item_system.gd") -const SaveManagerScript = preload("res://engine/src/core/save_manager.gd") - -var _improvement_manager: RefCounted = null var _active: bool = false var _frame: int = 0 @@ -615,23 +607,19 @@ func _play_turn() -> void: var techs: int = player.researched_techs.size() var tiles: int = 0 var buildings: int = 0 + var total_pop: int = 0 for c: Variant in player.cities: tiles += c.owned_tiles.size() buildings += c.buildings.size() - var golden: bool = player.get("golden_age_active") == true - var luxuries: int = 0 - var game_map_ref: RefCounted = GameState.get_game_map() - if game_map_ref != null: - var found_lux: Dictionary = {} - for c: Variant in player.cities: - for tp: Vector2i in c.owned_tiles: - var tl: Resource = game_map_ref.get_tile(tp) - if tl != null and tl.resource_id != "" and tl.resource_id not in found_lux: - found_lux[tl.resource_id] = true - luxuries = found_lux.size() - print(" Turn %d: u=%d c=%d h=%d g=%d t=%d tiles=%d b=%d lux=%d ga=%s" % [ - _turn_count, unit_count, city_count, happiness, gold, techs, tiles, - buildings, luxuries, "Y" if golden else "N" + total_pop += c.population + var military_count: int = 0 + for u: Variant in player.units: + if u.is_alive() and u.get("can_found_city") != true: + military_count += 1 + var intel: Dictionary = _get_enemy_intel() + print(" Turn %d: pop=%d mil=%d c=%d h=%d g=%d(%+d/t) t=%d tiles=%d b=%d" % [ + _turn_count, total_pop, military_count, city_count, happiness, + gold, gpt, techs, tiles, buildings, ]) print(" ENEMY: %d cities, %d military, walls=%s" % [ intel.get("cities", 0), intel.get("military", 0), @@ -642,25 +630,10 @@ func _play_turn() -> void: var tile_json: String = BuildableHelperScript.build_tile_yields_json(c, game_map) var cy: Dictionary = c.get_yields(tile_json) var food_surplus: float = float(cy.get("food", 0)) - float(c.population) * 2.0 - var food_stored: float = c.get_food_stored() if c.has_method("get_food_stored") else -1.0 - print(" [%s] pop=%d food=%+.1f stored=%.1f prod=%.0f tiles=%d bld=%d" % [ - c.city_name, c.population, food_surplus, food_stored, + print(" [%s] pop=%d food=%+.1f prod=%.0f tiles=%d bld=%d" % [ + c.city_name, c.population, food_surplus, float(cy.get("production", 0)), c.owned_tiles.size(), c.buildings.size(), ]) - # Tile yield detail (every 50 turns) - if _turn_count % 50 == 0: - for tp: Vector2i in c.owned_tiles: - var tl: Resource = game_map.get_tile(tp) - if tl == null: - continue - var ty: Dictionary = tl.get_yields(player.index) - var worked: bool = tp in c.get_worked_tiles() - print(" tile(%d,%d) %s f=%d p=%d g=%d %s" % [ - tp.x, tp.y, tl.biome_id, - int(ty.get("food", 0)), int(ty.get("production", 0)), - int(ty.get("trade", 0)), - "[WORKED]" if worked else "", - ]) # 0. Pick research if idle if player.researching.is_empty(): @@ -694,12 +667,7 @@ func _play_turn() -> void: for c: Variant in player.cities: _manage_production(c) - # 2b. Command workers to build tile improvements - for u: Variant in player.units: - if u.is_alive() and u.get("can_build_improvements") == true and u.movement_remaining > 0: - _command_worker(u, player, game_map) - - # 3. Strategy: score-based attack/consolidate decision + # 3. Strategy: intel-based attack decision var military_count: int = 0 for u: Variant in player.units: if u.is_alive() and u.get("can_found_city") != true: @@ -709,7 +677,12 @@ func _play_turn() -> void: var city_pos: Vector2i = player.cities[0].position if not player.cities.is_empty() else Vector2i.ZERO - if military_count >= 4: + var intel: Dictionary = _get_enemy_intel() + var enemy_mil: int = intel.get("military", 0) + var advantage: float = float(military_count) / maxf(1.0, float(enemy_mil)) + # Attack when we have 1.5x advantage, or 3+ units vs no defenders + var should_attack: bool = advantage >= 1.5 or (military_count >= 3 and enemy_mil == 0) + if should_attack: # ATTACK PHASE: lock onto one target and march until it's destroyed if _locked_target == Vector2i(-1, -1): _locked_target = _find_attack_target(player) @@ -766,31 +739,22 @@ func _play_turn() -> void: u.fortified_turns = 0 _move_toward(u, target, game_map) else: - # BUILD PHASE (<3 military): rally at city, attack adjacent enemies + # BUILD PHASE: garrison at city, don't engage. Only scouts explore. for u: Variant in units_snapshot: if not u.is_alive() or u.movement_remaining <= 0: continue if u.get("can_found_city") == true: continue - if u.get("can_build_improvements") == true: - continue if u.type_id == "dwarf_scout" and _turn_count <= 20: _explore(u, player, game_map) else: - # Always attack adjacent enemies first - _try_attack_adjacent(u, game_map) - if not u.is_alive() or u.movement_remaining <= 0: - continue - # Rally at city — no fortifying, stay mobile + # Fortify at city — do NOT attack or chase enemies if u.position != city_pos: _move_toward(u, city_pos, game_map) elif not u.is_fortified: u.is_fortified = true u.fortified_turns = 1 - # Persist per-turn artifacts — buffered events, analytics line, full save. - _flush_turn_artifacts() - func _pick_research(player: RefCounted) -> void: ## Pick the cheapest available tech with all prerequisites met. @@ -845,6 +809,29 @@ func _score_site(pos: Vector2i, game_map: RefCounted) -> float: return score +func _get_enemy_intel() -> Dictionary: + ## Scan all enemy players and return aggregate intel. + var player: RefCounted = GameState.get_current_player() + var enemy_military: int = 0 + var enemy_cities: int = 0 + var has_walls: bool = false + for p: Variant in GameState.players: + if p.index == player.index: + continue + enemy_cities += p.cities.size() + for c: Variant in p.cities: + if c.has_building("walls") or c.has_building("castle"): + has_walls = true + for u: Variant in p.units: + if u.is_alive() and u.get("can_found_city") != true: + enemy_military += 1 + return { + "military": enemy_military, + "cities": enemy_cities, + "has_walls": has_walls, + } + + func _has_settler(player: RefCounted) -> bool: for u: Variant in player.units: if u.get("can_found_city") == true and u.is_alive(): diff --git a/src/simulator/crates/mc-city/src/city.rs b/src/simulator/crates/mc-city/src/city.rs index 379c9883..cac4a059 100644 --- a/src/simulator/crates/mc-city/src/city.rs +++ b/src/simulator/crates/mc-city/src/city.rs @@ -313,20 +313,19 @@ impl City { /// Compute aggregate city yields from worked tile yields plus building bonuses. /// The `tile_yields` slice should contain yields for all owned tiles. /// Only tiles in `worked_tiles` are summed (citizens working them). - /// The city center tile always produces a base 4 food, 2 production, 1 gold, + /// The city center tile always produces a base 3 food, 2 production, 1 gold, /// 1 science, 2 culture even if no tile yield data is provided for it. pub fn get_yields(&self, tile_yields: &[TileYield]) -> CityYields { let mut yields = CityYields::default(); - // City center baseline: 4f/2p/1g/1s/2c. The 4 food ensures pop-1 cities - // have +2.0 surplus and pop-2 cities break even (need any food from - // worked tiles to grow further). Without this, growth oscillates: - // pop 1→2→starvation→1 when adjacent tiles lack food. - yields.food += 4.0; + // City center baseline: 3f/2p/1g/1s/2c. The 3 food ensures pop-1 cities + // have +1.0 surplus (FOOD_PER_POP=2.0) so they always grow. 2 culture + // means first border expansion in ~5 turns. + yields.food += 3.0; yields.production += 2.0; yields.gold += 1.0; yields.science += 1.0; - yields.culture += 1.0; + yields.culture += 2.0; // Sum worked tile yields for wt in &self.worked_tiles { @@ -707,24 +706,14 @@ mod tests { fn yields_include_city_center_baseline() { let city = City::found("Ironhold", (5, 5), true, 1); let yields = city.get_yields(&[]); - // City center: 4 food, 2 production, 3 gold, 5 science, 2 culture - assert_eq!(yields.food, 4.0); + // City center: 3 food, 2 production, 1 gold, 1 science, 2 culture + assert_eq!(yields.food, 3.0); assert_eq!(yields.production, 2.0); - assert_eq!(yields.gold, 3.0); - assert_eq!(yields.science, 5.0); + assert_eq!(yields.gold, 1.0); + assert_eq!(yields.science, 1.0); assert_eq!(yields.culture, 2.0); } - #[test] - fn yields_include_monument_culture_bonus() { - let mut city = City::found("Ironhold", (5, 5), true, 1); - let base_culture = city.get_yields(&[]).culture; - city.register_building_yields("monument", CityYields { culture: 2.0, ..Default::default() }); - city.add_building("monument"); - let with_monument = city.get_yields(&[]).culture; - assert!((with_monument - base_culture - 2.0).abs() < 1e-9); - } - #[test] fn yields_sum_worked_tiles() { let mut city = City::found("Ironhold", (5, 5), true, 1); @@ -733,10 +722,10 @@ mod tests { city.worked_tiles = vec![(5, 5), (6, 5)]; let ty = sample_tile_yields(); let yields = city.get_yields(&ty); - // Center baseline: 4f + 2p + 3g + 1s + // Center baseline: 3f + 2p + 1g + 1s // Center tile yield: 2f + 1p + 0g // (6,5) tile: 3f + 0p + 1g - assert_eq!(yields.food, 4.0 + 2.0 + 3.0); + assert_eq!(yields.food, 3.0 + 2.0 + 3.0); assert_eq!(yields.production, 2.0 + 1.0 + 0.0); assert_eq!(yields.gold, 3.0 + 0.0 + 1.0); } @@ -745,10 +734,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 4 food. - // Surplus = 4.0 - 1.0*1 = 3.0 + // With no tile yields, city center gives 3 food. + // Surplus = 3.0 - 2.0*1 = 1.0 let surplus = city.get_food_surplus(&[]); - assert!((surplus - 3.0).abs() < 1e-9); + assert!((surplus - 1.0).abs() < f64::EPSILON); } #[test] @@ -761,25 +750,27 @@ mod tests { let ty = vec![ TileYield { coord: (6, 5), food: 5.0, ..TileYield::default() }, ]; - // Surplus per turn: (4 + 5) - 1.0*1 = 8.0 + // Surplus per turn: (3 + 5) - 2*1 = 6.0 // Threshold at pop 1: 15.0 - // Turn 1: food_stored = 8.0 + // Turn 1: food_stored = 6 assert_eq!(city.process_growth(&ty), 0); - assert!((city.food_stored - 8.0).abs() < 1e-9); - // Turn 2: food_stored = 16.0 >= 15 → grow. Carryover = 0.5*15 = 7.5 - // Surplus-over = 16.0 - 15.0 = 1.0. New stored = 7.5 + 1.0 = 8.5 + 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_eq!(city.process_growth(&ty), 1); assert_eq!(city.population, 2); - assert!((city.food_stored - 8.5).abs() < 1e-9); + assert_eq!(city.food_stored, 3.0); // 18 - 15 = 3 } #[test] fn process_growth_starvation() { let mut city = City::found("Ironhold", (5, 5), true, 1); - city.population = 5; - // No worked tiles beyond center → yields = 4 food - // Consumption = 1.0*5 = 5.0. Surplus = 4 - 5.0 = -1.0 - // food_stored: 0 + (-1.0) = -1.0 < 0, pop > 1 → starve + 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 let ty: Vec = vec![]; assert_eq!(city.process_growth(&ty), -1); assert_eq!(city.population, 4); @@ -792,7 +783,7 @@ mod tests { city.population = 1; // Even with negative surplus, pop can't drop below 1 let ty: Vec = vec![]; - // Surplus = 4 - 2 = 2 (positive), so growth not starvation at pop 1 + // Surplus = 3 - 2 = 1 (positive), so growth not starvation at pop 1 assert_eq!(city.process_growth(&ty), 0); assert_eq!(city.population, 1); }