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.
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.
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
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.
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.
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.
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.
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
* feat(rag): Phase 2 — ChromaDB vector index over bulk card DB
* feat(rag): add routes/rag.py with build, search, status endpoints
* Update lab_api.pyfeat(rag): register rag_router in lab_api.py, remove old inline rag_status
* Update requirements.txtfeat(rag): add chromadb>=0.5.0 to requirements.txtfeat(rag): add chromadb to requirements.txt
_run_sim_thread_deepseek was manually calling engine.flush_ml_decisions()
and writing the .jsonl, then passing engine=engine to _finish_sim which
called flush_ml_decisions() a second time — returning an empty list and
producing empty training data files.
Fix: pass engine=None to _finish_sim when the deepseek thread has already
flushed manually, so _finish_sim skips the second flush entirely.