diff --git a/src/game/engine/src/generation/map_placer.gd b/src/game/engine/src/generation/map_placer.gd index 98b5d089..953c7d60 100644 --- a/src/game/engine/src/generation/map_placer.gd +++ b/src/game/engine/src/generation/map_placer.gd @@ -8,6 +8,7 @@ const GameMapScript = preload("res://engine/src/map/game_map.gd") const TileScript = preload("res://engine/src/map/tile.gd") const HexUtilsScript = preload("res://engine/src/map/hex_utils.gd") const StartPositionScript = preload("res://engine/src/generation/start_position.gd") +const StartBalancerScript = preload("res://engine/src/generation/start_balancer.gd") const VillageLairPlacerScript = preload("res://engine/src/generation/village_lair_placer.gd") ## Resource placement density per land tile (base, before multiplier) @@ -37,7 +38,7 @@ func place_all( place_resources(game_map, resource_mult) var start_strategy: String = settings.get("start_strategy", "") - var start_positions: Array[Vector2i] = _start_position.select_start_positions( + var start_positions: Array[Vector2i] = _select_starts( game_map, num_players, type_data, start_strategy ) game_map.start_positions = start_positions @@ -53,6 +54,30 @@ func place_all( _village_lair_placer.place_lairs(game_map, wild_mult, start_positions) +func _select_starts( + game_map: RefCounted, num_players: int, + type_data: Dictionary, start_strategy: String, +) -> Array[Vector2i]: + ## Fairness-balanced selection (StartBalancer) for small 2-player games when + ## the map_generator.use_balanced_starts flag is set; greedy otherwise. + ## StartBalancer considers resource-adjusted yields and enforces a 0.85 + ## fairness ratio between start zones, avoiding the systemic p0-gets-resource + ## bias of the greedy scorer that ignores tile.resource_id. + if start_strategy == "": + var cfg: Dictionary = DataLoader.get_setup_entry("map_generator") + var enabled: bool = bool(cfg.get("use_balanced_starts", false)) + var max_players: int = int(cfg.get("balanced_starts_max_players", 2)) + if enabled and num_players <= max_players: + var balanced: Array[Vector2i] = StartBalancerScript.ensure_fair_starts( + game_map, num_players, type_data, _rng + ) + if balanced.size() == num_players: + return balanced + return _start_position.select_start_positions( + game_map, num_players, type_data, start_strategy + ) + + # -- Natural wonders -- diff --git a/src/game/engine/src/generation/start_balancer.gd b/src/game/engine/src/generation/start_balancer.gd index 9d8899dd..6e535556 100644 --- a/src/game/engine/src/generation/start_balancer.gd +++ b/src/game/engine/src/generation/start_balancer.gd @@ -127,18 +127,20 @@ static func ensure_fair_starts( if fallback.is_empty(): return [] + var final_ratio: float = 0.0 for _pass: int in 3: var score_a: float = score_start_zone(game_map, fallback[0]).total_value var score_b: float = score_start_zone(game_map, fallback[1]).total_value var max_score: float = maxf(score_a, score_b) - var ratio: float = minf(score_a, score_b) / max_score if max_score > 0.0 else 0.0 - if ratio >= MIN_FAIRNESS_RATIO: + final_ratio = minf(score_a, score_b) / max_score if max_score > 0.0 else 0.0 + if final_ratio >= MIN_FAIRNESS_RATIO: break var weak: Vector2i = fallback[0] if score_a < score_b else fallback[1] if not _compensate_resources(game_map, weak, rng): _compensate_terrain(game_map, weak, rng) - push_warning("StartBalancer: returning best available pair — fairness threshold not fully met") + if final_ratio < MIN_FAIRNESS_RATIO: + push_warning("StartBalancer: returning best available pair — fairness threshold not fully met") return fallback