Forge's sim mode looks up decks by NAME in its own
%APPDATA%/Forge/decks/commander/ directory. Passing a full path like
D:\...\precon-decks\Counterpunch.dck causes 'Could not load deck'.
Now strips the path and .dck extension so Forge sees just the bare
name (e.g. 'Counterpunch') and finds it in its deck folder.
Precon .dck files must be copied to %APPDATA%/Forge/decks/commander/
for Forge to find them.
The condition 'mlLoggingEnabled && !quiet && mlDecisionLogger != null'
silently skipped ML training data extraction whenever quiet mode was
active. DecisionExtractor works with the same log lines regardless of
quiet mode, so the guard was unnecessary and prevented JSONL generation.
Now: mlLoggingEnabled && mlDecisionLogger != null — ML logging runs in
all modes as long as --ml-log is passed.
## New File
- PolicyControllerAi.java (840 lines): Extends Forge's PlayerControllerAi
to intercept AI decisions at chooseSpellAbilityToPlay() and route them
through the Python policy server. Falls back to Forge built-in AI on
any failure. Inner PolicyAiController overrides AiController with full
legal action gathering via ComputerUtilAbility.
## Wiring
- GameSession.java: Import + Javadoc for PolicyControllerAi decision flow.
Logs decision pipeline when policy server connects. End-of-game reward
submission via PolicyControllerAi lifecycle.
- BatchRunner.java: New setPolicyServer(url) method. Appends
--policy midrange --policyServer <url> to Forge subprocess command when
policy server URL is configured.
## Path Updates (Forge-Repo rename)
- README.md: Forge link → KoalaTrapLord/Forge-Repo, build path updated
- .env.example: FORGE_JAR_PATH and FORGE_WORKING_DIR → Forge-Repo
- lab_api.py: Auto-detect paths try Forge-Repo first, forge-repo as legacy
## MacroActionExecutor
- Added public isRemovalCard(), isDrawCard(), isRampCard() wrappers so
PolicyControllerAi can consult the same card-role databases.
Refs: Issue #83 Phase 2.4
- start-lab.bat: Set FORGE_DECKS_DIR to D:\ForgeCommander\commander-ai-lab\precon-decks
- BatchRunner.java: resolveForgeDecksPath() now checks PRECON_DECKS_DIR env var
first, then falls back to the hardcoded precon-decks path, before trying
%APPDATA%\Forge\decks\commander. Also pass full absolute path to Forge -d flag.
- .env.example: document PRECON_DECKS_DIR variable
- CAST_PATTERN: Add optional (?:Add To Stack:\s*)? prefix to match Forge's verbose log format
- LAND_PLAY_PATTERN: Add optional (?:Land:\s*)? prefix to match Forge's verbose log format
- Add [PARSE] diagnostic logging after combat stats parsing to confirm spells/lands/cmdr stats per seat
Root cause: Forge verbose output prefixes cast lines with "Add To Stack: " and land lines with "Land: "
but the regexes expected Ai(N) at the start of the line, causing 0 matches for all combat stats.
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