diff --git a/ml/eval/elo_tracker.py b/ml/eval/elo_tracker.py index d3a3905..1be0b15 100644 --- a/ml/eval/elo_tracker.py +++ b/ml/eval/elo_tracker.py @@ -16,6 +16,7 @@ Reuses `run_match`, `LearnedPolicy`, `HeuristicPolicy`, Usage: python -m ml.eval.elo_tracker python -m ml.eval.elo_tracker --episodes 50 --output data/elo_history.json + python -m ml.eval.elo_tracker --top-n 10 """ import json @@ -285,12 +286,17 @@ def run_generation_tournament( episodes_per_matchup: int = 50, playstyle: str = "midrange", k_factor: float = DEFAULT_K, + top_n: int = 10, ) -> EloResult: """ Discover generation checkpoints and run an ELO tournament. Looks for `gen-*/best_policy.pt` under checkpoint_dir. Also includes heuristic and random baselines. + + Args: + top_n: Only include the N most recent gen-* checkpoints. + 0 means include all (unbounded). Default is 10. """ policies: Dict[str, object] = { "heuristic": HeuristicPolicy(), @@ -299,7 +305,20 @@ def run_generation_tournament( ckpt_path = Path(checkpoint_dir) if ckpt_path.exists(): - for gen_dir in sorted(ckpt_path.glob("gen-*")): + gen_dirs = sorted(ckpt_path.glob("gen-*")) + + # Apply top_n cutoff — keep only the latest N generation dirs + if top_n > 0 and len(gen_dirs) > top_n: + skipped = len(gen_dirs) - top_n + logger.info( + "top_n=%d: skipping %d older generation(s), using %s … %s", + top_n, skipped, + gen_dirs[-top_n].name, + gen_dirs[-1].name, + ) + gen_dirs = gen_dirs[-top_n:] + + for gen_dir in gen_dirs: model_path = gen_dir / "best_policy.pt" if model_path.exists(): name = gen_dir.name # e.g., "gen-001" @@ -363,6 +382,10 @@ def main(): "--k-factor", type=float, default=32, help="ELO K-factor (default: 32)", ) + parser.add_argument( + "--top-n", type=int, default=10, + help="Only include the N most recent gen checkpoints (0 = all, default: 10)", + ) args = parser.parse_args() @@ -373,6 +396,7 @@ def main(): episodes_per_matchup=args.episodes, playstyle=args.playstyle, k_factor=args.k_factor, + top_n=args.top_n, ) # Print results diff --git a/routes/ml.py b/routes/ml.py index cb6a3b2..a1f50fa 100644 --- a/routes/ml.py +++ b/routes/ml.py @@ -1097,7 +1097,7 @@ _elo_tournament_state = _EloTournamentState() _elo_tournament_lock = threading.Lock() -def _run_elo_tournament_pipeline(episodes: int, playstyle: str): +def _run_elo_tournament_pipeline(episodes: int, playstyle: str, top_n: int): """Run ELO tournament in a background thread.""" try: import sys @@ -1108,7 +1108,7 @@ def _run_elo_tournament_pipeline(episodes: int, playstyle: str): with _elo_tournament_lock: _elo_tournament_state.running = True _elo_tournament_state.phase = "running" - _elo_tournament_state.message = "Running ELO tournament..." + _elo_tournament_state.message = f"Running ELO tournament (top {top_n} generations)..." _elo_tournament_state.error = None _elo_tournament_state.result = None @@ -1121,6 +1121,7 @@ def _run_elo_tournament_pipeline(episodes: int, playstyle: str): checkpoint_dir=ckpt_dir, episodes_per_matchup=episodes, playstyle=playstyle, + top_n=top_n, ) # Save to ELO history @@ -1155,29 +1156,37 @@ def _run_elo_tournament_pipeline(episodes: int, playstyle: str): @router.post("/api/ml/elo/tournament") async def ml_start_elo_tournament(request: FastAPIRequest): - """Start an ELO-rated tournament across all generation checkpoints.""" + """Start an ELO-rated tournament across generation checkpoints. + + Body params: + episodes (int): Games per matchup pair (default 50) + playstyle (str): Game playstyle (default midrange) + topN (int): Max number of most-recent gen checkpoints to include + (default 10, 0 = unlimited) + """ if _elo_tournament_state.running: raise HTTPException(409, "ELO tournament already in progress") body = await request.json() if await request.body() else {} episodes = body.get("episodes", 50) playstyle = body.get("playstyle", "midrange") + top_n = body.get("topN", 10) with _elo_tournament_lock: _elo_tournament_state.running = True _elo_tournament_state.phase = "starting" - _elo_tournament_state.message = "Initializing ELO tournament..." + _elo_tournament_state.message = f"Initializing ELO tournament (top {top_n} generations)..." _elo_tournament_state.result = None _elo_tournament_state.error = None thread = threading.Thread( target=_run_elo_tournament_pipeline, - args=(episodes, playstyle), + args=(episodes, playstyle, top_n), daemon=True, ) thread.start() - return {"status": "started", "episodes": episodes} + return {"status": "started", "episodes": episodes, "top_n": top_n} @router.get("/api/ml/elo/tournament/status")