104 lines
3.6 KiB
Python
104 lines
3.6 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""clan-signatures.py — per-clan behavior signature aggregated across all matchup-grid pairs.
|
||
|
|
|
||
|
|
Shows what each clan does differently when given the slot 0 (perspective) role,
|
||
|
|
across every pair it participates in. Useful for visualizing personality
|
||
|
|
differentiation that single-pair tier_peak deltas miss.
|
||
|
|
|
||
|
|
Usage:
|
||
|
|
python3 tools/clan-signatures.py <matchup-grid-batch-dir>
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
import statistics
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
CLANS = ["ironhold", "goldvein", "blackhammer", "deepforge", "runesmith"]
|
||
|
|
|
||
|
|
|
||
|
|
def med(xs: list[float]) -> str:
|
||
|
|
return f"{statistics.median(xs):.0f}" if xs else "-"
|
||
|
|
|
||
|
|
|
||
|
|
def main(argv: list[str]) -> int:
|
||
|
|
if len(argv) != 2:
|
||
|
|
print(f"Usage: {argv[0]} <batch-dir>", file=sys.stderr)
|
||
|
|
return 2
|
||
|
|
batch = Path(argv[1])
|
||
|
|
if not batch.is_dir():
|
||
|
|
print(f"Not a directory: {batch}", file=sys.stderr)
|
||
|
|
return 2
|
||
|
|
|
||
|
|
stats: dict[str, dict] = {
|
||
|
|
c: {
|
||
|
|
"games": 0, "victories": 0,
|
||
|
|
"tier_peaks": [], "units": [], "kills": [], "lost": [],
|
||
|
|
"gold": [], "combats": [], "techs": [], "cities": [],
|
||
|
|
}
|
||
|
|
for c in CLANS
|
||
|
|
}
|
||
|
|
|
||
|
|
for pair_dir in batch.iterdir():
|
||
|
|
if not pair_dir.is_dir() or "_vs_" not in pair_dir.name:
|
||
|
|
continue
|
||
|
|
for sub in pair_dir.iterdir():
|
||
|
|
if not sub.is_dir() or not sub.name.startswith("as_"):
|
||
|
|
continue
|
||
|
|
clan = sub.name[len("as_"):]
|
||
|
|
if clan not in stats:
|
||
|
|
continue
|
||
|
|
for game in sub.iterdir():
|
||
|
|
if not game.is_dir():
|
||
|
|
continue
|
||
|
|
ts = game / "turn_stats.jsonl"
|
||
|
|
if not ts.exists():
|
||
|
|
continue
|
||
|
|
try:
|
||
|
|
lines = [json.loads(l) for l in ts.read_text().splitlines() if l.strip()]
|
||
|
|
except Exception:
|
||
|
|
continue
|
||
|
|
if not lines:
|
||
|
|
continue
|
||
|
|
final = lines[-1]
|
||
|
|
ps = final.get("player_stats") or {}
|
||
|
|
agg = final.get("aggregate") or {}
|
||
|
|
p0 = ps.get("0") or {}
|
||
|
|
s = stats[clan]
|
||
|
|
s["games"] += 1
|
||
|
|
if final.get("outcome") == "victory":
|
||
|
|
s["victories"] += 1
|
||
|
|
s["tier_peaks"].append(p0.get("tier_peak", 0))
|
||
|
|
s["units"].append(p0.get("peak_unit_tier", 0))
|
||
|
|
s["kills"].append(p0.get("kills", 0))
|
||
|
|
s["lost"].append(p0.get("units_lost", 0))
|
||
|
|
s["gold"].append(p0.get("gold_peak", 0))
|
||
|
|
s["combats"].append(agg.get("total_combats", 0))
|
||
|
|
s["techs"].append(p0.get("techs", 0))
|
||
|
|
s["cities"].append(p0.get("cities", 0))
|
||
|
|
|
||
|
|
headers = ["clan", "games", "vic%", "tp", "unit", "kills", "lost", "gold", "combats", "techs", "cities"]
|
||
|
|
widths = [12, 7, 6, 4, 5, 6, 6, 6, 8, 6, 6]
|
||
|
|
print(" ".join(f"{h:<{w}}" for h, w in zip(headers, widths)))
|
||
|
|
print("-" * (sum(widths) + 2 * (len(widths) - 1)))
|
||
|
|
for clan in CLANS:
|
||
|
|
s = stats[clan]
|
||
|
|
if s["games"] == 0:
|
||
|
|
continue
|
||
|
|
vp = f"{s['victories'] * 100 // s['games']}%"
|
||
|
|
cells = [
|
||
|
|
clan, s["games"], vp,
|
||
|
|
med(s["tier_peaks"]), med(s["units"]),
|
||
|
|
med(s["kills"]), med(s["lost"]),
|
||
|
|
med(s["gold"]), med(s["combats"]),
|
||
|
|
med(s["techs"]), med(s["cities"]),
|
||
|
|
]
|
||
|
|
print(" ".join(f"{str(c):<{w}}" for c, w in zip(cells, widths)))
|
||
|
|
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
sys.exit(main(sys.argv))
|