Commit Graph
100 Commits
Author SHA1 Message Date
rgriffithandClaude Opus 4.6 920d7d9787 fix: remove player color border and owner bar from battlefield cards
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 15:55:56 -04:00
rgriffithandClaude Opus 4.6 52d51ebd5e feat: battlefield UI overhaul — life panels, card sizing, layout
- Move life/cmd-dmg/poison into 3-box life panels as a top-level
  overlay (z-index 50) centered at quadrant intersections
- Remove center emblem
- Match battlefield card size to hand cards (63x84px)
- Overlay zone-row and commander-row on battlefield edges to
  eliminate dead space and maximize battlefield area
- Restructure poison badges from per-player absolute positioning
  into life-panel counters box

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 15:50:35 -04:00
rgriffithandClaude Opus 4.6 db3675aa6a feat: enlarge hand card thumbnails to fill zone height
Scale .hand-thumb from 32x45px to 63x84px so cards fill the
hand zone vertically while maintaining MTG card proportions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 14:36:50 -04:00
rgriffith 99044342c2 fix: remove stray } after showManaPool stub (line 1856) 2026-04-04 13:33:37 -04:00
rgriffith d5fd648139 fix: update tracked paths — files moved from root to lan-client/ 2026-04-03 23:31:36 -04:00
rgriffith 022f89478d fix: restore ui.js from placeholder corruption + Phase 1 patches 2026-04-03 23:25:01 -04:00
KoalaTrapLord 336f913d4b feat(phase-1): no-op mana pool display functions in ui.js
- renderManaPool() -> stub (floating #mana-pool-display removed from UI)
- showManaPool() -> stub
- Remove 'Mana Auto-Clear' row from renderSettingsPanel()

Internal mana tracking state (manaPool object, canPayManaCost,
clearManaPool, clearAllManaPools, etc.) fully preserved in engine.js.
createInitialState() already had energy/experience/rad/emblems.
setupManaPoolDrag() in engine.js already a no-op stub.
index.html and style.css require no changes.
2026-04-03 18:54:22 -04:00
KoalaTrapLord 661847a4d8 feat(phase-1): no-op mana pool display functions in ui.js
- renderManaPool() → stub comment (floating #mana-pool-display removed)
- showManaPool() → stub comment
- Remove 'Mana Auto-Clear' row from renderSettingsPanel()

Internal mana tracking state (manaPool object, canPayManaCost,
clearManaPool, etc.) fully preserved in engine.js.
createInitialState() already had energy/experience/rad/emblems.
setupManaPoolDrag() in engine.js already stubbed.
index.html and style.css require no changes.

Closes #190 Phase 1
2026-04-03 18:51:15 -04:00
KoalaTrapLord e710fd5e03 placeholder 2026-04-03 18:50:51 -04:00
KoalaTrapLord 85af163123 Wire Forge IPC into POST /api/game/start via run_batch_subprocess + --policy flag 2026-04-03 17:57:35 -04:00
KoalaTrapLord f2e87feb4e Fix default port: 8000 -> 8080 2026-04-03 17:54:15 -04:00
KoalaTrapLord 948f8e0f8f Fix PS1 parse errors: escape apostrophes, fix string terminators, fix scriptblock braces 2026-04-03 17:52:08 -04:00
KoalaTrapLord 84d56bf901 Add Test-PolicyServer.ps1 — smoke-test health + decide endpoint from PowerShell 2026-04-03 17:50:04 -04:00
KoalaTrapLord 74d24d606c fix: wire PolicyClient into real Forge IPC path via Priority interception
Forge's sim mode does not honour --policy/--policyServer CLI flags, so
the Python brain was never called during live games. AI only played lands
because Forge ran its built-in AI for every decision.

Fix: when parseForgeLine() detects a Priority event for an AI seat, the
session now immediately calls policyClient.decide() with the current
state snapshot and POSTs the result to /api/policy/override before Forge
reads the next line. This mirrors what handleHumanTurn() already does
for seat 0. The synthetic fallback path is unchanged.
2026-04-03 17:42:01 -04:00
KoalaTrapLord 98e1092ebc feat: wire DecisionExporter into Forge pipeline + fix reward call signatures
- Add _export_forge_episode() adapter: translates ForgeEpisode transitions
  back into DecisionExporter.record_step() calls so Forge-sourced wins flow
  into the distillation JSONL pipeline (closes Phase 1 for real games)
- ppo_consumer() now accepts an optional `exporter: DecisionExporter` arg;
  begin_episode / record_step / end_episode are called around each episode
  as it is dequeued
- run_forge_pipeline() forwards the exporter kwarg down to ppo_consumer and
  calls exporter.flush() after the pipeline completes, returning the output
  path in the stats dict under "exporter_file"
- Fix snapshots_to_episode(): compute_terminal_reward() was called with
  non-existent kwargs `turns=` and `life_remaining=`; compute_intermediate_reward()
  was called with non-existent kwargs `life_delta=`, `cards_drawn=`,
  `creatures_played=`.  Both now use the correct signatures from reward.py
  (prev_state/curr_state/action_taken for intermediate; won/draw/timeout for
  terminal).  Forge snapshots carry `players` arrays so we reconstruct minimal
  prev/curr dicts for the intermediate path.

Ref: docs/CLOSED-LOOP-DISTILLATION.md Phase 1
2026-04-03 17:09:05 -04:00
KoalaTrapLord 9338ac881b fix(labeler): infer cast sub-type from hand contents when action.card absent
The Forge DecisionExporter writes action={"type":"cast"} without a card
field — action.get("card") and action.get("raw") always returned "",
causing every cast to fall through to the CAST_CREATURE default.

Fix: when card_name is empty, scan the active player's hand against
the removal / draw / ramp / commander name databases and pick the
highest-priority match found.  Priority order mirrors cast importance:
  removal > ramp > draw > commander > creature (default)

A hand-level match is a reasonable proxy for the decision made:
Forge's AI picks the highest-priority action from available options,
so if the hand contains a removal spell during main phase, the cast
action almost certainly represents that removal spell being played.

Also fixes print_label_distribution: was sorting by dict key (string)
instead of by index, causing the display order to be alphabetical
rather than matching the MacroAction enum order — making cast_creature
appear at position 0 in the sorted output even when it had counts.
Now prints in descending count order with correct enum-keyed lookup.
2026-04-03 16:55:59 -04:00
KoalaTrapLord c3ff9f8888 feat(data): add DeepSeek ds- source with 4x default upweight
_discover_jsonl_files now recognises ml-decisions-ds-*.jsonl as its own
"ds" source key instead of falling through to legacy/forge.

DatasetConfig.source_weights gains ds=4.0 as a default so the 8 ds files
(1775 records, ~5.8% raw) are oversampled to ~20% of effective training
tokens, matching their higher signal quality.

CLI gains --ds-weight argument (default 4.0) to override without code changes.

The training_manifest.json produced by PowerShell already encodes weight=4.0
per ds file; this change makes dataset_builder.py honour the same ratio
natively so the manifest and the builder stay in sync.
2026-04-03 16:46:57 -04:00
KoalaTrapLord e4f35a2d8b debug: log top-5 action probabilities on every decide() call
Adds a single INFO log line after each successful inference showing:
  - chosen action + confidence
  - top-5 actions sorted by probability
  - turn, phase, vector_source

This lets us diagnose the 'only plays lands' behaviour without a Python
REPL. Remove or downgrade to DEBUG once the training issue is resolved.
2026-04-03 16:08:50 -04:00
KoalaTrapLord 084742a56a docs: rewrite README — full feature coverage from 52 merged PRs 2026-04-03 20:04:32 +00:00
KoalaTrapLord 45a413837b docs: rewrite README — full feature coverage from 52 merged PRs 2026-04-03 20:04:32 +00:00
KoalaTrapLord 018852c92d fix: collapse cmdr_dmg dict to int in _encode_global before division
Forge sends cmdr_dmg as a dict keyed by opponent seat (e.g. {"0": 14,
"2": 7}). _encode_global read it raw and crashed on `dict / 21.0`.

Fix: resolve cmdr_dmg to a scalar at the read site — sum dict values if
it's a dict, cast to int if it's already a number, default to 0.
This is defensive for all call paths (live Forge IPC, JSONL training,
_build_encoder_snapshot pre-collapsed int) so no upstream change needed.
2026-04-03 15:46:36 -04:00
KoalaTrapLord 4ca21e5e29 debug: add exc_info=True + unique logger names to expose real traceback
Both 'Inference error' log lines use identical logger strings, making it
impossible to tell which except block is catching the crash. Also, broad
except blocks were swallowing the full traceback so the real crash site
and cause were hidden.

Changes:
- policy_server.py: logger renamed to 'ml.serving.policy_server';
  predict() and predict_batch() error handlers add exc_info=True
- routes/policy.py: logger renamed to 'routes.policy.decide';
  _infer_from_vector() error handler adds exc_info=True;
  decide() fallback warning adds exc_info=True

The next crash will print the full stack trace, pinpointing the exact
line and call chain causing the TypeError.
2026-04-03 15:39:54 -04:00
KoalaTrapLord 28665bf663 fix: apply _unwrap_logits guard in _infer_from_vector (routes/policy.py)
The crash `unsupported operand type(s) for /: 'dict' and 'float'` was
firing from _infer_from_vector(), which is the code path taken when
has_precomputed_vector() is True (GameSession 1.1.0+ snapshots with
a precomputed state_vector float[29]).

Unlike PolicyInferenceService.predict() — which was fixed in the
previous commit via _unwrap_logits — this function calls
policy_service.model(state_tensor) directly and immediately uses the
return value in arithmetic (logits / max(temperature, 0.01)), so any
tuple/dict return (DataParallel, torch.compile, PolicyValueNetwork
swap) causes the TypeError.

Fix: import _unwrap_logits from ml.serving.policy_server and apply it
to the raw model output in _infer_from_vector before the temperature
division and argmax calls. All three return shapes are now handled:
  - plain tensor  → passthrough (normal case, zero overhead)
  - tuple/list    → raw[0]  (PolicyValueNetwork (logits, value))
  - dict          → first recognised key or first tensor value
2026-04-03 15:16:49 -04:00
KoalaTrapLord 83d05294c8 fix: type-guard logits after model() to prevent 'dict / float' inference crash
If self.model is wrapped (DataParallel, torch.compile, or future
PolicyValueNetwork path), forward() can return a tuple (logits, value)
or even a dict.  Add an explicit unwrap immediately after the forward
call in both predict() and predict_batch() so every downstream
temperature/softmax operation always receives a plain tensor.
2026-04-03 14:55:58 -04:00
KoalaTrapLord e8f8766d59 fix: register policy routes at module scope before StaticFiles mounts
The root cause of the persistent /api/policy/health 404:
- app.mount("/", StaticFiles(...)) is called at module load time
- StaticFiles acts as a sub-app and consumes matching requests,
  returning its own 404 for unknown files rather than falling through
- register_policy_routes() was called in lifespan (runtime), AFTER
  the "/" mount was already in the route list — so the policy router
  was effectively unreachable

Fix: move route registration to module scope right after all other
app.include_router() calls and BEFORE any app.mount() calls. Only
the model load (_POLICY_SVC.load()) stays in lifespan.
2026-04-03 13:09:12 -04:00
KoalaTrapLord a69b273f5f fix: always register policy routes at startup regardless of model load state
Previously /api/policy/health returned 404 when the model wasn't loaded
because register_policy_routes() was gated behind _policy_svc.load().
The health endpoint itself already handles the unloaded state gracefully
(returns {ready: false, status: degraded}), so the router must be mounted
unconditionally so ai-bridge.js can poll it during startup.
2026-04-03 12:29:18 -04:00
KoalaTrapLord e55ac9aebf feat: add Scryfall image enrichment after .dck load — batch fetch card art for all zones 2026-04-03 11:30:30 -04:00
KoalaTrapLord d2b86ef2ed fix: wire precon-loader.js into index.html script load order 2026-04-03 11:19:37 -04:00
KoalaTrapLord 081dd66121 fix: add startGameWithDecks() and loadPreconDeckFromDck() for precon .dck loading into simulator 2026-04-03 11:17:27 -04:00
KoalaTrapLord 6d89d5bb08 fix: add PRECON_DIR env var (PRECON_DECKS_DIR was never read by lab_api.py) 2026-04-03 11:15:31 -04:00
KoalaTrapLord f29a5a31b5 feat: add /api/lab/precons and /api/lab/precons/deck endpoints
Fixes 404 errors from lan-client ai-bridge.js which calls these routes
to populate the deck selector. Routes were missing from lab.py — only
precon_router (routes/precon.py) existed but served different paths.

Also added PRECON_DECKS_DIR env var alias in lab_api.py _parse_args()
to match the .env variable name users commonly set.
2026-04-03 11:08:12 -04:00
KoalaTrapLord 370d4cd92f Merge pull request #200 from KoalaTrapLord/fix/serve-lan-client-from-backend
fix: Serve lan-client from backend at /lan (same-origin)
2026-04-03 10:48:01 -04:00
Commander AI Lab e90efe1654 fix: serve lan-client from backend at /lan on port 8080
- Mount lan-client/ as static files at /lan with html=True
- Mount precon-decks/ at /precon-decks/ for direct .dck access
- ai-bridge.js: use same-origin (empty URLs, auto-detect from window.location)
- engine.js: simplify deck fetch to use /api/lab/precons/deck endpoint + /precon-decks/ static fallback
- Eliminates cross-origin issues by serving everything from localhost:8080

Access the LAN battlefield at http://localhost:8080/lan/
2026-04-03 14:30:55 +00:00
KoalaTrapLord bf701397b0 Merge pull request #199 from KoalaTrapLord/fix/deck-loading-and-backend-detection
fix: Deck loading — wire setupDeckChoices, fetch .dck content, add precon/deck endpoint
2026-04-03 09:49:33 -04:00
Commander AI Lab 7750b43494 fix: deck loading — wire startGameWithDecks, fetch .dck content, parse [sections]
Three fixes that prevented precon decks from loading into actual gameplay:

1. ui.js: window.startGame() now calls startGameWithDecks() when
   setupDeckChoices has entries (the 3-tab selector stores choices there).
   Previously it called initGame() which looked for old deck-sel-{i}
   dropdowns that no longer exist.

2. engine.js: loadPreconForPlayer() now fetches .dck file content from
   the backend (/api/lab/precons/deck?fileName=...) when precon.decklist
   is empty. Falls back to fetching precon-decks/{file} directly.
   PRECON_DATA entries only have metadata (name, commander, set) — the
   actual card list lives in the .dck files.

3. engine.js: parseDeckList() now handles .dck bracket-style section
   headers like [Commander], [Main], [metadata]. Previously only matched
   bare text like 'COMMANDER:' without brackets. Also skips [metadata]
   lines (e.g. 'Name=Elven Empire').

4. routes/precon.py: Added GET /api/lab/precons/deck?fileName=... endpoint
   to serve raw .dck file content for the LAN client.
2026-04-03 13:45:15 +00:00
KoalaTrapLord fa58463cde Merge pull request #198 from KoalaTrapLord/fix/setup-screen-layout
fix: Setup screen overflows viewport — 2-col deck grid + scroll
2026-04-03 09:38:26 -04:00
Commander AI Lab 94b1e050d8 fix: setup screen overflows viewport — constrain height and use 2-col deck grid
- Add max-height: 92vh and overflow-y: auto to .setup-card
- Widen .setup-card to 860px to fit 2-column deck grid
- Change .deck-assignment-grid from vertical stack to 2-column grid
  so 4 player panels render in 2 rows instead of 4
- Tighten .deck-tab-content (170px) and .deck-precon-list (140px)
- Make top gradient bar sticky on scroll
- Responsive fallback to single column below 640px
2026-04-03 13:37:02 +00:00
KoalaTrapLord 8bfde0230b fix: add precons.js with 163 entries and fix backend precon UI refresh (#197)
- Generate lan-client/precons.js from precon-decks/precon-index.json so
  PRECON_DATA is always available for local/offline mode (163 precons)
- Add scripts/generate-precons-js.js for regeneration when precons update
- Fix _loadBackendPrecons() to re-render the precon tab UI when backend
  precons load successfully (was only updating status indicator text,
  not populating the dropdown)

Without precons.js, PRECON_DATA was undefined and the dropdown showed
'No precon data loaded' even though 163 .dck files exist in the repo.
2026-04-03 09:31:54 -04:00
KoalaTrapLordandClaude Opus 4.6 4214831e59 feat: LAN Battlefield App Integration with AI Pipeline (#196)
* feat: LAN Battlefield App integration with Commander-AI-Lab AI pipeline

Add the LAN Battlefield App as a browser client (lan-client/) that
connects to the Commander-AI-Lab backend for ML-powered AI opponents,
3-category deck selection, and enhanced player panels.

Phase 1 - Foundation:
- Remove floating mana pool display (keep internal tracking)
- Add energy, experience, rad, emblems fields to game state
- No-op renderManaPool(), showManaPool(), setupManaPoolDrag()

Phase 2 - Backend Connection (ai-bridge.js):
- BackendConnector: REST + WebSocket connectivity with retry/timeout
- GameStateBridge: Serializes engine.js state for /api/policy/decide
- MacroActionMapper: Translates 8 ML macro-actions to engine.js calls
- DeckSourceClient: Fetches precon/EDHREC decks from backend API

Phase 3 - Deck System:
- 3-tab deck selector (Precons / EDHREC / My Decks) replaces dropdown
- Precon grouping by set with collapsible sections and filtering
- EDHREC search integration (stub, ready for backend implementation)
- My Decks textarea import placeholder

Phase 4 - AI Integration:
- runAiTurn() priority chain: ML Pipeline > Forge > Local Heuristic
- _runMlAiTurn() async turn loop with multi-phase decision requests
- Backend session initialization on game start
- Automatic fallback to local AI on any ML pipeline error

Phase 5 - Enhanced Player Panels (player-panels.js):
- Collapsible panels with localStorage persistence
- Commander damage progress bars with warning/critical thresholds
- Poison, energy, experience, rad counter widgets
- Emblem tracking with add/remove

Phase 6 - Polish:
- Script loading order: precons > engine > ai-bridge > panels > ui
- Backend auto-detection on page load with status indicator
- Connection status display on setup screen

Closes #184, Closes #185, Closes #186, Closes #187, Closes #188, Closes #189

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: refine LAN battlefield integration with full JSDoc, proper phase IDs

- Add comprehensive JSDoc comments to all ai-bridge.js classes
- Use optional chaining for PHASES lookup in GameStateBridge
- Direct addLogEntry calls in player-panels.js (loaded after engine.js)
- Remove leftover mana pool CSS that was missed in initial pass
- Minor style.css cleanup for consistency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-03 09:16:27 -04:00
KoalaTrapLord e2568c731c fix: auto-load .env in state.py so dotenv works without explicit load_dotenv call
When running plain `python script.py` or bare uvicorn, .env is never
loaded automatically. Added a dotenv bootstrap block at the top of
state.py that finds and loads the nearest .env file (walks up from
models/ to the project root). Uses load_dotenv with override=False so
explicitly set environment variables always win over .env values.
Gracefully skips if python-dotenv is not installed.
2026-04-03 08:41:58 -04:00
KoalaTrapLord d6863b6d7e fix: auto-populate all CFG paths from environment variables
CFG.forge_dir, precon_dir, forge_decks_dir, forge_jar, and lab_jar
were only set via CLI args. When the server starts via uvicorn directly
(no CLI), all paths stayed empty and forge_card_loader could not find
the cardsfolder, causing 0 lands in every deck.

Fix: read FORGE_DIR, FORGE_JAR, PRECON_DIR, PRECON_DECKS_DIR,
FORGE_DECKS_DIR, and LAB_JAR from environment at import time, mirroring
the existing ANTHROPIC_API_KEY / XIMILAR_API_KEY pattern.
2026-04-03 04:56:44 -04:00
KoalaTrapLord 1fc99a5d4a fix: auto-discover precon-decks/ and sample-decks/ when CFG paths unset
_find_dck_file only searched CFG.precon_dir / CFG.forge_decks_dir, both
of which are empty when the server is started without CLI args. This
caused all precon deck lookups to return 0 cards.

Fix: add _REPO_ROOT and _BUILTIN_DECK_DIRS constants, then append
precon-decks/, sample-decks/, imported-decks/ to the search list
whenever they exist on disk, regardless of whether CFG paths are set.
2026-04-03 04:50:49 -04:00
KoalaTrapLord eed2c7f126 New HTML battlefield to test features before Unity (#182) 2026-04-03 04:39:33 -04:00
KoalaTrapLord 7a40b5deb8 fix: enrich type_line/oracle from Forge cache in .dck fallback loader
_load_deck_cards_from_dck was hardcoding type_line="" for every card,
causing Card.is_land() to return False for all lands when a deck is
loaded via the .dck file path (i.e. not in the SQLite DB). This meant
the engine never recognised lands as lands, breaking mana production.

Fix: call lookup_forge_card() per card and populate type_line,
oracle_text, cmc, mana_cost, power, toughness, and keywords from the
Forge cardsfolder cache. The cache is lazy-loaded and module-level so
there is no per-game overhead after the first deck load.
2026-04-03 04:01:59 -04:00
KoalaTrapLord af8970f3a8 fix: _load_deck_cards_by_name falls back to .dck file when deck not in DB
Precon decks (Elven_Empire, Blood_Rites, etc.) are stored as .dck files
on disk but not inserted into the SQLite decks/deck_cards tables.
The DeepSeek batch engine calls _load_deck_cards_by_name() which
previously returned [] for any deck not in the DB, causing
'No decks could be loaded.' errors.

Adds _find_dck_file() and _load_deck_cards_from_dck() helpers.
_find_dck_file searches CFG.precon_dir, CFG.forge_decks_dir, and
FORGE_DIR/res/decks/commander with exact + fuzzy matching.
_load_deck_cards_by_name now tries DB first, then .dck fallback.
2026-04-02 22:59:01 -04:00
KoalaTrapLord 53fd1c9257 fix: wire RESULTS_DIR and FORGE_DIR env vars into CFG at startup 2026-04-02 14:24:28 -04:00
KoalaTrapLord 6ead75f96b fix: set FORGE_WORKING_DIR to forge-gui (requires res/ folder for sim to start) 2026-04-02 02:33:14 -04:00
KoalaTrapLord 535bdd7c70 fix: make Forge deck paths fully portable using %~dp0 and auto-generate forge.profile.properties 2026-04-02 00:59:10 -04:00
KoalaTrapLord d98e6331e9 fix: point all deck lookups to precon-decks directory
- 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
2026-04-01 23:10:32 -04:00
KoalaTrapLord 2b3196267f fix: raise default clock from 300ms to 5000ms to prevent turn-1 draw games 2026-04-01 22:56:29 -04:00
rgriffith 433e8bc4d7 feat: wire precon card art through local /card-image/ API with Scryfall fallback 2026-04-01 22:09:13 -04:00
KoalaTrapLord d74dd0a6bf feat: wire precon card art through local /card-image/ API with Scryfall fallback (full file) 2026-04-01 21:45:15 -04:00
KoalaTrapLord 8037802dff feat: wire precon card art through local /card-image/ API with Scryfall fallback 2026-04-01 21:45:01 -04:00
KoalaTrapLord ca6c23a881 fix: wrap shutil.copy2 in try/except for install and install-batch endpoints 2026-04-01 21:32:19 -04:00
KoalaTrapLord a81772c68a fix: set cwd=CFG.forge_dir on Popen so Forge finds res/ (#184) 2026-04-01 19:41:52 -04:00
KoalaTrapLord 99ca498b88 docs: add Unity TCGEngine Commander board implementation plan 2026-04-01 14:59:46 -04:00
KoalaTrapLord 395ad06cdd fix: resolve deck names to full .dck paths in build_java_command (#183) 2026-04-01 14:57:57 -04:00
KoalaTrapLord e4a6ce644d fix(#176): draw-game life capture, DrawGameAnalyzer, clock 6000→3000 (#181)
Captures life-at-timeout snapshots for every draw game via [DRAW] [PARSE-SUMMARY] log lines. Adds services/draw_game_analyzer.py with DrawGameSnapshot + summarize_draw_games(). Wires drawGames summary into GET /api/lab/result response. Lowers batch clock default from 6000 to 3000 (30s per-game). Closes #176.
2026-04-01 14:40:29 -04:00
KoalaTrapLord 5411fd6bf6 fix(#175): alternate win condition parser & winType tagging (#179)
Adds win_condition_parser.py, GameResult.win_type field, and winType stamping on both the Java/Forge and DeepSeek batch paths. Closes #175.
2026-04-01 14:28:10 -04:00
KoalaTrapLord 55fdc2ec4a feat: add CreateBattlegroundScene editor script to build full scene programmatically 2026-03-30 20:31:09 -04:00
KoalaTrapLord ddfe8e601e fix(eval): parse batch JSON output instead of JSONL for Forge game results 2026-03-30 16:12:20 -04:00
KoalaTrapLord 9e3cd45936 fix(eval): require >= 3 decks for Forge availability check 2026-03-30 15:45:20 -04:00
KoalaTrapLord c91f4c90bc fix(eval): discover 4 decks for 4-player Commander (Forge requires >= 3) 2026-03-30 15:43:27 -04:00
KoalaTrapLord 80f67dd0a0 fix(eval): use precon-decks/ for deck discovery instead of decks/ 2026-03-30 15:28:19 -04:00
rgriffith 10220ec747 feat: add 130+ precon commander deck files 2026-03-30 15:19:29 -04:00
rgriffith 6fadeaa387 fix(eval): replace Java bridge imports with subprocess CLI launch for Forge eval 2026-03-30 14:23:52 -04:00
rgriffith 517c17047b fix: strip value_head keys when loading PPO checkpoint into PolicyNetwork 2026-03-30 13:31:59 -04:00
KoalaTrapLord c62e75af52 fix(tests): update _build_document assertions to match NL sentence format 2026-03-30 12:36:00 -04:00
KoalaTrapLord 11940835fc fix: validate ollama suggest_cards names against DB + Scryfall before pipeline merge
- Add validate_card_names() helper: checks collection_entries DB first,
  then falls back to Scryfall /cards/named?fuzzy= for unknowns.
  Drops any name that cannot be resolved (hallucinated cards).
- Inject validation in Step 4 immediately after each suggest_cards()
  call so hallucinated names never reach ratio enforcement or assembly.
- Log dropped hallucinations at WARNING level with count + examples.
- Resolves the 'Sonic Hedgehog Charger' / 'Sonic Boom' class of
  LLM-invented card names that were silently entering the deck.
2026-03-30 12:33:28 -04:00
KoalaTrapLord d12b47f874 fix: robust suggest_cards parsing — CSV fallback + one-shot JSON prompt
- Add _parse_card_list_fallback() to recover plain-text CSV output from
  the LLM when json_mode fails to enforce JSON (e.g. qwen2.5:7b drift)
- Harden suggest_cards system prompt with a one-shot JSON example so
  small models are less likely to return prose in the first place
- Log recovered names at WARNING level so the degradation is visible
  without being silent (previously returned [] with no recovery attempt)
- No behaviour changes to any other function
2026-03-30 12:26:28 -04:00
KoalaTrapLord 5b5bac42fb fix: use vars(row) instead of dict(row) in _build_collection_from_bulk
dict() on a types.SimpleNamespace raises TypeError: object is not iterable.
vars() correctly extracts the __dict__ attribute map. Fixes
TestBuildCollectionFailureTracking::test_high_failure_rate_raises_runtime_error.
2026-03-30 10:39:49 -04:00
rgriffith 574bb79a0b fix(eval): correct indentation for load/reload in _load_policy_service 2026-03-30 10:36:43 -04:00
KoalaTrapLord 5554ef4dd7 fix(eval): call load() for embeddings before reload() for checkpoint 2026-03-30 09:53:56 -04:00
KoalaTrapLord cf2d7ed806 fix(eval): use reload() with explicit checkpoint path instead of load()
load() hardcodes best_policy.pt but PPO trainer saves as best_ppo.pt, causing the model to never load during eval. reload() accepts an explicit path.
2026-03-30 09:51:22 -04:00
KoalaTrapLord aec1896094 fix(engine): promote assign_attackers/assign_blockers/resolve_combat_damage to public methods (#86)
The three split-combat helpers were accidentally defined as nested
functions inside _resolve_combat(), making them unreachable as
GameEngine.assign_attackers() etc. from tests and turn_manager.

Move them out to class-level methods with correct 4-space indentation.
_resolve_combat() now remains as the backward-compatible atomic path for
run_n(); the split methods are available for turn_manager priority
windows and direct testing.
2026-03-30 09:50:56 -04:00
KoalaTrapLord 75785ae918 fix(tests): update RESPONSE_PHASES assertion to match current turn_manager design
begin_combat/end_combat are dispatched as named priority windows inside
_phase_combat(), not as entries in RESPONSE_PHASES. The constant is
correctly {"upkeep", "end_step"} — test was asserting stale intent.
2026-03-29 20:39:37 -04:00
KoalaTrapLord 252e46b288 fix(tests): swap Delta/Epsilon for Zeta/Theta to avoid _land_kw collision
Delta matches \bdelta\b in the nonbasic land guard regex, causing enrich_card
to classify it as a land (cmc=0). Zeta and Theta have no land keyword matches.
2026-03-29 20:37:04 -04:00
KoalaTrapLord e1bb2e7209 feat(coach): Phase 1 sim stats pass-through in prompt template (#108)
Surfaces previously-computed-but-unused stats into the Claude prompt:

1. USER_PROMPT_TEMPLATE gains a new '### Sim Insights' section that shows:
   - Deck-level opening hand quality: avg keptInOpeningHandRate across all
     cards, plus the 3 worst offenders with their actual rates
   - Curve velocity: avg avgTurnCast weighted by castRate, flagging cards
     cast on avg turn > 6 as 'late'
   - Synergy outliers: bottom-3 synergyScore cards flagged as 'isolated'
   - Archetype weak spots: archetypes with winRate < 40% flagged explicitly

2. _format_archetypes() now annotates weak spots with [WEAK SPOT] and
   strong matchups with [STRENGTH] so Claude sees them immediately.

3. _format_top_cards() adds keptInOpeningHandRate to each line so
   the 'good' cards also show their hand-keepability signal.

4. New helper _format_sim_insights() computes all deck-level aggregates
   from CardPerformance list — no new fields on DeckMeta needed.

No schema changes. Fully backward-compatible with existing DeckReport.
2026-03-29 05:55:51 -04:00
KoalaTrapLord e77a945805 test(encoder): unit tests for StateEncoder global features + Step 4 total_power (#83 Step 4)
New test file covering:
- TestGlobalFeatureDimensions: output shape (29,), all values in [0,1]
- TestTotalPowerRealValue: real total_power used at index 7 when present
- TestTotalPowerHeuristic: creatures*3 fallback when total_power absent/zero
- TestLandCount: real lands used at index 13 when present; defaults to 0
- TestCreatureCount: real creatures used at index 6; fallback to bf len
- TestManaFeature: mana normalised and clamped to [0,1]
- TestPhaseOneHot: exactly one hot in indices 9-12 per player block
- TestIsActivePlayer: binary 0/1 at index 8
- TestLifeNormalisation: 40 life → 1.0, 0 life → 0.0
- TestTurnFeature: turn clamped to [0,1]
- TestFullEncode: total state vector shape (6177,), dtype float32
- TestEmptyPlayer: _empty_player() has all Step 4 keys
- TestLegacySnapshot: old JSONL snapshot without Step 4 keys still encodes"
2026-03-29 05:46:01 -04:00
KoalaTrapLord 3f5d6d12e8 feat: 4-player Commander Battleground UI (issues #138–#151)
* chore: add battleground UI task tracker (2026-03-28)

* feat(#142): add [NonSerialized] battleground runtime state to CardModel

* feat(#143): add LoadArtCrop and LoadCardRaw (RawImage) to ImageCache

* feat(#148): add GameStateManager singleton and GameState/SeatState POCOs

* feat(#138): add BattlegroundController scene orchestrator

* feat(#139): add LifeTrackerWidget, CommanderDamageRow, PoisonCounter

* feat(#140): add PhaseTrackerWidget and PhaseStepButton

* feat(#141): add TurnOrderBar and TurnOrderPill

* feat(#142): add CardZone and CardView battlefield zone components

* feat(#143): add CommanderZoneWidget with art crop, cast counter, and tax display

* feat(#144): add TokenModel POCO, TokenPanelController, and TokenRowView

* chore: mark issues 1-8 as complete in task tracker

* feat(#146): add StackZoneController, StackItemView, and ManaPoolDisplay

* feat(#147 #150 #151): add LobbySetupModal, ZoneViewerModal, EliminationHandler; add Play Live button to SimulationController

* feat(#151): add Play Live button to SimulationController

* feat(#149): add GameWebSocketClient with reconnect, ping, and main-thread dispatch

* fix: add PostGameAction to ApiClient + update TODO.md all issues complete

- ApiClient.cs: add PostGameAction() wrapper → POST /api/game/action
  (fixes compile error in HumanActionBar.cs)
- TODO.md: mark issues #145, #146, #147, #149, #150, #151 as ✅
  all 14 battleground issues now committed

* feat(game): add routes/game.py + wire lab_api + fix GameWebSocketClient game_id URL

- routes/game.py: POST /api/game/start, POST /api/game/action (new)
- lab_api.py: import + register game_router
- GameWebSocketClient.cs: read game_id from PlayerPrefs, build
  ws://{host}/ws/game/{game_id} — fixes URL mismatch with ws_game.py

* fix(lobby): wire OnConfirm to POST /api/game/start, save game_id to PlayerPrefs

- OnConfirm() now calls ApiClient.PostGameStart() with 4-seat config
- On success: saves BattleSeats + GameId + WsUrl to PlayerPrefs,
  loads Battleground scene
- On failure: shows inline error label (red), re-enables Confirm button
- Added StartGameRequest/Response POCOs matching routes/game.py schema
- Added _errorLabel [SerializeField] for user-facing error feedback
2026-03-29 05:35:29 -04:00
KoalaTrapLord 915c745911 fix(eval): remove Set-StrictMode, fix curly-quotes and -- in strings (issue #153) 2026-03-29 05:31:58 -04:00
KoalaTrapLord 00374c1f03 feat(eval): add run_benchmark.ps1 — full benchmark pipeline for issue #153 2026-03-29 05:13:21 -04:00
KoalaTrapLord 6e71ba88fd fix(encoder): read real total_power from snapshot dict at global index 7 (#83 Step 4)
Previously _encode_global() always computed:
  total_power = creatures * 3  (heuristic)

Now reads p.get('total_power', 0) first, falling back to the heuristic
only when the key is absent (legacy JSONL snapshots without Step 4 data).

Also wires total_toughness, artifacts, and enchantments into _empty_player()
so padding players have consistent keys.
2026-03-29 05:13:19 -04:00
KoalaTrapLord 73d8e8d220 test(policy): add Step 4 board-stats tests + fix schema alias (#83 Step 4)
- TestBoardStatsWiring: verifies creaturesOnField, landCount,
  totalPowerOnBoard, totalToughnessOnBoard, artifactsOnField,
  enchantmentsOnField are accepted without 422 and survive
  a round-trip through the fallback encoder path
- TestBoardStatsFallback: verifies resolver fallbacks when Forge
  fields are absent (creatures inferred from bf length,
  total_power falls back to creatures * 3 heuristic,
  lands defaults to 0)
- TestSchemaAlias: verifies snapshot_schema Field(alias='schema')
  still accepts JSON key 'schema' without 422
- Removes now-irrelevant UserWarning (schema shadow fixed)
2026-03-29 04:51:47 -04:00
KoalaTrapLord bff7b3a81e feat(eval): add forge_evaluator, eval_policy, compare_eval for issue #153
- ml/eval/forge_evaluator.py: reusable eval harness wrapping
  MultiThreadBatchRunner + PolicyInferenceService; collects win rate,
  avg turns, life differential, commander damage, illegal action rate,
  policy entropy per game; writes results/eval-{run_id}.json
- scripts/eval_policy.py: CLI to run N Forge eval games with any
  checkpoint vs forge-builtin or policy-server opponent; supports
  --threads, --clock, --seed, --greedy, --temperature
- scripts/compare_eval.py: loads two result JSONs, computes absolute
  win-rate delta + 95% CI, runs Fisher's exact test for significance,
  prints formatted comparison table and pass/fail verdict
2026-03-29 04:49:43 -04:00
KoalaTrapLord b1c17359f0 feat(policy): wire real Forge board stats into encoder snapshot (#83 Step 4)
Previously _build_encoder_snapshot() stubbed:
  - lands = 0  (always zero)
  - creatures = len(bf_names)  (name-list length, ignores tokens/etc)
  - mana = mana_available  (was already correct)
  - total_power / total_toughness / artifacts / enchantments = missing

Now reads real pre-computed values from PlayerZoneState:
  - creatures      ← creaturesOnField   (Forge board stat)
  - lands          ← landCount          (Forge board stat)
  - total_power    ← totalPowerOnBoard  (fed to global feature index 7)
  - total_toughness← totalToughnessOnBoard
  - artifacts      ← artifactsOnField
  - enchantments   ← enchantmentsOnField
  - mana           ← manaAvailable (unchanged)

Also replaces the `creatures * 3` proxy in _build_encoder_snapshot with
the real totalPowerOnBoard value so index-7 of the global feature vector
tracks actual power rather than a heuristic.

PlayerZoneState gains 5 new optional int fields (default 0) matching
DecisionSnapshot.PlayerSnapshot field names so JSON from both the live
IPC path and the JSONL training path deserialise without changes.
2026-03-29 04:49:40 -04:00
KoalaTrapLord 7440d0ce17 test(policy): add round-trip tests for /api/policy/decide state_vector path (#83 Step 2)
Covers:
  - health endpoint returns {ready, status, model_loaded}
  - precomputed fast path: state_vector float[29] → vector_source=precomputed
  - correct 6177-dim tensor shape reaches the mock model
  - all 8 MacroAction values are valid responses
  - encoder fallback: missing state_vector → vector_source=encoder
  - encoder fallback: wrong-length state_vector → vector_source=encoder
  - GameSession field name aliases (turnNumber, activePlayer,
    commanderTax, handCount, commandZone) are accepted without 422
  - 503 when model not loaded
  - /api/policy/stats tracks precomputed_vector_hits vs encoder_fallbacks
  - both phase strings (MAIN1, main_1) round-trip correctly

Uses a MockPolicyInferenceService that captures the tensor shape passed
to the model so we can assert on dim without needing a real checkpoint.
2026-03-29 04:37:09 -04:00
KoalaTrapLord 4d73f134c2 feat(policy): consume state_vector from GameSession snapshots (#83 Step 2)
DecideRequest now accepts an optional state_vector field (float[29]).
When present, /api/policy/decide uses it directly for the global scalar
block instead of re-deriving it from nested player fields.

StateEncoder gets a new encode_with_precomputed_global() path that:
  - Accepts the precomputed float[29] global block from Java
  - Still runs full zone embedding pooling (hand/battlefield/graveyard/
    command_zone) from the players[] zone contents
  - Appends the playstyle one-hot as normal
  - Returns the same 6177-dim tensor the policy network expects

Fallback: if state_vector is absent or wrong length, encode() is called
as before so all existing callers (batch predict, JSONL training) are
unaffected.

Also maps GameSession player field names to encoder field names:
  handCount  -> hand size proxy when hand[] is empty (AI seats)
  battlefield list -> creatures count proxy
  commanderTax -> cmdr_tax
  turnNumber  -> turn
  activePlayer -> active_seat
2026-03-29 04:32:52 -04:00
KoalaTrapLord 126684bb94 feat(bridge): Phase 2.3 — wire GameSession to real Forge IPC subprocess (issue #83)
Replaces synthetic stub loop with a live Forge process:
- Spawns Forge with --policy + --policyServer flags so Forge calls
  back to /api/policy/decide at each AI decision point
- Reads Forge stdout line-by-line and parses zone/phase/life updates
  via the same regex patterns used in BatchRunner
- Syncs full board state (battlefield, hand, graveyard, exile,
  command zone, stack) on every Forge state-change line
- Routes human input from humanActionQueue via a side-channel
  HTTP POST to /api/policy/override on the policy server
- Handles activePlayer + priorityPlayer correctly for 3-4 player pods
- Falls back to synthetic stub loop if Forge subprocess fails to start
- Watchdog thread kills stalled Forge process after 10 min
2026-03-29 04:31:49 -04:00
KoalaTrapLord 882e2370c1 fix(#127): persist built_at in ChromaDB collection metadata as sidecar fallback
- _write_build_meta: after writing build_meta.json, also stamps built_at
  onto the collection via col.modify() (best-effort, swallows exceptions)
- _collection_is_current: if sidecar built_at is missing/zero, falls back
  to collection.metadata["built_at"] so a deleted sidecar doesn't force
  an unnecessary full rebuild when the ChromaDB data is still valid
2026-03-29 04:29:35 -04:00
KoalaTrapLord bbca0b7985 fix(#126): add batch insert retry logic with exponential backoff
- Add BATCH_RETRY_LIMIT and BATCH_RETRY_DELAY constants after DEFAULT_BATCH_SIZE
- Add _insert_batch_with_retry() helper with exponential backoff
- Replace both bare try/except insert blocks in _build_collection_from_bulk
  with calls to the retry helper so transient ChromaDB errors are retried
  up to BATCH_RETRY_LIMIT times before a batch is counted as failed
2026-03-29 04:24:58 -04:00
KoalaTrapLord da8de77704 feat(bridge): add buildStateVector() emitting state_vector float[] in snapshots (#83 Step 1)
Adds a buildStateVector(int seat) method to GameSession that produces
a float[29] global scalar vector matching STATE_DIMS layout in
ml/config/scope.py:

  Per-player block (14 features × 2 players = 28):
    [0]  life_total           / 40.0
    [1]  commander_damage     / 21.0  (stub 0.0 until real Forge tracks it)
    [2]  mana_available       / 20.0  (stub 0.0 until mana pool wired)
    [3]  commander_tax        / 10.0
    [4]  cards_in_hand        / 15.0
    [5]  cards_in_graveyard   / 100.0
    [6]  creatures_on_battlefield / 30.0
    [7]  total_power_on_board / 100.0  (stub = creature count until P/T data)
    [8]  is_active_player     (0 or 1)
    [9]  phase one-hot[0]  main1
    [10] phase one-hot[1]  combat
    [11] phase one-hot[2]  main2
    [12] phase one-hot[3]  end
    [13] (padding / reserved)

  Global:
    [28] turn_number / max_turns (25)

buildStateSnapshot() now includes "state_vector": float[] so that
PolicyClient.decide(JsonObject) sends a machine-readable vector the
Python /api/policy/decide endpoint can pass directly to the encoder
without re-parsing the nested human-readable fields.

Stub fields (mana_available, commander_damage, total_power_on_board)
are zero until real Forge board state is wired in Step 4.
2026-03-29 04:24:55 -04:00
KoalaTrapLord 11990c2fd3 feat(ml): add --data-source forge flag to PPO trainer (issue #83 phase 1.3)
Wires forge_episode_generator into the PPO training loop:
- New --data-source flag: synthetic (default) | forge | mixed
- New --forge-results-dir, --forge-batch-id, --forge-weight flags
- ForgeDataSource class: runs run_forge_pipeline() in background thread,
  drains episodes into RolloutBuffer via episodes_to_buffer()
- Mixed mode: blends Forge episodes + synthetic self-play per iteration
  using --forge-weight ratio (0.0–1.0)
- Synthetic path unchanged — no regression for existing training runs
2026-03-29 03:02:30 -04:00
KoalaTrapLord aefd0cace6 feat(rag): Phase 1 — rewrite build_card_text to natural-language format (#137)
* feat(rag): Phase 1 — rewrite build_card_text to natural-language format

* feat(rag): Phase 2 — Forge-enriched ChromaDB RAG store (2a–2e)

Phase 2a: Forge lazy import guard (_try_import_forge_lookup)
Phase 2b: _build_document appends Forge trigger/replacement/static/keyword tags
Phase 2c: _build_metadata adds has_trigger, has_replacement, has_static, trigger_modes fields
Phase 2d: query_cards extended with require_trigger, require_replacement, require_static params + ChromaDB where clause support
Phase 2e: get_rag_stats reports forge_enriched and forge_coverage; _write_build_meta tracks coverage; build loop counts enriched cards

* feat(rag): Phase 3 — add mtgrules second ChromaDB collection

Adds build_rules_index() and query_rules() operating on a separate
'mtgrules' ChromaDB collection. Chunks MagicCompRules.txt by rule-number
boundaries, embeds each chunk, and exposes semantic search. Completely
independent from the mtg_oracle card collection.

* feat(coach): Phase 4 — behavior-aware RAG + rules context in coach_service

Phase 4a: run_coaching_session uses dual RAG query
  - Primary: commander synergy (n=10)
  - Secondary: require_replacement=True for graveyard/protection cards (n=5)
  - Deduplicates by name before merging
Phase 4b: query_rules() fetches 3 Commander rules chunks to ground the prompt
  - Passed as rules_context kwarg to build_user_prompt()

* feat(rag): Phase 5 — add /api/rag/build-rules and /api/rag/query-rules endpoints
2026-03-29 02:11:38 -04:00
KoalaTrapLord ac536999eb Update lab_api.pyfeat: wire validate_forge_config into lifespan startup 2026-03-28 14:48:43 -04:00
KoalaTrapLord 2e43511d87 feat: add validate_forge_config() startup check
Validates FORGE_DIR at startup with clear warning if cardsfolder not found.
Checks both forge-gui/res/cardsfolder and res/cardsfolder layouts.
2026-03-28 14:34:50 -04:00
KoalaTrapLord 1bebe38e66 fix: forge_card_loader path resolution + reset_forge_cache helper
- _get_cards_folder() now tries both forge-gui/res/cardsfolder and res/cardsfolder
  so FORGE_DIR can point to either the repo root or the forge-gui subdirectory
- Add reset_forge_cache() to allow hot-reloading without server restart
2026-03-28 14:31:42 -04:00
KoalaTrapLord eca1653024 fix: backfill basic lands on card_entries to guarantee 99 cards
The previous backfill operated on seen_names dict which silently
ignored basics already present (dict keys are unique). Now backfills
directly on card_entries list after Scryfall fetch, incrementing
quantity on existing basics or appending new CardEntry objects.
Distributes shortfall evenly across commander color identity basics.
2026-03-28 00:26:42 -04:00
KoalaTrapLord 08f1cb7581 fix(deckgen): backfill basic lands after cross-category dedup to reach 99 cards 2026-03-28 00:10:52 -04:00
KoalaTrapLord 9d9e8675a9 fix: restore missing # on line 27 section banner in rules.py (SyntaxError on import) 2026-03-27 23:36:02 -04:00