- Add max-height: 92vh and overflow-y: auto to .setup-card so it scrolls
instead of extending past the screen
- Widen .setup-card to 860px to accommodate 2-column deck grid
- Change .deck-assignment-grid from vertical stack to 2-column grid so
4 player panels fit in 2 rows instead of 4
- Reduce .deck-tab-content max-height to 170px and .deck-precon-list to
140px for tighter panels
- Make gradient bar sticky so it stays visible when scrolling
- Add responsive fallback to single column below 640px
- 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.
* 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>
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.
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.
_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.
_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.
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.
- 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
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.
- 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.
- 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
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.
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.
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.
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.
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.
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"
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.