Calls summarize_draw_games(state.draw_game_snapshots) after the existing
win-type injection and embeds the result under data['drawGames'] before
returning JSONResponse. The likelyCause field immediately surfaces which
deck most often leads at timeout so the Dictate of Kruphix loop theory
can be confirmed from the dashboard without log-diving.
Part of fix#176 (companion to services/draw_game_analyzer.py).
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.
* feat(83): add PolicyClient for live Forge-to-policy-server IPC (#83)
Phase 2.1: Bridge between Forge decision points and Python policy
server for online training. Calls /api/policy/decide endpoint,
collects (state, action, reward) tuples in real-time, and flushes
them to the server for online PPO updates.
Falls back to Forge built-in AI if server is unavailable.
* feat(83): add /api/policy/* routes for live Forge IPC (#83)
Phase 2.2: Policy decision routes for real-time Forge IPC.
- POST /api/policy/decide — full game state inference with log_prob/value
- POST /api/policy/collect — online learning tuple batch collection
- POST /api/policy/reward — end-of-game reward propagation
- GET /api/policy/health — readiness check for Java PolicyClient
- GET /api/policy/stats — session statistics
Includes Forge phase mapping and PlayerZoneState with full zone contents.
* feat(83): integrate PolicyClient into GameSession for bidirectional sync (#83)
Create services/logging.py as the canonical home for setup_logging(),
all log format constants, named loggers, and _ml_logging_enabled.
Update all route modules and lab_api.py to import logging symbols from
services.logging and all other symbols from their canonical sources
(models.state, models.requests, models.responses, services.database,
services.deck_service, services.scryfall, services.import_helpers,
services.import_service, services.precon_service, services.forge_runner).
Delete routes/shared.py — the re-export hub is no longer needed.
Closes#61
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The hardcoded 192.168.0.240 IP was a developer-specific LAN address
that breaks out-of-the-box for anyone running Ollama locally or via
WSL2 on Windows. Switch every default and docstring reference to
localhost so the project works without manual configuration.
Files changed:
- coach/config.py: LLM_URL and DECK_GEN_BASE_URL defaults
- ui/simulator.html: API endpoint input value and placeholder
- lab_api.py: remove LAN IP from default CORS origins
- routes/coach.py: warning message text
- src/commander_ai_lab/sim/deepseek_brain.py: docstrings
- src/commander_ai_lab/sim/deepseek_engine.py: docstring example
- src/commander_ai_lab/sim/validator_brain.py: docstring example
Co-authored-by: Claude Code <noreply@anthropic.com>
_get_validator_brain() defaulted VALIDATOR_MODEL to deepseek-r1:14b
and VALIDATOR_TIMEOUT to 120.0, conflicting with the brain config.
Changed to deepseek-r1:8b and 300.0 to match. Fixes issue #95 item 4.
Implements the three approved Phase 5 architecture decisions:
1. Zone embedding boundary: add explicit documentation in StateDimensions
and StateEncoder._encode_zones making the 6144-dim zero-padded zone
contract visible to future maintainers.
2. SQLite WAL online learning store: new OnlineLearningStore class backed
by WAL-mode SQLite for concurrent read/write on single-machine baseline.
New /api/policy/decide endpoint collects decisions during inference.
Stats endpoint at /api/policy/decide/stats.
3. Forge-only default weights: change ppo_weight default from 0.5 to 0.0
across scope.py, DistillationConfig, DatasetConfig, and CLI defaults.
Add MIXED_MODE_PRESETS dict with forge_only, forge_90_10, forge_80_20
presets. Distill/start endpoint now accepts a "preset" parameter.
27 new tests covering all three decisions (all passing).
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>