Added _send_to_graveyard() helper that checks card.is_commander
and routes to command_zone (resetting tapped/turn_played) or
graveyard. Applied to all 4 death paths: removal, board wipe,
blocker death, and attacker death in combat.
Second pass in _play_spells() checks command_zone for castable
commanders, applies commander_tax, taps lands, places on
battlefield, and increments tax by 2 for subsequent casts.
- Add commander_names param to run(), run_n(), _create_state()
- Commander card flagged is_commander=True and placed in
player.command_zone instead of shuffled into library/hand
- Only first matching card is extracted (handles singleton rule)
- Backward compatible: commander_names defaults to None
Adds command_zone (list[Card]) and commander_tax (int, +2 per cast)
to the base Player model so the headless engine can track commander
zone and tax natively without requiring CommanderPlayer wrappers.
- run_n() accepts 2-4 decks; run() wraps for backward compat
- _select_attack_target() heuristic for multiplayer combat targeting
- N-player elimination order tracking with finish positions
- Removal scans all opponents for scariest creature
- ML snapshot captures all N players
- Log entries use per-seat boards dict with legacy boardA/boardB for 2-player
Replace all print() calls with structured logging using the Python
logging module. This provides:
- Named loggers per subsystem: commander_ai_lab.api, .batch, .sim,
.coach, .deckgen, .collection, .scanner, .ml, .cache, .pplx
- Dual output: console (StreamHandler) + rotating file (RotatingFileHandler)
at logs/commander-ai-lab.log (25 MB x 5 backups = 125 MB max)
- Configurable via env vars: CAL_LOG_DIR, CAL_LOG_MAX_MB, CAL_LOG_BACKUP_COUNT
- --verbose / -v flag for DEBUG-level output
- overnight-run.py: separate rotating log at logs/overnight-run.log (10 MB x 5)
- Sub-module loggers in ml/, scanner/, deck_sources/, src/ all propagate
to the root commander_ai_lab logger
- Stripped redundant [TAG] prefixes from messages (logger name carries context)
- Format: '2026-03-13 14:00:00 INFO [commander_ai_lab.batch] message'
Files changed: lab_api.py, overnight-run.py, and 11 sub-module .py files
176 total print() calls converted (101 in lab_api.py, 76 in sub-modules)
GameResult.to_dict() now returns both legacy keys (winner, playerA, playerB)
that the UI expects AND new N-player keys (winningSeat, playerResults) for
the Java BatchResult schema.
Issue #1 — Winner's life total now captured:
- Parse verbose Forge log for life-total-change events
- Track last-known life per seat via 3 regex patterns
- Winner's final life set from log data (not left at default 40)
- Losers who died to mill/poison retain their actual life total
Issue #2 — Win condition classification formalized:
- New WinCondition enum (schema/WinCondition.java) with 8 categories
- Centralized Forge loss-reason-string → enum mapping (regex-based)
- UNKNOWN fallback with logging for unrecognized loss reasons
- All classification now goes through WinCondition.classify()
Issue #3 — JVM startup overhead reduced:
- Added -XX:+UseSerialGC (lighter GC for batch processes)
- Added -XX:TieredStopAtLevel=1 (faster JIT, less compilation)
- ForgeWorker stub class for future persistent batch mode
- Estimated reduction: ~2-3s → ~1-1.5s per game startup
Issue #4 — Single-threaded throughput optimization:
- New CLI flags: --ai-simplified, --ai-think-time, --benchmark
- JVM flags passed through to Forge subprocess
- Benchmark mode: run 1 game before batch, report wall/Forge/overhead times
- API support: aiSimplified, aiThinkTimeMs in StartRequest
Issue #5 — Threading, progress, and backpressure:
- --threads 0 or -1 auto-detects optimal count from CPU topology
- detectOptimalThreads() estimates physical cores (excludes HT)
- ProgressCallback interface for real-time sims/sec reporting
- Per-game progress emitted as [PROGRESS] lines, parsed by lab_api.py
- --max-queue flag for graceful backpressure (limits concurrent JVMs)
- Final throughput summary printed at batch completion
- StatusResponse now includes simsPerSec field
Also: .gitignore restored, lab_api.py updated with all new API fields