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()