magicciv/time-to-tier-peak.py
2026-04-26 19:55:00 -07:00

132 lines
5 KiB
Python
Executable file

#!/usr/bin/env python3
"""time-to-tier-peak.py — when does each AI clan first hit its end-of-game tier_peak (era)?
Mirrors `tools/time-to-peak-unit.py` but tracks `tier_peak` (era progression
1..=10) instead of `peak_unit_tier` (unit-class 1..=6). Useful for the
warcouncil p1-29 target: "Hard/Insane AI should reach tier_peak ≥ 10 by T200".
Usage:
python3 tools/time-to-tier-peak.py <batch-dir> [<batch-dir>...]
"""
from __future__ import annotations
import json
import statistics
import sys
from pathlib import Path
def scan_game(game_dir: Path) -> list[dict]:
ts = game_dir / "turn_stats.jsonl"
meta = game_dir / "meta.json"
if not ts.exists() or not meta.exists():
return []
try:
meta_data = json.loads(meta.read_text())
player_clans = meta_data.get("player_clans") or {}
difficulty = meta_data.get("game_settings", {}).get("difficulty", "?")
except Exception:
return []
max_per_player: dict[str, int] = {}
end_turn = 0
turn_at_tier: dict[str, dict[int, int]] = {}
try:
for line in ts.read_text().splitlines():
if not line.strip():
continue
entry = json.loads(line)
end_turn = entry.get("turn", end_turn)
for pid, stats in (entry.get("player_stats") or {}).items():
tp = stats.get("tier_peak", 0)
if tp > max_per_player.get(pid, 0):
max_per_player[pid] = tp
# Record first turn each tier was reached (only once per tier).
player_tiers = turn_at_tier.setdefault(pid, {})
for tier in range(1, tp + 1):
if tier not in player_tiers:
player_tiers[tier] = entry.get("turn", 0)
except Exception:
return []
out = []
for pid, max_tp in max_per_player.items():
if max_tp == 0:
continue
turns = turn_at_tier.get(pid, {})
out.append({
"game": game_dir.name,
"difficulty": difficulty,
"player": pid,
"clan": player_clans.get(pid, "?"),
"max_tier_peak": max_tp,
"turn_first_max": turns.get(max_tp, end_turn),
"turn_first_t10": turns.get(10), # may be None
"turn_first_t6": turns.get(6),
"end_turn": end_turn,
})
return out
def main(argv: list[str]) -> int:
if len(argv) < 2:
print(f"Usage: {argv[0]} <batch-dir> [<batch-dir>...]", file=sys.stderr)
return 2
all_records: list[dict] = []
for arg in argv[1:]:
root = Path(arg)
if not root.is_dir():
print(f"skip non-dir: {root}", file=sys.stderr)
continue
for game in sorted(root.glob("game_*")) + sorted(root.glob("**/game_*")):
if not game.is_dir():
continue
all_records.extend(scan_game(game))
if not all_records:
print("No completed games found.")
return 1
print(f'{"game":<48} {"diff":<8} {"clan":<13} {"max_tp":<7} {"t_max":<6} {"t_t6":<6} {"t_t10":<7} {"end":<5}')
print("-" * 100)
for r in all_records:
t10 = "" if r["turn_first_t10"] is None else str(r["turn_first_t10"])
t6 = "" if r["turn_first_t6"] is None else str(r["turn_first_t6"])
print(
f'{r["game"][:46]:<48} {r["difficulty"]:<8} {r["clan"][:12]:<13} '
f'{r["max_tier_peak"]:<7} {r["turn_first_max"]:<6} {t6:<6} {t10:<7} {r["end_turn"]:<5}'
)
# Aggregate by difficulty
by_diff: dict[str, list[dict]] = {}
for r in all_records:
by_diff.setdefault(r["difficulty"], []).append(r)
print()
print(f'{"diff":<10} {"n":<4} {"med_max_tp":<11} {"med_turn_max":<13} {"reached_t10":<13} {"med_t10_turn":<13} {"med_end":<8}')
print("-" * 75)
for diff in sorted(by_diff):
rs = by_diff[diff]
med_max = statistics.median([r["max_tier_peak"] for r in rs])
med_turn = statistics.median([r["turn_first_max"] for r in rs])
t10_records = [r for r in rs if r["turn_first_t10"] is not None]
n_t10 = len(t10_records)
med_t10_turn = statistics.median([r["turn_first_t10"] for r in t10_records]) if t10_records else None
med_end = statistics.median([r["end_turn"] for r in rs])
t10_str = f"{n_t10}/{len(rs)}"
med_t10_str = f"{med_t10_turn:.0f}" if med_t10_turn else ""
print(f'{diff:<10} {len(rs):<4} {med_max:<11.1f} {med_turn:<13.0f} {t10_str:<13} {med_t10_str:<13} {med_end:<8.0f}')
# User-target check: is tier_peak=10 reached by T200?
print()
print("=== p1-29 user target check: tier_peak >= 10 by T200 ===")
for diff in sorted(by_diff):
rs = by_diff[diff]
hit_t10_by_200 = sum(1 for r in rs if r["turn_first_t10"] is not None and r["turn_first_t10"] <= 200)
verdict = "PASS" if hit_t10_by_200 >= len(rs) // 2 else "fail"
print(f" {diff}: {hit_t10_by_200}/{len(rs)} games reached tier_peak=10 by T200 — {verdict}")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))