feat(generation): Introduce balanced start position generation algorithm for multiplayer maps

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
autocommit 2026-04-16 13:52:26 -07:00
parent 73ca3fd3bf
commit e0b37c3b60
2 changed files with 31 additions and 4 deletions

View file

@ -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 --

View file

@ -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