savegame fix + chart updates

This commit is contained in:
sam-paech 2025-07-05 00:27:02 +10:00
parent 7edc7c465f
commit e351aa3841
4 changed files with 211 additions and 41 deletions

View file

@ -26,6 +26,7 @@ from __future__ import annotations
import logging
import re
import json
from pathlib import Path
from typing import List
@ -64,7 +65,16 @@ def _numeric_columns(df: pd.DataFrame, extra_exclude: set[str] | None = None) ->
def _phase_sort_key(ph: str) -> tuple[int, int]:
"""Convert 'S1901M' → (1901, 0)."""
"""
Sort key that keeps normal phases chronological and forces the literal
string 'COMPLETED' to the very end.
'S1901M' (1901, 0)
'COMPLETED' (9999, 9)
"""
if ph.upper() == "COMPLETED":
return (9999, 9) # always last
year = int(ph[1:5]) if len(ph) >= 5 and ph[1:5].isdigit() else 0
season = _SEASON_ORDER.get(ph[0], 9)
return year, season
@ -75,6 +85,27 @@ def _phase_index(series: pd.Series) -> pd.Series:
mapping = {ph: i for i, ph in enumerate(uniq)}
return series.map(mapping)
def _map_game_id_to_run_dir(exp_dir: Path) -> dict[str, str]:
"""
Reads each runs/run_xxxxx/lmvsgame.json file and returns
{game_id_string: 'run_xxxxx'}.
"""
mapping: dict[str, str] = {}
runs_root = exp_dir / "runs"
for run_dir in runs_root.glob("run_*"):
json_path = run_dir / "lmvsgame.json"
if not json_path.exists():
continue
try:
with json_path.open(encoding="utf-8") as fh:
data = json.load(fh)
gid = str(data.get("id", "")) # use top-level "id"
if gid:
mapping[gid] = run_dir.name
except Exception: # corrupt / unreadable → skip
continue
return mapping
# ───────────────────────── plots ────────────────────────────
def _plot_game_level(all_games: pd.DataFrame, plot_dir: Path) -> None:
@ -130,7 +161,15 @@ def _plot_game_level(all_games: pd.DataFrame, plot_dir: Path) -> None:
def _plot_phase_level(all_phase: pd.DataFrame, plot_dir: Path) -> None:
def _plot_phase_level(
all_phase: pd.DataFrame,
plot_dir: Path,
title_suffix: str = "",
) -> None:
"""
Plots aggregated phase metrics. If *title_suffix* is supplied it is
appended to each chart title handy for per-run plots.
"""
if all_phase.empty:
return
plot_dir.mkdir(parents=True, exist_ok=True)
@ -147,10 +186,10 @@ def _plot_phase_level(all_phase: pd.DataFrame, plot_dir: Path) -> None:
)
n_phases = agg["phase_index"].nunique()
fig_base_width = max(8, n_phases * 0.1 + 4) # 0.45 in per label + padding
fig_w = max(8, n_phases * 0.1 + 4)
for col in num_cols:
plt.figure(figsize=(fig_base_width, 6))
plt.figure(figsize=(fig_w, 6))
sns.lineplot(
data=agg,
x="phase_index",
@ -169,13 +208,40 @@ def _plot_phase_level(all_phase: pd.DataFrame, plot_dir: Path) -> None:
rotation=90,
fontsize=8,
)
title = col.replace("_", " ").title()
if title_suffix:
title = f"{title} {title_suffix}"
plt.title(title)
plt.xlabel("Game Phase")
plt.title(col.replace("_", " ").title())
plt.tight_layout()
plt.savefig(plot_dir / f"{_sanitize(col)}.png", dpi=140)
plt.close()
def _plot_phase_level_per_game(
all_phase: pd.DataFrame,
root_dir: Path,
gameid_to_rundir: dict[str, str],
) -> None:
"""
Writes one folder of phase-plots per iteration.
Folder name and chart titles use the run directory (e.g. run_00003).
"""
if all_phase.empty or "game_id" not in all_phase.columns:
return
for game_id, sub in all_phase.groupby("game_id"):
run_label = gameid_to_rundir.get(str(game_id), f"game_{_sanitize(str(game_id))}")
target = root_dir / run_label
# ── critical change: drop global phase_index so we rebuild a dense one ──
sub = sub.copy().drop(columns=["phase_index"], errors="ignore")
_plot_phase_level(sub, target, title_suffix=run_label)
# ───────────────────────── entry-point ─────────────────────────
def run(experiment_dir: Path, ctx: dict) -> None: # pylint: disable=unused-argument
root = experiment_dir / "analysis" / "statistical_game_analysis"
@ -210,5 +276,12 @@ def run(experiment_dir: Path, ctx: dict) -> None: # pylint: disable=unused-argu
sns.set_theme(style="whitegrid")
_plot_game_level(all_game_df, plots_root / "game")
_plot_phase_level(all_phase_df, plots_root / "phase")
game_map = _map_game_id_to_run_dir(experiment_dir)
_plot_phase_level_per_game(
all_phase_df,
plots_root / "phase_by_game",
game_map,
)
log.info("statistical_game_analysis: plots written → %s", plots_root)