- 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>
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.
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"
- 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)
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.
* feat(issue-80): Phase 1 — add services/forge_card_loader.py
* feat(issue-80): Phase 2 — add 5 Forge fields to Card dataclass
* feat(issue-80): Phase 3+4 — wire Forge into enrich_card/score_card
* feat(issue-80): Phase 5 — add tests/sim/test_forge_enrichment.py
* fix(issue-80): resolve PR #81 blocking issues — import paths, indentation, partial Forge fallback
- Move forge_card_loader.py from services/ into src/commander_ai_lab/sim/
so the import in rules.py uses a proper intra-package path instead of
crossing package boundaries via services.forge_card_loader
- Fix indentation of Forge enrichment comment in models.py (8→4 spaces)
- Fix enrich_card() early return on partial Forge data: re-check
has_real_data after _apply_forge_data so cards with incomplete Forge
entries still fall through to known-cards / default heuristics
- Fix flawed test assertion in test_etb_via_forge_mode_equals_text_detection:
oracle text "draw a card" was adding a +3 spell_draw bonus, making the
score comparison invalid; replaced with side-effect-free ETB text
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Code <claude-code@anthropic.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Refactor the headless engine to walk through all 12 MTG turn phases in
order (untap → upkeep → draw → main1 → begin_combat → declare_attackers
→ declare_blockers → combat_damage → end_combat → main2 → end_step →
cleanup) instead of the previous flat per-player block.
Key changes:
- Add Phase str enum and PHASE_ORDER/SORCERY_PHASES constants to models
- Track current_phase and active_player_index on SimState
- Sync sim.turn with the loop turn counter (fixes latent summoning-
sickness bug where turn_played was always compared against 0)
- Add second main phase (main2) with shared 2-spell budget across
main1 + main2
- _play_spells now accepts max_spells param and returns count played
- Unify turn_manager.py PHASES/ACTION_PHASES/RESPONSE_PHASES to derive
from the canonical Phase enum in models.py
- 33 regression tests covering phase enum, phase progression, spell
budget, game log structure, backward compat, and turn manager
constant consistency
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add London mulligan rule to the headless game engine:
- GameEngine now accepts mulligan_rule param ("london" default, "none" for legacy)
- AI heuristic: mulligan 0-1 land or 6-7 land hands; relax after 2+ mulligans
- After keeping, bottom N lowest-scored cards (London mulligan rule)
- Cap at MAX_MULLIGANS=4 to prevent infinite loops on degenerate decks
- Track mulligans and opening_hand_lands in PlayerStats
- Serialize both fields in to_dict() for batch-result schema compatibility
- 28 targeted regression tests covering unit helpers, integration,
N-player, commander interaction, serialization, and legacy mode
Co-authored-by: Claude Agent <claude-agent@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add centralized deal_damage() method to GameEngine that handles life
reduction, stat tracking, commander damage (both per-seat and per-card),
and elimination checks. Route both burn spells and combat damage through
this unified path, building on the dual-dict commander damage model
merged in PR #89.
- Add is_direct_damage / direct_damage_amount fields to Card
- Add burn spell detection via known-card table + oracle regex fallback
- Route burn spells in _play_spells() through deal_damage()
- Refactor _resolve_combat() to route per-attacker damage through deal_damage()
- Add 38 regression tests covering deal_damage(), burn detection, spell routing,
combat routing, and full-game integration
Co-authored-by: Claude Code <noreply@anthropic.com>
Track commander damage at combat-damage time by checking `is_commander`
on each attacking creature. Damage from commander creatures is now
recorded in both a per-seat aggregate (`commander_damage_received`) and
a per-card breakdown (`commander_damage_by_card`), enabling correct
21-damage lethal checks for partner/multi-commander games.
Changes:
- engine._resolve_combat: identify commander attackers, accumulate their
damage (including trample overflow and double strike), write to both
tracking dicts, and check commander-damage elimination after combat.
- models.Player: add commander_damage_received, commander_damage_by_card,
and is_dead_to_commander_damage() so the headless engine can track
commander damage without the full CommanderGameState wrapper.
- game_state.CommanderPlayer: replace the separate
commander_damage_received field with a property delegating to
base.Player to avoid split-brain state between engine and wrapper.
- game_state.CommanderGameState.deal_commander_damage: accept optional
commander_name for per-card tracking.
- 22 new regression tests covering unblocked, blocked, trample, double
strike, lifelink, partner, cumulative, and lethal scenarios.
Partial fix for Issue #86 item 2.
Co-authored-by: Claude <noreply@anthropic.com>
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>
- Replace SELECT * + Python-side color loop in _get_collection_for_colors()
with projected SELECT + NOT EXISTS json_each() sub-query
- Apply same pattern to _build_collection_summary(), removing card_fits_identity()
- Add partial index idx_ce_quantity on collection_entries(quantity) WHERE qty > 0
- Handle NULL and empty-string color_identity via CASE guard
- Fix colorless commander bug: old code skipped color check entirely for []
- Add scripts/bench_deckgen.py benchmark (3-5x speedup on filtered queries)
- Add tests/test_deckgen_color_filter.py with 7 regression tests