Commit Graph
689 Commits
Author SHA1 Message Date
rgriffithandClaude Opus 4.6 69bdd94ef2 feat(lan-client): hide AI hands + persist Scryfall image cache
AI hand privacy (rendering layer only):
- renderHandZone(): when player.isAI, render face-down violet card backs
  (dark purple diagonal hatch + ornate ring) instead of face-up thumbnails.
  Count stays public (standard MTG). No image, name, preview, drag, or
  click-to-open-modal on face-down thumbs.
- renderZoneModal(): filter the "Hand" tab out of the zone modal when
  inspecting an AI opponent; reset activeZoneTab to 'graveyard' if it was
  'hand' when an AI modal opens.
- New CSS: .hand-thumb.face-down (+ .overflow variant) with violet hatch
  pattern, radial glow, and centered purple ring pseudo-element.

Scryfall image cache persistence (eliminates per-game refetching):
- _scryfallCache now saves to localStorage under commanderLab.scryfallCache.v1
- 30-day TTL, 20,000-entry cap, graceful quota-exceeded handling
- Debounced auto-save (750ms) after each batch enrichment
- Cache-hit logging: logs hit ratio when refetching, or "from cache" when
  a whole game's card pool is already in localStorage
- clearScryfallCache() helper exposed on window for manual purges

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 18:32:43 -04:00
rgriffithandClaude Opus 4.6 8fbebc21f3 feat(lan-client): charcoal felt battlefield + top-bar polish + return-to-start
Battlefield (charcoal felt):
- Darken --felt / --felt-light / --felt-dark to #181818 / #202020 / #0d0d0d
- Tighter --felt-line opacities (0.06 / 0.12) on the darker surface
- Finer-grain noise texture (baseFrequency 1.2, tile 180px) at 0.14 alpha
  with mix-blend-mode: overlay so the grain rides on the felt
- Deeper vignette (35% center, rgba(0,0,0,0.6) at edges)

Top bar:
- .top-bar-logo: 14px → 28px, align-self: stretch, flex-centered so
  "COMMANDER" fills the full 44px height
- .phase-step: 10px → 11px, roomier padding (2px 8px), gap 2→4px,
  letter-spacing 0.02em
- .btn-util: 22×22 → 32×32, font 10px → 15px (readable icons again)
- New .btn-util.btn-util-exit ✕ button (red-tinted, wired to
  confirmReset() → return to setup screen)
- Remove deck generator (⚡) and collection (📚) util buttons from the
  top bar (those panels are still reachable from other entry points)

Setup screen:
- Swap .setup-title from MTG gold to the same neon violet sign effect
  as the top-bar logo (4-layer text-shadow, uppercase, ls 0.08em)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 18:05:47 -04:00
rgriffithandClaude Opus 4.6 7cfdecb2c3 feat(lan-client): polish setup screen — charcoal bg, no rainbow bar, fit-to-screen
- .setup-card background: rgba(26,26,46,0.97) violet-tint → rgba(22,22,22,0.97)
  true charcoal that matches --color-surface-3
- Remove the 3px rainbow p1→p4 gradient strip from .setup-card::before
  (display: none)
- Compress spacing so the full lobby fits without scrolling:
  - padding 2.5rem → 1.25rem 1.5rem
  - title text-3xl → text-2xl, subtitle 2rem margin → 0.85rem
  - section margin 1.5rem → 0.75rem
  - label text-sm → text-xs with tighter margin
  - player-count-btn padding 0.75rem → 0.4rem
  - deck-precon-list max-height 140px → 90px
  - btn-start padding 1rem → 0.6rem

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 17:53:27 -04:00
rgriffithandClaude Opus 4.6 38f3837f5a feat(lan-client): animated smoky purple summoning-sick overlay
Replace the nearly-invisible black diagonal stripes with a layered
animated purple hatch so sick creatures read instantly as "can't attack":

- Double-layer repeating-linear-gradient (two purple stripe opacities)
- sick-drift animation (3s linear) — stripes crawl diagonally
- sick-pulse animation (2.4s ease-in-out) — soft opacity breathing
- Inset 14px purple box-shadow for hazy edge glow

Matches the --accent / violet palette from the AI Lab redesign.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 17:42:27 -04:00
rgriffithandClaude Opus 4.6 681888adcd feat(lan-client): port pure-black + violet + neon palette from AI Lab redesign
Apply the same 5-step visual redesign to the LAN client's MTG battlefield
theme while preserving functional game colors (mana, felt table, player
quadrants p1-p4).

- Swap Google Fonts import from Cinzel+Inter to Barlow Condensed + Barlow
- Pure black surfaces (#080808 / 0e / 11 / 16 / 1a)
- Swap primary accent from blue #5b8dd9 to electric violet #8b5cf6
- Square industrial radii (2/4/6/8px)
- Replace all hardcoded 'Cinzel' / 'Inter' with var(--font-display) /
  var(--font-body)
- Pure black top bar with violet underglow shadow
- Convert .top-bar-logo "Commander" title from gold to 4-layer violet
  neon sign with hover brightening
- Sweep 42 hardcoded rgba(91,141,217,...) blue values downstream to
  rgba(139,92,246,...) violet across buttons, zone tabs, stack items,
  combat UI, etc.

MTG functional colors (mana W/U/B/R/G, felt table, player quadrants)
remain intact.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 17:39:30 -04:00
rgriffithandClaude Opus 4.6 88c0d94d39 feat(ui): swap Inter for Barlow / Barlow Condensed display font
Load Barlow + Barlow Condensed from Google Fonts across all 8 HTML
entry points. In global.css introduce --font-display (Barlow Condensed)
alongside --font (Barlow), then wire the display font into every
chrome/heading selector: h1-h6, .page-header, .panel-header, .btn,
.stat-label/.stat-value, .form-label, .badge-status, .data-table
thead, .tag, .nav-brand, .nav-links. Body copy stays in Barlow for
readability.

Also normalize a few hardcoded font-families to design-system vars:
collection.css Cascadia Code stacks and deckgenerator.css Courier New
now use var(--font-mono); canvas rendering in distillation.js and
ai-lab.js swapped from Inter to Barlow so charts match the new type.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 17:26:06 -04:00
KoalaTrapLord 4fdddf332e fix: add logging.basicConfig to trainer.py so training output is visible
The trainer used logger.info() for all output but never configured a
handler, so training ran silently with no console output.
2026-04-05 18:10:09 +00:00
KoalaTrapLord 213b9dc32d fix: total_mem → total_memory in trainer.py (PyTorch API) 2026-04-05 18:06:50 +00:00
KoalaTrapLord 2af1d58104 fix: write ML JSONL to commander-ai-lab/results/ (not Forge CWD)
LabCli passed relative 'results' to enableMlLogging(), which resolved
to Forge-Repo/forge-gui/results/ because the Java subprocess CWD is
forgeWorkDir. Now uses outputPath's parent directory (absolute) so
JSONL training data goes to commander-ai-lab/results/ where the
Python ML pipeline and Training page expect it.
2026-04-05 17:29:21 +00:00
KoalaTrapLord 547a84d8c2 fix: DecisionExtractor regex patterns to match actual Forge verbose output
Forge verbose log uses different verb forms and prefixes than the
patterns expected:
  - Turn: 'Turn: Turn 5 (Ai(1)' not 'Turn 5 (Ai(1)'
  - Cast: 'cast' (no s) not 'casts', 'Add To Stack:' prefix, no period
  - Land: 'played' not 'plays', 'Land:' prefix, card ID suffix '(76)'
  - Draw: 'drew' variant, card ID suffix

Updated all four patterns to match both old and new Forge output
formats. This is the final fix for the training data pipeline —
DecisionExtractor will now find cast/land/draw/attack events in
the Forge game log and emit JSONL training snapshots.
2026-04-05 17:24:34 +00:00
KoalaTrapLord 71b86b74eb fix: compile error in BatchRunner deck name log line 2026-04-05 16:55:38 +00:00
KoalaTrapLord d00093897a fix: read Name= from .dck header for Forge deck lookup
Forge's sim mode looks up decks by the Name= field inside the .dck
file metadata header, NOT by filename. For example:
  Filename: Counter_Intelligence.dck
  Header:   Name=Counter Intelligence
  Forge expects: 'Counter Intelligence' (with space)

BatchRunner now reads the Name= field from the .dck file and passes
that to Forge. Falls back to filename with underscores→spaces if
the Name= field can't be read.
2026-04-05 16:54:38 +00:00
KoalaTrapLord 0b01f1c467 fix: pass bare deck names to Forge sim (not full paths)
Forge's sim mode looks up decks by NAME in its own
%APPDATA%/Forge/decks/commander/ directory. Passing a full path like
D:\...\precon-decks\Counterpunch.dck causes 'Could not load deck'.

Now strips the path and .dck extension so Forge sees just the bare
name (e.g. 'Counterpunch') and finds it in its deck folder.

Precon .dck files must be copied to %APPDATA%/Forge/decks/commander/
for Forge to find them.
2026-04-05 16:47:54 +00:00
KoalaTrapLord 65873555bc fix: show all batch log lines in Live Output (was truncated to last 30)
Frontend: removed .slice(-30) — now shows all lines from backend.
Backend: bumped log line limit from 200 to 2000 so full Forge game
output is captured for debugging.
2026-04-05 16:35:21 +00:00
KoalaTrapLord 8995d743af fix: remove !quiet guard from ML decision logging in BatchRunner
The condition 'mlLoggingEnabled && !quiet && mlDecisionLogger != null'
silently skipped ML training data extraction whenever quiet mode was
active. DecisionExtractor works with the same log lines regardless of
quiet mode, so the guard was unnecessary and prevented JSONL generation.

Now: mlLoggingEnabled && mlDecisionLogger != null — ML logging runs in
all modes as long as --ml-log is passed.
2026-04-05 16:19:56 +00:00
KoalaTrapLord 14ff543135 fix: DecisionLogger JSONL filename prefix — ml-decisions-sim-* for dataset_builder discovery
The Java DecisionLogger was writing files as ml-decisions-{batchId}.jsonl
but the Python dataset_builder.py scans for ml-decisions-sim-*.jsonl to
categorize them as 'forge' source training data.

This one-line fix adds the 'sim-' prefix so the Forge batch heuristic
path now feeds directly into the ML training pipeline:

  BatchRunner → DecisionExtractor → DecisionLogger
    → results/ml-decisions-sim-{batchId}.jsonl
    → dataset_builder.py discovers as 'forge' source
    → encode → train

The full chain was already wired (Issue #83 Phase 1 infrastructure),
this naming mismatch was the only thing preventing it from working.

Refs: Issue #83 Phase 1
2026-04-05 16:04:12 +00:00
rgriffithandClaude Opus 4.6 ea23f97b9c chore: ignore runtime artifacts, add patch scripts & training manifest
Ignore static/, logs/, data/rag_chroma/, api/, image_cache/, .claude/,
precon-decks/, and dependency-reduced-pom.xml — all runtime/build
artifacts that were polluting git status. Also stage the one-off
patch scripts (fix_paths.py, patch_eval.py, patch_phase1.py,
forge_evaluator_FIXED.py) and training_manifest.json so they're
tracked alongside the code they support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 10:48:35 -04:00
KoalaTrapLord a23fdc55a4 feat: PolicyControllerAi integration — Forge AI decision interception via policy server
## New File
- PolicyControllerAi.java (840 lines): Extends Forge's PlayerControllerAi
  to intercept AI decisions at chooseSpellAbilityToPlay() and route them
  through the Python policy server. Falls back to Forge built-in AI on
  any failure. Inner PolicyAiController overrides AiController with full
  legal action gathering via ComputerUtilAbility.

## Wiring
- GameSession.java: Import + Javadoc for PolicyControllerAi decision flow.
  Logs decision pipeline when policy server connects. End-of-game reward
  submission via PolicyControllerAi lifecycle.
- BatchRunner.java: New setPolicyServer(url) method. Appends
  --policy midrange --policyServer <url> to Forge subprocess command when
  policy server URL is configured.

## Path Updates (Forge-Repo rename)
- README.md: Forge link → KoalaTrapLord/Forge-Repo, build path updated
- .env.example: FORGE_JAR_PATH and FORGE_WORKING_DIR → Forge-Repo
- lab_api.py: Auto-detect paths try Forge-Repo first, forge-repo as legacy

## MacroActionExecutor
- Added public isRemovalCard(), isDrawCard(), isRampCard() wrappers so
  PolicyControllerAi can consult the same card-role databases.

Refs: Issue #83 Phase 2.4
2026-04-05 14:47:15 +00:00
rgriffithandClaude Opus 4.6 4cae97ab44 fix(lan-client): compact phase pills, wrap NEXT PHASE/NEXT TURN labels
The phase track (BEGIN · MAIN 1 · COMBAT · END) was losing labels to
ellipsis on narrow top bars, and the primary action buttons were
clipping "NEXT PHASE"/"NEXT TURN" because they had white-space: nowrap
and a fixed height.

.phase-step:
  - tighter padding (1px 5px) and gap (2px)
  - no letter-spacing, smaller border-radius, flex-shrink: 1 so pills
    compress uniformly when the top bar is crowded
  - merged duplicate .phase-step.active rule and made it bolder so the
    active step still reads well after the shrink

.btn-phase (NEXT PHASE / NEXT TURN):
  - white-space: normal + word-break: break-word enable two-line wrap
  - height: auto + min-height: 34px lets the label breathe inside
    the 44px top bar
  - min-width: 64px / max-width: 80px keeps the buttons square-ish
  - display: flex centers the wrapped text vertically & horizontally
  - align-items: stretch on .top-bar-actions so both buttons match
    the tallest sibling's height

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 10:39:18 -04:00
rgriffithandClaude Opus 4.6 d308e8f413 fix: AI land-only bug — enforce 1-land/turn, populate legal moves, tune policy
Addresses the three root causes in commander-ai-lab-land-only-bug-analysis.md
plus several collateral fixes discovered along the way.

P0 — Land-per-turn enforcement (engine.py, deepseek_engine.py, turn_manager.py)
  * Guard _play_land() against double plays by tracking
    _lands_played_this_turn vs _land_plays_this_turn_limit on each player
  * Reset per-turn counters at the start of each player's turn
  * Ramp cards that grant additional plays can bump the limit

P0 — Legal move pipeline (new legal_move_generator.py, game_state.py)
  * New generate_legal_moves() emits the full macro-action set
    (play_land, cast_creature, cast_removal, cast_ramp, cast_draw,
    cast_commander, attack, block, pass_priority) based on phase,
    active player, land-drop state, mana, and commander tax
  * get_legal_moves() delegates to the new generator instead of the
    pass-only stub that blocked the turn_manager/policy-server path
  * apply_move() gains handlers for play_land, cast_spell,
    cast_commander, and declare_attacker — spells resolve to
    battlefield or graveyard, stats update correctly

P1 — Policy exploration (LearnedPolicy.java, scope.py)
  * Default greedy=false so the neural policy samples its
    distribution instead of always taking argmax
  * Raise ppo_entropy_coeff 0.01 → 0.04 to keep the action
    distribution flat during PPO self-play and prevent the
    pass/land collapse

Collateral fixes pulled in during the same session:
  * lan-client/precon-loader.js: await enrichAllZonesWithScryfall()
    so opening-hand cards have typeLine/cmc before the AI's first turn
  * lan-client/ui.js: closeZoneModal auto-shuffles the library
    whenever the zone modal closes
  * routes/game.py: BatchState() was missing total_games/threads,
    crashing POST /api/game/start
  * routes/deckgen.py: dead code after raise left headers undefined,
    breaking the Anthropic API path

All sim/phase/integration tests pass (650+).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 10:34:37 -04:00
rgriffithandClaude Opus 4.6 44123f7622 fix: AI only plays lands — sync card enrichment, mana pool check, field alignment
- Add _enrichCardFromCache() in precon-loader.js for synchronous card
  enrichment at draw time (typeLine, cmc, oracle roles available before
  async Scryfall batch completes)
- Call _enrichCardFromCache in drawCard() so AI has complete card data
  on its first turn
- Improve _canAfford in ai-bridge.js: check actual mana pool before
  falling back to untapped land count; free spells (cmc 0) always castable
- Fix totalCards field name mismatch in ui.js and engine.js (lab.py
  returns totalCards, not card_count/total_cards)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 01:43:29 -04:00
rgriffithandClaude Opus 4.6 9eaa41a4ae feat: deck dropdown, user-decks pipeline, deckgen fixes, gpt-oss removal
- Add user-decks/ directory and USER_DECKS_DIR config for exported decks
- Update export-to-sim to write to user-decks/ with static mount in lab_api
- Replace import/paste UI with API-backed "My Decks" dropdown in LAN client
  and AI Lab, fetching from GET /api/lab/decks with source grouping
- Fix GET /api/lab/decks to JOIN deck_cards for totalCards count
- Fix deckgenerator: add missing DOM elements (dg-strategy-bar, dg-stats-bar,
  dg-card-groups) inside #dg-results, null guards on render functions,
  fix setLoading always hiding results
- Replace gpt-oss:20b with claude-opus-4-20250514 across all files
- Add user-decks/ to _BUILTIN_DECK_DIRS in deck_service.py
- Enrich missing type_line from Forge card cache in DB deck path
- Collapse import bar behind <details> toggle in AI Lab

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 01:17:30 -04:00
rgriffithandClaude Opus 4.6 409aba26f4 feat: AI bug fixes, deck/collection migration, and enrichment pipeline
- Fix AI turn pipeline: add cmc to Scryfall cache, tag isRamp/isRemoval/
  isBoardWipe from oracle text during enrichment, enable post-combat spell
  casting for all difficulty levels
- Migrate deck builder, deck generator, and collection from ui/ to
  lan-client/ as iframe-embedded panels with LAB_API_BASE patching
- Fix ai-bridge.js: turnNumber, deck_name, battlefield serialization,
  5-playstyle mapping, window exports for cross-script access
- Fix engine.js: battlefieldCards flat array (not player.battlefield),
  aiDeclareAttacks extraction, combat overlay fixes
- Fix collection.js: template literal, column visibility alignment,
  set_code key, oracle text column, sortable category
- Add exile/graveyard last-card image rendering in ui.js
- Make state_encoder.py resilient to mixed input types
- Enrich type_line from Scryfall at card insert in deckbuilder.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 00:05:23 -04:00
rgriffithandClaude Opus 4.6 30c3ab5a89 fix: pass gameState to _canAfford in MacroActionMapper
Static method was referencing bare gameState which is always null.
Now accepts gameState as parameter, and all 5 callers pass it through.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 17:02:05 -04:00
KoalaTrapLord b7f9c119cb feat(overnight): add --ds-weight arg to pass DeepSeek upsampling weight to /api/ml/train 2026-04-04 17:01:10 -04:00
rgriffithandClaude Opus 4.6 bbb7f7a966 fix: _getBattlefieldCards reads battlefieldCards instead of battlefield
Was reading gameState.battlefield (doesn't exist) causing AI to
receive empty board state. Now reads gameState.battlefieldCards
and matches by ownerIndex.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 17:00:39 -04:00
rgriffithandClaude Opus 4.6 ef739e1f49 feat: UI improvements — silver commander border, life panel border, hand drag-and-drop, card placement
- Add bright silver 3D smoky border around commander card zones
- Wrap all 4 life panels in a single bordered container
- Add drag-and-drop from hand zone to battlefield with ghost preview
- Update card placement to center of battlefield, avoiding overlaid zones
- Fix card dimensions in placement calc (110x150 -> 63x84)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 16:50:56 -04:00
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) v0.1.0-phase1 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