magicciv/tools/clan-signatures.py
Natalie 8e3107b92a feat(@projects/@magic-civilization): update tech-tree and mcts service implementation
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-04-25 22:48:40 -07:00

103 lines
3.6 KiB
Python
Executable file

#!/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))