From 000cb911f83d57812ec057f9f8ff63e690ebf8d5 Mon Sep 17 00:00:00 2001 From: Natalie Date: Sun, 12 Apr 2026 23:30:03 -0700 Subject: [PATCH] =?UTF-8?q?feat(@projects/@magic-civilization):=20?= =?UTF-8?q?=E2=9C=A8=20improve=20city=20placement=20scoring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- src/game/engine/scenes/tests/auto_play.gd | 66 ++++++------------- .../src/modules/ai/simple_heuristic_ai.gd | 2 +- 2 files changed, 22 insertions(+), 46 deletions(-) diff --git a/src/game/engine/scenes/tests/auto_play.gd b/src/game/engine/scenes/tests/auto_play.gd index f8bdff2b..f034a263 100644 --- a/src/game/engine/scenes/tests/auto_play.gd +++ b/src/game/engine/scenes/tests/auto_play.gd @@ -909,7 +909,7 @@ func _pick_research(player: RefCounted) -> void: func _score_site(pos: Vector2i, game_map: RefCounted) -> float: - ## Score a hex as a city site. Food*2 + production*1.5 + resources + mixed-yield bonus. + ## Score a hex as a city site. Food*2 + production*1.5 + resources. var tile: Resource = game_map.get_tile(pos) if tile == null: return 0.0 @@ -918,14 +918,7 @@ func _score_site(pos: Vector2i, game_map: RefCounted) -> float: return 0.0 var score: float = 0.0 var y: Dictionary = tile.get_yields(-1) - var center_food: int = int(y.get("food", 0)) - var center_prod: int = int(y.get("production", 0)) - score += float(center_food) * 2.0 + float(center_prod) * 1.5 - # Heavy penalty for food-starved center biomes (mountain/tundra/desert). - if tile.biome_id in ["mountains", "tundra", "desert"]: - score -= 5.0 - var neighbor_food: int = 0 - var neighbor_prod: int = 0 + score += float(y.get("food", 0)) * 2.0 + float(y.get("production", 0)) * 1.5 for n: Vector2i in HexUtilsScript.get_neighbors(pos): var norm: Vector2i = HexUtilsScript.normalize_position(n, game_map.width, game_map.height, game_map.wrap_mode) var nt: Resource = game_map.get_tile(norm) @@ -934,46 +927,14 @@ func _score_site(pos: Vector2i, game_map: RefCounted) -> float: if nt.biome_id in water: score += 0.5 continue - if nt.biome_id == "river": - score += 2.0 var ny: Dictionary = nt.get_yields(-1) - var nf: int = int(ny.get("food", 0)) - var np: int = int(ny.get("production", 0)) - neighbor_food += nf - neighbor_prod += np - score += float(nf) * 0.5 + float(np) * 0.3 + score += float(ny.get("food", 0)) * 0.5 + float(ny.get("production", 0)) * 0.3 if nt.resource_id != "": - score += 3.0 - # Mixed-yield reward: at least one food + one prod in neighbors. - if neighbor_food >= 1 and neighbor_prod >= 1: - score += 2.0 + score += 2.0 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_founder(player: RefCounted) -> bool: +func _has_settler(player: RefCounted) -> bool: for u: Variant in player.units: if u.get("can_found_city") == true and u.is_alive(): return true @@ -1192,7 +1153,22 @@ func _command_unit(unit: Variant, player: RefCounted, game_map: RefCounted) -> v _command_worker(unit, player, game_map) return if unit.get("can_found_city") == true: - _decide_founder(unit, player, game_map) + # Check distance from existing cities + var too_close: bool = false + for c: Variant in player.cities: + if HexUtilsScript.hex_distance(unit.position, c.position) < 5: + too_close = true + break + # Score current tile quality + var site_quality: float = _score_site(unit.position, game_map) + if too_close or site_quality < 3.0: + _explore(unit, player, game_map) + else: + if _world_map != null and _world_map.has_method("_select_unit"): + _world_map._select_unit(unit) + if _world_map != null and _world_map.has_method("_on_found_city_pressed"): + _world_map._on_found_city_pressed() + print(" Founded city at %s (quality=%.1f)" % [unit.position, site_quality]) return var target_pos: Vector2i = _find_attack_target(player) 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 e3c5a98d..410ec84c 100644 --- a/src/game/engine/src/modules/ai/simple_heuristic_ai.gd +++ b/src/game/engine/src/modules/ai/simple_heuristic_ai.gd @@ -328,7 +328,7 @@ static func _decide_founder_action( if far_enough_from_own and clear_of_enemies: # Check tile quality — only found if the current tile is decent var quality: float = _score_city_site(unit.position) - if quality >= 1.0 or dist_own >= FOUND_MIN_DIST_OWN + 3: + if quality >= 3.0 or dist_own >= FOUND_MIN_DIST_OWN + 3: # Good site, or we've wandered far enough — settle here return { "type": "found_city",