132 lines
5 KiB
Python
Executable file
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))
|