feat(world-map): ✨ Implement deterministic tiebreaker cascade (score, cities, population, units) for victory conditions
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
d5938ca7df
commit
2915bba1dc
1 changed files with 60 additions and 13 deletions
|
|
@ -212,36 +212,75 @@ func _on_victory(player_index: int, victory_type: String) -> void:
|
|||
|
||||
|
||||
func _pick_winner_by_score() -> int:
|
||||
## Highest score wins; ties resolved deterministically by lower player index.
|
||||
## Tiebreaker cascade: score → cities → population → units → true draw (-1).
|
||||
## Without the cascade, strict `>` on score alone makes player 0 win every
|
||||
## tie — and because both arena players run the same heuristic as the same
|
||||
## race, scores always match. A true draw returns -1 and is serialized as
|
||||
## victory_type "draw" by _finish / _build_result_dict.
|
||||
var game_map: RefCounted = GameState.get_game_map()
|
||||
var best_idx: int = 0
|
||||
var best_score: int = -1
|
||||
var vm: VictoryManagerScript = _victory_manager as VictoryManagerScript
|
||||
var leaders: Array[int] = []
|
||||
var leader_stats: Array = []
|
||||
|
||||
for player: Variant in GameState.players:
|
||||
if not player is PlayerScript:
|
||||
continue
|
||||
var s: int = (_victory_manager as VictoryManagerScript).calculate_score(
|
||||
player, game_map
|
||||
)
|
||||
if s > best_score:
|
||||
best_score = s
|
||||
best_idx = player.index
|
||||
return best_idx
|
||||
var stats: Array = [
|
||||
vm.calculate_score(player, game_map),
|
||||
player.cities.size(),
|
||||
_total_population(player),
|
||||
player.units.size(),
|
||||
]
|
||||
if leaders.is_empty():
|
||||
leaders = [player.index]
|
||||
leader_stats = stats
|
||||
continue
|
||||
var cmp: int = _compare_stats(stats, leader_stats)
|
||||
if cmp > 0:
|
||||
leaders = [player.index]
|
||||
leader_stats = stats
|
||||
elif cmp == 0:
|
||||
leaders.append(player.index)
|
||||
|
||||
if leaders.size() != 1:
|
||||
return -1
|
||||
return leaders[0]
|
||||
|
||||
|
||||
func _compare_stats(a: Array, b: Array) -> int:
|
||||
## Lexicographic comparison across the tiebreaker axes.
|
||||
## Returns +1 if a beats b, -1 if b beats a, 0 if identical on every axis.
|
||||
for i: int in range(a.size()):
|
||||
var av: int = int(a[i])
|
||||
var bv: int = int(b[i])
|
||||
if av > bv:
|
||||
return 1
|
||||
if av < bv:
|
||||
return -1
|
||||
return 0
|
||||
|
||||
|
||||
# ── Result writing ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
func _finish(winner_index: int, victory_type: String) -> void:
|
||||
## winner_index == -1 signals a true draw — _build_result_dict translates
|
||||
## that into winner_name: "" and victory_type: "draw" regardless of the
|
||||
## caller-supplied type, so downstream consumers (ai-arena.sh summary,
|
||||
## tournament scorers) don't have to special-case both axes.
|
||||
_finished = true
|
||||
var result: Dictionary = _build_result_dict(winner_index, victory_type)
|
||||
_write_result(result)
|
||||
var winner_display: String = str(result.get("winner_name", "?"))
|
||||
if winner_display.is_empty():
|
||||
winner_display = "<draw>"
|
||||
print(
|
||||
(
|
||||
"[AI ARENA] match=%s finished: winner=%s type=%s turn=%d"
|
||||
% [
|
||||
_match_id,
|
||||
str(result.get("winner_name", "?")),
|
||||
victory_type,
|
||||
winner_display,
|
||||
str(result.get("victory_type", victory_type)),
|
||||
int(result.get("final_turn", 0)),
|
||||
]
|
||||
)
|
||||
|
|
@ -278,6 +317,14 @@ func _build_result_dict(winner_index: int, victory_type: String) -> Dictionary:
|
|||
if player.index == winner_index:
|
||||
winner_name = player.player_name
|
||||
|
||||
# winner_index == -1 is the canonical "no winner" sentinel from the
|
||||
# tiebreaker cascade. Force victory_type to "draw" so the result file
|
||||
# is internally consistent even if the caller passed "score".
|
||||
var final_victory_type: String = victory_type
|
||||
if winner_index == -1:
|
||||
final_victory_type = "draw"
|
||||
winner_name = ""
|
||||
|
||||
var duration_seconds: float = float(
|
||||
Time.get_ticks_msec() - _start_ticks_msec
|
||||
) / 1000.0
|
||||
|
|
@ -287,7 +334,7 @@ func _build_result_dict(winner_index: int, victory_type: String) -> Dictionary:
|
|||
"seed": _seed,
|
||||
"winner_index": winner_index,
|
||||
"winner_name": winner_name,
|
||||
"victory_type": victory_type,
|
||||
"victory_type": final_victory_type,
|
||||
"final_turn": GameState.turn_number,
|
||||
"turn_limit": _turn_limit,
|
||||
"players": players_payload,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue