saumwacht-godot/tools/luneraria/analyze_balance.py
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

55 lines
3.3 KiB
Python

#!/usr/bin/env python3
"""Summarise a balance_run JSON: side fairness, profile and Bund strength, match length."""
import json, sys
from collections import Counter, defaultdict
rows = json.load(open(sys.argv[1]))
side = Counter(); lengths = []; limit = 0; timeouts = 0; consistent = 0
prof_w = Counter(); prof_n = Counter(); bund_w = Counter(); bund_n = Counter(); boss = Counter()
max_combat = 0.0; hero_levels = []
for row in rows:
results = []
for key, flip in (("normal", False), ("swapped", True)):
game = row[key]
profiles = row["profiles"][3:] + row["profiles"][:3] if flip else row["profiles"]
bunds = row["bunds"][3:] + row["bunds"][:3] if flip else row["bunds"]
side[game["result"]] += 1
lengths.append(game["rounds"]); timeouts += game["timeouts"]; max_combat = max(max_combat, game["max_combat"])
hero_levels += [l for l in game["hero_levels"] if l > 0]
for e in game["boss"]:
boss[e.split(":")[1]] += 1
if game["result"] == "testlimit":
limit += 1
continue
winner = 0 if game["result"] == "blau" else 1 if game["result"] == "rot" else -1
# Winning composition: which original half won (0 = first three slots of the row).
results.append(-1 if winner < 0 else (winner if not flip else 1 - winner))
for slot in range(6):
prof_n[profiles[slot]] += 1; bund_n[bunds[slot]] += 1
if slot // 3 == winner:
prof_w[profiles[slot]] += 1; bund_w[bunds[slot]] += 1
if len(results) == 2 and results[0] == results[1] and results[0] >= 0:
consistent += 1
games = sum(side.values())
print(f"Partien {games} · Ergebnisse {dict(side)} · Testlimit {limit} · Zeitfenster-Kämpfe {timeouts} · längster Kampf {max_combat:.0f} s")
print(f"Wellen bis Ende: min {min(lengths)} · median {sorted(lengths)[len(lengths)//2]} · max {max(lengths)}")
print(f"Seitentausch: dieselbe Zusammensetzung gewinnt beide Male in {consistent}/{len(rows)} Paaren")
print("Profile (Siege/Einsätze):", {p: f"{prof_w[p]}/{prof_n[p]} ({100*prof_w[p]/max(1,prof_n[p]):.0f} %)" for p in sorted(prof_n)})
print("Bünde:", {b: f"{bund_w[b]}/{bund_n[b]} ({100*bund_w[b]/max(1,bund_n[b]):.0f} %)" for b in sorted(bund_n)})
print("Boss:", dict(boss), "· Heldenstufen am Ende (Median):", sorted(hero_levels)[len(hero_levels)//2] if hero_levels else "-")
if rows and all(row.get("matchup") for row in rows):
# Pure profile duels: wins of the row's first profile over its second, both sides.
print("Duelle (Siege Zeile gegen Spalte, je 2 Partien mit Seitentausch):")
names = sorted({row["profiles"][0] for row in rows} | {row["profiles"][3] for row in rows})
wins = defaultdict(int)
for row in rows:
a, b = row["profiles"][0], row["profiles"][3]
for key, flip in (("normal", False), ("swapped", True)):
r = row[key]["result"]
if r in ("blau", "rot"):
first_won = (r == "blau") != flip
wins[(a, b) if first_won else (b, a)] += 1
print(" " + " ".join(f"{n[:6]:>7}" for n in names) + " gesamt")
for n in names:
total = sum(wins[(n, m)] for m in names)
print(f"{n[:12]:<12}" + " ".join(f"{('-' if n == m else wins[(n, m)]):>7}" for m in names) + f" {total}/{2 * (len(names) - 1)}")