The precon deck list was missing after the monolith refactor because
PRECON_DIR was hardcoded in services/precon_service.py with no CLI arg,
env var, or auto-resolve fallback — unlike forge_jar, forge_dir, etc.
Changes:
- Add precon_dir field to Config class (models/state.py)
- Add --precon-dir CLI arg and PRECON_DIR env var support
- Add _resolve_precon_dir() with project-root, parent-dir, and
forge_dir-relative detection; creates dir as fallback for downloads
- Refactor precon_service.py to use _get_precon_dir() helper that reads
CFG.precon_dir at runtime instead of a hardcoded module-level Path
- Update routes/precon.py to use _get_precon_dir() for install endpoints
- Wire _on_startup to resolve precon_dir for uvicorn-direct launches
- Remove unused PRECON_DIR/PRECON_INDEX imports from routes/lab.py
- Audit confirms all other CFG path fields already have fallbacks
Same pattern as the previous _resolve_forge_jar() fix — when --forge-dir
is not passed via CLI or FORGE_DIR env var, auto-detect the Forge
installation directory by:
1. Deriving from the already-resolved forge_jar path (parent of target/)
2. Checking common sibling directories (forge/, forge-repo/)
3. Checking platform user data dirs (~/.forge, ~/Forge, %APPDATA%/Forge)
Also audited all other CFG fields — forge_jar, forge_decks_dir, lab_jar,
port, results_dir all already have proper defaults or auto-resolve
fallbacks. Added Forge Dir, Lab JAR, and Results Dir to startup log
output for better diagnostics.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
During the refactor from the monolith into services/, the forge JAR path
had no auto-resolve fallback — unlike lab_jar and forge_decks_dir which
both had resolver functions. When --forge-jar CLI arg and FORGE_JAR env
var are both unset, CFG.forge_jar stayed "" causing POST /api/lab/start
to fail with "Forge JAR not found: ".
Add _resolve_forge_jar() that scans common sibling directories for
forge-gui-desktop-*-jar-with-dependencies.jar (matching the pattern
from start-lab.bat), and wire it as the fallback in main().
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The port of run_batch_subprocess from lab_api_backup.py to
services/forge_runner.py truncated the function signature to 8 params +
**kwargs, but the route handler in routes/lab.py passes 13 positional
arguments, causing TypeError at runtime when POST /api/lab/start fires.
Restore all 13 explicit parameters (policy_style, policy_greedy,
ai_simplified, ai_think_time_ms, max_queue_depth) to match the caller
and pass them as keyword args to build_java_command. Also compute
policy_server from CFG.port and add error handling, matching the
original backup implementation.
Additionally, _run_process_blocking was writing to wrong BatchState
attribute names (state.status, state.games_done, state.games_total,
state.throughput, state.log, state.started_at, state.ended_at) instead
of the correct ones (state.running, state.completed_games,
state.total_games, state.sims_per_sec, state.log_lines, state.elapsed_ms).
This caused the /api/lab/status endpoint to never see progress updates.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds SimulatorPage at /simulator with 4 deck slot selection, config panel, progress polling, per-seat win rate charts, and stats table. Backend: POST /api/sim/run-n-player endpoint using engine.run_n() for 2-4 player batch sims.
- Add POST /api/sim/run-n-player endpoint accepting 2-4 deck IDs for
N-player batch simulations using the existing engine.run_n() method
- Add _run_sim_thread_n_player tracking per-seat wins and stats
- Create SimulatorPage.tsx with 4 deck selection slots, config row,
progress bar, results table, CSS bar chart, and per-game logs
- Add simulator types in frontend/src/types/simulator.ts
- Add /simulator route and nav bar to App.tsx
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Moved all 15 /api/ml/* endpoints, 4 state dicts, and 3 background
pipeline functions from lab_api.py into routes/ml.py. Wired via
app.include_router(ml_router) in lab_api.py.
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.