Script to run sequential batch sims across 10 precon decks with
rotating playstyles (midrange, aggro, control, combo). Generates
ML decision data for training.
- 210 unique 4-player matchups from 10 precon decks
- Cycles through playstyles each batch
- Polls for completion, handles errors gracefully
- Ensures ML logging is enabled before starting
- Prints summary with new decision count at end
Usage: python scripts/overnight-batch.py --hours 8
The distillation pipeline was looking for JSONL decision files in
data/results/ but they are stored in results/ (matching CFG.results_dir).
This caused all generations to fail at Step 1 with:
'No ml-decisions-*.jsonl files found in data/results'
Fixed:
- routes/ml.py: results_dir and history_dir now use 'results/' not 'data/results/'
- ml/training/distillation_loop.py: updated DistillationConfig defaults to match
- History endpoint paths updated to match
- showMetaPicker() now guards for no backend with clear message,
and lazy-loads commanders on first open (fixes race condition
with Promise.all where loadMetaCommanders ran before detectBackend)
- filterMetaCommanders() hits live /api/lab/meta/search endpoint
when backend is connected, falls back to local array filter
- Search input debounced at 250ms to avoid hammering API
- Add _hoverTimer and HOVER_DELAY (450ms) properties to CardPreview
- mouseenter starts a delay timer before showing the popup
- mousemove only repositions if popup is already visible
- mouseleave clears the timer and hides immediately
- hide() now also clears any pending timer
- Create ml/eval/elo_tracker.py:
- EloTracker class with standard ELO formula (K=32, start=1200)
- EloResult dataclass for tournament output
- EloHistory class for JSON persistence (data/elo_history.json)
- run_elo_tournament(): round-robin ELO-rated matches using
existing run_match() from tournament.py
- run_generation_tournament(): discovers gen-*/best_policy.pt
checkpoints and runs cross-generation ELO tournament
- CLI entry point: python -m ml.eval.elo_tracker
- Add ELO API endpoints to routes/ml.py:
- POST /api/ml/elo/tournament — start background ELO tournament
- GET /api/ml/elo/tournament/status — poll progress
- GET /api/ml/elo/history — load ELO history from disk
- Integrate ELO convergence in distillation_loop.py:
- New config fields: elo_convergence_enabled (default False),
elo_convergence_threshold (20.0), elo_tournament_episodes (30)
- New field: GenerationRecord.elo_rating
- _run_elo_mini_tournament() runs after quality gate pass
- _check_convergence() checks ELO delta when enabled
- Add ELO UI to distillation page:
- ELO Ratings panel with canvas multi-line chart showing per-model
ELO progression across generations
- Run Tournament button with progress indicator
- ELO leaderboard table with rank, rating, and visual bars
- Responsive chart resize handling
Closes#70
- Add distillation API endpoints to routes/ml.py:
- POST /api/ml/distill/start — start closed-loop distillation pipeline
- GET /api/ml/distill/status — poll loop status and generation data
- POST /api/ml/distill/stop — graceful stop after current generation
- GET /api/ml/distill/history — load persisted generation history
- Add 'Distill' nav entry in ui/nav.js with loop icon
- Create ui/distillation.html:
- Status overview cards (status, generation, best win rate, gate passes)
- Control panel with hyperparameter inputs for supervised, PPO, and
quality gate settings
- Start/Stop buttons with progress bar and phase badge
- Win rate trend chart (canvas-based, no dependencies)
- Data composition stacked bar chart (Forge vs PPO per generation)
- Generation history table with status, metrics, and gate results
- Create ui/distillation.js:
- Polling loop for real-time status updates during distillation
- Canvas chart rendering for win rate trends and data composition
- History table rendering with color-coded status tags
- Responsive resize handling for charts
- Create ui/distillation.css:
- Page-specific styles following training.css patterns
- Chart container, history table, and responsive grid styles
Closes#69
Implements the closed-loop distillation pipeline that automates:
1. Supervised train on current dataset (Forge + PPO data)
2. PPO self-play using supervised model as starting policy
3. Export winning PPO decisions to JSONL
4. Quality gate check (win rate threshold)
5. If passed, merge into dataset and repeat
6. If failed, discard batch, adjust hyperparameters, retry
New files:
- ml/training/distillation_loop.py — DistillationLoop orchestrator,
DistillationConfig, GenerationRecord, CLI entry point
Updated files:
- ml/config/scope.py — DistillationDefaults dataclass with all
default hyperparameters for the pipeline
Features:
- Generation tagging: data and models tagged with gen-NNN
- Convergence detection: stops when win rate plateaus over N gens
- Automatic retry with LR reduction + entropy boost on gate failure
- Per-generation history saved to JSON for observability
- Full CLI with all tunable params
- progress_callback for UI integration (Phase 5 ready)
Closes#68
Phase 2 of closed-loop distillation pipeline.
- Scan both ml-decisions-sim-*.jsonl (Forge) and ml-decisions-ppo-*.jsonl (PPO)
- Add source_weights config for configurable sampling ratios
- Add min_reward_threshold filter for PPO data quality
- Tag each sample with source for downstream analysis
- New CLI args: --forge-weight, --ppo-weight, --min-reward
- NPZ output now includes 'sources' array
Phase 1 of closed-loop distillation pipeline.
Adds DecisionExporter class that captures PPO self-play decisions
as JSONL files compatible with the Forge DecisionSnapshot schema.
Includes PPO-specific metadata fields (source, model_version,
reward, episode_return) and configurable win-only filtering.
The guard condition was inverted: `if not deck_row:` entered the query
block when no deck was found, causing `deck_row[0]` to crash with
'NoneType' object is not subscriptable. Flipped to `if deck_row:`
so the query runs only when the deck exists.
- Add ml_log param, ml_decisions list, flush_ml_decisions() to GameEngine
- Capture ML snapshots at each player turn in run_n()
- Stamp game_outcome (win/loss per seat) on all decisions
- Add _capture_ml_snapshot() matching DeepSeekGameEngine format
- Add game_id and archetype params to run_n() for ML tracking
- Import logging, uuid; add logger
Extract player_explanation from validation dict in each turn log
entry within _run_game_loop(). The React frontend can now render
validator explanations when player_explanation is non-null.
Add "validation" key to _log_decision() entry dict in deepseek_brain.py.
Captures result.get("validation") (None if validator disabled)
for R1-14B output preservation and future analysis.