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.
- 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)
- 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
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.
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.
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
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
- _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
- 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
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.
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
- _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
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.
* 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
- Add SubstitutionMethod enum to track how each card was resolved
- smart_substitute() now always returns a valid card string (never None/drops)
fallback chain: exact -> fuzzy -> functional analog -> original kept -> basic land
- apply_substitutions() loop no longer silently drops cards on failure
- _pad_to_99() circuit breaker added before BuildResult validation
- Pre-validation count logging per category for fast regression diagnosis
- Startup assertion: sum(SLOT_BUDGET) == 99 raises on app boot if misconfigured
- Prompt injection: slot budget + exact card count requirement sent to LLM
- run_substitution() anthropic fallback failure now pads with basic land instead of leaving slot missing
- SubstitutionBatchResult gains padded_slots field (> 0 = circuit breaker fired)
- anthropic_client.py: all 4 methods (chat_structured, chat_plain, achat_structured, achat_plain) now use .messages.stream() context manager
- coach_service.py: inline aclient.messages.create() replaced with async streaming via aclient.messages.stream()
- _parse_response() updated to accept pre-extracted text string (since stream gives us text directly)
- check_status() left as non-streaming (max_tokens=10, safe from the 10min rule)
Completes the sim → weights → deck builder feedback loop.
- Add run_update_weights() to overnight-run.py
- Add auto_update_weights config key (default True)
- Add --no-update-weights, --weights-min-games, --weights-lr CLI flags
- Runs before neural net training so both pipelines get fresh data
Closes#112
- Add sim_insights.py to translate learned_weights.json into LLM prompt text
- Add sim_context param to suggest_cards() and enforce_deck_ratios() in ollama_client.py
- Wire get_sim_insights() into build_deck pipeline (Steps 4 & 6)
- Fully backwards compatible — no-op until real sim data exists
- distillation.html: tooltips on all 10 params + ELO Ratings header
- simulator.html: tooltips on Opponent, Games, Record logs, API Endpoint, Model, Temp, Timeout
- coach.html: tooltips on Power Level, Meta Focus, Budget, Focus Areas, Similar Cards Results
- deckgenerator.html: tooltips on Target Bracket, Use my collection, Smart Substitution, AI Model
- All pages now share tooltip styles from global.css (no per-page duplication needed)