saumwacht-godot/tools/luneraria/balance_run.gd
Martin Stickelbroeck faf0fbdef0 M3: Balance, Kartenprops, sieben Volksrepräsentanten und Abnahme
Bots planen Items/Champions vor der Armee und helfen deterministisch,
damit Spiegelmatches fair bleiben. Boss und Profile neu abgestimmt;
Balance-, Stress- und Volks-Werkzeuge sowie Kartenprops ergänzt.
2026-09-28 00:41:05 +02:00

74 lines
4.1 KiB
GDScript

extends Node
## Balance run: random team compositions (profiles and Bunds per slot), each played twice
## with the sides swapped. A QA round limit is reported as "Testlimit", never as a result.
## Run: godot --headless --path . res://tools/luneraria/balance_run.tscn -- --compositions=20 --limit=60 --out=res://docs/luneraria/logs/m3/balance.json
## add --matchups for all pairs of pure profile teams instead of random compositions.
func _ready() -> void:
call_deferred("run")
func play(options: Dictionary, limit: int) -> Dictionary:
var state := LuneMatch.new()
state.setup(options)
var lane_losses: Array[int] = [0, 0]
while state.phase != LuneMatch.Phase.OVER and state.round_number <= limit:
for slot: int in range(6):
LuneBot.plan(state, slot)
state.begin_combat()
state.finish_combat()
for team: int in range(2):
for lane: String in LuneMap.LANES:
lane_losses[team] += int(state.towers_alive(team, lane) == 0)
var result: String = "testlimit"
if state.phase == LuneMatch.Phase.OVER:
result = "draw" if state.winner == LuneMatch.DRAW else ("blau" if state.winner == 0 else "rot")
var gold_curve: Array = state.history.map(func(h: Dictionary) -> int: return (h.gold as Array).reduce(func(a: int, b: int) -> int: return a + b, 0))
return {"result": result, "rounds": state.history.size(), "lane_losses_at_end": lane_losses,
"boss": state.boss_log.map(func(e: Dictionary) -> String: return "%d:%s:%s" % [e.round, e.event, str(e.get("team", ""))]),
"hero_levels": state.history[-1].hero_levels if not state.history.is_empty() else [],
"timeouts": state.history.filter(func(h: Dictionary) -> bool: return h.end_reason == "timeout").size(),
"max_combat": state.history.map(func(h: Dictionary) -> float: return h.combat_time).max(), "total_gold_curve": gold_curve}
func run() -> void:
var compositions: int = 20
var limit: int = 60
var matchups: bool = false
var out: String = "res://docs/luneraria/logs/m3/balance.json"
for arg: String in OS.get_cmdline_user_args():
if arg.begins_with("--compositions="): compositions = int(arg.substr(15))
elif arg.begins_with("--limit="): limit = int(arg.substr(8))
elif arg.begins_with("--out="): out = arg.substr(6)
elif arg == "--matchups": matchups = true
var rng := RandomNumberGenerator.new()
rng.seed = 20260928
var names: Array = LuneBot.PROFILES.keys()
# --matchups: every pair of pure profile teams (all three slots alike) with the same
# Bund order on both sides, so only the strategy differs. Otherwise random compositions.
var setups: Array = []
if matchups:
for i: int in range(names.size()):
for j: int in range(i + 1, names.size()):
setups.append({"profiles": [names[i], names[i], names[i], names[j], names[j], names[j]],
"bunds": ["wurzelbund", "glutpakt", "wurzelbund", "wurzelbund", "glutpakt", "wurzelbund"], "seed": 2000 + setups.size()})
else:
for index: int in range(compositions):
var drawn := {"profiles": [], "bunds": [], "seed": 1000 + index}
for slot: int in range(6):
drawn.profiles.append(names[rng.randi() % names.size()])
drawn.bunds.append(DataCatalog.FACTIONS[rng.randi() % DataCatalog.FACTIONS.size()])
setups.append(drawn)
var rows: Array = []
for index: int in range(setups.size()):
var profiles: Array = setups[index].profiles
var bunds: Array = setups[index].bunds
var seed: int = int(setups[index].seed)
var normal: Dictionary = play({"seed": seed, "human_slot": -1, "profiles": profiles, "bunds": bunds}, limit)
var swapped_profiles: Array = profiles.slice(3, 6) + profiles.slice(0, 3)
var swapped_bunds: Array = bunds.slice(3, 6) + bunds.slice(0, 3)
var swapped: Dictionary = play({"seed": seed, "human_slot": -1, "profiles": swapped_profiles, "bunds": swapped_bunds}, limit)
var row := {"index": index, "seed": seed, "profiles": profiles, "bunds": bunds, "matchup": matchups, "normal": normal, "swapped": swapped}
rows.append(row)
print("BALANCE %02d %s | %s -> %s (%d) / swapped %s (%d)" % [index, str(profiles.slice(0, 3)), str(profiles.slice(3, 6)), normal.result, normal.rounds, swapped.result, swapped.rounds])
var file := FileAccess.open(out, FileAccess.WRITE)
file.store_string(JSON.stringify(rows, "\t"))
file.close()
get_tree().quit()