The-Podium / game_log.py
MannKnee's picture
Initial deployment of The Podium
496ca41
Raw
History Blame Contribute Delete
10.6 kB
"""
Per-game session logging for troubleshooting and prompt refinement.
Writes one JSONL file per game under logs/ plus a human-readable summary .txt.
"""
from __future__ import annotations
import json
import os
import uuid
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any, Dict, List, Optional
import config
import content
if TYPE_CHECKING:
from engine import GameState, RoundLog, WordEntry
def _ts() -> str:
return datetime.now(timezone.utc).isoformat()
def _pool_snapshot(pool: List[WordEntry]) -> List[Dict[str, Any]]:
return [
{
"word": w.word, "category": w.category, "tier": w.tier,
"used": w.used, "round_used": w.round_used,
}
for w in pool
]
def _archetype_snapshot(arch: Optional[content.Archetype]) -> Optional[Dict[str, str]]:
if arch is None:
return None
return {"id": arch.id}
def _debater_snapshot(debater: Optional[content.Debater]) -> Optional[Dict[str, str]]:
if debater is None:
return None
return {
"id": debater.id, "name": debater.name, "emoji": debater.emoji,
"type": debater.type, "stats": debater.stats,
}
def _ability_snapshot(card: content.AbilityCard) -> Dict[str, Any]:
return {
"id": card.id,
"name": card.name,
"emoji": card.emoji,
"description": card.description,
"timing": card.timing,
}
def _judge_snapshot(judge: content.Judge) -> Dict[str, str]:
return {
"id": judge.id,
"name": judge.name,
"emoji": judge.emoji,
"bias_blurb": judge.bias_blurb,
}
def _llm_info() -> Dict[str, str]:
model = config.ANTHROPIC_MODEL if config.LLM_PROVIDER == "anthropic" else config.HF_MODEL
return {"provider": config.LLM_PROVIDER, "model": model}
def _sanitize_ability_args(args: Optional[Dict]) -> Optional[Dict]:
if not args or not args.get("word_flagged"):
return args
out = dict(args)
for key in ("wildcard_word", "plant_word"):
if key in out:
out[key] = config.FLAGGED_WORD_REPLACEMENT
return out
class GameLogger:
"""Append-only JSONL logger for a single game session."""
def __init__(self, game_id: str, log_dir: Optional[str] = None):
self.game_id = game_id
self.log_dir = log_dir or config.LOG_DIR
os.makedirs(self.log_dir, exist_ok=True)
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
base = f"game_{stamp}_{game_id[:8]}"
self.jsonl_path = os.path.join(self.log_dir, f"{base}.jsonl")
self.txt_path = os.path.join(self.log_dir, f"{base}.txt")
self._events: List[Dict[str, Any]] = []
def _write(self, event: Dict[str, Any]) -> None:
event = {"ts": _ts(), "game_id": self.game_id, **event}
self._events.append(event)
with open(self.jsonl_path, "a", encoding="utf-8") as f:
f.write(json.dumps(event, ensure_ascii=False) + "\n")
def log_game_setup(
self,
state: GameState,
player_flavor: str,
opponent_flavor: str,
) -> None:
self._write({
"event": "game_setup",
"llm": _llm_info(),
"player_pool": {
"flavor_seed": player_flavor,
"words": _pool_snapshot(state.player_pool),
},
"opponent_pool": {
"flavor_seed": opponent_flavor,
"words": _pool_snapshot(state.opponent_pool),
},
"opponent_roster": [_debater_snapshot(d) for d in state.opponent_roster],
"abilities_hand": [_ability_snapshot(c) for c in state.abilities_hand],
"abilities_enabled": config.ABILITIES_ENABLED,
})
def log_round_setup(
self,
state: GameState,
topic_meta: Dict[str, str],
) -> None:
self._write({
"event": "round_setup",
"round": state.round_num,
"topic": state.current_topic,
"topic_seed_word": topic_meta.get("seed_word"),
"topic_frame": topic_meta.get("frame"),
"judge": _judge_snapshot(state.current_judge),
"player_side": state.current_player_side,
"opponent_side": state.current_opponent_side,
"first_to_argue": state.current_first_to_argue,
})
def log_round_result(
self,
log: RoundLog,
opponent_rationale: str = "",
appeal: bool = False,
) -> None:
self._write({
"event": "round_appeal" if appeal else "round_result",
"round": log.round_num,
"topic": log.topic,
"judge": _judge_snapshot(log.judge),
"player_side": log.player_side,
"opponent_side": log.opponent_side,
"first_to_argue": log.first_to_argue,
"player_debater": _debater_snapshot(getattr(log, "player_debater", None)),
"opponent_debater": _debater_snapshot(getattr(log, "opponent_debater", None)),
"player_words": log.player_words,
"opponent_words": log.opponent_words,
"opponent_selection_rationale": opponent_rationale,
"player_argument": log.player_argument,
"opponent_argument": log.opponent_argument,
"ability_played": log.ability_played,
"ability_args": _sanitize_ability_args(log.ability_args),
"content_flag_active": log.content_flag_active,
"content_flag_ability": log.content_flag_ability,
"judge_result": log.judge_result,
"scores": {
"player": log.judge_result.get("player_score"),
"opponent": log.judge_result.get("opponent_score"),
},
})
def log_game_end(self, state: GameState) -> None:
total_p = sum(p for p, o in state.scores)
total_o = sum(o for p, o in state.scores)
rounds_won = sum(1 for p, o in state.scores if p > o)
rounds_lost = sum(1 for p, o in state.scores if o > p)
self._write({
"event": "game_end",
"winner": state.winner,
"rounds_won": rounds_won,
"rounds_lost": rounds_lost,
"total_points": {"player": total_p, "opponent": total_o},
"per_round_scores": state.scores,
})
self._write_summary_txt(state)
def _write_summary_txt(self, state: GameState) -> None:
lines = [
f"THE PODIUM — Game Log",
f"Game ID: {self.game_id}",
f"Log file: {self.jsonl_path}",
f"LLM: {_llm_info()['provider']} / {_llm_info()['model']}",
"",
"=" * 60,
"SETUP",
"=" * 60,
]
for ev in self._events:
if ev["event"] == "game_setup":
lines.append(f"Player flavor seed: {ev['player_pool']['flavor_seed']}")
lines.append(f"Player words: {', '.join(w['word'] for w in ev['player_pool']['words'])}")
lines.append(f"Opponent flavor seed: {ev['opponent_pool']['flavor_seed']}")
lines.append(f"Opponent words: {', '.join(w['word'] for w in ev['opponent_pool']['words'])}")
opp_roster = ev.get("opponent_roster", [])
lines.append("Opponent roster: " + ", ".join(
f"{d['emoji']} {d['name']}" for d in opp_roster
))
if ev["abilities_hand"]:
lines.append("Ability hand: " + ", ".join(
f"{c['emoji']} {c['name']}" for c in ev["abilities_hand"]
))
break
for ev in self._events:
if ev["event"] not in ("round_result", "round_appeal"):
continue
lines += [
"",
"=" * 60,
f"ROUND {ev['round']}" + (" (APPEAL RE-SCORE)" if ev["event"] == "round_appeal" else ""),
"=" * 60,
f"Topic: {ev['topic']}",
f"Judge: {ev['judge']['emoji']} {ev['judge']['name']}",
f"Sides: player={ev['player_side']}, opponent={ev['opponent_side']}",
f"First to argue: {ev['first_to_argue']}",
f"Player words: {', '.join(ev['player_words'])}",
f"Opponent words: {', '.join(ev['opponent_words'])}",
]
if ev.get("ability_played"):
lines.append(f"Ability played: {ev['ability_played']} {ev.get('ability_args') or ''}")
lines += [
"",
"--- Player argument ---",
ev["player_argument"],
"",
"--- Opponent argument ---",
ev["opponent_argument"],
"",
f"Scores: player={ev['scores']['player']} opponent={ev['scores']['opponent']}",
f"Verdict: {ev['judge_result'].get('verdict', '')}",
]
breakdown = ev["judge_result"].get("breakdown", {})
if breakdown:
lines.append(f"Breakdown: {json.dumps(breakdown)}")
for ev in self._events:
if ev["event"] == "game_end":
lines += [
"",
"=" * 60,
"FINAL",
"=" * 60,
f"Winner: {ev['winner']}",
f"Rounds: {ev['rounds_won']} - {ev['rounds_lost']}",
f"Total points: player={ev['total_points']['player']} opponent={ev['total_points']['opponent']}",
]
with open(self.txt_path, "w", encoding="utf-8") as f:
f.write("\n".join(lines) + "\n")
# ---------------------------------------------------------------------------
# Module-level helpers used by engine.py
# ---------------------------------------------------------------------------
_loggers: Dict[str, GameLogger] = {}
def new_game_logger() -> GameLogger:
game_id = uuid.uuid4().hex
logger = GameLogger(game_id)
_loggers[game_id] = logger
return logger
def get_logger(game_id: Optional[str]) -> Optional[GameLogger]:
if not game_id or not config.GAME_LOGGING_ENABLED:
return None
return _loggers.get(game_id)
def log_if_enabled(state: GameState, fn_name: str, *args, **kwargs) -> None:
"""Call a GameLogger method if logging is enabled for this state."""
logger = get_logger(getattr(state, "log_id", None))
if logger is None:
return
getattr(logger, fn_name)(*args, **kwargs)