Compare commits

...
Author SHA1 Message Date
Commander AI Lab 694f085827 fix: add precons.js with 163 precon entries and fix backend precon UI refresh
- Generate lan-client/precons.js from precon-decks/precon-index.json
  so PRECON_DATA is always available for local/offline mode
- Add scripts/generate-precons-js.js for re-generation
- Fix _loadBackendPrecons() to actually re-render the precon tab UI
  when backend precons load (was only updating status indicator text)

Fixes the empty precon dropdown and 'No precon data loaded' message.
2026-04-03 13:29:24 +00:00
Claude Code 2a6e3ac893 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>
2026-04-03 12:46:11 +00:00
Claude 36742d3cd5 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>
2026-04-03 12:44:27 +00:00
KoalaTrapLord d6863b6d7e fix: auto-populate all CFG paths from environment variables
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.
2026-04-03 04:56:44 -04:00
KoalaTrapLord 1fc99a5d4a fix: auto-discover precon-decks/ and sample-decks/ when CFG paths unset
_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.
2026-04-03 04:50:49 -04:00
KoalaTrapLord eed2c7f126 New HTML battlefield to test features before Unity (#182) 2026-04-03 04:39:33 -04:00
KoalaTrapLord 7a40b5deb8 fix: enrich type_line/oracle from Forge cache in .dck fallback loader
_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.
2026-04-03 04:01:59 -04:00
KoalaTrapLord af8970f3a8 fix: _load_deck_cards_by_name falls back to .dck file when deck not in DB
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.
2026-04-02 22:59:01 -04:00
KoalaTrapLord 53fd1c9257 fix: wire RESULTS_DIR and FORGE_DIR env vars into CFG at startup 2026-04-02 14:24:28 -04:00
KoalaTrapLord 6ead75f96b fix: set FORGE_WORKING_DIR to forge-gui (requires res/ folder for sim to start) 2026-04-02 02:33:14 -04:00
KoalaTrapLord 535bdd7c70 fix: make Forge deck paths fully portable using %~dp0 and auto-generate forge.profile.properties 2026-04-02 00:59:10 -04:00
KoalaTrapLord d98e6331e9 fix: point all deck lookups to precon-decks directory
- 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
2026-04-01 23:10:32 -04:00
KoalaTrapLord 2b3196267f fix: raise default clock from 300ms to 5000ms to prevent turn-1 draw games 2026-04-01 22:56:29 -04:00
rgriffith 433e8bc4d7 feat: wire precon card art through local /card-image/ API with Scryfall fallback 2026-04-01 22:09:13 -04:00
KoalaTrapLord d74dd0a6bf feat: wire precon card art through local /card-image/ API with Scryfall fallback (full file) 2026-04-01 21:45:15 -04:00
KoalaTrapLord 8037802dff feat: wire precon card art through local /card-image/ API with Scryfall fallback 2026-04-01 21:45:01 -04:00
KoalaTrapLord ca6c23a881 fix: wrap shutil.copy2 in try/except for install and install-batch endpoints 2026-04-01 21:32:19 -04:00
KoalaTrapLord a81772c68a fix: set cwd=CFG.forge_dir on Popen so Forge finds res/ (#184) 2026-04-01 19:41:52 -04:00
KoalaTrapLord 99ca498b88 docs: add Unity TCGEngine Commander board implementation plan 2026-04-01 14:59:46 -04:00
KoalaTrapLord 395ad06cdd fix: resolve deck names to full .dck paths in build_java_command (#183) 2026-04-01 14:57:57 -04:00
KoalaTrapLord e4a6ce644d fix(#176): draw-game life capture, DrawGameAnalyzer, clock 6000→3000 (#181)
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.
2026-04-01 14:40:29 -04:00
KoalaTrapLord 5411fd6bf6 fix(#175): alternate win condition parser & winType tagging (#179)
Adds win_condition_parser.py, GameResult.win_type field, and winType stamping on both the Java/Forge and DeepSeek batch paths. Closes #175.
2026-04-01 14:28:10 -04:00
KoalaTrapLord 55fdc2ec4a feat: add CreateBattlegroundScene editor script to build full scene programmatically 2026-03-30 20:31:09 -04:00
KoalaTrapLord ddfe8e601e fix(eval): parse batch JSON output instead of JSONL for Forge game results 2026-03-30 16:12:20 -04:00
KoalaTrapLord 9e3cd45936 fix(eval): require >= 3 decks for Forge availability check 2026-03-30 15:45:20 -04:00
KoalaTrapLord c91f4c90bc fix(eval): discover 4 decks for 4-player Commander (Forge requires >= 3) 2026-03-30 15:43:27 -04:00
KoalaTrapLord 80f67dd0a0 fix(eval): use precon-decks/ for deck discovery instead of decks/ 2026-03-30 15:28:19 -04:00
rgriffith 10220ec747 feat: add 130+ precon commander deck files 2026-03-30 15:19:29 -04:00
rgriffith 6fadeaa387 fix(eval): replace Java bridge imports with subprocess CLI launch for Forge eval 2026-03-30 14:23:52 -04:00
rgriffith 517c17047b fix: strip value_head keys when loading PPO checkpoint into PolicyNetwork 2026-03-30 13:31:59 -04:00
KoalaTrapLord c62e75af52 fix(tests): update _build_document assertions to match NL sentence format 2026-03-30 12:36:00 -04:00
KoalaTrapLord 11940835fc fix: validate ollama suggest_cards names against DB + Scryfall before pipeline merge
- 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.
2026-03-30 12:33:28 -04:00
KoalaTrapLord d12b47f874 fix: robust suggest_cards parsing — CSV fallback + one-shot JSON prompt
- 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
2026-03-30 12:26:28 -04:00
KoalaTrapLord 5b5bac42fb fix: use vars(row) instead of dict(row) in _build_collection_from_bulk
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.
2026-03-30 10:39:49 -04:00
rgriffith 574bb79a0b fix(eval): correct indentation for load/reload in _load_policy_service 2026-03-30 10:36:43 -04:00
KoalaTrapLord 5554ef4dd7 fix(eval): call load() for embeddings before reload() for checkpoint 2026-03-30 09:53:56 -04:00
KoalaTrapLord cf2d7ed806 fix(eval): use reload() with explicit checkpoint path instead of load()
load() hardcodes best_policy.pt but PPO trainer saves as best_ppo.pt, causing the model to never load during eval. reload() accepts an explicit path.
2026-03-30 09:51:22 -04:00
KoalaTrapLord aec1896094 fix(engine): promote assign_attackers/assign_blockers/resolve_combat_damage to public methods (#86)
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.
2026-03-30 09:50:56 -04:00
KoalaTrapLord 75785ae918 fix(tests): update RESPONSE_PHASES assertion to match current turn_manager design
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.
2026-03-29 20:39:37 -04:00
KoalaTrapLord 252e46b288 fix(tests): swap Delta/Epsilon for Zeta/Theta to avoid _land_kw collision
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.
2026-03-29 20:37:04 -04:00
KoalaTrapLord e1bb2e7209 feat(coach): Phase 1 sim stats pass-through in prompt template (#108)
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.
2026-03-29 05:55:51 -04:00
KoalaTrapLord e77a945805 test(encoder): unit tests for StateEncoder global features + Step 4 total_power (#83 Step 4)
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"
2026-03-29 05:46:01 -04:00
KoalaTrapLord 3f5d6d12e8 feat: 4-player Commander Battleground UI (issues #138–#151)
* chore: add battleground UI task tracker (2026-03-28)

* feat(#142): add [NonSerialized] battleground runtime state to CardModel

* feat(#143): add LoadArtCrop and LoadCardRaw (RawImage) to ImageCache

* feat(#148): add GameStateManager singleton and GameState/SeatState POCOs

* feat(#138): add BattlegroundController scene orchestrator

* feat(#139): add LifeTrackerWidget, CommanderDamageRow, PoisonCounter

* feat(#140): add PhaseTrackerWidget and PhaseStepButton

* feat(#141): add TurnOrderBar and TurnOrderPill

* feat(#142): add CardZone and CardView battlefield zone components

* feat(#143): add CommanderZoneWidget with art crop, cast counter, and tax display

* feat(#144): add TokenModel POCO, TokenPanelController, and TokenRowView

* chore: mark issues 1-8 as complete in task tracker

* feat(#146): add StackZoneController, StackItemView, and ManaPoolDisplay

* feat(#147 #150 #151): add LobbySetupModal, ZoneViewerModal, EliminationHandler; add Play Live button to SimulationController

* feat(#151): add Play Live button to SimulationController

* feat(#149): add GameWebSocketClient with reconnect, ping, and main-thread dispatch

* fix: add PostGameAction to ApiClient + update TODO.md all issues complete

- ApiClient.cs: add PostGameAction() wrapper → POST /api/game/action
  (fixes compile error in HumanActionBar.cs)
- TODO.md: mark issues #145, #146, #147, #149, #150, #151 as ✅
  all 14 battleground issues now committed

* feat(game): add routes/game.py + wire lab_api + fix GameWebSocketClient game_id URL

- routes/game.py: POST /api/game/start, POST /api/game/action (new)
- lab_api.py: import + register game_router
- GameWebSocketClient.cs: read game_id from PlayerPrefs, build
  ws://{host}/ws/game/{game_id} — fixes URL mismatch with ws_game.py

* fix(lobby): wire OnConfirm to POST /api/game/start, save game_id to PlayerPrefs

- OnConfirm() now calls ApiClient.PostGameStart() with 4-seat config
- On success: saves BattleSeats + GameId + WsUrl to PlayerPrefs,
  loads Battleground scene
- On failure: shows inline error label (red), re-enables Confirm button
- Added StartGameRequest/Response POCOs matching routes/game.py schema
- Added _errorLabel [SerializeField] for user-facing error feedback
2026-03-29 05:35:29 -04:00
KoalaTrapLord 915c745911 fix(eval): remove Set-StrictMode, fix curly-quotes and -- in strings (issue #153) 2026-03-29 05:31:58 -04:00
KoalaTrapLord 00374c1f03 feat(eval): add run_benchmark.ps1 — full benchmark pipeline for issue #153 2026-03-29 05:13:21 -04:00
KoalaTrapLord 6e71ba88fd fix(encoder): read real total_power from snapshot dict at global index 7 (#83 Step 4)
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.
2026-03-29 05:13:19 -04:00
KoalaTrapLord 73d8e8d220 test(policy): add Step 4 board-stats tests + fix schema alias (#83 Step 4)
- 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)
2026-03-29 04:51:47 -04:00
KoalaTrapLord bff7b3a81e feat(eval): add forge_evaluator, eval_policy, compare_eval for issue #153
- 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
2026-03-29 04:49:43 -04:00
KoalaTrapLord b1c17359f0 feat(policy): wire real Forge board stats into encoder snapshot (#83 Step 4)
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.
2026-03-29 04:49:40 -04:00
KoalaTrapLord 7440d0ce17 test(policy): add round-trip tests for /api/policy/decide state_vector path (#83 Step 2)
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.
2026-03-29 04:37:09 -04:00
KoalaTrapLord 4d73f134c2 feat(policy): consume state_vector from GameSession snapshots (#83 Step 2)
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
2026-03-29 04:32:52 -04:00
KoalaTrapLord 126684bb94 feat(bridge): Phase 2.3 — wire GameSession to real Forge IPC subprocess (issue #83)
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
2026-03-29 04:31:49 -04:00
KoalaTrapLord 882e2370c1 fix(#127): persist built_at in ChromaDB collection metadata as sidecar fallback
- _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
2026-03-29 04:29:35 -04:00
KoalaTrapLord bbca0b7985 fix(#126): add batch insert retry logic with exponential backoff
- 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
2026-03-29 04:24:58 -04:00
KoalaTrapLord da8de77704 feat(bridge): add buildStateVector() emitting state_vector float[] in snapshots (#83 Step 1)
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.
2026-03-29 04:24:55 -04:00
KoalaTrapLord 11990c2fd3 feat(ml): add --data-source forge flag to PPO trainer (issue #83 phase 1.3)
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
2026-03-29 03:02:30 -04:00
KoalaTrapLord aefd0cace6 feat(rag): Phase 1 — rewrite build_card_text to natural-language format (#137)
* feat(rag): Phase 1 — rewrite build_card_text to natural-language format

* feat(rag): Phase 2 — Forge-enriched ChromaDB RAG store (2a–2e)

Phase 2a: Forge lazy import guard (_try_import_forge_lookup)
Phase 2b: _build_document appends Forge trigger/replacement/static/keyword tags
Phase 2c: _build_metadata adds has_trigger, has_replacement, has_static, trigger_modes fields
Phase 2d: query_cards extended with require_trigger, require_replacement, require_static params + ChromaDB where clause support
Phase 2e: get_rag_stats reports forge_enriched and forge_coverage; _write_build_meta tracks coverage; build loop counts enriched cards

* feat(rag): Phase 3 — add mtgrules second ChromaDB collection

Adds build_rules_index() and query_rules() operating on a separate
'mtgrules' ChromaDB collection. Chunks MagicCompRules.txt by rule-number
boundaries, embeds each chunk, and exposes semantic search. Completely
independent from the mtg_oracle card collection.

* feat(coach): Phase 4 — behavior-aware RAG + rules context in coach_service

Phase 4a: run_coaching_session uses dual RAG query
  - Primary: commander synergy (n=10)
  - Secondary: require_replacement=True for graveyard/protection cards (n=5)
  - Deduplicates by name before merging
Phase 4b: query_rules() fetches 3 Commander rules chunks to ground the prompt
  - Passed as rules_context kwarg to build_user_prompt()

* feat(rag): Phase 5 — add /api/rag/build-rules and /api/rag/query-rules endpoints
2026-03-29 02:11:38 -04:00
KoalaTrapLord ac536999eb Update lab_api.pyfeat: wire validate_forge_config into lifespan startup 2026-03-28 14:48:43 -04:00
KoalaTrapLord 2e43511d87 feat: add validate_forge_config() startup check
Validates FORGE_DIR at startup with clear warning if cardsfolder not found.
Checks both forge-gui/res/cardsfolder and res/cardsfolder layouts.
2026-03-28 14:34:50 -04:00
KoalaTrapLord 1bebe38e66 fix: forge_card_loader path resolution + reset_forge_cache helper
- _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
2026-03-28 14:31:42 -04:00
KoalaTrapLord eca1653024 fix: backfill basic lands on card_entries to guarantee 99 cards
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.
2026-03-28 00:26:42 -04:00
KoalaTrapLord 08f1cb7581 fix(deckgen): backfill basic lands after cross-category dedup to reach 99 cards 2026-03-28 00:10:52 -04:00
KoalaTrapLord 9d9e8675a9 fix: restore missing # on line 27 section banner in rules.py (SyntaxError on import) 2026-03-27 23:36:02 -04:00
KoalaTrapLord 64526b7bee fix: exclude lands from cmc_buckets in _build_deck_report_from_db (matches deck_service behaviour) 2026-03-27 20:38:49 -04:00
KoalaTrapLord e9732d292f fix: expand _land_kw, hoist to module level, guard has_real_data for cmc=0 lands after Forge lookup 2026-03-27 20:36:22 -04:00
KoalaTrapLord fa5349ea7c fix: basic land name fallback in _build_deck_report_from_db when type_line is missing from collection join 2026-03-27 20:32:15 -04:00
KoalaTrapLord 1c491bc2b0 fix: add Land role detection to _detect_card_roles (fixes basics showing as Unknown) 2026-03-27 20:28:35 -04:00
KoalaTrapLord 81fe5108d2 fix: coach_apply rollback on partial write, 4-space reindent; ws_game hoist inline imports 2026-03-27 20:24:55 -04:00
KoalaTrapLord 61fa37dc48 fix: lifespan indent error, get_running_loop, remove double rag init, register policy routes 2026-03-27 20:22:42 -04:00
KoalaTrapLord 0118950939 fix: add anthropic_api_key to Config with env-var default (fixes AttributeError in ASGI mode) 2026-03-27 20:20:45 -04:00
KoalaTrapLord 22cf23b24d fix: correct query_cards kwarg names in coach_service (n= -> n_results=, colors= -> color_identity=) 2026-03-27 20:17:14 -04:00
KoalaTrapLord 0010b0f394 refactor: RAG improvements — shared card_text, Chroma fallback, staleness monitor (fixes #133, #134, #135) (#136)
* refactor: add shared build_card_text() utility

* refactor: unify _build_document via shared card_text + add is_chroma_available/check_staleness (fixes #133, #134, #135)

* refactor: delegate embeddings._build_card_text to shared utility

* feat: add background RAG staleness monitor to lab_api lifespan (fixes #135)
2026-03-27 16:46:33 -04:00
KoalaTrapLord d0cdafa66a feat(rag): Phase 3 — inject RAG cards into coach prompts (#132)
* feat(rag): add RAG card injection to prompt_template.py

* feat(rag): wire RAG query into coach_service.py

* test(rag): add Phase 3 RAG injection tests
2026-03-27 15:20:49 -04:00
KoalaTrapLord bcf558418a fix(rag): squash all RAG/ChromaDB bugs #123–#130
Fixes critical API contract mismatch, batch_size wiring, lifespan init, failure tracking, build_meta persistence, mana_cost in embeddings, pyproject.toml deps, and adds full test suite.

Closes #123, #124, #125, #126, #127, #128, #129, #130
2026-03-27 13:24:41 -04:00
254 changed files with 92353 additions and 1903 deletions

No files matched your search

+17 -31
View File
@@ -1,36 +1,22 @@
# Commander AI Lab - Environment Variables
# Copy this file to .env and fill in your API keys.
# .env is gitignored and will NOT be committed.
# Commander AI Lab — Environment Variables
# Copy this file to .env and fill in your values.
# start-lab.bat will auto-load .env if it exists.
# Ximilar API Key (card scanner / visual AI recognition)
# Get yours at: https://app.ximilar.com/
XIMILAR_API_KEY=your-ximilar-api-key-here
# Anthropic API Key (AI deck generation & coaching with Claude Opus)
# Get yours at: https://console.anthropic.com/settings/keys
# ── API Keys ──────────────────────────────────────────────────────
ANTHROPIC_API_KEY=your-anthropic-api-key-here
# Anthropic model for deck generation and coaching (Claude Opus recommended)
ANTHROPIC_MODEL=claude-opus-4-20250514
# Coach chat provider: "local" (Ollama) or "anthropic" (Claude API)
COACH_PROVIDER=anthropic
XIMILAR_API_KEY=your-ximilar-key-here
# CORS allowed origins (comma-separated)
# Default: http://localhost:5173,http://localhost:8080
# Add your LAN IP if accessing from another machine on your network
ALLOWED_ORIGINS=http://localhost:5173,http://localhost:8080
# ── Forge JAR Path ────────────────────────────────────────────────
# Point this to your Forge GUI desktop fat JAR.
FORGE_JAR_PATH=D:/ForgeCommander/forge-repo/forge-gui-desktop/target/forge-gui-desktop-2.0.12-SNAPSHOT-jar-with-dependencies.jar
# ── Dual-Model AI Brain ─────────────────────────────────────
# Primary brain: GPT-OSS 20B (turn planner / decision maker)
BRAIN_API_BASE=http://localhost:11434
BRAIN_MODEL=gpt-oss:20b
BRAIN_TEMPERATURE=0.3
BRAIN_MAX_TOKENS=1024
BRAIN_TIMEOUT=120.0
# IMPORTANT: Forge MUST be launched from the forge-gui directory (not forge-gui-desktop/target).
# The JAR looks for res/ relative to its working directory. Without this, you get
# ExceptionInInitializerError on startup.
FORGE_WORKING_DIR=D:/ForgeCommander/forge-repo/forge-gui
# Validator brain: DeepSeek-R1-Distill-Qwen-14B (legality + explanation)
VALIDATOR_ENABLED=true
VALIDATOR_API_BASE=http://localhost:11434
VALIDATOR_MODEL=deepseek-r1:14b
VALIDATOR_TEMPERATURE=0.0
VALIDATOR_MAX_TOKENS=2048
VALIDATOR_TIMEOUT=120.0
# ── Deck Paths ────────────────────────────────────────────────────
# Directory containing your precon .dck files.
# Forge reads deck files directly from this folder.
PRECON_DECKS_DIR=D:\ForgeCommander\commander-ai-lab\precon-decks
FORGE_DECKS_DIR=D:\ForgeCommander\commander-ai-lab\precon-decks
+50 -46
View File
@@ -5,7 +5,6 @@ Main orchestrator: loads deck reports, identifies underperformers,
finds replacement candidates via embeddings, builds prompts,
calls the LLM, parses responses, and persists coaching sessions.
"""
import json
import logging
import uuid
@@ -65,8 +64,7 @@ class CoachService:
self.embeddings = embeddings
ensure_dirs()
# ── Deck Report I/O ────────────────────────────────────
# ── Deck Report I/O ──────────────────────
def load_deck_report(self, deck_id: str) -> Optional[DeckReport]:
"""Load a deck report from disk."""
report_path = DECK_REPORTS_DIR / f"{deck_id}.json"
@@ -92,8 +90,7 @@ class CoachService:
return []
return [f.stem for f in DECK_REPORTS_DIR.glob("*.json")]
# ── Coach Session I/O ──────────────────────────────
# ── Coach Session I/O ──────────────────
def save_session(self, session: CoachSession) -> Path:
"""Persist a coaching session to disk and SQLite."""
COACH_SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
@@ -187,8 +184,7 @@ class CoachService:
continue
return sessions
# ── Main Coaching Pipeline ───────────────────────────────
# ── Main Coaching Pipeline ───────────────────────
async def run_coaching_session(
self,
deck_id: str,
@@ -224,7 +220,6 @@ class CoachService:
raise ValueError(f"Deck report not found for: {deck_id}")
deck_card_names = [c.name for c in report.cards]
candidates: Dict[str, List[dict]] = {}
underperformers = report.underperformers[:MAX_UNDERPERFORMERS]
if not underperformers:
@@ -253,11 +248,42 @@ class CoachService:
else:
logger.warning("Embeddings not loaded — skipping candidate search")
# Phase 4a: behavior-aware RAG — primary + replacement-effect secondary query
rag_cards: List[dict] = []
try:
from services.rag_store import query_cards
# Primary: commander synergy context
base_query = f"{report.commander} {' '.join(report.colorIdentity)} commander deck"
rag_cards = query_cards(base_query, n_results=10, color_identity=report.colorIdentity)
# Secondary: replacement effects / graveyard hate / protection
if len(rag_cards) < 15:
repl_cards = query_cards(
f"replacement effect graveyard {' '.join(report.colorIdentity)}",
n_results=5,
color_identity=report.colorIdentity,
require_replacement=True,
)
# Deduplicate by name before merging
existing_names = {c["name"] for c in rag_cards}
rag_cards += [c for c in repl_cards if c["name"] not in existing_names]
logger.info("RAG returned %d cards for '%s'", len(rag_cards), deck_id)
except Exception as e:
logger.warning("RAG query failed (non-fatal): %s", e)
# Phase 4b: rules context for grounding the coach prompt
rules_context: List[dict] = []
try:
from services.rag_store import query_rules
rules_context = query_rules(
f"Commander rules {report.commander} {' '.join(report.colorIdentity)}",
n_results=3,
)
except Exception:
pass
system_prompt = build_system_prompt(report, goals)
user_prompt = build_user_prompt(report, candidates)
user_prompt = build_user_prompt(report, candidates, rag_cards=rag_cards, rules_context=rules_context)
logger.info("Calling LLM for deck: %s (provider=%s)", deck_id, COACH_PROVIDER)
if COACH_PROVIDER == "anthropic":
import anthropic as _anthropic
anthropic_key = ANTHROPIC_API_KEY
@@ -306,19 +332,16 @@ class CoachService:
completionTokens=llm_response.completion_tokens,
goals=goals,
)
if llm_response.parsed_json:
self._populate_session_from_json(session, llm_response.parsed_json)
else:
session.rawTextExplanation = llm_response.content
session.summary = "LLM returned non-JSON response. See raw explanation."
self.save_session(session)
logger.info("Coaching session saved: %s", session_id)
return session
# ── Quick Digest ──────────────────────────────────────────
# ── Quick Digest ──────────────────────────────
async def run_quick_digest(
self,
deck_id: str,
@@ -332,10 +355,18 @@ class CoachService:
if report is None:
raise ValueError(f"Deck report not found for: {deck_id}")
# Phase 4a: light RAG query for quick digest
rag_cards: List[dict] = []
try:
from services.rag_store import query_cards
query_text = f"{report.commander} {' '.join(report.colorIdentity)}"
rag_cards = query_cards(query_text, n_results=8, color_identity=report.colorIdentity)
except Exception:
pass
from .prompt_template import build_quick_system_prompt, build_user_prompt
system_prompt = build_quick_system_prompt(report, goals)
user_prompt = build_user_prompt(report, {}) # no candidates for speed
user_prompt = build_user_prompt(report, {}, rag_cards=rag_cards) # no candidates for speed
logger.info("Quick digest for deck: %s (provider=%s)", deck_id, COACH_PROVIDER)
if COACH_PROVIDER == "anthropic":
@@ -392,18 +423,15 @@ class CoachService:
else:
session.rawTextExplanation = llm_response.content
session.summary = "Quick digest returned non-JSON. See raw explanation."
self.save_session(session)
return session
# ── JSON Parser ───────────────────────────────────────────
# ── JSON Parser ───────────────────────────────
def _populate_session_from_json(self, session: CoachSession, data: dict):
"""Parse LLM JSON response into CoachSession fields."""
session.summary = data.get("summary", "")
session.rawTextExplanation = data.get("rawTextExplanation", "")
session.manaBaseAdvice = data.get("manaBaseAdvice")
hints = data.get("heuristicHints", [])
session.heuristicHints = hints if isinstance(hints, list) else [str(hints)]
@@ -414,7 +442,6 @@ class CoachService:
reason=cut_data.get("reason", ""),
replacementOptions=cut_data.get("replacementOptions", []),
))
for add_data in data.get("suggestedAdds", []):
if isinstance(add_data, dict):
session.suggestedAdds.append(SuggestedAdd(
@@ -423,8 +450,6 @@ class CoachService:
reason=add_data.get("reason", ""),
synergyWith=add_data.get("synergyWith", []),
))
# Phase 2
for item in data.get("upgradePriority", []):
if isinstance(item, dict):
session.upgradePriority.append(UpgradePriorityItem(
@@ -434,7 +459,6 @@ class CoachService:
reasoning=item.get("reasoning", ""),
expectedImpact=item.get("expectedImpact", ""),
))
cd = data.get("commanderDependency")
if isinstance(cd, dict):
session.commanderDependency = CommanderDependency(
@@ -442,7 +466,6 @@ class CoachService:
dependentCards=cd.get("dependentCards", []),
recoveryPlan=cd.get("recoveryPlan", ""),
)
ma = data.get("mulliganAnalysis")
if isinstance(ma, dict):
session.mulliganAnalysis = MulliganAnalysis(
@@ -450,23 +473,17 @@ class CoachService:
worstOffenders=ma.get("worstOffenders", []),
recommendation=ma.get("recommendation", ""),
)
# Phase 3: Removal coverage
rc = data.get("removalCoverage")
if isinstance(rc, dict):
session.removalCoverage = RemovalCoverage(**{
k: rc.get(k, "" if k != "gaps" else [])
for k in ["creatures", "artifacts", "enchantments", "planeswalkers", "lands", "massRemoval", "gaps"]
})
# Phase 3: Ramp quality
rq = data.get("rampQuality")
if isinstance(rq, dict):
session.rampQuality = RampQuality(**{
k: rq.get(k, "") for k in ["manaRocks", "landFetch", "manaDorks", "costReducers", "fragility", "canReachFourByTurnThree"]
})
# Phase 3: Draw engine profile
dep = data.get("drawEngineProfile")
if isinstance(dep, dict):
session.drawEngineProfile = DrawEngineProfile(
@@ -475,8 +492,6 @@ class CoachService:
assessment=dep.get("assessment", ""),
sustainability=dep.get("sustainability", ""),
)
# Phase 3: Win conditions
for wc in data.get("winConditions", []):
if isinstance(wc, dict):
session.winConditions.append(WinCondition(
@@ -485,8 +500,6 @@ class CoachService:
independence=wc.get("independence", ""),
description=wc.get("description", ""),
))
# Phase 3: Anti-synergy flags
for af in data.get("antiSynergyFlags", []):
if isinstance(af, dict):
session.antiSynergyFlags.append(AntiSynergyFlag(
@@ -494,8 +507,6 @@ class CoachService:
conflict=af.get("conflict", ""),
severity=af.get("severity", ""),
))
# Phase 5: Pod presence
pp = data.get("podPresence")
if isinstance(pp, dict):
session.podPresence = PodPresence(
@@ -504,8 +515,6 @@ class CoachService:
adversarialRating=pp.get("adversarialRating", ""),
recommendation=pp.get("recommendation", ""),
)
# Phase 5: Tempo assessment
ta = data.get("tempoAssessment")
if isinstance(ta, dict):
session.tempoAssessment = TempoAssessment(
@@ -514,8 +523,6 @@ class CoachService:
survivalWindow=ta.get("survivalWindow", ""),
assessment=ta.get("assessment", ""),
)
# Phase 5: Meta matchups
for mm in data.get("metaMatchups", []):
if isinstance(mm, dict):
session.metaMatchups.append(MetaMatchup(
@@ -526,12 +533,10 @@ class CoachService:
overallRating=mm.get("overallRating", ""),
))
# ── Status Check ─────────────────────────────────────────
# ── Status Check ──────────────────────────────
def get_status(self) -> CoachStatus:
"""Check the health of all coach subsystems."""
status = CoachStatus()
if COACH_PROVIDER == "anthropic":
anthropic_key = ANTHROPIC_API_KEY
if anthropic_key:
@@ -550,7 +555,6 @@ class CoachService:
status.llmModels = llm_status.get("models", [])
if not status.llmConnected:
status.error = llm_status.get("error", "Local LLM not reachable")
status.embeddingsLoaded = self.embeddings.loaded
status.embeddingCards = self.embeddings.card_count
status.deckReportsAvailable = len(self.list_deck_reports())
+3 -9
View File
@@ -28,6 +28,7 @@ from urllib.parse import quote
import numpy as np
from services.card_text import build_card_text
from .config import EMBEDDINGS_DIR, EMBEDDINGS_NPZ, EMBEDDINGS_PARQUET
logger = logging.getLogger("coach.embeddings")
@@ -114,15 +115,8 @@ def _fetch_scryfall_card(card_name: str) -> Optional[dict]:
def _build_card_text(card: dict) -> str:
"""Build the text string to embed, matching minimaxir/mtg-embeddings format."""
parts = [card["name"]]
if card.get("mana_cost"):
parts.append(card["mana_cost"])
if card.get("type_line"):
parts.append(card["type_line"])
if card.get("oracle_text"):
parts.append(card["oracle_text"])
return " | ".join(parts)
"""Delegate to shared build_card_text() -- see services/card_text.py."""
return build_card_text(card)
class CardMatch:
+168 -17
View File
@@ -4,6 +4,13 @@ Commander AI Lab — Coach Prompt Template
Constructs system and user prompts for the LLM coach.
Injects deck report data, underperformer analysis, and
candidate replacement cards into a structured template.
Phase 1 additions (#108):
- Sim Insights section: deck-level opening hand quality, curve velocity,
synergy outliers, and archetype weak spots surfaced directly in the
user prompt so Claude reasons from numbers, not vibes.
- _format_archetypes() annotates weak spots and strengths inline.
- _format_top_cards() adds keptInOpeningHandRate per card.
"""
from typing import List, Dict, Optional
@@ -12,9 +19,9 @@ from .models import DeckReport, CoachGoals, CardPerformance
from .config import MAX_CANDIDATES_PER_UNDERPERFORMER, MAX_UNDERPERFORMERS
# ══════════════════════════════════════════════════════════════
# ════════════════════════════════════════════════════════════
# System Prompt
# ══════════════════════════════════════════════════════════════
# ════════════════════════════════════════════════════════════
SYSTEM_PROMPT_TEMPLATE = """You are an expert Magic: The Gathering Commander deck coach.
You analyze deck performance reports from simulation data and provide specific,
@@ -36,6 +43,11 @@ RULES:
- When reasoning about cuts, also consider keptInOpeningHandRate (low = bad in opening hands) and avgTurnCast (high = too slow for the curve).
- When evaluating synergy, use the synergyScore: cards below 0.1 have weak co-occurrence with the rest of the deck.
- Use perArchetypeWinRates to identify which matchup types this deck struggles with and tailor suggestions accordingly.
- The Sim Insights section shows pre-computed deck-level signals — use these directly in your reasoning.
- Cards flagged as [OPENING HAND TRAP] have keptInOpeningHandRate below the deck average — treat as mulligan liabilities.
- Cards flagged as [LATE CASTER] have avgTurnCast > 6 — consider whether they fit the deck's tempo.
- Cards flagged as [ISOLATED] have synergyScore in the bottom 3 of the deck — they may not support the gameplan.
- Archetypes flagged as [WEAK SPOT] in the Archetype Win Rates section are where this deck loses most — prioritize fixes for those matchups.
- Provide at least 5 heuristic hints covering different strategic dimensions.
- Assess how dependent the deck is on the commander being in play.
- Evaluate opening hand quality and identify cards that cause bad mulligans.
@@ -110,7 +122,7 @@ You MUST respond ONLY with valid JSON matching this exact structure:
"Detailed strategic tip explaining the reasoning and data behind the suggestion"
],
"manaBaseAdvice": "Specific mana base improvement suggestion including land count, color fixing, and utility lands, or null if mana base is solid",
"rawTextExplanation": "3-5 paragraph in-depth analysis covering: (1) overall deck strategy assessment and how well the deck executes its game plan based on simulation data, (2) mana curve and resource analysis with specific numbers including avgTurnCast for key cards, (3) card synergy evaluation highlighting the strongest and weakest interactions using synergyScore data, (4) meta considerations and matchup-specific insights referencing perArchetypeWinRates, and (5) prioritized list of improvements ranked by expected impact on win rate",
"rawTextExplanation": "3-5 paragraph in-depth analysis covering: (1) overall deck strategy assessment and how well the deck executes its game plan based on simulation data, (2) mana curve and resource analysis with specific numbers including avgTurnCast for key cards, (3) card synergy evaluation highlighting the strongest and weakest interactions using synergyScore data, (4) meta considerations and matchup-specific insights referencing perArchetypeWinRates and the [WEAK SPOT] archetypes flagged in Sim Insights, and (5) prioritized list of improvements ranked by expected impact on win rate",
"removalCoverage": {{
"creatures": "Assessment of creature removal",
"artifacts": "Assessment of artifact removal",
@@ -174,9 +186,9 @@ You MUST respond ONLY with valid JSON matching this exact structure:
Do NOT include any text outside the JSON object. No markdown fences, no preamble."""
# ══════════════════════════════════════════════════════════════
# ════════════════════════════════════════════════════════════
# Quick Digest Prompt
# ══════════════════════════════════════════════════════════════
# ════════════════════════════════════════════════════════════
QUICK_DIGEST_SYSTEM_PROMPT = """You are an expert Magic: The Gathering Commander deck coach.
Provide a FAST summary analysis of this deck. Be concise.
@@ -184,6 +196,8 @@ Provide a FAST summary analysis of this deck. Be concise.
RULES:
- NEVER suggest cutting the commander ({commander}).
- Only suggest cards within the deck's color identity: {color_identity}.
- Cards flagged [OPENING HAND TRAP] have low keptInOpeningHandRate — prime cut candidates.
- Cards flagged [WEAK SPOT] in archetype win rates show where the deck loses most.
{goals_section}
You MUST respond ONLY with valid JSON matching this exact structure:
@@ -207,9 +221,9 @@ You MUST respond ONLY with valid JSON matching this exact structure:
Do NOT include any text outside the JSON object. No markdown fences, no preamble."""
# ══════════════════════════════════════════════════════════════
# User Prompt Builder
# ══════════════════════════════════════════════════════════════
# ════════════════════════════════════════════════════════════
# User Prompt Template
# ════════════════════════════════════════════════════════════
USER_PROMPT_TEMPLATE = """## Deck Report: {deck_name}
Commander: {commander}
@@ -224,6 +238,9 @@ Avg Game Length: {avg_game_length:.1f} turns
- Card Types: {type_summary}
- Functional Roles: {role_summary}
### Sim Insights (pre-computed deck-level signals)
{sim_insights_section}
### Matchup Data
{matchup_section}
@@ -236,6 +253,9 @@ Avg Game Length: {avg_game_length:.1f} turns
### Underperforming Cards (candidates for cuts)
{underperformer_section}
### RAG-Retrieved Similar Cards (from ChromaDB)
{rag_section}
### Candidate Replacement Cards (from embeddings search)
{candidate_section}
@@ -243,6 +263,7 @@ Avg Game Length: {avg_game_length:.1f} turns
Based on this simulation data, provide your coaching suggestions."""
def _format_goals_section(goals: Optional[CoachGoals]) -> str:
"""Format goals into system prompt rules."""
if not goals:
@@ -273,7 +294,11 @@ def _format_curve(buckets: List[int]) -> str:
def _format_top_cards(cards: List[CardPerformance], top_n: int = 10) -> str:
"""Format the best performing cards with extended stats."""
"""Format the best performing cards with extended stats.
Phase 1: adds keptInOpeningHandRate so Claude sees hand quality for
the good cards too, not just the underperformers.
"""
sorted_cards = sorted(cards, key=lambda c: c.impactScore, reverse=True)
lines = []
for c in sorted_cards[:top_n]:
@@ -282,7 +307,9 @@ def _format_top_cards(cards: List[CardPerformance], top_n: int = 10) -> str:
lines.append(
f"- {c.name}: impact={c.impactScore:.3f}, "
f"castRate={c.castRate:.1%}, drawnRate={c.drawnRate:.1%}, "
f"synergy={c.synergyScore:.3f}{turn_str}{tags}"
f"synergy={c.synergyScore:.3f}, "
f"keptInOpeningHand={c.keptInOpeningHandRate:.1%}"
f"{turn_str}{tags}"
)
return "\n".join(lines) if lines else "No card performance data available."
@@ -296,8 +323,16 @@ def _format_underperformers(cards: List[CardPerformance],
c = card_map.get(name)
if c:
turn_str = f", avgTurnCast={c.avgTurnCast:.1f}" if c.avgTurnCast is not None else ""
flags = []
if c.keptInOpeningHandRate < 0.25:
flags.append("OPENING HAND TRAP")
if c.avgTurnCast is not None and c.avgTurnCast > 6:
flags.append("LATE CASTER")
if c.synergyScore < 0.1:
flags.append("ISOLATED")
flag_str = f" [{', '.join(flags)}]" if flags else ""
lines.append(
f"- {c.name}: impact={c.impactScore:.3f}, "
f"- {c.name}{flag_str}: impact={c.impactScore:.3f}, "
f"deadCardRate={c.deadCardRate:.1%}, "
f"castRate={c.castRate:.1%}, "
f"clunkiness={c.clunkinessScore:.3f}, "
@@ -310,6 +345,87 @@ def _format_underperformers(cards: List[CardPerformance],
return "\n".join(lines) if lines else "No clear underperformers identified."
def _format_sim_insights(cards: List[CardPerformance],
archetype_rates: Dict[str, float]) -> str:
"""Compute and format deck-level sim signals for the Sim Insights section.
Phase 1 (#108): surfaces pre-computed per-card stats as deck-level
aggregates so Claude reasons from numbers directly.
Signals produced:
1. Opening hand quality — deck avg keptInOpeningHandRate + worst offenders
2. Curve velocity — castRate-weighted avg avgTurnCast + late-caster flags
3. Synergy outliers — bottom-3 synergyScore cards flagged as [ISOLATED]
4. Archetype weak spots — archetypes < 40% win rate flagged [WEAK SPOT]
"""
if not cards:
return "No simulation data available."
lines = []
# ── 1. Opening hand quality ───────────────────────────────────────────
hand_rates = [c.keptInOpeningHandRate for c in cards if c.keptInOpeningHandRate > 0]
if hand_rates:
avg_keep = sum(hand_rates) / len(hand_rates)
lines.append(f"Opening Hand Quality: deck avg keptInOpeningHandRate = {avg_keep:.1%}")
# Worst offenders: cards significantly below the deck average
threshold = avg_keep * 0.6 # >40% below avg is an outlier
offenders = sorted(
[c for c in cards if c.keptInOpeningHandRate > 0 and c.keptInOpeningHandRate < threshold],
key=lambda c: c.keptInOpeningHandRate
)[:3]
if offenders:
lines.append(" Opening hand traps (keptInOpeningHandRate far below deck avg):")
for c in offenders:
lines.append(f" - {c.name}: {c.keptInOpeningHandRate:.1%} [OPENING HAND TRAP]")
else:
lines.append("Opening Hand Quality: no keptInOpeningHandRate data available")
# ── 2. Curve velocity ───────────────────────────────────────────────
cast_cards = [c for c in cards if c.avgTurnCast is not None and c.castRate > 0]
if cast_cards:
total_weight = sum(c.castRate for c in cast_cards)
if total_weight > 0:
weighted_avg_turn = sum(
c.avgTurnCast * c.castRate for c in cast_cards
) / total_weight
lines.append(f"Curve Velocity: castRate-weighted avg avgTurnCast = {weighted_avg_turn:.1f}")
late_casters = sorted(
[c for c in cast_cards if c.avgTurnCast > 6],
key=lambda c: c.avgTurnCast,
reverse=True
)[:3]
if late_casters:
lines.append(" Late casters (avgTurnCast > 6 — may not fit the curve):")
for c in late_casters:
lines.append(f" - {c.name}: avgTurnCast={c.avgTurnCast:.1f} [LATE CASTER]")
else:
lines.append("Curve Velocity: no avgTurnCast data available")
# ── 3. Synergy outliers ──────────────────────────────────────────────
synergy_cards = sorted(
[c for c in cards if c.castRate > 0.05], # ignore near-never-cast cards
key=lambda c: c.synergyScore
)
if synergy_cards:
bottom3 = synergy_cards[:3]
lines.append("Synergy Outliers (lowest synergyScore among cast cards):")
for c in bottom3:
lines.append(f" - {c.name}: synergyScore={c.synergyScore:.3f} [ISOLATED]")
# ── 4. Archetype weak spots ──────────────────────────────────────────
if archetype_rates:
weak = {k: v for k, v in archetype_rates.items() if v < 0.40}
if weak:
lines.append("Archetype Weak Spots (win rate < 40%):")
for arch, wr in sorted(weak.items(), key=lambda x: x[1]):
lines.append(f" - vs {arch}: {wr:.1%} [WEAK SPOT]")
else:
lines.append("Archetype Weak Spots: none (all matchups >= 40% win rate)")
return "\n".join(lines) if lines else "No sim insights available."
def _format_candidates(candidates: Dict[str, List[dict]]) -> str:
"""Format replacement candidates grouped by the card they'd replace."""
if not candidates:
@@ -337,6 +453,24 @@ def _format_candidates(candidates: Dict[str, List[dict]]) -> str:
return "\n".join(lines)
def _format_rag_cards(rag_cards: List[dict]) -> str:
"""Format RAG-retrieved cards from ChromaDB vector search."""
if not rag_cards:
return "No RAG results available."
lines = []
for card in rag_cards[:15]:
name = card.get("name", "Unknown")
types = card.get("types", "")
mv = card.get("mana_value", "?")
mc = card.get("mana_cost", "")
dist = card.get("distance", 0)
text = card.get("text", "")[:80]
lines.append(f"- {name} ({mc}, MV:{mv}, {types}) [distance: {dist:.3f}]")
if text:
lines.append(f" Oracle: {text}")
return "\n".join(lines)
def _format_matchups(report: DeckReport) -> str:
"""Format per-opponent matchup data."""
if not report.matchups:
@@ -351,13 +485,23 @@ def _format_matchups(report: DeckReport) -> str:
def _format_archetypes(report: DeckReport) -> str:
"""Format per-archetype win rates from DeckMeta."""
"""Format per-archetype win rates from DeckMeta.
Phase 1: annotates weak spots ([WEAK SPOT] < 40%) and strengths
([STRENGTH] >= 55%) so Claude sees the signal immediately.
"""
rates = report.meta.perArchetypeWinRates
if not rates:
return "No archetype breakdown available."
lines = []
for archetype, wr in sorted(rates.items(), key=lambda x: x[1]):
lines.append(f"- vs {archetype}: {wr:.1%}")
if wr < 0.40:
tag = " [WEAK SPOT]"
elif wr >= 0.55:
tag = " [STRENGTH]"
else:
tag = ""
lines.append(f"- vs {archetype}: {wr:.1%}{tag}")
return "\n".join(lines)
@@ -375,9 +519,9 @@ def _format_combos(report: DeckReport) -> str:
return "\n".join(lines)
# ══════════════════════════════════════════════════════════════
# ════════════════════════════════════════════════════════════
# Main Builder Functions
# ══════════════════════════════════════════════════════════════
# ════════════════════════════════════════════════════════════
def build_system_prompt(report: DeckReport,
goals: Optional[CoachGoals] = None) -> str:
@@ -402,8 +546,12 @@ def build_quick_system_prompt(report: DeckReport,
def build_user_prompt(report: DeckReport,
candidates: Dict[str, List[dict]] = None) -> str:
"""Build the user prompt with full deck report data."""
candidates: Dict[str, List[dict]] = None,
rag_cards: List[dict] = None) -> str:
"""Build the user prompt with full deck report data.
Phase 1: adds sim_insights_section computed from card-level stats.
"""
color_str = "/".join(report.colorIdentity) if report.colorIdentity else "unknown"
type_parts = [f"{k}={v}" for k, v in report.structure.cardTypeCounts.items()] \
if report.structure.cardTypeCounts else ["unknown"]
@@ -420,11 +568,14 @@ def build_user_prompt(report: DeckReport,
curve_summary=_format_curve(report.structure.curveBuckets),
type_summary=", ".join(type_parts),
role_summary=", ".join(role_parts),
sim_insights_section=_format_sim_insights(
report.cards, report.meta.perArchetypeWinRates),
matchup_section=_format_matchups(report),
archetype_section=_format_archetypes(report),
top_cards=_format_top_cards(report.cards),
underperformer_section=_format_underperformers(
report.cards, report.underperformers),
candidate_section=_format_candidates(candidates or {}),
rag_section=_format_rag_cards(rag_cards or []),
combo_section=_format_combos(report),
)
+785
View File
@@ -0,0 +1,785 @@
# Commander-AI-Lab — Unity TCGEngine Commander Board: Implementation Plan (v5)
> **Architecture Principle:** Forge = Rules Authority · FastAPI = Hub · TCGEngine Unity = Renderer · Dual-Brain AI = Decision Engine
---
## System Architecture Overview
```
┌─────────────────────────────────────────────────────────────────┐
│ UNITY (TCGEngine — 2D Board) │
│ PlayerArea x4: Hand | Library | Graveyard | Exile | Command │
│ Battlefield zones per player · Card art from local cache │
│ GameNetworkManager.cs ←── WebSocket ──► FastAPI │
└─────────────────────────────────────────────────────────────────┘
▲ ▼
┌─────────────────────────────────────────────────────────────────┐
│ FASTAPI (commander-ai-lab) │
│ /ws/game — WebSocket relay │
│ /game/action — receive player/AI action, forward to Forge │
│ ForgeSubprocessManager — stdin/stdout pipe to Forge JVM │
│ CardEnricher — injects art_url from local image cache │
│ AIOrchestrator — routes decisions to DeepSeek-R1 or GPT-OSS │
│ ChromaDB RAG — card knowledge scoped per player deck │
│ ai_decisions.jsonl — logs every AI decision (PPO dataset) │
└─────────────────────────────────────────────────────────────────┘
▲ ▼
┌─────────────────────────────────────────────────────────────────┐
│ FORGE (Java — Headless Rules Engine) │
│ Full Commander rules: tax, 21-commander-damage, 40 life │
│ 4-player turn queue · Zone redirects · State-based actions │
│ Emits game state JSON to stdout · Receives actions via stdin │
└─────────────────────────────────────────────────────────────────┘
```
---
## Dual-Brain AI Decision Flow
This is the **core feature** of Commander-AI-Lab. Every AI player decision runs through this pipeline:
```
Forge emits GameStateDTO (JSON)
└─► FastAPI detects active player is AI
└─► select_brain(phase, stack_size)
├─ DeepSeek-R1-14B → combat, stack decisions, removal
└─ GPT-OSS-20B → main phase, ramp, card advantage
└─► ChromaDB.query(player_deck_cards, top_k=5)
└─► build_prompt(state, rag_context, personality)
└─► LLM.generate(prompt)
└─► parse_action_json(response)
├─ validate vs legal_actions[]
├─ log to ai_decisions.jsonl
└─► Forge stdin ← chosen action
```
### Brain Selector (`ai/brain_selector.py`)
```python
from enum import Enum
class Brain(Enum):
DEEPSEEK = "deepseek-r1:14b"
GPT_OSS = "gpt-oss-20b" # adjust to your Ollama model tag
def select_brain(phase: str, stack_size: int) -> Brain:
"""
DeepSeek-R1: combat math, instant-speed stack decisions, targeted removal.
GPT-OSS: sorcery-speed strategy, ramp sequencing, card selection.
"""
combat_phases = {"declare_attackers", "declare_blockers", "combat_damage"}
if phase in combat_phases or stack_size > 0:
return Brain.DEEPSEEK
return Brain.GPT_OSS
```
### Prompt Builder (`ai/prompt_builder.py`)
```python
from chromadb import Client
from .personality import get_personality_prompt
chroma = Client()
card_collection = chroma.get_collection("mtg_cards")
def build_prompt(state: dict, player_id: str, personality: str) -> str:
# Pull card knowledge scoped to THIS player's deck
player_cards = [c["name"] for c in state["players"][player_id]["library"]]
rag_results = card_collection.query(
query_texts=player_cards[:10],
n_results=5
)
card_context = "\n".join(rag_results["documents"][0])
personality_prompt = get_personality_prompt(personality)
return f"""You are an AI playing Magic: The Gathering Commander.
{personality_prompt}
## Current Game State
- Phase: {state['phase']}
- Active Player: {player_id}
- Life Totals: {state['life_totals']}
- Commander Damage Dealt: {state['commander_damage']}
- Stack: {state['stack']}
- Your Hand: {[c['name'] for c in state['players'][player_id]['hand']]}
- Battlefield: {[c['name'] for c in state['players'][player_id]['battlefield']]}
- Available Mana: {state['players'][player_id]['mana_pool']}
## Legal Actions
{state['legal_actions']}
## Card Knowledge (RAG Context)
{card_context}
Respond with a single JSON object:
{{"action": "<action_type>", "card_id": "<id_or_null>", "targets": [], "reasoning": "<brief>"}}
"""
```
### Personality System (`ai/personality.py`)
```python
import random
PERSONALITIES = {
"aggro_beatdown": "You play aggressively. Attack every turn. Remove blockers. Win through combat damage as fast as possible.",
"control_lockdown": "You play control. Counter key threats. Hold up mana for interaction. Win only when the board is locked.",
"combo_enabler": "You dig for your combo pieces. Every decision is evaluated by how it advances your win condition.",
"political_diplomat": "You form alliances. Attack the leader. Make deals. Never be the archenemy.",
"voltron_commander": "Everything buffs your commander. Protect it at all costs. Win through commander damage.",
"stax_prison": "You tax and restrict opponents. Deploy hate pieces. Win through attrition and resource denial.",
"tempo_value": "You trade efficiently. Generate card advantage. Win by being ahead on resources.",
"chaos_wildcard": "You make unpredictable decisions. Prioritize fun and chaos over optimal play.",
}
def assign_personalities(player_ids: list[str], deck_archetypes: dict) -> dict[str, str]:
"""
Assign a personality per AI player at game creation.
80% chance: archetype-matched personality.
20% chance: random wildcard for unpredictability.
"""
assignments = {}
for pid in player_ids:
archetype = deck_archetypes.get(pid, "tempo_value")
if random.random() < 0.20:
assignments[pid] = random.choice(list(PERSONALITIES.keys()))
else:
assignments[pid] = archetype
return assignments
def get_personality_prompt(personality: str) -> str:
return f"## Your Playstyle\n{PERSONALITIES.get(personality, PERSONALITIES['tempo_value'])}"
```
### AI Orchestrator (`ai/ai_orchestrator.py`)
```python
import json, time, logging
from pathlib import Path
from .brain_selector import select_brain, Brain
from .prompt_builder import build_prompt
import ollama
LOG_PATH = Path("logs/ai_decisions.jsonl")
LOG_PATH.parent.mkdir(exist_ok=True)
async def get_ai_action(state: dict, player_id: str, personality: str) -> dict:
brain = select_brain(state["phase"], len(state["stack"]))
prompt = build_prompt(state, player_id, personality)
t_start = time.time()
try:
response = ollama.chat(
model=brain.value,
messages=[{"role": "user", "content": prompt}],
options={"temperature": 0.3, "num_predict": 256}
)
raw = response["message"]["content"]
# Extract JSON from response (model may wrap in markdown)
action = json.loads(raw[raw.find("{"):raw.rfind("}")+1])
except Exception as e:
logging.warning(f"AI parse failed ({brain.value}): {e}. Passing turn.")
action = {"action": "pass", "card_id": None, "targets": [], "reasoning": "parse_error"}
# Validate against Forge legal actions
if action.get("action") not in [a["type"] for a in state["legal_actions"]]:
logging.warning(f"Illegal action returned by {brain.value}. Falling back to pass.")
action = {"action": "pass", "card_id": None, "targets": [], "reasoning": "illegal_action"}
# Log for PPO training dataset
log_entry = {
"timestamp": time.time(),
"player_id": player_id,
"brain": brain.value,
"personality": personality,
"phase": state["phase"],
"action": action,
"reasoning": action.get("reasoning", ""),
"latency_ms": round((time.time() - t_start) * 1000),
}
with open(LOG_PATH, "a") as f:
f.write(json.dumps(log_entry) + "\n")
return action
```
---
## Phase 0 — Local Card Image Cache
**Build this first.** Every downstream system depends on local art URLs.
### Step 1 — Bulk Download (`scripts/download_card_images.py`)
```python
import requests, json, os
from pathlib import Path
from tqdm import tqdm
BULK_URL = "https://api.scryfall.com/bulk-data/default-cards"
IMAGE_DIR = Path("static/card-images")
IMAGE_DIR.mkdir(parents=True, exist_ok=True)
def download_images():
# Get bulk data URL
meta = requests.get(BULK_URL).json()
cards = requests.get(meta["download_uri"], stream=True).json()
for card in tqdm(cards, desc="Downloading card images"):
image_uris = card.get("image_uris") or {}
url = image_uris.get("normal")
if not url:
continue # skip DFCs without top-level image_uris
safe_name = card["name"].replace("/", "_").replace(" ", "_")
set_code = card.get("set", "unknown")
filename = IMAGE_DIR / set_code / f"{safe_name}_{card['collector_number']}.jpg"
filename.parent.mkdir(parents=True, exist_ok=True)
if filename.exists():
continue # already cached
img_data = requests.get(url, timeout=10).content
filename.write_bytes(img_data)
if __name__ == "__main__":
download_images()
```
### Step 2 — Build SQLite Index (`scripts/build_image_index.py`)
```python
import sqlite3
from pathlib import Path
IMAGE_DIR = Path("static/card-images")
DB_PATH = Path("data/card_images.db")
DB_PATH.parent.mkdir(exist_ok=True)
def build_index():
conn = sqlite3.connect(DB_PATH)
conn.execute("""
CREATE TABLE IF NOT EXISTS card_images (
name TEXT PRIMARY KEY,
path TEXT NOT NULL
)
""")
for img_path in IMAGE_DIR.rglob("*.jpg"):
card_name = img_path.stem.rsplit("_", 1)[0].replace("_", " ")
conn.execute("INSERT OR REPLACE INTO card_images VALUES (?, ?)",
(card_name, str(img_path)))
conn.commit()
conn.close()
if __name__ == "__main__":
build_index()
```
### Step 3 — Card Enricher (`api/card_enricher.py`)
```python
import sqlite3
from pathlib import Path
DB_PATH = Path("data/card_images.db")
STATIC_URL = "http://localhost:8000/static/card-images"
def enrich_card(card: dict) -> dict:
"""Inject local art_url into every CardDTO before sending to Unity."""
conn = sqlite3.connect(DB_PATH)
cursor = conn.execute("SELECT path FROM card_images WHERE name = ?", (card["name"],))
row = cursor.fetchone()
conn.close()
if row:
relative = Path(row[0]).relative_to("static/card-images")
card["art_url"] = f"{STATIC_URL}/{relative}"
else:
card["art_url"] = f"{STATIC_URL}/card_back.jpg"
return card
```
### Step 4 — FastAPI Static Mount (`main.py` addition)
```python
from fastapi.staticfiles import StaticFiles
app.mount("/static/card-images", StaticFiles(directory="static/card-images"), name="card-images")
```
---
## Phase 1 — Forge Bridge (Rules Engine)
### ForgeSubprocessManager (`forge/subprocess_manager.py`)
```python
import subprocess, json, asyncio, threading
from pathlib import Path
FORGE_JAR = Path("forge/forge-game.jar")
class ForgeSubprocessManager:
def __init__(self):
self.process = None
self._lock = threading.Lock()
def start(self, deck_paths: list[str], format: str = "commander"):
cmd = [
"java", "-jar", str(FORGE_JAR),
"--headless",
"--format", format,
"--decks", *deck_paths,
"--output", "json"
]
self.process = subprocess.Popen(
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1
)
def read_state(self) -> dict:
with self._lock:
line = self.process.stdout.readline()
return json.loads(line)
def send_action(self, action: dict):
with self._lock:
self.process.stdin.write(json.dumps(action) + "\n")
self.process.stdin.flush()
def stop(self):
if self.process:
self.process.terminate()
```
### Data Transfer Objects (`api/dtos.py`)
```python
from pydantic import BaseModel
from typing import Optional
class CardDTO(BaseModel):
id: str
name: str
type: str
power: Optional[str]
toughness: Optional[str]
mana_cost: str
art_url: str = ""
tapped: bool = False
zone: str = "hand"
class PlayerDTO(BaseModel):
id: str
life: int
commander_damage: dict[str, int]
commander_tax: int
hand: list[CardDTO]
battlefield: list[CardDTO]
graveyard: list[CardDTO]
exile: list[CardDTO]
command_zone: list[CardDTO]
library_count: int
mana_pool: dict[str, int]
is_ai: bool
personality: Optional[str]
class GameStateDTO(BaseModel):
game_id: str
phase: str
active_player: str
priority: str
turn_number: int
players: list[PlayerDTO]
stack: list[dict]
legal_actions: list[dict]
winner: Optional[str]
```
---
## Phase 2 — FastAPI WebSocket Game Server
### Game Loop (`api/game_loop.py`)
```python
import asyncio, uuid
from .dtos import GameStateDTO
from forge.subprocess_manager import ForgeSubprocessManager
from ai.ai_orchestrator import get_ai_action
from ai.personality import assign_personalities
from api.card_enricher import enrich_card
active_games: dict[str, dict] = {}
async def start_game(deck_paths: list[str], ai_players: list[str]) -> str:
game_id = str(uuid.uuid4())
forge = ForgeSubprocessManager()
forge.start(deck_paths)
# Assign AI personalities at game creation
archetypes = detect_archetypes(deck_paths, ai_players) # ChromaDB query
personalities = assign_personalities(ai_players, archetypes)
active_games[game_id] = {
"forge": forge,
"ai_players": ai_players,
"personalities": personalities,
"websockets": []
}
asyncio.create_task(game_loop(game_id))
return game_id
async def game_loop(game_id: str):
game = active_games[game_id]
forge = game["forge"]
while True:
raw_state = forge.read_state()
if raw_state.get("winner"):
await broadcast(game_id, raw_state)
break
# Enrich every card with local art_url
state = enrich_state(raw_state)
await broadcast(game_id, state)
priority_player = state["priority"]
if priority_player in game["ai_players"]:
personality = game["personalities"][priority_player]
action = await get_ai_action(state, priority_player, personality)
forge.send_action(action)
# else: wait for Unity client to send action via WebSocket
async def broadcast(game_id: str, state: dict):
game = active_games[game_id]
for ws in game["websockets"]:
await ws.send_json(state)
def enrich_state(state: dict) -> dict:
for player in state["players"]:
for zone in ["hand", "battlefield", "graveyard", "exile", "command_zone"]:
player[zone] = [enrich_card(c) for c in player.get(zone, [])]
return state
```
### WebSocket Endpoint (`api/routes/game.py`)
```python
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from api.game_loop import active_games, start_game
router = APIRouter()
@router.websocket("/ws/game/{game_id}")
async def game_ws(websocket: WebSocket, game_id: str):
await websocket.accept()
game = active_games.get(game_id)
if not game:
await websocket.close(code=4004)
return
game["websockets"].append(websocket)
try:
while True:
data = await websocket.receive_json()
# Human player action forwarded directly to Forge
game["forge"].send_action(data)
except WebSocketDisconnect:
game["websockets"].remove(websocket)
```
---
## Phase 3 — Unity TCGEngine Board (2D)
### Project Layout
```
unity-commander-ui/
├── Assets/
│ ├── TCGEngine/ ← TCGEngine base (unmodified)
│ ├── Commander/
│ │ ├── Scripts/
│ │ │ ├── GameNetworkManager.cs ← WebSocket ↔ FastAPI
│ │ │ ├── CommanderBoardUI.cs ← 4-player layout controller
│ │ │ ├── PlayerAreaUI.cs ← per-player zone panel
│ │ │ ├── CardImageLoader.cs ← local cache texture fetcher
│ │ │ ├── ZonePanel.cs ← Hand/Library/Graveyard/Exile/Command
│ │ │ └── AIThinkingIndicator.cs ← spinner while AI brain is deciding
│ │ └── Prefabs/
│ │ ├── CardPrefab.prefab
│ │ ├── PlayerAreaPrefab.prefab
│ │ └── ZonePanelPrefab.prefab
│ └── Scenes/
│ └── CommanderBoard.unity
```
### Board Layout (matches screenshot)
```
┌──────────────────────────────────────────────────────────────────┐
│ [LIBRARY][GRAVEYARD][EXILE] [────── HAND ──────] (Player 3) │
│ [COMMAND] │
├──────────────────────────────┬───────────────────────────────────┤
│ │ │
│ │ │
│ BATTLEFIELD │ BATTLEFIELD │
│ (Player 1) │ (Player 2) │
│ BATTLEFIELD (center) │
│ BATTLEFIELD │ BATTLEFIELD │
│ (Player 4) │ (Player 3) │
│ │ │
├──────────────────────────────┴───────────────────────────────────┤
│ [COMMAND] │
│ [LIBRARY][GRAVEYARD][EXILE] [────── HAND ──────] (Player 1) │
└──────────────────────────────────────────────────────────────────┘
```
### GameNetworkManager.cs
```csharp
using UnityEngine;
using NativeWebSocket;
using Newtonsoft.Json;
public class GameNetworkManager : MonoBehaviour
{
public static GameNetworkManager Instance { get; private set; }
[SerializeField] private string serverUrl = "ws://localhost:8000/ws/game/";
[SerializeField] private string gameId;
private WebSocket _socket;
public delegate void GameStateReceived(GameStateDTO state);
public event GameStateReceived OnGameStateReceived;
async void Awake()
{
Instance = this;
_socket = new WebSocket($"{serverUrl}{gameId}");
_socket.OnMessage += (bytes) =>
{
var json = System.Text.Encoding.UTF8.GetString(bytes);
var state = JsonConvert.DeserializeObject<GameStateDTO>(json);
UnityMainThreadDispatcher.Enqueue(() => OnGameStateReceived?.Invoke(state));
};
await _socket.Connect();
}
public async void SendAction(object action)
{
var json = JsonConvert.SerializeObject(action);
await _socket.SendText(json);
}
void Update() => _socket?.DispatchMessageQueue();
async void OnDestroy() => await _socket?.Close();
}
```
### CardImageLoader.cs
```csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
public class CardImageLoader : MonoBehaviour
{
private static Dictionary<string, Texture2D> _cache = new();
public static IEnumerator LoadCardArt(string artUrl, RawImage target)
{
if (_cache.TryGetValue(artUrl, out var cached))
{
target.texture = cached;
yield break;
}
using var req = UnityWebRequestTexture.GetTexture(artUrl);
yield return req.SendWebRequest();
if (req.result == UnityWebRequest.Result.Success)
{
var tex = DownloadHandlerTexture.GetContent(req);
_cache[artUrl] = tex;
target.texture = tex;
}
else
{
Debug.LogWarning($"Failed to load card art: {artUrl}");
}
}
}
```
### AIThinkingIndicator.cs
```csharp
using UnityEngine;
using UnityEngine.UI;
/// Shows a spinner + brain label while the AI is deciding.
public class AIThinkingIndicator : MonoBehaviour
{
[SerializeField] private GameObject spinnerPanel;
[SerializeField] private Text brainLabel;
[SerializeField] private Text personalityLabel;
public void Show(string brain, string personality)
{
spinnerPanel.SetActive(true);
brainLabel.text = $"🧠 {brain}";
personalityLabel.text = $"🎭 {personality}";
}
public void Hide() => spinnerPanel.SetActive(false);
}
```
---
## Phase 4 — Commander Zone UIs
### Zone Definitions per Player
| Zone Panel | Content | Notes |
|---|---|---|
| `LIBRARY` | Count only | Click → draw/look at top |
| `GRAVEYARD` | Scrollable card list | Full card art on hover |
| `EXILE` | Scrollable card list | Includes face-down indicators |
| `HAND` | Wide panel, card art | Hidden to opponents |
| `COMMAND` | Commander card(s) | Shows commander tax counter |
| `BATTLEFIELD` | Open grid | Tapped/untapped state, counters |
### Commander-Specific UI Elements
```csharp
public class CommanderZoneUI : MonoBehaviour
{
[SerializeField] private Text taxCounter; // "Tax: +{2N}"
[SerializeField] private RawImage commanderArt;
[SerializeField] private Text commanderDamageDealt; // per opponent
public void Refresh(PlayerDTO player)
{
int tax = player.CommanderTax;
taxCounter.text = tax > 0 ? $"+{tax * 2}" : "";
if (player.CommandZone.Count > 0)
StartCoroutine(CardImageLoader.LoadCardArt(
player.CommandZone[0].ArtUrl, commanderArt));
}
}
```
---
## Phase 5 — Commander Rules Validation Layer
Forge handles all rules natively. FastAPI validates the AI response is within the legal actions list Forge returns — no custom rules code needed in Python or Unity.
Key Commander rules confirmed handled by Forge headless:
- **Commander Tax** — +2 generic per previous cast from command zone
- **21 Commander Damage** — tracked per attacker/defender pair
- **40 Starting Life**
- **Zone Redirect** — commander goes to command zone instead of graveyard/exile (player choice)
- **Color Identity** — deck validation at game start
- **Singleton** — deck validation at game start
---
## Phase 6 — AI Decision Logging & PPO Pipeline
Every AI decision written to `logs/ai_decisions.jsonl`:
```json
{
"timestamp": 1743520800.123,
"player_id": "player_2",
"brain": "deepseek-r1:14b",
"personality": "aggro_beatdown",
"phase": "declare_attackers",
"action": {"action": "attack", "card_id": "c_042", "targets": ["player_1"], "reasoning": "Open attack, no blockers visible"},
"latency_ms": 847
}
```
This log becomes the **supervised learning dataset** for the next PPO training iteration. Each (state, action) pair from winning games is a positive training example.
---
## Project Directory Structure
```
commander-ai-lab/
├── main.py ← FastAPI app, mounts static files
├── api/
│ ├── dtos.py ← Pydantic DTOs
│ ├── card_enricher.py ← Injects art_url into CardDTOs
│ ├── game_loop.py ← Main game orchestration loop
│ └── routes/
│ └── game.py ← WebSocket + REST endpoints
├── ai/
│ ├── ai_orchestrator.py ← Main AI decision entry point
│ ├── brain_selector.py ← DeepSeek-R1 vs GPT-OSS routing
│ ├── prompt_builder.py ← Game state + RAG → prompt
│ └── personality.py ← 8 personalities + assignment
├── forge/
│ └── subprocess_manager.py ← Forge JVM stdin/stdout bridge
├── scripts/
│ ├── download_card_images.py ← One-time Scryfall bulk download
│ └── build_image_index.py ← SQLite name→path index
├── static/
│ └── card-images/ ← ~2GB local image cache
│ └── {set_code}/
│ └── {CardName}_{num}.jpg
├── data/
│ └── card_images.db ← SQLite image index
├── logs/
│ └── ai_decisions.jsonl ← PPO training dataset
├── unity-commander-ui/ ← Unity 2D TCGEngine project
└── docs/
└── implementation_plan.md ← This document
```
---
## Phased Timeline
| Phase | Scope | Est. Time |
|---|---|---|
| **0** | Scryfall image download + SQLite index + FastAPI static mount | 1 day |
| **1** | Forge subprocess bridge + DTO definitions | 2–3 days |
| **2** | AI Orchestrator + brain selector + prompt builder + personality system | 3–4 days |
| **3** | Unity TCGEngine project setup + WebSocket GameNetworkManager | 3–4 days |
| **4** | PlayerAreaUI + ZonePanels + CardImageLoader + board layout | 3–4 days |
| **5** | Commander zone UI (tax counter, commander damage, zone redirect) | 1–2 days |
| **6** | AI decision logging + end-to-end game loop test | 1–2 days |
**Total estimated: ~3 weeks to first playable Commander game with live AI opponents.**
---
## Key Design Decisions
| Decision | Rationale |
|---|---|
| Forge = rules authority | Forge already handles all Commander rules natively. No custom rules code. |
| TCGEngine = dumb renderer | All game logic lives in Forge. Unity only renders state it receives. |
| FastAPI = hub | Bridges Forge ↔ Unity ↔ AI. Single point of truth. |
| Dual-brain routing | DeepSeek-R1 excels at analytical/math decisions; GPT-OSS at strategic sequencing. |
| ChromaDB scoped per player | RAG context relevant to the active player's deck, not global card knowledge. |
| Personality persists per game | Consistent playstyle per AI across all 4 players per session. |
| Local image cache | Eliminates network latency for card art. ~2GB one-time download. |
| ai_decisions.jsonl | Every AI decision is a future PPO training sample. |
+25492
View File
File diff suppressed because it is too large. Load diff
+634
View File
@@ -0,0 +1,634 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MTG Commander Battlefield</title>
<link rel="stylesheet" href="style.css?v=20260403_0300">
</head>
<body>
<!-- ====================================================
SETUP SCREEN
==================================================== -->
<div id="setup-screen">
<!-- Populated by ui.js initSetupScreen() -->
</div>
<!-- ====================================================
TABLE SURFACE — 4-Quadrant Battlefield
==================================================== -->
<div class="table-surface" id="game-battlefield" style="display:none;">
<!-- TOP BAR: Phase tracker + controls -->
<div id="top-bar">
<span class="top-bar-logo">Commander</span>
<div class="turn-counter-badge" id="turn-counter">Turn <span>1</span></div>
<div class="active-player-indicator" id="active-player-indicator">
<span class="active-player-dot" id="active-dot" style="background:#c0392b;"></span>
Player 1's Turn
</div>
<!-- Phase track -->
<div class="phase-track-wrapper">
<div class="phase-track" id="phase-track"></div>
</div>
<!-- Phase/turn action buttons -->
<div class="top-bar-actions">
<button class="btn-phase" onclick="nextPhase()" title="Advance to next phase (N)">Next Phase</button>
<button class="btn-phase btn-next-turn" onclick="nextTurn()" title="End turn, advance to next player (T)">Next Turn</button>
</div>
<!-- Utility buttons -->
<div class="top-bar-utils">
<button class="btn-util" id="btn-undo" onclick="undo()" title="Undo (Ctrl+Z)" disabled>&#x21A9;</button>
<button class="btn-util" id="btn-redo" onclick="redo()" title="Redo (Ctrl+Y)" disabled>&#x21AA;</button>
<button class="btn-util" id="btn-triggers" onclick="toggleTriggerStack()" title="Trigger Stack">&#x2747;</button>
<button class="btn-util" onclick="openDiceModal()" title="Dice &amp; Coins (G)">&#x1F3B2;</button>
<button class="btn-util" onclick="checkStateBasedActions()" title="Check State-Based Actions">SBA</button>
<button class="btn-util" onclick="toggleSettingsPanel()" title="Settings">&#x2699;</button>
<button class="btn-util" id="btn-search-toggle" onclick="document.getElementById('card-search-sidebar').classList.toggle('collapsed')" title="Card Search (S)">&#x1F50D;</button>
</div>
<!-- Storm counter -->
<span id="storm-badge" style="display:none;font-size:10px;color:var(--mtg-gold);background:rgba(200,168,67,0.15);border:1px solid rgba(200,168,67,0.3);border-radius:4px;padding:2px 7px;white-space:nowrap;">Storm: 0</span>
</div>
<!-- DIVIDER LINES -->
<div class="divider-h"></div>
<div class="divider-v"></div>
<!-- CORNER ACCENTS -->
<div class="corner-accent tl"></div>
<div class="corner-accent tr"></div>
<div class="corner-accent bl"></div>
<div class="corner-accent br"></div>
<!-- CENTER EMBLEM -->
<div class="center-emblem">
<svg viewBox="0 0 100 100" fill="none">
<circle cx="50" cy="50" r="42" stroke="rgba(255,255,255,0.2)" stroke-width="1.5"/>
<circle cx="50" cy="50" r="28" stroke="rgba(255,255,255,0.1)" stroke-width="1"/>
<polygon points="50,15 58.5,37 82,37 63,51 69,74 50,60 31,74 37,51 18,37 41.5,37"
fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.2)" stroke-width="0.8"/>
</svg>
</div>
<!-- ================================================
4-QUADRANT BATTLEFIELD
================================================ -->
<div class="battlefield">
<!-- PLAYER 1: Top-Left (Crimson) -->
<div class="player-area p1" id="player-area-1">
<span class="player-label" id="player-label-1">Player 1</span>
<div class="life-counter" id="life-counter-1">
<button onclick="adjustLife(0,-1)" aria-label="Decrease P1 life">&#x2212;</button>
<span class="life-counter-value" id="life-0" onclick="showSetLifeDialog(0)">40</span>
<button onclick="adjustLife(0,1)" aria-label="Increase P1 life">+</button>
</div>
<div class="poison-badge" id="poison-0" style="display:none;" onclick="adjustPoison(0,1)" title="Poison counters (click +1, right-click -1)" oncontextmenu="event.preventDefault();adjustPoison(0,-1)">0 poison</div>
<!-- Zone Row: Library, Exile, Graveyard, Hand -->
<div class="zone-row" id="zone-row-1">
<div class="zone library" id="lib-zone-1" data-player="1" data-zone="library"
onclick="openZoneModal(0,'library')" data-tooltip="Library">
<span class="card-count" id="lib-count-1">0</span>
<span class="zone-icon">&#x1F4DA;</span>
<span class="zone-name">Library</span>
</div>
<div class="zone exile" id="exile-zone-1" data-player="1" data-zone="exile"
onclick="openZoneModal(0,'exile')" data-tooltip="Exile">
<span class="card-count" id="exile-count-1" style="display:none;">0</span>
<span class="zone-icon">&#x2B21;</span>
<span class="zone-name">Exile</span>
</div>
<div class="zone graveyard" id="gv-zone-1" data-player="1" data-zone="graveyard"
onclick="openZoneModal(0,'graveyard')" data-tooltip="Graveyard">
<span class="card-count" id="gv-count-1" style="display:none;">0</span>
<span class="zone-icon">&#x2620;</span>
<span class="zone-name">Grave</span>
</div>
<div class="zone hand" id="hand-zone-1" data-player="1" data-zone="hand"
onclick="openZoneModal(0,'hand')" data-tooltip="Hand">
<span class="zone-icon">&#x1F0CF;</span>
<span class="zone-name">Hand (<span id="hand-count-1">0</span>)</span>
</div>
</div>
<!-- Commander Row -->
<div class="commander-row" id="cmd-row-1">
<div class="zone commander" id="cmd-zone-1" data-player="1" data-zone="command"
onclick="openZoneModal(0,'command')" data-tooltip="Command Zone">
<span class="zone-icon">&#x1F451;</span>
<span class="zone-name">Commander</span>
</div>
</div>
<!-- Battle Zone -->
<div class="battle-zone" id="battle-zone-1">
<div class="battle-zone-inner"></div>
<span class="battle-zone-label">Battlefield</span>
<!-- Battlefield cards injected here -->
</div>
</div>
<!-- PLAYER 2: Top-Right (Sapphire) -->
<div class="player-area p2" id="player-area-2">
<span class="player-label" id="player-label-2">Player 2</span>
<div class="life-counter" id="life-counter-2">
<button onclick="adjustLife(1,-1)" aria-label="Decrease P2 life">&#x2212;</button>
<span class="life-counter-value" id="life-1" onclick="showSetLifeDialog(1)">40</span>
<button onclick="adjustLife(1,1)" aria-label="Increase P2 life">+</button>
</div>
<div class="poison-badge" id="poison-1" style="display:none;" onclick="adjustPoison(1,1)" oncontextmenu="event.preventDefault();adjustPoison(1,-1)">0 poison</div>
<div class="zone-row" id="zone-row-2">
<div class="zone library" id="lib-zone-2" data-player="2" data-zone="library"
onclick="openZoneModal(1,'library')" data-tooltip="Library">
<span class="card-count" id="lib-count-2">0</span>
<span class="zone-icon">&#x1F4DA;</span>
<span class="zone-name">Library</span>
</div>
<div class="zone exile" id="exile-zone-2" data-player="2" data-zone="exile"
onclick="openZoneModal(1,'exile')" data-tooltip="Exile">
<span class="card-count" id="exile-count-2" style="display:none;">0</span>
<span class="zone-icon">&#x2B21;</span>
<span class="zone-name">Exile</span>
</div>
<div class="zone graveyard" id="gv-zone-2" data-player="2" data-zone="graveyard"
onclick="openZoneModal(1,'graveyard')" data-tooltip="Graveyard">
<span class="card-count" id="gv-count-2" style="display:none;">0</span>
<span class="zone-icon">&#x2620;</span>
<span class="zone-name">Grave</span>
</div>
<div class="zone hand" id="hand-zone-2" data-player="2" data-zone="hand"
onclick="openZoneModal(1,'hand')" data-tooltip="Hand">
<span class="zone-icon">&#x1F0CF;</span>
<span class="zone-name">Hand (<span id="hand-count-2">0</span>)</span>
</div>
</div>
<div class="commander-row" id="cmd-row-2">
<div class="zone commander" id="cmd-zone-2" data-player="2" data-zone="command"
onclick="openZoneModal(1,'command')" data-tooltip="Command Zone">
<span class="zone-icon">&#x1F451;</span>
<span class="zone-name">Commander</span>
</div>
</div>
<div class="battle-zone" id="battle-zone-2">
<div class="battle-zone-inner"></div>
<span class="battle-zone-label">Battlefield</span>
</div>
</div>
<!-- PLAYER 3: Bottom-Left (Emerald) -->
<div class="player-area p3" id="player-area-3">
<!-- Battle zone on top for bottom players -->
<div class="battle-zone" id="battle-zone-3">
<div class="battle-zone-inner"></div>
<span class="battle-zone-label">Battlefield</span>
</div>
<div class="commander-row" id="cmd-row-3">
<div class="zone commander" id="cmd-zone-3" data-player="3" data-zone="command"
onclick="openZoneModal(2,'command')" data-tooltip="Command Zone">
<span class="zone-icon">&#x1F451;</span>
<span class="zone-name">Commander</span>
</div>
</div>
<div class="zone-row" id="zone-row-3">
<div class="zone library" id="lib-zone-3" data-player="3" data-zone="library"
onclick="openZoneModal(2,'library')" data-tooltip="Library">
<span class="card-count" id="lib-count-3">0</span>
<span class="zone-icon">&#x1F4DA;</span>
<span class="zone-name">Library</span>
</div>
<div class="zone exile" id="exile-zone-3" data-player="3" data-zone="exile"
onclick="openZoneModal(2,'exile')" data-tooltip="Exile">
<span class="card-count" id="exile-count-3" style="display:none;">0</span>
<span class="zone-icon">&#x2B21;</span>
<span class="zone-name">Exile</span>
</div>
<div class="zone graveyard" id="gv-zone-3" data-player="3" data-zone="graveyard"
onclick="openZoneModal(2,'graveyard')" data-tooltip="Graveyard">
<span class="card-count" id="gv-count-3" style="display:none;">0</span>
<span class="zone-icon">&#x2620;</span>
<span class="zone-name">Grave</span>
</div>
<div class="zone hand" id="hand-zone-3" data-player="3" data-zone="hand"
onclick="openZoneModal(2,'hand')" data-tooltip="Hand">
<span class="zone-icon">&#x1F0CF;</span>
<span class="zone-name">Hand (<span id="hand-count-3">0</span>)</span>
</div>
</div>
<span class="player-label" id="player-label-3">Player 3</span>
<div class="life-counter" id="life-counter-3">
<button onclick="adjustLife(2,-1)" aria-label="Decrease P3 life">&#x2212;</button>
<span class="life-counter-value" id="life-2" onclick="showSetLifeDialog(2)">40</span>
<button onclick="adjustLife(2,1)" aria-label="Increase P3 life">+</button>
</div>
<div class="poison-badge" id="poison-2" style="display:none;" onclick="adjustPoison(2,1)" oncontextmenu="event.preventDefault();adjustPoison(2,-1)">0 poison</div>
</div>
<!-- PLAYER 4: Bottom-Right (Gold) -->
<div class="player-area p4" id="player-area-4">
<div class="battle-zone" id="battle-zone-4">
<div class="battle-zone-inner"></div>
<span class="battle-zone-label">Battlefield</span>
</div>
<div class="commander-row" id="cmd-row-4">
<div class="zone commander" id="cmd-zone-4" data-player="4" data-zone="command"
onclick="openZoneModal(3,'command')" data-tooltip="Command Zone">
<span class="zone-icon">&#x1F451;</span>
<span class="zone-name">Commander</span>
</div>
</div>
<div class="zone-row" id="zone-row-4">
<div class="zone library" id="lib-zone-4" data-player="4" data-zone="library"
onclick="openZoneModal(3,'library')" data-tooltip="Library">
<span class="card-count" id="lib-count-4">0</span>
<span class="zone-icon">&#x1F4DA;</span>
<span class="zone-name">Library</span>
</div>
<div class="zone exile" id="exile-zone-4" data-player="4" data-zone="exile"
onclick="openZoneModal(3,'exile')" data-tooltip="Exile">
<span class="card-count" id="exile-count-4" style="display:none;">0</span>
<span class="zone-icon">&#x2B21;</span>
<span class="zone-name">Exile</span>
</div>
<div class="zone graveyard" id="gv-zone-4" data-player="4" data-zone="graveyard"
onclick="openZoneModal(3,'graveyard')" data-tooltip="Graveyard">
<span class="card-count" id="gv-count-4" style="display:none;">0</span>
<span class="zone-icon">&#x2620;</span>
<span class="zone-name">Grave</span>
</div>
<div class="zone hand" id="hand-zone-4" data-player="4" data-zone="hand"
onclick="openZoneModal(3,'hand')" data-tooltip="Hand">
<span class="zone-icon">&#x1F0CF;</span>
<span class="zone-name">Hand (<span id="hand-count-4">0</span>)</span>
</div>
</div>
<span class="player-label" id="player-label-4">Player 4</span>
<div class="life-counter" id="life-counter-4">
<button onclick="adjustLife(3,-1)" aria-label="Decrease P4 life">&#x2212;</button>
<span class="life-counter-value" id="life-3" onclick="showSetLifeDialog(3)">40</span>
<button onclick="adjustLife(3,1)" aria-label="Increase P4 life">+</button>
</div>
<div class="poison-badge" id="poison-3" style="display:none;" onclick="adjustPoison(3,1)" oncontextmenu="event.preventDefault();adjustPoison(3,-1)">0 poison</div>
</div>
</div><!-- /.battlefield -->
<!-- COMBAT SVG OVERLAY (arrows) -->
<svg id="combat-overlay" xmlns="http://www.w3.org/2000/svg" style="position:absolute;inset:44px 0 0 0;pointer-events:none;z-index:30;">
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="rgba(211,32,42,0.7)"/>
</marker>
</defs>
<g id="combat-arrows-svg"></g>
</svg>
<!-- MANA POOL DISPLAY (floating, bottom center) -->
<div id="mana-pool-display" style="display:none;">
<span class="mana-pool-label">MANA:</span>
<span class="mana-empty">Empty</span>
</div>
<!-- FLOATING LOG -->
<div id="floating-log" class="floating-log"></div>
<!-- SHORTCUT HINT -->
<div class="shortcut-hint"></div>
<!-- CARD SEARCH SIDEBAR (right side, collapsible) -->
<div id="card-search-sidebar" class="collapsed">
<div class="sidebar-header">
<span class="sidebar-title">Card Search</span>
<button class="sidebar-toggle" onclick="document.getElementById('card-search-sidebar').classList.toggle('collapsed')" title="Close search">&#x2715;</button>
</div>
<div class="search-input-wrap">
<input class="search-input" id="card-search-input" type="text" placeholder="Search Scryfall..." autocomplete="off"/>
</div>
<div class="search-status" id="search-status">Type to search cards...</div>
<div class="card-results" id="card-results"></div>
</div>
<!-- Sidebar edge tab -->
<div id="sidebar-edge-tab" class="sidebar-edge-tab" title="Toggle card search">&#x2039;</div>
</div><!-- /.table-surface / #game-battlefield -->
<!-- ====================================================
CONTEXT MENU
==================================================== -->
<div id="context-menu"></div>
<!-- ====================================================
FLOATING CARD PREVIEW
==================================================== -->
<div id="card-preview">
<img src="" alt="Card Preview"/>
<div class="card-preview-pt-overlay" id="card-preview-pt-overlay"></div>
<div class="card-preview-info">
<div class="card-preview-name"></div>
<div class="card-preview-type"></div>
<div class="card-preview-oracle"></div>
<div class="card-preview-pt"></div>
</div>
</div>
<!-- ====================================================
ZONE MODAL
==================================================== -->
<div id="zone-modal">
<div class="zone-modal-card">
<div class="zone-modal-header">
<span class="zone-modal-title" id="zone-modal-title">Player Zones</span>
<button class="btn-close" onclick="closeZoneModal()">&#x2715;</button>
</div>
<div class="zone-modal-tabs" id="zone-modal-tabs">
<!-- Populated by renderZoneModal() -->
</div>
<div class="zone-modal-body" id="zone-modal-body">
<!-- Populated by renderZoneModal() -->
</div>
</div>
</div>
<!-- ====================================================
DICE MODAL
==================================================== -->
<div id="dice-modal">
<div class="dice-modal-card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
<span style="font-family:var(--font-display);font-size:14px;font-weight:700;color:var(--color-text-muted);text-transform:uppercase;letter-spacing:0.08em;">Dice &amp; Coins</span>
<button class="btn-close" onclick="closeDiceModal()">&#x2715;</button>
</div>
<div class="dice-result-display" id="dice-result-display">&#x2014;</div>
<div class="dice-label" id="dice-label">Choose a die</div>
<div class="dice-buttons">
<button class="btn-dice" onclick="rollDice(4)">d4</button>
<button class="btn-dice" onclick="rollDice(6)">d6</button>
<button class="btn-dice" onclick="rollDice(8)">d8</button>
<button class="btn-dice" onclick="rollDice(10)">d10</button>
<button class="btn-dice" onclick="rollDice(12)">d12</button>
<button class="btn-dice" onclick="rollDice(20)">d20</button>
<button class="btn-dice" onclick="rollDice(100)">d100</button>
<button class="btn-dice coin" onclick="flipCoin()">Coin Flip</button>
</div>
</div>
</div>
<!-- ====================================================
TOKEN MODAL
==================================================== -->
<div id="token-modal">
<div class="token-modal-card">
<div class="modal-header">
<span class="modal-title">Create Token</span>
<button class="btn-close" onclick="closeTokenModal()">&#x2715;</button>
</div>
<div class="modal-body" id="token-modal-body">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:12px;">
<div>
<label style="font-size:11px;font-weight:600;color:var(--color-text);display:block;margin-bottom:4px;">Token Name</label>
<input class="game-input" id="token-name" type="text" placeholder="e.g. 1/1 White Soldier"/>
</div>
<div>
<label style="font-size:11px;font-weight:600;color:var(--color-text);display:block;margin-bottom:4px;">Type Line</label>
<input class="game-input" id="token-type" type="text" placeholder="Creature — Soldier"/>
</div>
<div>
<label style="font-size:11px;font-weight:600;color:var(--color-text);display:block;margin-bottom:4px;">Power/Toughness</label>
<input class="game-input" id="token-pt" type="text" placeholder="1/1"/>
</div>
<div>
<label style="font-size:11px;font-weight:600;color:var(--color-text);display:block;margin-bottom:4px;">Count</label>
<input class="game-input" id="token-count" type="number" value="1" min="1" max="20"/>
</div>
</div>
<div style="margin-bottom:10px;">
<label style="font-size:11px;font-weight:600;color:var(--color-text);display:block;margin-bottom:4px;">Keywords (comma-separated)</label>
<input class="game-input" id="token-keywords" type="text" placeholder="Flying, Vigilance"/>
</div>
<div style="margin-bottom:12px;">
<label style="font-size:11px;font-weight:600;color:var(--color-text);display:block;margin-bottom:4px;">Player</label>
<select class="game-input" id="token-player-select" style="height:28px;">
<option value="0">Player 1</option>
<option value="1">Player 2</option>
<option value="2">Player 3</option>
<option value="3">Player 4</option>
</select>
</div>
<div style="display:flex;gap:8px;">
<button class="btn btn-primary" onclick="createTokenFromModal()">Create Token(s)</button>
<button class="btn btn-cancel" onclick="closeTokenModal()">Cancel</button>
</div>
</div>
</div>
</div>
<!-- ====================================================
DECK MODAL
==================================================== -->
<div id="deck-modal">
<div class="deck-modal-card">
<div class="modal-header">
<span class="modal-title">Deck Manager</span>
<button class="btn-close" onclick="closeDeckModal()">&#x2715;</button>
</div>
<div class="modal-body" id="deck-modal-body">
<!-- Populated by renderDeckModalContent() -->
</div>
</div>
</div>
<!-- ====================================================
COMMANDER DAMAGE MATRIX MODAL
==================================================== -->
<div id="cmd-dmg-matrix-modal">
<!-- Populated by renderCmdDmgMatrix() -->
</div>
<!-- ====================================================
TRIGGER STACK PANEL
==================================================== -->
<div id="trigger-stack-panel">
<div class="trigger-stack-card">
<div class="trigger-stack-header">
<span class="trigger-stack-title">Trigger Stack</span>
<button class="btn-close" style="font-size:14px;width:20px;height:20px;" onclick="closeTriggerStack()">&#x2715;</button>
</div>
<div class="trigger-stack-body" id="trigger-stack-body">
<div class="trigger-stack-empty">No pending triggers.</div>
</div>
<div class="trigger-stack-actions">
<button class="btn-resolve" onclick="resolveNextTrigger()">Resolve Next</button>
<button class="btn-resolve-all" onclick="resolveAllTriggers()">Resolve All</button>
</div>
</div>
</div>
<!-- ====================================================
SETTINGS PANEL
==================================================== -->
<div id="settings-panel">
<div class="modal-header" style="flex-shrink:0;">
<span class="modal-title">Settings</span>
<button class="btn-close" onclick="toggleSettingsPanel()">&#x2715;</button>
</div>
<div class="settings-body" id="settings-body" style="flex:1;overflow-y:auto;padding:12px;">
<!-- Populated by renderSettingsPanel() -->
</div>
<div style="padding:10px 14px;border-top:1px solid var(--color-border);display:flex;gap:8px;flex-wrap:wrap;flex-shrink:0;">
<button class="btn btn-primary" onclick="openDiceModal()">&#x1F3B2; Dice</button>
<button class="btn btn-gold" onclick="openCmdDmgMatrix()">CMD Damage Matrix</button>
<button class="btn btn-cancel" onclick="checkStateBasedActions()">Check SBA</button>
<button class="btn btn-cancel" onclick="clearAllManaPools()">Clear All Mana</button>
<button class="btn btn-cancel" onclick="autoArrangeBattlefield()">Auto-Arrange</button>
</div>
</div>
<!-- ====================================================
PRIORITY BANNER
==================================================== -->
<div id="priority-banner">
<span class="priority-text" id="priority-text">Waiting for priority...</span>
<button class="btn-priority-pass" onclick="passPriority()">Pass Priority</button>
<button class="btn-close" style="font-size:13px;" onclick="closePriorityWindow()">&#x2715;</button>
</div>
<!-- ====================================================
LEGAL ACTIONS BAR
==================================================== -->
<div id="legal-actions-bar">
<!-- Populated by renderLegalActionsBar() -->
</div>
<!-- ====================================================
UNDO HISTORY PANEL
==================================================== -->
<div id="undo-history-panel">
<!-- Populated by renderUndoHistory() -->
</div>
<!-- ====================================================
MANA COLOR PICKER (dynamically appended) -->
<!-- Zone card context menu (dynamically appended) -->
<!-- SBA alert panel (dynamically appended) -->
<!-- Set-life dialog (dynamically appended) -->
<!-- ====================================================
SCRIPTS
==================================================== -->
<script src="precons.js?v=20260403_0300"></script>
<script src="engine.js?v=20260403_0300"></script>
<script src="ui.js?v=20260403_0300"></script>
<script>
// ============================================================
// STARTUP & BRIDGE FUNCTIONS
// ============================================================
// Token creation from modal
function createTokenFromModal() {
if (!gameState) return;
const name = document.getElementById('token-name').value.trim() || '1/1 Token';
const typeLine = document.getElementById('token-type').value.trim() || 'Creature';
const pt = document.getElementById('token-pt').value.trim() || '1/1';
const count = parseInt(document.getElementById('token-count').value) || 1;
const keywords = document.getElementById('token-keywords').value.trim();
const playerIdx = parseInt(document.getElementById('token-player-select').value) || 0;
const parts = pt.split('/');
const power = parts[0] ? parts[0].trim() : '1';
const toughness = parts[1] ? parts[1].trim() : '1';
for (let i = 0; i < count; i++) {
addCardToBattlefield(name, '', typeLine, playerIdx, undefined, undefined,
keywords ? keywords.replace(/,/g, ' ') : '',
{ power, toughness, pt, isToken: true });
}
addLogEntry('Created ' + count + 'x <strong>' + name + '</strong> token(s) for Player ' + (playerIdx + 1));
closeTokenModal();
}
// Pass priority
function passPriority() {
addLogEntry('Priority passed.');
closePriorityWindow();
}
// Open precon browser (stub - opens deck modal for current player)
function openPreconBrowser() {
if (gameState) openDeckModal(gameState.currentPlayerIndex);
}
// Open collection modal (stub)
function openCollectionModal() { showToast('Collection tracker available in full version.'); }
// Scry dialog stub
function showScryDialog(playerId) {
if (!gameState) return;
const player = gameState.players[playerId];
if (!player || player.zones.library.length === 0) {
addLogEntry('!! ' + (player ? player.name : 'Player') + ' has no cards to scry.');
return;
}
const count = Math.min(parseInt(prompt('Scry how many? (1-7)', '1') || '1'), 7, player.zones.library.length);
if (isNaN(count) || count < 1) return;
const topCards = player.zones.library.slice(0, count);
const names = topCards.map(c => c.name || '?').join(', ');
addLogEntry('<strong>' + player.name + '</strong> scrys ' + count + ': [' + names + ']');
}
// Mill dialog stub
function showMillDialog(playerId) {
if (!gameState) return;
const count = parseInt(prompt('Mill how many cards?', '1') || '1');
if (isNaN(count) || count < 1) return;
millCards(playerId, count);
}
// Become monarch
function becomeMonarch(playerId) {
if (!gameState) return;
gameState.monarchPlayerIndex = playerId;
const player = gameState.players[playerId];
addLogEntry('&#x1F451; <strong>' + (player ? player.name : 'Player') + '</strong> becomes the Monarch!');
updateMonarchBadges();
}
// Empower the life counter right-click on the quadrant labels
document.addEventListener('DOMContentLoaded', function() {
// Wire right-click on life values to set-life dialog
for (var i = 0; i < 4; i++) {
(function(idx) {
var lifeEl = document.getElementById('life-' + idx);
if (lifeEl) {
lifeEl.addEventListener('contextmenu', function(e) {
e.preventDefault();
showSetLifeDialog(idx);
});
}
})(i);
}
// Life counter right-click on quadrant buttons too
// (already handled via inline onclick in HTML)
});
</script>
</body>
</html>
+66 -22
View File
@@ -26,7 +26,7 @@ except ImportError:
print("ERROR: FastAPI not installed. Run: pip install fastapi uvicorn")
import sys; sys.exit(1)
from models.state import CFG, COMMANDER_META, load_commander_meta
from models.state import CFG, COMMANDER_META, load_commander_meta, validate_forge_config
from services.database import init_collection_db
from services.logging import setup_logging
from services.precon_service import PRECON_INDEX, download_precon_database
@@ -42,6 +42,7 @@ from routes.deckgen import router as deckgen_router
from routes.coach import router as coach_router, init_coach_service
from routes.ml import router as ml_router
from routes.ws_game import router as ws_game_router
from routes.game import router as game_router
log = logging.getLogger("commander_ai_lab.api")
@@ -50,25 +51,62 @@ _allowed = os.environ.get("ALLOWED_ORIGINS", _DEFAULT_ORIGINS)
allowed_origins = [o.strip() for o in _allowed.split(",") if o.strip()]
# ── Lifespan (replaces deprecated @app.on_event) ───────────────────────────────
# ── Lifespan (replaces deprecated @app.on_event) ────────────────────────────────────────────────────────
@asynccontextmanager
async def _lifespan(application: FastAPI):
# ──── STARTUP ────
if not CFG.precon_dir:
CFG.precon_dir = _resolve_precon_dir()
init_collection_db()
validate_forge_config()
if not PRECON_INDEX:
download_precon_database()
if not COMMANDER_META:
load_commander_meta()
loop = asyncio.get_running_loop()
# RAG Phase 1: kick off Scryfall bulk download in background thread.
# run_in_executor returns a Future; we do NOT await it so startup
# completes immediately and the download runs concurrently.
from services.scryfall_bulk import ensure_bulk_db
loop = asyncio.get_event_loop()
loop.run_in_executor(None, ensure_bulk_db)
# RAG Phase 2: kick off ChromaDB vector store build in background thread.
from services.rag_store import ensure_rag_store
loop.run_in_executor(None, ensure_rag_store)
# RAG Phase 3: background staleness monitor (rebuilds if >14 days old)
async def _rag_staleness_monitor():
"""Periodically check if the RAG index needs rebuilding."""
import time as _time
while True:
await asyncio.sleep(6 * 3600) # check every 6 hours
try:
from services.rag_store import check_staleness, build_index
info = check_staleness()
if info.get("stale"):
log.info("RAG staleness monitor: %s — triggering rebuild.", info["reason"])
loop.run_in_executor(None, lambda: build_index(force=True))
else:
log.debug("RAG staleness monitor: index is current (%.1f days).", info.get("age_days", 0))
except Exception as exc:
log.warning("RAG staleness monitor failed: %s", exc)
asyncio.ensure_future(_rag_staleness_monitor())
# Policy routes: register if ML policy model is available
try:
from ml.serving.policy_server import PolicyInferenceService
from routes.policy import register_policy_routes
_policy_svc = PolicyInferenceService()
if _policy_svc.load():
register_policy_routes(application, _policy_svc)
log.info("Policy routes registered (/api/policy/*)")
else:
log.warning("Policy model not loaded — /api/policy/* routes inactive")
except ImportError:
log.info("Policy service not available (ml.serving not installed)")
except Exception as e:
log.warning("Policy route registration failed (non-fatal): %s", e)
yield # ← server is running
# ──── SHUTDOWN ────
@@ -76,7 +114,7 @@ async def _lifespan(application: FastAPI):
await async_close_db()
# ── App ────────────────────────────────────────────────────────────────
# ── App ─────────────────────────────────────────────────────────────────────────────
app = FastAPI(title="Commander AI Lab API", version="3.0.0", lifespan=_lifespan)
app.add_middleware(
CORSMiddleware,
@@ -98,9 +136,10 @@ app.include_router(deckgen_router)
app.include_router(coach_router)
app.include_router(ws_game_router)
app.include_router(ml_router)
app.include_router(game_router) # live game session routes
# ── API endpoints ────────────────────────────────────────────────────────────
# ── API endpoints ─────────────────────────────────────────────────────────────────────────────
# NOTE: all @app.get routes MUST be registered before app.mount() calls.
# Once StaticFiles is mounted at "/", it registers a wildcard catch-all that
# shadows any routes added after it.
@@ -110,9 +149,10 @@ async def health_check():
"""Health check endpoint for load balancers and Unity client polling."""
return {"status": "ok"}
# ── Static UI (MUST come after all @app.get routes) ────────────────────────────
_legacy_ui_dir = Path(__file__).parent / "ui"
_spa_dir = Path(__file__).parent / "frontend" / "commander-ai-lab-ui" / "dist"
# ── Static UI (MUST come after all @app.get routes) ──────────────────────────────────────────
from pathlib import Path as _Path
_legacy_ui_dir = _Path(__file__).parent / "ui"
_spa_dir = _Path(__file__).parent / "frontend" / "commander-ai-lab-ui" / "dist"
if _legacy_ui_dir.exists():
app.mount("/", StaticFiles(directory=str(_legacy_ui_dir), html=True), name="ui")
@@ -130,14 +170,16 @@ elif _spa_dir.exists():
return FileResponse(str(_spa_dir / "index.html"))
# ── Argument parsing + auto-detection helpers ─────────────────────────────────
# ── Argument parsing + auto-detection helpers ────────────────────────────────────────────────────────
def _parse_args():
p = argparse.ArgumentParser(description="Commander AI Lab API Server")
p.add_argument("--forge-jar", default=os.environ.get("FORGE_JAR", ""))
p.add_argument("--forge-dir", default=os.environ.get("FORGE_DIR", ""))
# Support both FORGE_DIR and FORGE_WORKING_DIR (alias for backwards compat)
p.add_argument("--forge-dir", default=os.environ.get("FORGE_DIR") or os.environ.get("FORGE_WORKING_DIR", ""))
p.add_argument("--forge-decks-dir", default=os.environ.get("FORGE_DECKS_DIR", ""))
p.add_argument("--lab-jar", default=os.environ.get("LAB_JAR", ""))
p.add_argument("--precon-dir", default=os.environ.get("PRECON_DIR", ""))
p.add_argument("--results-dir", default=os.environ.get("RESULTS_DIR", ""))
p.add_argument("--port", type=int, default=int(os.environ.get("LAB_PORT", "8080")))
p.add_argument("--ximilar-key", default=os.environ.get("XIMILAR_API_KEY", ""))
p.add_argument("--anthropic-key", default=os.environ.get("ANTHROPIC_API_KEY", ""))
@@ -212,7 +254,7 @@ def _resolve_forge_jar() -> str:
project_root.parent / "forge-repo" / "forge-gui-desktop" / "target",
project_root.parent / "forge" / "forge-gui-desktop" / "target",
]
forge_dir_env = os.environ.get("FORGE_DIR", "")
forge_dir_env = os.environ.get("FORGE_DIR", "") or os.environ.get("FORGE_WORKING_DIR", "")
if forge_dir_env:
fd = Path(forge_dir_env)
candidates.append(fd.parent / "forge-gui-desktop" / "target")
@@ -277,6 +319,15 @@ def main():
CFG.ximilar_api_key = args.ximilar_key
CFG.anthropic_api_key = args.anthropic_key
# Wire RESULTS_DIR: use explicit env/arg if set, otherwise keep default "results"
# but resolve it to an absolute path relative to this file so it works
# regardless of what directory the server is launched from.
if args.results_dir:
CFG.results_dir = args.results_dir
else:
# Default: results/ next to lab_api.py (always absolute)
CFG.results_dir = str(Path(__file__).parent / "results")
from services.forge_runner import get_java17
j17 = get_java17()
@@ -297,16 +348,9 @@ def main():
init_collection_db()
init_coach_service()
# RAG Phase 1: synchronous boot-time check so logs are visible.
# The async lifespan handler also fires it in a thread on uvicorn startup,
# but calling here ensures it runs (and logs) before the first request.
from services.scryfall_bulk import ensure_bulk_db
ensure_bulk_db()
# RAG Phase 1 + 2 are handled by _lifespan when uvicorn starts.
# No need to call them here — doing so causes a double-run.
# RAG Phase 2: synchronous boot-time ChromaDB build.
from services.rag_store import ensure_rag_store
ensure_rag_store()
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=CFG.port, log_level="info")
File diff suppressed because it is too large. Load diff
+25612
View File
File diff suppressed because it is too large. Load diff
+643
View File
@@ -0,0 +1,643 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MTG Commander Battlefield</title>
<link rel="stylesheet" href="style.css?v=20260403_0300">
</head>
<body>
<!-- ====================================================
SETUP SCREEN
==================================================== -->
<div id="setup-screen">
<!-- Populated by ui.js initSetupScreen() -->
</div>
<!-- ====================================================
TABLE SURFACE — 4-Quadrant Battlefield
==================================================== -->
<div class="table-surface" id="game-battlefield" style="display:none;">
<!-- TOP BAR: Phase tracker + controls -->
<div id="top-bar">
<span class="top-bar-logo">Commander</span>
<div class="turn-counter-badge" id="turn-counter">Turn <span>1</span></div>
<div class="active-player-indicator" id="active-player-indicator">
<span class="active-player-dot" id="active-dot" style="background:#c0392b;"></span>
Player 1's Turn
</div>
<!-- Phase track -->
<div class="phase-track-wrapper">
<div class="phase-track" id="phase-track"></div>
</div>
<!-- Phase/turn action buttons -->
<div class="top-bar-actions">
<button class="btn-phase" onclick="nextPhase()" title="Advance to next phase (N)">Next Phase</button>
<button class="btn-phase btn-next-turn" onclick="nextTurn()" title="End turn, advance to next player (T)">Next Turn</button>
</div>
<!-- Utility buttons -->
<div class="top-bar-utils">
<button class="btn-util" id="btn-undo" onclick="undo()" title="Undo (Ctrl+Z)" disabled>&#x21A9;</button>
<button class="btn-util" id="btn-redo" onclick="redo()" title="Redo (Ctrl+Y)" disabled>&#x21AA;</button>
<button class="btn-util" id="btn-triggers" onclick="toggleTriggerStack()" title="Trigger Stack">&#x2747;</button>
<button class="btn-util" onclick="openDiceModal()" title="Dice &amp; Coins (G)">&#x1F3B2;</button>
<button class="btn-util" onclick="checkStateBasedActions()" title="Check State-Based Actions">SBA</button>
<button class="btn-util" onclick="toggleSettingsPanel()" title="Settings">&#x2699;</button>
<button class="btn-util" id="btn-search-toggle" onclick="document.getElementById('card-search-sidebar').classList.toggle('collapsed')" title="Card Search (S)">&#x1F50D;</button>
</div>
<!-- Storm counter -->
<span id="storm-badge" style="display:none;font-size:10px;color:var(--mtg-gold);background:rgba(200,168,67,0.15);border:1px solid rgba(200,168,67,0.3);border-radius:4px;padding:2px 7px;white-space:nowrap;">Storm: 0</span>
</div>
<!-- DIVIDER LINES -->
<div class="divider-h"></div>
<div class="divider-v"></div>
<!-- CORNER ACCENTS -->
<div class="corner-accent tl"></div>
<div class="corner-accent tr"></div>
<div class="corner-accent bl"></div>
<div class="corner-accent br"></div>
<!-- CENTER EMBLEM -->
<div class="center-emblem">
<svg viewBox="0 0 100 100" fill="none">
<circle cx="50" cy="50" r="42" stroke="rgba(255,255,255,0.2)" stroke-width="1.5"/>
<circle cx="50" cy="50" r="28" stroke="rgba(255,255,255,0.1)" stroke-width="1"/>
<polygon points="50,15 58.5,37 82,37 63,51 69,74 50,60 31,74 37,51 18,37 41.5,37"
fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.2)" stroke-width="0.8"/>
</svg>
</div>
<!-- ================================================
4-QUADRANT BATTLEFIELD
================================================ -->
<div class="battlefield">
<!-- PLAYER 1: Top-Left (Crimson) -->
<div class="player-area p1" id="player-area-1">
<span class="player-label" id="player-label-1">Player 1</span>
<div class="life-counter" id="life-counter-1">
<button onclick="adjustLife(0,-1)" aria-label="Decrease P1 life">&#x2212;</button>
<span class="life-counter-value" id="life-0" onclick="showSetLifeDialog(0)">40</span>
<button onclick="adjustLife(0,1)" aria-label="Increase P1 life">+</button>
</div>
<div class="poison-badge" id="poison-0" style="display:none;" onclick="adjustPoison(0,1)" title="Poison counters (click +1, right-click -1)" oncontextmenu="event.preventDefault();adjustPoison(0,-1)">0 poison</div>
<!-- Zone Row: Library, Exile, Graveyard, Hand -->
<div class="zone-row" id="zone-row-1">
<div class="zone library" id="lib-zone-1" data-player="1" data-zone="library"
onclick="openZoneModal(0,'library')" data-tooltip="Library">
<span class="card-count" id="lib-count-1">0</span>
<span class="zone-icon">&#x1F4DA;</span>
<span class="zone-name">Library</span>
</div>
<div class="zone exile" id="exile-zone-1" data-player="1" data-zone="exile"
onclick="openZoneModal(0,'exile')" data-tooltip="Exile">
<span class="card-count" id="exile-count-1" style="display:none;">0</span>
<span class="zone-icon">&#x2B21;</span>
<span class="zone-name">Exile</span>
</div>
<div class="zone graveyard" id="gv-zone-1" data-player="1" data-zone="graveyard"
onclick="openZoneModal(0,'graveyard')" data-tooltip="Graveyard">
<span class="card-count" id="gv-count-1" style="display:none;">0</span>
<span class="zone-icon">&#x2620;</span>
<span class="zone-name">Grave</span>
</div>
<div class="zone hand" id="hand-zone-1" data-player="1" data-zone="hand"
onclick="openZoneModal(0,'hand')" data-tooltip="Hand">
<span class="zone-icon">&#x1F0CF;</span>
<span class="zone-name">Hand (<span id="hand-count-1">0</span>)</span>
</div>
</div>
<!-- Commander Row -->
<div class="commander-row" id="cmd-row-1">
<div class="zone commander" id="cmd-zone-1" data-player="1" data-zone="command"
onclick="openZoneModal(0,'command')" data-tooltip="Command Zone">
<span class="zone-icon">&#x1F451;</span>
<span class="zone-name">Commander</span>
</div>
</div>
<!-- Battle Zone -->
<div class="battle-zone" id="battle-zone-1">
<div class="battle-zone-inner"></div>
<span class="battle-zone-label">Battlefield</span>
<!-- Battlefield cards injected here -->
</div>
</div>
<!-- PLAYER 2: Top-Right (Sapphire) -->
<div class="player-area p2" id="player-area-2">
<span class="player-label" id="player-label-2">Player 2</span>
<div class="life-counter" id="life-counter-2">
<button onclick="adjustLife(1,-1)" aria-label="Decrease P2 life">&#x2212;</button>
<span class="life-counter-value" id="life-1" onclick="showSetLifeDialog(1)">40</span>
<button onclick="adjustLife(1,1)" aria-label="Increase P2 life">+</button>
</div>
<div class="poison-badge" id="poison-1" style="display:none;" onclick="adjustPoison(1,1)" oncontextmenu="event.preventDefault();adjustPoison(1,-1)">0 poison</div>
<div class="zone-row" id="zone-row-2">
<div class="zone library" id="lib-zone-2" data-player="2" data-zone="library"
onclick="openZoneModal(1,'library')" data-tooltip="Library">
<span class="card-count" id="lib-count-2">0</span>
<span class="zone-icon">&#x1F4DA;</span>
<span class="zone-name">Library</span>
</div>
<div class="zone exile" id="exile-zone-2" data-player="2" data-zone="exile"
onclick="openZoneModal(1,'exile')" data-tooltip="Exile">
<span class="card-count" id="exile-count-2" style="display:none;">0</span>
<span class="zone-icon">&#x2B21;</span>
<span class="zone-name">Exile</span>
</div>
<div class="zone graveyard" id="gv-zone-2" data-player="2" data-zone="graveyard"
onclick="openZoneModal(1,'graveyard')" data-tooltip="Graveyard">
<span class="card-count" id="gv-count-2" style="display:none;">0</span>
<span class="zone-icon">&#x2620;</span>
<span class="zone-name">Grave</span>
</div>
<div class="zone hand" id="hand-zone-2" data-player="2" data-zone="hand"
onclick="openZoneModal(1,'hand')" data-tooltip="Hand">
<span class="zone-icon">&#x1F0CF;</span>
<span class="zone-name">Hand (<span id="hand-count-2">0</span>)</span>
</div>
</div>
<div class="commander-row" id="cmd-row-2">
<div class="zone commander" id="cmd-zone-2" data-player="2" data-zone="command"
onclick="openZoneModal(1,'command')" data-tooltip="Command Zone">
<span class="zone-icon">&#x1F451;</span>
<span class="zone-name">Commander</span>
</div>
</div>
<div class="battle-zone" id="battle-zone-2">
<div class="battle-zone-inner"></div>
<span class="battle-zone-label">Battlefield</span>
</div>
</div>
<!-- PLAYER 3: Bottom-Left (Emerald) -->
<div class="player-area p3" id="player-area-3">
<!-- Battle zone on top for bottom players -->
<div class="battle-zone" id="battle-zone-3">
<div class="battle-zone-inner"></div>
<span class="battle-zone-label">Battlefield</span>
</div>
<div class="commander-row" id="cmd-row-3">
<div class="zone commander" id="cmd-zone-3" data-player="3" data-zone="command"
onclick="openZoneModal(2,'command')" data-tooltip="Command Zone">
<span class="zone-icon">&#x1F451;</span>
<span class="zone-name">Commander</span>
</div>
</div>
<div class="zone-row" id="zone-row-3">
<div class="zone library" id="lib-zone-3" data-player="3" data-zone="library"
onclick="openZoneModal(2,'library')" data-tooltip="Library">
<span class="card-count" id="lib-count-3">0</span>
<span class="zone-icon">&#x1F4DA;</span>
<span class="zone-name">Library</span>
</div>
<div class="zone exile" id="exile-zone-3" data-player="3" data-zone="exile"
onclick="openZoneModal(2,'exile')" data-tooltip="Exile">
<span class="card-count" id="exile-count-3" style="display:none;">0</span>
<span class="zone-icon">&#x2B21;</span>
<span class="zone-name">Exile</span>
</div>
<div class="zone graveyard" id="gv-zone-3" data-player="3" data-zone="graveyard"
onclick="openZoneModal(2,'graveyard')" data-tooltip="Graveyard">
<span class="card-count" id="gv-count-3" style="display:none;">0</span>
<span class="zone-icon">&#x2620;</span>
<span class="zone-name">Grave</span>
</div>
<div class="zone hand" id="hand-zone-3" data-player="3" data-zone="hand"
onclick="openZoneModal(2,'hand')" data-tooltip="Hand">
<span class="zone-icon">&#x1F0CF;</span>
<span class="zone-name">Hand (<span id="hand-count-3">0</span>)</span>
</div>
</div>
<span class="player-label" id="player-label-3">Player 3</span>
<div class="life-counter" id="life-counter-3">
<button onclick="adjustLife(2,-1)" aria-label="Decrease P3 life">&#x2212;</button>
<span class="life-counter-value" id="life-2" onclick="showSetLifeDialog(2)">40</span>
<button onclick="adjustLife(2,1)" aria-label="Increase P3 life">+</button>
</div>
<div class="poison-badge" id="poison-2" style="display:none;" onclick="adjustPoison(2,1)" oncontextmenu="event.preventDefault();adjustPoison(2,-1)">0 poison</div>
</div>
<!-- PLAYER 4: Bottom-Right (Gold) -->
<div class="player-area p4" id="player-area-4">
<div class="battle-zone" id="battle-zone-4">
<div class="battle-zone-inner"></div>
<span class="battle-zone-label">Battlefield</span>
</div>
<div class="commander-row" id="cmd-row-4">
<div class="zone commander" id="cmd-zone-4" data-player="4" data-zone="command"
onclick="openZoneModal(3,'command')" data-tooltip="Command Zone">
<span class="zone-icon">&#x1F451;</span>
<span class="zone-name">Commander</span>
</div>
</div>
<div class="zone-row" id="zone-row-4">
<div class="zone library" id="lib-zone-4" data-player="4" data-zone="library"
onclick="openZoneModal(3,'library')" data-tooltip="Library">
<span class="card-count" id="lib-count-4">0</span>
<span class="zone-icon">&#x1F4DA;</span>
<span class="zone-name">Library</span>
</div>
<div class="zone exile" id="exile-zone-4" data-player="4" data-zone="exile"
onclick="openZoneModal(3,'exile')" data-tooltip="Exile">
<span class="card-count" id="exile-count-4" style="display:none;">0</span>
<span class="zone-icon">&#x2B21;</span>
<span class="zone-name">Exile</span>
</div>
<div class="zone graveyard" id="gv-zone-4" data-player="4" data-zone="graveyard"
onclick="openZoneModal(3,'graveyard')" data-tooltip="Graveyard">
<span class="card-count" id="gv-count-4" style="display:none;">0</span>
<span class="zone-icon">&#x2620;</span>
<span class="zone-name">Grave</span>
</div>
<div class="zone hand" id="hand-zone-4" data-player="4" data-zone="hand"
onclick="openZoneModal(3,'hand')" data-tooltip="Hand">
<span class="zone-icon">&#x1F0CF;</span>
<span class="zone-name">Hand (<span id="hand-count-4">0</span>)</span>
</div>
</div>
<span class="player-label" id="player-label-4">Player 4</span>
<div class="life-counter" id="life-counter-4">
<button onclick="adjustLife(3,-1)" aria-label="Decrease P4 life">&#x2212;</button>
<span class="life-counter-value" id="life-3" onclick="showSetLifeDialog(3)">40</span>
<button onclick="adjustLife(3,1)" aria-label="Increase P4 life">+</button>
</div>
<div class="poison-badge" id="poison-3" style="display:none;" onclick="adjustPoison(3,1)" oncontextmenu="event.preventDefault();adjustPoison(3,-1)">0 poison</div>
</div>
</div><!-- /.battlefield -->
<!-- COMBAT SVG OVERLAY (arrows) -->
<svg id="combat-overlay" xmlns="http://www.w3.org/2000/svg" style="position:absolute;inset:44px 0 0 0;pointer-events:none;z-index:30;">
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="rgba(211,32,42,0.7)"/>
</marker>
</defs>
<g id="combat-arrows-svg"></g>
</svg>
<!-- FLOATING LOG -->
<div id="floating-log" class="floating-log"></div>
<!-- SHORTCUT HINT -->
<div class="shortcut-hint"></div>
<!-- CARD SEARCH SIDEBAR (right side, collapsible) -->
<div id="card-search-sidebar" class="collapsed">
<div class="sidebar-header">
<span class="sidebar-title">Card Search</span>
<button class="sidebar-toggle" onclick="document.getElementById('card-search-sidebar').classList.toggle('collapsed')" title="Close search">&#x2715;</button>
</div>
<div class="search-input-wrap">
<input class="search-input" id="card-search-input" type="text" placeholder="Search Scryfall..." autocomplete="off"/>
</div>
<div class="search-status" id="search-status">Type to search cards...</div>
<div class="card-results" id="card-results"></div>
</div>
<!-- Sidebar edge tab -->
<div id="sidebar-edge-tab" class="sidebar-edge-tab" title="Toggle card search">&#x2039;</div>
</div><!-- /.table-surface / #game-battlefield -->
<!-- ====================================================
CONTEXT MENU
==================================================== -->
<div id="context-menu"></div>
<!-- ====================================================
FLOATING CARD PREVIEW
==================================================== -->
<div id="card-preview">
<img src="" alt="Card Preview"/>
<div class="card-preview-pt-overlay" id="card-preview-pt-overlay"></div>
<div class="card-preview-info">
<div class="card-preview-name"></div>
<div class="card-preview-type"></div>
<div class="card-preview-oracle"></div>
<div class="card-preview-pt"></div>
</div>
</div>
<!-- ====================================================
ZONE MODAL
==================================================== -->
<div id="zone-modal">
<div class="zone-modal-card">
<div class="zone-modal-header">
<span class="zone-modal-title" id="zone-modal-title">Player Zones</span>
<button class="btn-close" onclick="closeZoneModal()">&#x2715;</button>
</div>
<div class="zone-modal-tabs" id="zone-modal-tabs">
<!-- Populated by renderZoneModal() -->
</div>
<div class="zone-modal-body" id="zone-modal-body">
<!-- Populated by renderZoneModal() -->
</div>
</div>
</div>
<!-- ====================================================
DICE MODAL
==================================================== -->
<div id="dice-modal">
<div class="dice-modal-card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
<span style="font-family:var(--font-display);font-size:14px;font-weight:700;color:var(--color-text-muted);text-transform:uppercase;letter-spacing:0.08em;">Dice &amp; Coins</span>
<button class="btn-close" onclick="closeDiceModal()">&#x2715;</button>
</div>
<div class="dice-result-display" id="dice-result-display">&#x2014;</div>
<div class="dice-label" id="dice-label">Choose a die</div>
<div class="dice-buttons">
<button class="btn-dice" onclick="rollDice(4)">d4</button>
<button class="btn-dice" onclick="rollDice(6)">d6</button>
<button class="btn-dice" onclick="rollDice(8)">d8</button>
<button class="btn-dice" onclick="rollDice(10)">d10</button>
<button class="btn-dice" onclick="rollDice(12)">d12</button>
<button class="btn-dice" onclick="rollDice(20)">d20</button>
<button class="btn-dice" onclick="rollDice(100)">d100</button>
<button class="btn-dice coin" onclick="flipCoin()">Coin Flip</button>
</div>
</div>
</div>
<!-- ====================================================
TOKEN MODAL
==================================================== -->
<div id="token-modal">
<div class="token-modal-card">
<div class="modal-header">
<span class="modal-title">Create Token</span>
<button class="btn-close" onclick="closeTokenModal()">&#x2715;</button>
</div>
<div class="modal-body" id="token-modal-body">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:12px;">
<div>
<label style="font-size:11px;font-weight:600;color:var(--color-text);display:block;margin-bottom:4px;">Token Name</label>
<input class="game-input" id="token-name" type="text" placeholder="e.g. 1/1 White Soldier"/>
</div>
<div>
<label style="font-size:11px;font-weight:600;color:var(--color-text);display:block;margin-bottom:4px;">Type Line</label>
<input class="game-input" id="token-type" type="text" placeholder="Creature — Soldier"/>
</div>
<div>
<label style="font-size:11px;font-weight:600;color:var(--color-text);display:block;margin-bottom:4px;">Power/Toughness</label>
<input class="game-input" id="token-pt" type="text" placeholder="1/1"/>
</div>
<div>
<label style="font-size:11px;font-weight:600;color:var(--color-text);display:block;margin-bottom:4px;">Count</label>
<input class="game-input" id="token-count" type="number" value="1" min="1" max="20"/>
</div>
</div>
<div style="margin-bottom:10px;">
<label style="font-size:11px;font-weight:600;color:var(--color-text);display:block;margin-bottom:4px;">Keywords (comma-separated)</label>
<input class="game-input" id="token-keywords" type="text" placeholder="Flying, Vigilance"/>
</div>
<div style="margin-bottom:12px;">
<label style="font-size:11px;font-weight:600;color:var(--color-text);display:block;margin-bottom:4px;">Player</label>
<select class="game-input" id="token-player-select" style="height:28px;">
<option value="0">Player 1</option>
<option value="1">Player 2</option>
<option value="2">Player 3</option>
<option value="3">Player 4</option>
</select>
</div>
<div style="display:flex;gap:8px;">
<button class="btn btn-primary" onclick="createTokenFromModal()">Create Token(s)</button>
<button class="btn btn-cancel" onclick="closeTokenModal()">Cancel</button>
</div>
</div>
</div>
</div>
<!-- ====================================================
DECK MODAL
==================================================== -->
<div id="deck-modal">
<div class="deck-modal-card">
<div class="modal-header">
<span class="modal-title">Deck Manager</span>
<button class="btn-close" onclick="closeDeckModal()">&#x2715;</button>
</div>
<div class="modal-body" id="deck-modal-body">
<!-- Populated by renderDeckModalContent() -->
</div>
</div>
</div>
<!-- ====================================================
COMMANDER DAMAGE MATRIX MODAL
==================================================== -->
<div id="cmd-dmg-matrix-modal">
<!-- Populated by renderCmdDmgMatrix() -->
</div>
<!-- ====================================================
TRIGGER STACK PANEL
==================================================== -->
<div id="trigger-stack-panel">
<div class="trigger-stack-card">
<div class="trigger-stack-header">
<span class="trigger-stack-title">Trigger Stack</span>
<button class="btn-close" style="font-size:14px;width:20px;height:20px;" onclick="closeTriggerStack()">&#x2715;</button>
</div>
<div class="trigger-stack-body" id="trigger-stack-body">
<div class="trigger-stack-empty">No pending triggers.</div>
</div>
<div class="trigger-stack-actions">
<button class="btn-resolve" onclick="resolveNextTrigger()">Resolve Next</button>
<button class="btn-resolve-all" onclick="resolveAllTriggers()">Resolve All</button>
</div>
</div>
</div>
<!-- ====================================================
SETTINGS PANEL
==================================================== -->
<div id="settings-panel">
<div class="modal-header" style="flex-shrink:0;">
<span class="modal-title">Settings</span>
<button class="btn-close" onclick="toggleSettingsPanel()">&#x2715;</button>
</div>
<div class="settings-body" id="settings-body" style="flex:1;overflow-y:auto;padding:12px;">
<!-- Populated by renderSettingsPanel() -->
</div>
<div style="padding:10px 14px;border-top:1px solid var(--color-border);display:flex;gap:8px;flex-wrap:wrap;flex-shrink:0;">
<button class="btn btn-primary" onclick="openDiceModal()">&#x1F3B2; Dice</button>
<button class="btn btn-gold" onclick="openCmdDmgMatrix()">CMD Damage Matrix</button>
<button class="btn btn-cancel" onclick="checkStateBasedActions()">Check SBA</button>
<button class="btn btn-cancel" onclick="autoArrangeBattlefield()">Auto-Arrange</button>
</div>
</div>
<!-- ====================================================
PRIORITY BANNER
==================================================== -->
<div id="priority-banner">
<span class="priority-text" id="priority-text">Waiting for priority...</span>
<button class="btn-priority-pass" onclick="passPriority()">Pass Priority</button>
<button class="btn-close" style="font-size:13px;" onclick="closePriorityWindow()">&#x2715;</button>
</div>
<!-- ====================================================
LEGAL ACTIONS BAR
==================================================== -->
<div id="legal-actions-bar">
<!-- Populated by renderLegalActionsBar() -->
</div>
<!-- ====================================================
UNDO HISTORY PANEL
==================================================== -->
<div id="undo-history-panel">
<!-- Populated by renderUndoHistory() -->
</div>
<!-- ====================================================
MANA COLOR PICKER (dynamically appended) -->
<!-- Zone card context menu (dynamically appended) -->
<!-- SBA alert panel (dynamically appended) -->
<!-- Set-life dialog (dynamically appended) -->
<!-- ====================================================
SCRIPTS
==================================================== -->
<script src="precons.js?v=20260403_0300"></script>
<script src="engine.js?v=20260403_0300"></script>
<script src="ai-bridge.js?v=20260403_0300"></script>
<script src="player-panels.js?v=20260403_0300"></script>
<script src="ui.js?v=20260403_0300"></script>
<script>
// ============================================================
// STARTUP & BRIDGE FUNCTIONS
// ============================================================
// Token creation from modal
function createTokenFromModal() {
if (!gameState) return;
const name = document.getElementById('token-name').value.trim() || '1/1 Token';
const typeLine = document.getElementById('token-type').value.trim() || 'Creature';
const pt = document.getElementById('token-pt').value.trim() || '1/1';
const count = parseInt(document.getElementById('token-count').value) || 1;
const keywords = document.getElementById('token-keywords').value.trim();
const playerIdx = parseInt(document.getElementById('token-player-select').value) || 0;
const parts = pt.split('/');
const power = parts[0] ? parts[0].trim() : '1';
const toughness = parts[1] ? parts[1].trim() : '1';
for (let i = 0; i < count; i++) {
addCardToBattlefield(name, '', typeLine, playerIdx, undefined, undefined,
keywords ? keywords.replace(/,/g, ' ') : '',
{ power, toughness, pt, isToken: true });
}
addLogEntry('Created ' + count + 'x <strong>' + name + '</strong> token(s) for Player ' + (playerIdx + 1));
closeTokenModal();
}
// Pass priority
function passPriority() {
addLogEntry('Priority passed.');
closePriorityWindow();
}
// Open precon browser (stub - opens deck modal for current player)
function openPreconBrowser() {
if (gameState) openDeckModal(gameState.currentPlayerIndex);
}
// Open collection modal (stub)
function openCollectionModal() { showToast('Collection tracker available in full version.'); }
// Scry dialog stub
function showScryDialog(playerId) {
if (!gameState) return;
const player = gameState.players[playerId];
if (!player || player.zones.library.length === 0) {
addLogEntry('!! ' + (player ? player.name : 'Player') + ' has no cards to scry.');
return;
}
const count = Math.min(parseInt(prompt('Scry how many? (1-7)', '1') || '1'), 7, player.zones.library.length);
if (isNaN(count) || count < 1) return;
const topCards = player.zones.library.slice(0, count);
const names = topCards.map(c => c.name || '?').join(', ');
addLogEntry('<strong>' + player.name + '</strong> scrys ' + count + ': [' + names + ']');
}
// Mill dialog stub
function showMillDialog(playerId) {
if (!gameState) return;
const count = parseInt(prompt('Mill how many cards?', '1') || '1');
if (isNaN(count) || count < 1) return;
millCards(playerId, count);
}
// Become monarch
function becomeMonarch(playerId) {
if (!gameState) return;
gameState.monarchPlayerIndex = playerId;
const player = gameState.players[playerId];
addLogEntry('&#x1F451; <strong>' + (player ? player.name : 'Player') + '</strong> becomes the Monarch!');
updateMonarchBadges();
}
// Empower the life counter right-click on the quadrant labels
document.addEventListener('DOMContentLoaded', function() {
// Wire right-click on life values to set-life dialog
for (var i = 0; i < 4; i++) {
(function(idx) {
var lifeEl = document.getElementById('life-' + idx);
if (lifeEl) {
lifeEl.addEventListener('contextmenu', function(e) {
e.preventDefault();
showSetLifeDialog(idx);
});
}
})(i);
}
// Life counter right-click on quadrant buttons too
// (already handled via inline onclick in HTML)
// Auto-detect Commander-AI-Lab backend on page load
if (typeof AIBridge !== 'undefined') {
AIBridge.init().then(function(status) {
var indicator = document.getElementById('backend-status-indicator');
if (indicator) {
indicator.textContent = status.connected
? (status.modelLoaded ? '\u{1F9E0} ML Backend Connected' : '\u26a1 Backend Connected (no ML model)')
: '\u{1F4E1} Local Mode (no backend)';
indicator.className = 'backend-status ' +
(status.connected ? (status.modelLoaded ? 'connected-ml' : 'connected') : 'disconnected');
}
});
}
});
</script>
</body>
</html>
+270
View File
@@ -0,0 +1,270 @@
// player-panels.js — Enhanced Player Panel Component
// Provides collapsible panels with life, commander damage, counters, and emblems.
'use strict';
/**
* Create an enhanced player panel widget for a quadrant.
* Replaces the basic life counter with a full tracking panel.
*
* @param {object} player - Player object from gameState
* @param {number} quadrant - 1-4 (matches CSS class p1-p4)
* @param {boolean} isActive - Whether this is the active player
*/
function createEnhancedPlayerPanel(player, quadrant, isActive) {
var panel = document.createElement('div');
panel.className = 'enhanced-player-panel' +
(isActive ? ' active-turn' : '') +
(player.eliminated ? ' eliminated' : '');
panel.id = 'enhanced-panel-' + player.id;
panel.setAttribute('data-player-id', player.id);
// Track collapse state
var collapseKey = 'panel-collapsed-' + player.id;
var isCollapsed = localStorage.getItem(collapseKey) === 'true';
// ── Header Row ──────────────────────────────────────────
var headerHtml =
'<div class="epp-header" onclick="togglePlayerPanel(' + player.id + ')">' +
'<span class="epp-collapse-arrow">' + (isCollapsed ? '\u25b8' : '\u25be') + '</span>' +
'<div class="epp-name-dot" style="background:' + player.color + '"></div>' +
'<span class="epp-name">' + player.name + '</span>' +
(player.isAI ? '<span class="epp-ai-badge">AI</span>' : '') +
'<span class="epp-life-badge">' +
'\u2665 ' + player.life +
(player.life < player.startingLife
? ' / ' + player.startingLife
: '') +
'</span>' +
'</div>';
// ── Life Counter Row ────────────────────────────────────
var lifeHtml =
'<div class="epp-life-row">' +
'<button class="epp-life-btn minus" onclick="adjustLife(' + player.id + ',-1)" ' +
'oncontextmenu="event.preventDefault();adjustLife(' + player.id + ',-5)" ' +
'title="Left: -1 | Right: -5">\u2212</button>' +
'<div class="epp-life-value" id="epp-life-' + player.id + '" ' +
'onclick="showSetLifeDialog(' + player.id + ')">' +
player.life +
'</div>' +
'<button class="epp-life-btn plus" onclick="adjustLife(' + player.id + ',1)" ' +
'oncontextmenu="event.preventDefault();adjustLife(' + player.id + ',5)" ' +
'title="Left: +1 | Right: +5">+</button>' +
'</div>';
// ── Commander Damage Section ────────────────────────────
var otherPlayers = gameState.players.filter(function(p) {
return p.id !== player.id && !p.eliminated;
});
var cmdDmgHtml =
'<div class="epp-section">' +
'<div class="epp-section-label">Commander Damage</div>' +
'<div class="epp-cmd-dmg-grid">';
otherPlayers.forEach(function(op) {
var dmg = player.commanderDamage[op.id] || 0;
var pct = Math.round((dmg / 21) * 100);
var dangerClass = dmg >= 18 ? 'critical' : dmg >= 10 ? 'warning' : '';
cmdDmgHtml +=
'<div class="epp-cmd-dmg-item ' + dangerClass + '">' +
'<div class="epp-cmd-dmg-bar" style="width:' + pct + '%"></div>' +
'<div class="epp-cmd-dmg-dot" style="background:' + op.color + '"></div>' +
'<span class="epp-cmd-dmg-name">' + op.name + '</span>' +
'<span class="epp-cmd-dmg-value">' + dmg + '/21</span>' +
'<button class="epp-counter-btn" ' +
'onclick="event.stopPropagation();adjustCommanderDamage(' + player.id + ',' + op.id + ',1)" ' +
'title="Add commander damage">+</button>' +
'<button class="epp-counter-btn" ' +
'onclick="event.stopPropagation();adjustCommanderDamage(' + player.id + ',' + op.id + ',-1)" ' +
'title="Remove commander damage">\u2212</button>' +
'</div>';
});
cmdDmgHtml += '</div></div>';
// ── Counters Section ────────────────────────────────────
var poison = player.poison || 0;
var energy = player.energy || 0;
var experience = player.experience || 0;
var rad = player.rad || 0;
var countersHtml =
'<div class="epp-section">' +
'<div class="epp-section-label">Counters</div>' +
'<div class="epp-counters-grid">' +
_buildCounterWidget('Poison', '\u2620', poison, 10,
'adjustPoison(' + player.id + ',1)',
'adjustPoison(' + player.id + ',-1)',
poison >= 8 ? 'critical' : poison >= 5 ? 'warning' : '') +
_buildCounterWidget('Energy', '\u269b', energy, 0,
'adjustEnergy(' + player.id + ',1)',
'adjustEnergy(' + player.id + ',-1)', '') +
_buildCounterWidget('Experience', '\u2605', experience, 0,
'adjustExperience(' + player.id + ',1)',
'adjustExperience(' + player.id + ',-1)', '') +
_buildCounterWidget('Rad', '\u2622', rad, 0,
'adjustRad(' + player.id + ',1)',
'adjustRad(' + player.id + ',-1)', '') +
'</div>' +
'</div>';
// ── Emblems Section ─────────────────────────────────────
var emblems = player.emblems || [];
var emblemsHtml =
'<div class="epp-section">' +
'<div class="epp-section-label">Emblems</div>' +
'<div class="epp-emblems">';
if (emblems.length === 0) {
emblemsHtml += '<span class="epp-no-emblems">None</span>';
} else {
emblems.forEach(function(emb, idx) {
emblemsHtml +=
'<div class="epp-emblem-chip" title="' + emb.name + '">' +
'<span class="epp-emblem-name">' + emb.name + '</span>' +
'<button class="epp-emblem-remove" ' +
'onclick="event.stopPropagation();removeEmblem(' + player.id + ',' + idx + ')">\u00d7</button>' +
'</div>';
});
}
emblemsHtml +=
'<button class="epp-add-emblem-btn" ' +
'onclick="event.stopPropagation();showAddEmblemDialog(' + player.id + ')" ' +
'title="Add an emblem">+ Emblem</button>' +
'</div>' +
'</div>';
// ── Assemble Panel ──────────────────────────────────────
var bodyHtml =
'<div class="epp-body" id="epp-body-' + player.id + '" ' +
'style="' + (isCollapsed ? 'display:none;' : '') + '">' +
lifeHtml + cmdDmgHtml + countersHtml + emblemsHtml +
'</div>';
panel.innerHTML = headerHtml + bodyHtml;
return panel;
}
function _buildCounterWidget(label, icon, value, threshold, onInc, onDec, dangerClass) {
return '<div class="epp-counter ' + dangerClass + '">' +
'<div class="epp-counter-icon">' + icon + '</div>' +
'<div class="epp-counter-info">' +
'<span class="epp-counter-value">' + value + '</span>' +
'<span class="epp-counter-label">' + label + '</span>' +
'</div>' +
'<div class="epp-counter-btns">' +
'<button class="epp-counter-btn" onclick="event.stopPropagation();' + onInc + '">+</button>' +
'<button class="epp-counter-btn" onclick="event.stopPropagation();' + onDec + '">\u2212</button>' +
'</div>' +
'</div>';
}
function togglePlayerPanel(playerId) {
var body = document.getElementById('epp-body-' + playerId);
var panel = document.getElementById('enhanced-panel-' + playerId);
if (!body || !panel) return;
var isCollapsed = body.style.display === 'none';
body.style.display = isCollapsed ? '' : 'none';
var arrow = panel.querySelector('.epp-collapse-arrow');
if (arrow) arrow.textContent = isCollapsed ? '\u25be' : '\u25b8';
localStorage.setItem('panel-collapsed-' + playerId, !isCollapsed);
}
// Counter adjustment functions
function adjustEnergy(playerId, amount) {
if (!gameState) return;
var player = gameState.players[playerId];
if (!player) return;
player.energy = Math.max(0, (player.energy || 0) + amount);
addLogEntry('<strong>' + player.name + '</strong> energy: ' + player.energy);
renderEnhancedPanels();
}
function adjustExperience(playerId, amount) {
if (!gameState) return;
var player = gameState.players[playerId];
if (!player) return;
player.experience = Math.max(0, (player.experience || 0) + amount);
addLogEntry('<strong>' + player.name + '</strong> experience: ' + player.experience);
renderEnhancedPanels();
}
function adjustRad(playerId, amount) {
if (!gameState) return;
var player = gameState.players[playerId];
if (!player) return;
player.rad = Math.max(0, (player.rad || 0) + amount);
addLogEntry('<strong>' + player.name + '</strong> rad counters: ' + player.rad);
renderEnhancedPanels();
}
function showAddEmblemDialog(playerId) {
var name = prompt('Enter emblem name (e.g., "Sorin, Vengeful Bloodlord"):');
if (!name || !name.trim()) return;
if (!gameState) return;
var player = gameState.players[playerId];
if (!player) return;
if (!player.emblems) player.emblems = [];
player.emblems.push({ name: name.trim() });
addLogEntry('<strong>' + player.name + '</strong> gained emblem: ' + name.trim());
renderEnhancedPanels();
}
function removeEmblem(playerId, idx) {
if (!gameState) return;
var player = gameState.players[playerId];
if (!player || !player.emblems) return;
var removed = player.emblems.splice(idx, 1);
if (removed.length > 0) {
addLogEntry('<strong>' + player.name + '</strong> lost emblem: ' + removed[0].name);
}
renderEnhancedPanels();
}
/**
* Re-render all enhanced player panels in their quadrant positions.
*/
function renderEnhancedPanels() {
if (!gameState) return;
for (var i = 0; i < gameState.players.length; i++) {
var player = gameState.players[i];
var quadrant = i + 1;
var areaEl = document.getElementById('player-area-' + quadrant);
if (!areaEl) continue;
// Find or create the enhanced panel container
var container = areaEl.querySelector('.epp-container');
if (!container) {
container = document.createElement('div');
container.className = 'epp-container';
// Insert before the zone row
var zoneRow = areaEl.querySelector('.zone-row');
if (zoneRow) {
areaEl.insertBefore(container, zoneRow);
} else {
areaEl.appendChild(container);
}
}
var isActive = gameState.currentPlayerIndex === i;
var panel = createEnhancedPlayerPanel(player, quadrant, isActive);
container.innerHTML = '';
container.appendChild(panel);
// Hide the old life counter (we have our own now)
var oldLifeCounter = areaEl.querySelector('.life-counter');
if (oldLifeCounter) oldLifeCounter.style.display = 'none';
// Hide old poison badge (we have counters section)
var oldPoison = areaEl.querySelector('.poison-badge');
if (oldPoison) oldPoison.style.display = 'none';
}
}
+167
View File
@@ -0,0 +1,167 @@
// Auto-generated from precon-decks/precon-index.json
// Run: node scripts/generate-precons-js.js to regenerate
var PRECON_DATA = [
{ id: "Counterpunch", name: "Counterpunch", commander: "Ghave, Guru of Spores", set: "Commander 2011", fileName: "Counterpunch.dck", colors: [], year: 2011 },
{ id: "Devour_for_Power_CMD", name: "Devour for Power", commander: "The Mimeoplasm", set: "Commander 2011", fileName: "Devour_for_Power_CMD.dck", colors: [], year: 2011 },
{ id: "Heavenly_Inferno_CMD", name: "Heavenly Inferno", commander: "Kaalia of the Vast", set: "Commander 2011", fileName: "Heavenly_Inferno_CMD.dck", colors: [], year: 2011 },
{ id: "Mirror_Mastery", name: "Mirror Mastery", commander: "Riku of Two Reflections", set: "Commander 2011", fileName: "Mirror_Mastery.dck", colors: [], year: 2011 },
{ id: "Political_Puppets", name: "Political Puppets", commander: "Zedruu the Greathearted", set: "Commander 2011", fileName: "Political_Puppets.dck", colors: [], year: 2011 },
{ id: "Eternal_Bargain", name: "Eternal Bargain", commander: "Oloro, Ageless Ascetic", set: "Commander 2013", fileName: "Eternal_Bargain.dck", colors: [], year: 2013 },
{ id: "Evasive_Maneuvers_C13", name: "Evasive Maneuvers", commander: "Derevi, Empyrial Tactician", set: "Commander 2013", fileName: "Evasive_Maneuvers_C13.dck", colors: [], year: 2013 },
{ id: "Mind_Seize", name: "Mind Seize", commander: "Jeleva, Nephalia's Scourge", set: "Commander 2013", fileName: "Mind_Seize.dck", colors: [], year: 2013 },
{ id: "Nature_of_the_Beast", name: "Nature of the Beast", commander: "Marath, Will of the Wild", set: "Commander 2013", fileName: "Nature_of_the_Beast.dck", colors: [], year: 2013 },
{ id: "Power_Hungry", name: "Power Hungry", commander: "Prossh, Skyraider of Kher", set: "Commander 2013", fileName: "Power_Hungry.dck", colors: [], year: 2013 },
{ id: "Built_From_Scratch_C14", name: "Built From Scratch", commander: "Daretti, Scrap Savant", set: "Commander 2014", fileName: "Built_From_Scratch_C14.dck", colors: [], year: 2014 },
{ id: "Forged_In_Stone", name: "Forged In Stone", commander: "Nahiri, the Lithomancer", set: "Commander 2014", fileName: "Forged_In_Stone.dck", colors: [], year: 2014 },
{ id: "Guided_By_Nature", name: "Guided By Nature", commander: "Freyalise, Llanowar's Fury", set: "Commander 2014", fileName: "Guided_By_Nature.dck", colors: [], year: 2014 },
{ id: "Peer_Through_Time", name: "Peer Through Time", commander: "Teferi, Temporal Archmage", set: "Commander 2014", fileName: "Peer_Through_Time.dck", colors: [], year: 2014 },
{ id: "Sworn_To_Darkness", name: "Sworn To Darkness", commander: "Ob Nixilis of the Black Oath", set: "Commander 2014", fileName: "Sworn_To_Darkness.dck", colors: [], year: 2014 },
{ id: "Call_the_Spirits", name: "Call the Spirits", commander: "Daxos the Returned", set: "Commander 2015", fileName: "Call_the_Spirits.dck", colors: [], year: 2015 },
{ id: "Plunder_the_Graves_C15", name: "Plunder the Graves", commander: "Meren of Clan Nel Toth", set: "Commander 2015", fileName: "Plunder_the_Graves_C15.dck", colors: [], year: 2015 },
{ id: "Seize_Control", name: "Seize Control", commander: "Mizzix of the Izmagnus", set: "Commander 2015", fileName: "Seize_Control.dck", colors: [], year: 2015 },
{ id: "Swell_the_Host", name: "Swell the Host", commander: "Ezuri, Claw of Progress", set: "Commander 2015", fileName: "Swell_the_Host.dck", colors: [], year: 2015 },
{ id: "Wade_into_Battle_C15", name: "Wade into Battle", commander: "Kalemne, Disciple of Iroas", set: "Commander 2015", fileName: "Wade_into_Battle_C15.dck", colors: [], year: 2015 },
{ id: "Breed_Lethality_C16", name: "Breed Lethality", commander: "Atraxa, Praetors' Voice", set: "Commander 2016", fileName: "Breed_Lethality_C16.dck", colors: [], year: 2016 },
{ id: "Entropic_Uprising", name: "Entropic Uprising", commander: "Yidris, Maelstrom Wielder", set: "Commander 2016", fileName: "Entropic_Uprising.dck", colors: [], year: 2016 },
{ id: "Invent_Superiority", name: "Invent Superiority", commander: "Breya, Etherium Shaper", set: "Commander 2016", fileName: "Invent_Superiority.dck", colors: [], year: 2016 },
{ id: "Open_Hostility", name: "Open Hostility", commander: "Saskia the Unyielding", set: "Commander 2016", fileName: "Open_Hostility.dck", colors: [], year: 2016 },
{ id: "Stalwart_Unity", name: "Stalwart Unity", commander: "Kynaios and Tiro of Meletis", set: "Commander 2016", fileName: "Stalwart_Unity.dck", colors: [], year: 2016 },
{ id: "Evasive_Maneuvers_CMA", name: "Evasive Maneuvers", commander: "Derevi, Empyrial Tactician", set: "Commander Anthology", fileName: "Evasive_Maneuvers_CMA.dck", colors: [], year: 2017 },
{ id: "Guided_by_Nature", name: "Guided by Nature", commander: "Freyalise, Llanowar's Fury", set: "Commander Anthology", fileName: "Guided_by_Nature.dck", colors: [], year: 2017 },
{ id: "Heavenly_Inferno_CMA", name: "Heavenly Inferno", commander: "Kaalia of the Vast", set: "Commander Anthology", fileName: "Heavenly_Inferno_CMA.dck", colors: [], year: 2017 },
{ id: "Plunder_the_Graves_CMA", name: "Plunder the Graves", commander: "Meren of Clan Nel Toth", set: "Commander Anthology", fileName: "Plunder_the_Graves_CMA.dck", colors: [], year: 2017 },
{ id: "Arcane_Wizardry", name: "Arcane Wizardry", commander: "Inalla, Archmage Ritualist", set: "Commander 2017", fileName: "Arcane_Wizardry.dck", colors: [], year: 2017 },
{ id: "Draconic_Domination", name: "Draconic Domination", commander: "The Ur-Dragon", set: "Commander 2017", fileName: "Draconic_Domination.dck", colors: [], year: 2017 },
{ id: "Feline_Ferocity", name: "Feline Ferocity", commander: "Arahbo, Roar of the World", set: "Commander 2017", fileName: "Feline_Ferocity.dck", colors: [], year: 2017 },
{ id: "Vampiric_Bloodlust", name: "Vampiric Bloodlust", commander: "Edgar Markov", set: "Commander 2017", fileName: "Vampiric_Bloodlust.dck", colors: [], year: 2017 },
{ id: "Breed_Lethality_CM2", name: "Breed Lethality", commander: "Atraxa, Praetors' Voice", set: "Commander Anthology Volume II", fileName: "Breed_Lethality_CM2.dck", colors: [], year: 2018 },
{ id: "Built_From_Scratch_CM2", name: "Built From Scratch", commander: "Daretti, Scrap Savant", set: "Commander Anthology Volume II", fileName: "Built_From_Scratch_CM2.dck", colors: [], year: 2018 },
{ id: "Devour_for_Power_CM2", name: "Devour for Power", commander: "The Mimeoplasm", set: "Commander Anthology Volume II", fileName: "Devour_for_Power_CM2.dck", colors: [], year: 2018 },
{ id: "Wade_into_Battle_CM2", name: "Wade into Battle", commander: "Kalemne, Disciple of Iroas", set: "Commander Anthology Volume II", fileName: "Wade_into_Battle_CM2.dck", colors: [], year: 2018 },
{ id: "Adaptive_Enchantment", name: "Adaptive Enchantment", commander: "Estrid, the Masked", set: "Commander 2018", fileName: "Adaptive_Enchantment.dck", colors: [], year: 2018 },
{ id: "Exquisite_Invention", name: "Exquisite Invention", commander: "Saheeli, the Gifted", set: "Commander 2018", fileName: "Exquisite_Invention.dck", colors: [], year: 2018 },
{ id: "Natures_Vengeance", name: "Nature's Vengeance", commander: "Lord Windgrace", set: "Commander 2018", fileName: "Natures_Vengeance.dck", colors: [], year: 2018 },
{ id: "Subjective_Reality", name: "Subjective Reality", commander: "Aminatou, the Fateshifter", set: "Commander 2018", fileName: "Subjective_Reality.dck", colors: [], year: 2018 },
{ id: "Faceless_Menace", name: "Faceless Menace", commander: "Kadena, Slinking Sorcerer", set: "Commander 2019", fileName: "Faceless_Menace.dck", colors: [], year: 2019 },
{ id: "Merciless_Rage", name: "Merciless Rage", commander: "Anje Falkenrath", set: "Commander 2019", fileName: "Merciless_Rage.dck", colors: [], year: 2019 },
{ id: "Mystic_Intellect", name: "Mystic Intellect", commander: "Sevinne, the Chronoclasm", set: "Commander 2019", fileName: "Mystic_Intellect.dck", colors: [], year: 2019 },
{ id: "Primal_Genesis", name: "Primal Genesis", commander: "Ghired, Conclave Exile", set: "Commander 2019", fileName: "Primal_Genesis.dck", colors: [], year: 2019 },
{ id: "Arcane_Maelstrom", name: "Arcane Maelstrom", commander: "Kalamax, the Stormsire", set: "Commander 2020", fileName: "Arcane_Maelstrom.dck", colors: [], year: 2020 },
{ id: "Enhanced_Evolution", name: "Enhanced Evolution", commander: "Otrimi, the Ever-Playful", set: "Commander 2020", fileName: "Enhanced_Evolution.dck", colors: [], year: 2020 },
{ id: "Ruthless_Regiment", name: "Ruthless Regiment", commander: "Jirina Kudro", set: "Commander 2020", fileName: "Ruthless_Regiment.dck", colors: [], year: 2020 },
{ id: "Symbiotic_Swarm", name: "Symbiotic Swarm", commander: "Kathril, Aspect Warper", set: "Commander 2020", fileName: "Symbiotic_Swarm.dck", colors: [], year: 2020 },
{ id: "Timeless_Wisdom", name: "Timeless Wisdom", commander: "Gavi, Nest Warden", set: "Commander 2020", fileName: "Timeless_Wisdom.dck", colors: [], year: 2020 },
{ id: "Lands_Wrath", name: "Land's Wrath", commander: "Obuun, Mul Daya Ancestor", set: "Zendikar Rising Commander", fileName: "Lands_Wrath.dck", colors: [], year: 2020 },
{ id: "Sneak_Attack", name: "Sneak Attack", commander: "Anowon, the Ruin Thief", set: "Zendikar Rising Commander", fileName: "Sneak_Attack.dck", colors: [], year: 2020 },
{ id: "Arm_for_Battle", name: "Arm for Battle", commander: "Wyleth, Soul of Steel", set: "Commander Legends", fileName: "Arm_for_Battle.dck", colors: [], year: 2020 },
{ id: "Reap_the_Tides", name: "Reap the Tides", commander: "Aesi, Tyrant of Gyre Strait", set: "Commander Legends", fileName: "Reap_the_Tides.dck", colors: [], year: 2020 },
{ id: "Elven_Empire", name: "Elven Empire", commander: "Lathril, Blade of the Elves", set: "Kaldheim Commander", fileName: "Elven_Empire.dck", colors: [], year: 2021 },
{ id: "Phantom_Premonition", name: "Phantom Premonition", commander: "Ranar the Ever-Watchful", set: "Kaldheim Commander", fileName: "Phantom_Premonition.dck", colors: [], year: 2021 },
{ id: "Lorehold_Legacies", name: "Lorehold Legacies", commander: "Osgir, the Reconstructor", set: "Commander 2021", fileName: "Lorehold_Legacies.dck", colors: [], year: 2021 },
{ id: "Prismari_Performance", name: "Prismari Performance", commander: "Zaffai, Thunder Conductor", set: "Commander 2021", fileName: "Prismari_Performance.dck", colors: [], year: 2021 },
{ id: "Quantum_Quandrix", name: "Quantum Quandrix", commander: "Adrix and Nev, Twincasters", set: "Commander 2021", fileName: "Quantum_Quandrix.dck", colors: [], year: 2021 },
{ id: "Silverquill_Statement", name: "Silverquill Statement", commander: "Breena, the Demagogue", set: "Commander 2021", fileName: "Silverquill_Statement.dck", colors: [], year: 2021 },
{ id: "Witherbloom_Witchcraft", name: "Witherbloom Witchcraft", commander: "Willowdusk, Essence Seer", set: "Commander 2021", fileName: "Witherbloom_Witchcraft.dck", colors: [], year: 2021 },
{ id: "Aura_of_Courage", name: "Aura of Courage", commander: "Galea, Kindler of Hope", set: "Forgotten Realms Commander", fileName: "Aura_of_Courage.dck", colors: [], year: 2021 },
{ id: "Draconic_Rage", name: "Draconic Rage", commander: "Vrondiss, Rage of Ancients", set: "Forgotten Realms Commander", fileName: "Draconic_Rage.dck", colors: [], year: 2021 },
{ id: "Dungeons_of_Death", name: "Dungeons of Death", commander: "Sefris of the Hidden Ways", set: "Forgotten Realms Commander", fileName: "Dungeons_of_Death.dck", colors: [], year: 2021 },
{ id: "Planar_Portal", name: "Planar Portal", commander: "Prosper, Tome-Bound", set: "Forgotten Realms Commander", fileName: "Planar_Portal.dck", colors: [], year: 2021 },
{ id: "Coven_Counters", name: "Coven Counters", commander: "Leinore, Autumn Sovereign", set: "Midnight Hunt Commander", fileName: "Coven_Counters.dck", colors: [], year: 2021 },
{ id: "Undead_Unleashed", name: "Undead Unleashed", commander: "Wilhelt, the Rotcleaver", set: "Midnight Hunt Commander", fileName: "Undead_Unleashed.dck", colors: [], year: 2021 },
{ id: "Spirit_Squadron", name: "Spirit Squadron", commander: "Millicent, Restless Revenant", set: "Crimson Vow Commander", fileName: "Spirit_Squadron.dck", colors: [], year: 2021 },
{ id: "Vampiric_Bloodline", name: "Vampiric Bloodline", commander: "Strefan, Maurer Progenitor", set: "Crimson Vow Commander", fileName: "Vampiric_Bloodline.dck", colors: [], year: 2021 },
{ id: "Buckle_Up", name: "Buckle Up", commander: "Kotori, Pilot Prodigy", set: "Neon Dynasty Commander", fileName: "Buckle_Up.dck", colors: [], year: 2022 },
{ id: "Upgrades_Unleashed", name: "Upgrades Unleashed", commander: "Chishiro, the Shattered Blade", set: "Neon Dynasty Commander", fileName: "Upgrades_Unleashed.dck", colors: [], year: 2022 },
{ id: "Heads_I_Win,_Tails_You_Lose", name: "Heads I Win, Tails You Lose", commander: "Zndrsplt, Eye of Wisdom", set: "Secret Lair Drop", fileName: "Heads_I_Win,_Tails_You_Lose.dck", colors: [], year: 2022 },
{ id: "Bedecked_Brokers", name: "Bedecked Brokers", commander: "Perrie, the Pulverizer", set: "New Capenna Commander", fileName: "Bedecked_Brokers.dck", colors: [], year: 2022 },
{ id: "Cabaretti_Cacophony", name: "Cabaretti Cacophony", commander: "Kitt Kanto, Mayhem Diva", set: "New Capenna Commander", fileName: "Cabaretti_Cacophony.dck", colors: [], year: 2022 },
{ id: "Maestros_Massacre", name: "Maestros Massacre", commander: "Anhelo, the Painter", set: "New Capenna Commander", fileName: "Maestros_Massacre.dck", colors: [], year: 2022 },
{ id: "Obscura_Operation", name: "Obscura Operation", commander: "Kamiz, Obscura Oculus", set: "New Capenna Commander", fileName: "Obscura_Operation.dck", colors: [], year: 2022 },
{ id: "Riveteers_Rampage", name: "Riveteers Rampage", commander: "Henzie \"Toolbox\" Torre", set: "New Capenna Commander", fileName: "Riveteers_Rampage.dck", colors: [], year: 2022 },
{ id: "Draconic_Dissent", name: "Draconic Dissent", commander: "Firkraag, Cunning Instigator", set: "Commander Legends: Battle for Baldur's Gate", fileName: "Draconic_Dissent.dck", colors: [], year: 2022 },
{ id: "Exit_from_Exile", name: "Exit from Exile", commander: "Faldorn, Dread Wolf Herald", set: "Commander Legends: Battle for Baldur's Gate", fileName: "Exit_from_Exile.dck", colors: [], year: 2022 },
{ id: "Mind_Flayarrrs", name: "Mind Flayarrrs", commander: "Captain N'ghathrod", set: "Commander Legends: Battle for Baldur's Gate", fileName: "Mind_Flayarrrs.dck", colors: [], year: 2022 },
{ id: "Party_Time", name: "Party Time", commander: "Nalia de'Arnise", set: "Commander Legends: Battle for Baldur's Gate", fileName: "Party_Time.dck", colors: [], year: 2022 },
{ id: "Legends_Legacy", name: "Legends' Legacy", commander: "Dihada, Binder of Wills", set: "Dominaria United Commander", fileName: "Legends_Legacy.dck", colors: [], year: 2022 },
{ id: "Painbow", name: "Painbow", commander: "Jared Carthalion", set: "Dominaria United Commander", fileName: "Painbow.dck", colors: [], year: 2022 },
{ id: "Forces_of_the_Imperium", name: "Forces of the Imperium", commander: "Inquisitor Greyfax", set: "Warhammer 40,000 Commander", fileName: "Forces_of_the_Imperium.dck", colors: [], year: 2022 },
{ id: "Forces_of_the_Imperium_Collectors_Edition", name: "Forces of the Imperium Collector's Edition", commander: "Inquisitor Greyfax", set: "Warhammer 40,000 Commander", fileName: "Forces_of_the_Imperium_Collectors_Edition.dck", colors: [], year: 2022 },
{ id: "Necron_Dynasties", name: "Necron Dynasties", commander: "Szarekh, the Silent King", set: "Warhammer 40,000 Commander", fileName: "Necron_Dynasties.dck", colors: [], year: 2022 },
{ id: "Necron_Dynasties_Collectors_Edition", name: "Necron Dynasties Collector's Edition", commander: "Szarekh, the Silent King", set: "Warhammer 40,000 Commander", fileName: "Necron_Dynasties_Collectors_Edition.dck", colors: [], year: 2022 },
{ id: "The_Ruinous_Powers", name: "The Ruinous Powers", commander: "Abaddon the Despoiler", set: "Warhammer 40,000 Commander", fileName: "The_Ruinous_Powers.dck", colors: [], year: 2022 },
{ id: "The_Ruinous_Powers_Collectors_Edition", name: "The Ruinous Powers Collector's Edition", commander: "Abaddon the Despoiler", set: "Warhammer 40,000 Commander", fileName: "The_Ruinous_Powers_Collectors_Edition.dck", colors: [], year: 2022 },
{ id: "Tyranid_Swarm", name: "Tyranid Swarm", commander: "The Swarmlord", set: "Warhammer 40,000 Commander", fileName: "Tyranid_Swarm.dck", colors: [], year: 2022 },
{ id: "Tyranid_Swarm_Collectors_Edition", name: "Tyranid Swarm Collector's Edition", commander: "The Swarmlord", set: "Warhammer 40,000 Commander", fileName: "Tyranid_Swarm_Collectors_Edition.dck", colors: [], year: 2022 },
{ id: "Mishras_Burnished_Banner", name: "Mishra's Burnished Banner", commander: "Mishra, Eminent One", set: "The Brothers' War Commander", fileName: "Mishras_Burnished_Banner.dck", colors: [], year: 2022 },
{ id: "Urzas_Iron_Alliance", name: "Urza's Iron Alliance", commander: "Urza, Chief Artificer", set: "The Brothers' War Commander", fileName: "Urzas_Iron_Alliance.dck", colors: [], year: 2022 },
{ id: "Chaos_Incarnate", name: "Chaos Incarnate", commander: "Kardur, Doomscourge", set: "Starter Commander Decks", fileName: "Chaos_Incarnate.dck", colors: [], year: 2022 },
{ id: "Draconic_Destruction", name: "Draconic Destruction", commander: "Atarka, World Render", set: "Starter Commander Decks", fileName: "Draconic_Destruction.dck", colors: [], year: 2022 },
{ id: "First_Flight", name: "First Flight", commander: "Isperia, Supreme Judge", set: "Starter Commander Decks", fileName: "First_Flight.dck", colors: [], year: 2022 },
{ id: "Grave_Danger", name: "Grave Danger", commander: "Gisa and Geralf", set: "Starter Commander Decks", fileName: "Grave_Danger.dck", colors: [], year: 2022 },
{ id: "Token_Triumph", name: "Token Triumph", commander: "Emmara, Soul of the Accord", set: "Starter Commander Decks", fileName: "Token_Triumph.dck", colors: [], year: 2022 },
{ id: "Corrupting_Influence", name: "Corrupting Influence", commander: "Ixhel, Scion of Atraxa", set: "Phyrexia: All Will Be One Commander", fileName: "Corrupting_Influence.dck", colors: [], year: 2023 },
{ id: "Rebellion_Rising", name: "Rebellion Rising", commander: "Neyali, Suns' Vanguard", set: "Phyrexia: All Will Be One Commander", fileName: "Rebellion_Rising.dck", colors: [], year: 2023 },
{ id: "From_Cute_to_Brute", name: "From Cute to Brute", commander: "Esika, God of the Tree", set: "Secret Lair Drop", fileName: "From_Cute_to_Brute.dck", colors: [], year: 2023 },
{ id: "Elven_Council", name: "Elven Council", commander: "Galadriel, Elven-Queen", set: "Tales of Middle-earth Commander", fileName: "Elven_Council.dck", colors: [], year: 2023 },
{ id: "Food_and_Fellowship", name: "Food and Fellowship", commander: "Frodo, Adventurous Hobbit", set: "Tales of Middle-earth Commander", fileName: "Food_and_Fellowship.dck", colors: [], year: 2023 },
{ id: "Riders_of_Rohan", name: "Riders of Rohan", commander: "Éowyn, Shieldmaiden", set: "Tales of Middle-earth Commander", fileName: "Riders_of_Rohan.dck", colors: [], year: 2023 },
{ id: "The_Hosts_of_Mordor", name: "The Hosts of Mordor", commander: "Sauron, Lord of the Rings", set: "Tales of Middle-earth Commander", fileName: "The_Hosts_of_Mordor.dck", colors: [], year: 2023 },
{ id: "Eldrazi_Unbound", name: "Eldrazi Unbound", commander: "Zhulodok, Void Gorger", set: "Commander Masters", fileName: "Eldrazi_Unbound.dck", colors: [], year: 2023 },
{ id: "Enduring_Enchantments", name: "Enduring Enchantments", commander: "Anikthea, Hand of Erebos", set: "Commander Masters", fileName: "Enduring_Enchantments.dck", colors: [], year: 2023 },
{ id: "Planeswalker_Party", name: "Planeswalker Party", commander: "Commodore Guff", set: "Commander Masters", fileName: "Planeswalker_Party.dck", colors: [], year: 2023 },
{ id: "Sliver_Swarm", name: "Sliver Swarm", commander: "Sliver Gravemother", set: "Commander Masters", fileName: "Sliver_Swarm.dck", colors: [], year: 2023 },
{ id: "Angels_Theyre_Just_Like_Us_but_Cooler_and_with_Wings", name: "Angels: They're Just Like Us but Cooler and with Wings", commander: "Gisela, the Broken Blade", set: "Secret Lair Drop", fileName: "Angels_Theyre_Just_Like_Us_but_Cooler_and_with_Wings.dck", colors: [], year: 2023 },
{ id: "Fae_Dominion", name: "Fae Dominion", commander: "Tegwyll, Duke of Splendor", set: "Wilds of Eldraine Commander", fileName: "Fae_Dominion.dck", colors: [], year: 2023 },
{ id: "Virtue_and_Valor", name: "Virtue and Valor", commander: "Ellivere of the Wild Court", set: "Wilds of Eldraine Commander", fileName: "Virtue_and_Valor.dck", colors: [], year: 2023 },
{ id: "Ahoy_Mateys", name: "Ahoy Mateys", commander: "Admiral Brass, Unsinkable", set: "The Lost Caverns of Ixalan Commander", fileName: "Ahoy_Mateys.dck", colors: [], year: 2023 },
{ id: "Blood_Rites", name: "Blood Rites", commander: "Clavileño, First of the Blessed", set: "The Lost Caverns of Ixalan Commander", fileName: "Blood_Rites.dck", colors: [], year: 2023 },
{ id: "Explorers_of_the_Deep", name: "Explorers of the Deep", commander: "Hakbal of the Surging Soul", set: "The Lost Caverns of Ixalan Commander", fileName: "Explorers_of_the_Deep.dck", colors: [], year: 2023 },
{ id: "Veloci-Ramp-Tor", name: "Veloci-Ramp-Tor", commander: "Pantlaza, Sun-Favored", set: "The Lost Caverns of Ixalan Commander", fileName: "Veloci-Ramp-Tor.dck", colors: [], year: 2023 },
{ id: "Raining_Cats_and_Dogs", name: "Raining Cats and Dogs", commander: "Rin and Seri, Inseparable", set: "Secret Lair Drop", fileName: "Raining_Cats_and_Dogs.dck", colors: [], year: 2024 },
{ id: "Blame_Game", name: "Blame Game", commander: "Nelly Borca, Impulsive Accuser", set: "Murders at Karlov Manor Commander", fileName: "Blame_Game.dck", colors: [], year: 2024 },
{ id: "Deadly_Disguise", name: "Deadly Disguise", commander: "Kaust, Eyes of the Glade", set: "Murders at Karlov Manor Commander", fileName: "Deadly_Disguise.dck", colors: [], year: 2024 },
{ id: "Deep_Clue_Sea", name: "Deep Clue Sea", commander: "Morska, Undersea Sleuth", set: "Murders at Karlov Manor Commander", fileName: "Deep_Clue_Sea.dck", colors: [], year: 2024 },
{ id: "Revenant_Recon", name: "Revenant Recon", commander: "Mirko, Obsessive Theorist", set: "Murders at Karlov Manor Commander", fileName: "Revenant_Recon.dck", colors: [], year: 2024 },
{ id: "Hail,_Caesar", name: "Hail, Caesar", commander: "Caesar, Legion's Emperor", set: "Fallout", fileName: "Hail,_Caesar.dck", colors: [], year: 2024 },
{ id: "Mutant_Menace", name: "Mutant Menace", commander: "The Wise Mothman", set: "Fallout", fileName: "Mutant_Menace.dck", colors: [], year: 2024 },
{ id: "Science", name: "Science!", commander: "Dr. Madison Li", set: "Fallout", fileName: "Science.dck", colors: [], year: 2024 },
{ id: "Scrappy_Survivors", name: "Scrappy Survivors", commander: "Dogmeat, Ever Loyal", set: "Fallout", fileName: "Scrappy_Survivors.dck", colors: [], year: 2024 },
{ id: "Desert_Bloom", name: "Desert Bloom", commander: "Yuma, Proud Protector", set: "Outlaws of Thunder Junction Commander", fileName: "Desert_Bloom.dck", colors: [], year: 2024 },
{ id: "Grand_Larceny", name: "Grand Larceny", commander: "Gonti, Canny Acquisitor", set: "Outlaws of Thunder Junction Commander", fileName: "Grand_Larceny.dck", colors: [], year: 2024 },
{ id: "Most_Wanted", name: "Most Wanted", commander: "Olivia, Opulent Outlaw", set: "Outlaws of Thunder Junction Commander", fileName: "Most_Wanted.dck", colors: [], year: 2024 },
{ id: "Quick_Draw", name: "Quick Draw", commander: "Stella Lee, Wild Card", set: "Outlaws of Thunder Junction Commander", fileName: "Quick_Draw.dck", colors: [], year: 2024 },
{ id: "Creative_Energy", name: "Creative Energy", commander: "Satya, Aetherflux Genius", set: "Modern Horizons 3 Commander", fileName: "Creative_Energy.dck", colors: [], year: 2024 },
{ id: "Creative_Energy_Collectors_Edition", name: "Creative Energy Collector's Edition", commander: "Satya, Aetherflux Genius", set: "Modern Horizons 3 Commander", fileName: "Creative_Energy_Collectors_Edition.dck", colors: [], year: 2024 },
{ id: "Eldrazi_Incursion", name: "Eldrazi Incursion", commander: "Ulalek, Fused Atrocity", set: "Modern Horizons 3 Commander", fileName: "Eldrazi_Incursion.dck", colors: [], year: 2024 },
{ id: "Eldrazi_Incursion_Collectors_Edition", name: "Eldrazi Incursion Collector's Edition", commander: "Ulalek, Fused Atrocity", set: "Modern Horizons 3 Commander", fileName: "Eldrazi_Incursion_Collectors_Edition.dck", colors: [], year: 2024 },
{ id: "Graveyard_Overdrive", name: "Graveyard Overdrive", commander: "Disa the Restless", set: "Modern Horizons 3 Commander", fileName: "Graveyard_Overdrive.dck", colors: [], year: 2024 },
{ id: "Graveyard_Overdrive_Collectors_Edition", name: "Graveyard Overdrive Collector's Edition", commander: "Disa the Restless", set: "Modern Horizons 3 Commander", fileName: "Graveyard_Overdrive_Collectors_Edition.dck", colors: [], year: 2024 },
{ id: "Tricky_Terrain", name: "Tricky Terrain", commander: "Omo, Queen of Vesuva", set: "Modern Horizons 3 Commander", fileName: "Tricky_Terrain.dck", colors: [], year: 2024 },
{ id: "Tricky_Terrain_Collectors_Edition", name: "Tricky Terrain Collector's Edition", commander: "Omo, Queen of Vesuva", set: "Modern Horizons 3 Commander", fileName: "Tricky_Terrain_Collectors_Edition.dck", colors: [], year: 2024 },
{ id: "Animated_Army", name: "Animated Army", commander: "Bello, Bard of the Brambles", set: "Bloomburrow Commander", fileName: "Animated_Army.dck", colors: [], year: 2024 },
{ id: "Family_Matters", name: "Family Matters", commander: "Zinnia, Valley's Voice", set: "Bloomburrow Commander", fileName: "Family_Matters.dck", colors: [], year: 2024 },
{ id: "Peace_Offering", name: "Peace Offering", commander: "Ms. Bumbleflower", set: "Bloomburrow Commander", fileName: "Peace_Offering.dck", colors: [], year: 2024 },
{ id: "Squirreled_Away", name: "Squirreled Away", commander: "Hazel of the Rootbloom", set: "Bloomburrow Commander", fileName: "Squirreled_Away.dck", colors: [], year: 2024 },
{ id: "20_Ways_to_Win", name: "20 Ways to Win", commander: "Go-Shintai of Life's Origin", set: "Secret Lair Drop", fileName: "20_Ways_to_Win.dck", colors: [], year: 2024 },
{ id: "Eternal_Might", name: "Eternal Might", commander: "Temmet, Naktamun's Will", set: "Aetherdrift Commander", fileName: "Eternal_Might.dck", colors: [], year: 2025 },
{ id: "Living_Energy", name: "Living Energy", commander: "Saheeli, Radiant Creator", set: "Aetherdrift Commander", fileName: "Living_Energy.dck", colors: [], year: 2025 },
{ id: "Abzan_Armor", name: "Abzan Armor", commander: "Felothar the Steadfast", set: "Tarkir: Dragonstorm Commander", fileName: "Abzan_Armor.dck", colors: [], year: 2025 },
{ id: "Jeskai_Striker", name: "Jeskai Striker", commander: "Shiko and Narset, Unified", set: "Tarkir: Dragonstorm Commander", fileName: "Jeskai_Striker.dck", colors: [], year: 2025 },
{ id: "Mardu_Surge", name: "Mardu Surge", commander: "Zurgo Stormrender", set: "Tarkir: Dragonstorm Commander", fileName: "Mardu_Surge.dck", colors: [], year: 2025 },
{ id: "Sultai_Arisen", name: "Sultai Arisen", commander: "Teval, the Balanced Scale", set: "Tarkir: Dragonstorm Commander", fileName: "Sultai_Arisen.dck", colors: [], year: 2025 },
{ id: "Temur_Roar", name: "Temur Roar", commander: "Eshki, Temur's Roar", set: "Tarkir: Dragonstorm Commander", fileName: "Temur_Roar.dck", colors: [], year: 2025 },
{ id: "Everyones_Invited", name: "Everyone's Invited!", commander: "Morophon, the Boundless", set: "Secret Lair Drop", fileName: "Everyones_Invited.dck", colors: [], year: 2025 },
{ id: "Counter_Blitz_(FINAL_FANTASY_X)", name: "Counter Blitz (FINAL FANTASY X)", commander: "Tidus, Yuna's Guardian", set: "Final Fantasy Commander", fileName: "Counter_Blitz_(FINAL_FANTASY_X).dck", colors: [], year: 2025 },
{ id: "Counter_Blitz_Collectors_Edition_(FINAL_FANTASY_X)", name: "Counter Blitz Collector's Edition (FINAL FANTASY X)", commander: "Tidus, Yuna's Guardian", set: "Final Fantasy Commander", fileName: "Counter_Blitz_Collectors_Edition_(FINAL_FANTASY_X).dck", colors: [], year: 2025 },
{ id: "Limit_Break_(FINAL_FANTASY_VII)", name: "Limit Break (FINAL FANTASY VII)", commander: "Cloud, Ex-SOLDIER", set: "Final Fantasy Commander", fileName: "Limit_Break_(FINAL_FANTASY_VII).dck", colors: [], year: 2025 },
{ id: "Limit_Break_Collectors_Edition_(FINAL_FANTASY_VII)", name: "Limit Break Collector's Edition (FINAL FANTASY VII)", commander: "Cloud, Ex-SOLDIER", set: "Final Fantasy Commander", fileName: "Limit_Break_Collectors_Edition_(FINAL_FANTASY_VII).dck", colors: [], year: 2025 },
{ id: "Revival_Trance_(FINAL_FANTASY_VI)", name: "Revival Trance (FINAL FANTASY VI)", commander: "Terra, Herald of Hope", set: "Final Fantasy Commander", fileName: "Revival_Trance_(FINAL_FANTASY_VI).dck", colors: [], year: 2025 },
{ id: "Revival_Trance_Collectors_Edition_(FINAL_FANTASY_VI)", name: "Revival Trance Collector's Edition (FINAL FANTASY VI)", commander: "Terra, Herald of Hope", set: "Final Fantasy Commander", fileName: "Revival_Trance_Collectors_Edition_(FINAL_FANTASY_VI).dck", colors: [], year: 2025 },
{ id: "Scions_&_Spellcraft_(FINAL_FANTASY_XIV)", name: "Scions & Spellcraft (FINAL FANTASY XIV)", commander: "Y'shtola, Night's Blessed", set: "Final Fantasy Commander", fileName: "Scions_&_Spellcraft_(FINAL_FANTASY_XIV).dck", colors: [], year: 2025 },
{ id: "Scions_&_Spellcraft_Collectors_Edition_(FINAL_FANTASY_XIV)", name: "Scions & Spellcraft Collector's Edition (FINAL FANTASY XIV)", commander: "Y'shtola, Night's Blessed", set: "Final Fantasy Commander", fileName: "Scions_&_Spellcraft_Collectors_Edition_(FINAL_FANTASY_XIV).dck", colors: [], year: 2025 },
{ id: "Counter_Intelligence", name: "Counter Intelligence", commander: "Inspirit, Flagship Vessel", set: "Edge of Eternities Commander", fileName: "Counter_Intelligence.dck", colors: [], year: 2025 },
{ id: "World_Shaper", name: "World Shaper", commander: "Hearthhull, the Worldseed", set: "Edge of Eternities Commander", fileName: "World_Shaper.dck", colors: [], year: 2025 },
{ id: "Blight_Curse", name: "Blight Curse", commander: "Auntie Ool, Cursewretch", set: "Lorwyn Eclipsed Commander", fileName: "Blight_Curse.dck", colors: [], year: 2026 },
{ id: "Dance_of_the_Elements", name: "Dance of the Elements", commander: "Ashling, the Limitless", set: "Lorwyn Eclipsed Commander", fileName: "Dance_of_the_Elements.dck", colors: [], year: 2026 },
{ id: "Turtle_Power", name: "Turtle Power!", commander: "Heroes in a Half Shell", set: "Teenage Mutant Ninja Turtles Eternal", fileName: "Turtle_Power.dck", colors: [], year: 2026 }
];
+3430
View File
File diff suppressed because it is too large. Load diff
+2325
View File
File diff suppressed because it is too large. Load diff
+54 -21
View File
@@ -178,6 +178,23 @@ class StateEncoder:
[0:29] - Global features (life, mana, turn, phase, etc.)
[29:6173] - Zone embeddings (4 zones × 2 players × 768)
[6173:6177] - Playstyle one-hot (4 dims)
Global feature layout per player block (14 features × 2 players + 1 turn = 29):
idx 0 life (normalised /40)
idx 1 cmdr_dmg (normalised /21)
idx 2 mana (normalised /20, clamped)
idx 3 cmdr_tax (normalised /10, clamped)
idx 4 hand_size (normalised /15, clamped)
idx 5 graveyard_size (normalised /100, clamped)
idx 6 creatures (normalised /30, clamped)
idx 7 total_power (normalised /100, clamped)
└─ real Forge value when present;
falls back to creatures*3 heuristic
idx 8 is_active (binary 0/1)
idx 9-12 phase one-hot (4 dims: main_1/combat/main_2/end)
idx 13 lands (normalised /15, clamped)
--- repeat for player 1 at idx 14-27 ---
idx 28 turn (normalised /max_turns)
"""
def __init__(self, card_index: CardEmbeddingIndex):
@@ -197,7 +214,6 @@ class StateEncoder:
"""
players = decision.get("players", [])
if len(players) < 2:
# Pad to 2 players for 1v1
while len(players) < 2:
players.append(self._empty_player(len(players)))
@@ -218,55 +234,71 @@ class StateEncoder:
return state.astype(np.float32)
def _encode_global(self, decision: dict, players: list) -> np.ndarray:
"""Encode global scalar features. Shape: (29,)"""
"""Encode global scalar features. Shape: (29,)
Per-player block (14 features each, 2 players = 28) + 1 turn feature.
Index 7 priority for total_power:
1. p['total_power'] — real Forge value from Step 4 / _build_encoder_snapshot()
2. p['creatures'] * 3 — legacy heuristic for old JSONL snapshots
"""
features = []
for p in players[:2]: # Exactly 2 players for 1v1
# Per-player scalars (10 each)
for p in players[:2]:
# idx 0: life
life = p.get("life", 40)
features.append(life / 40.0) # Normalized life
features.append(life / 40.0)
# idx 1: commander damage taken
cmdr_dmg = p.get("cmdr_dmg", 0)
features.append(cmdr_dmg / 21.0) # Normalized commander damage
features.append(cmdr_dmg / 21.0)
# idx 2: mana available
mana = p.get("mana", 0)
features.append(min(mana / 20.0, 1.0)) # Normalized mana
features.append(min(mana / 20.0, 1.0))
# idx 3: commander tax
cmdr_tax = p.get("cmdr_tax", 0)
features.append(min(cmdr_tax / 10.0, 1.0)) # Normalized tax
features.append(min(cmdr_tax / 10.0, 1.0))
# idx 4: cards in hand
hand_size = len(p.get("hand", []))
features.append(min(hand_size / 15.0, 1.0))
# idx 5: graveyard size
grave_size = len(p.get("graveyard", []))
features.append(min(grave_size / 100.0, 1.0))
# idx 6: creatures on battlefield
creatures = p.get("creatures", 0)
features.append(min(creatures / 30.0, 1.0))
# Total power on board (not yet tracked accurately; use creatures as proxy)
total_power = creatures * 3 # rough heuristic: avg 3 power
# idx 7: total power on board
# Use real Forge value when available (Step 4); fall back to heuristic
# for legacy JSONL snapshots that predate Step 4.
raw_power = p.get("total_power", 0)
total_power = raw_power if raw_power > 0 else creatures * 3
features.append(min(total_power / 100.0, 1.0))
# Is active player
# idx 8: is active player
is_active = 1.0 if p.get("seat") == decision.get("active_seat") else 0.0
features.append(is_active)
# Land count (extra feature — helps distinguish ramp strategies)
lands = p.get("lands", 0)
features.append(min(lands / 15.0, 1.0))
# Phase one-hot (4 dims per player)
# idx 9-12: phase one-hot (4 dims)
phase_str = decision.get("phase", "main_1")
phase_oh = [0.0] * 4
try:
phase_enum = GamePhase(phase_str)
phase_oh[PHASE_TO_IDX[phase_enum]] = 1.0
except (ValueError, KeyError):
phase_oh[0] = 1.0 # Default to main_1
phase_oh[0] = 1.0 # default to main_1
features.extend(phase_oh)
# Turn number (1 feature, shared)
# idx 13: land count
lands = p.get("lands", 0)
features.append(min(lands / 15.0, 1.0))
# idx 28: turn number (shared, 1 feature)
turn = decision.get("turn", 1)
features.append(min(turn / GAME_SCOPE.max_turns, 1.0))
@@ -290,8 +322,6 @@ class StateEncoder:
for p in players[:2]:
for zone_key in zone_keys:
cards = p.get(zone_key, [])
# mean_pool_zone returns a zero vector when cards is empty,
# preserving the fixed 768-dim slot size.
pooled = self.card_index.mean_pool_zone(cards)
vectors.append(pooled)
@@ -304,7 +334,6 @@ class StateEncoder:
style = Playstyle(playstyle_str.lower())
vec[PLAYSTYLE_TO_IDX[style]] = 1.0
except (ValueError, KeyError):
# Default: midrange
vec[PLAYSTYLE_TO_IDX[Playstyle.MIDRANGE]] = 1.0
return vec
@@ -317,6 +346,10 @@ class StateEncoder:
"mana": 0,
"cmdr_tax": 0,
"creatures": 0,
"total_power": 0,
"total_toughness": 0,
"artifacts": 0,
"enchantments": 0,
"lands": 0,
"hand": [],
"battlefield": [],
+548
View File
@@ -0,0 +1,548 @@
"""
Commander AI Lab — Forge Evaluator (Issue #153)
═════════════════════════════════════════════════
Reusable eval harness that:
1. Starts the policy inference server pointing at a given checkpoint
2. Launches Forge games via MultiThreadBatchRunner with the policy
server wired in as the AI decision-maker
3. Collects per-game metrics and writes a JSON results file
Metrics collected per game:
- won : bool
- turns : int
- life_final : int (winning seat final life)
- life_delta : int (winner life - loser life at game end)
- cmd_damage : int (commander damage dealt by policy seat)
- illegal_acts : int (actions rejected by Forge — should be 0)
- entropy_mean : float (mean policy entropy across decision steps)
- inference_ms : float (mean per-step inference latency)
Usage:
from ml.eval.forge_evaluator import ForgeEvaluator, EvalConfig
cfg = EvalConfig(
checkpoint_path="ml/models/checkpoints/forge-trained/final.pt",
forge_jar="forge/forge-gui-desktop-2.0.12-SNAPSHOT-jar-with-dependencies.jar",
deck_files=["decks/cedh_test_a.dck", "decks/cedh_test_b.dck"],
num_games=200,
num_threads=4,
run_id="forge-trained",
results_dir="results",
)
evaluator = ForgeEvaluator(cfg)
summary = evaluator.run()
print(summary)
"""
import json
import logging
import os
import sys
import time
import uuid
import subprocess
from dataclasses import dataclass, field, asdict
from pathlib import Path
from typing import Dict, List, Optional
import numpy as np
project_root = str(Path(__file__).parent.parent.parent)
if project_root not in sys.path:
sys.path.insert(0, project_root)
logger = logging.getLogger("ml.eval.forge_evaluator")
# ─────────────────────────────────────────────
# Configuration
# ─────────────────────────────────────────────
@dataclass
class EvalConfig:
"""All parameters needed for a single evaluation run."""
# Model
checkpoint_path: str = "ml/models/checkpoints/best_policy.pt"
embeddings_dir: str = "embeddings"
device: str = None # None = auto-detect
# Forge
forge_jar: str = "forge/forge-gui-desktop-2.0.12-SNAPSHOT-jar-with-dependencies.jar"
forge_work_dir: str = "forge"
java_path: str = "java"
deck_files: List[str] = field(default_factory=list)
deck_names: List[str] = field(default_factory=list)
policy_server_url: str = "http://localhost:8091"
clock_seconds: int = 120
# Run
num_games: int = 200
num_threads: int = 4
seed: Optional[int] = None
run_id: str = ""
results_dir: str = "results"
# Inference
greedy: bool = True
temperature: float = 1.0
def __post_init__(self):
if not self.run_id:
self.run_id = str(uuid.uuid4())[:8]
# ─────────────────────────────────────────────
# Per-game result
# ─────────────────────────────────────────────
@dataclass
class GameRecord:
game_index: int
won: bool
turns: int
life_final_policy: int # life total of policy seat at game end
life_final_opponent: int
life_delta: int # policy life - opponent life
commander_damage_dealt: int
illegal_actions: int
entropy_mean: float # mean per-step policy entropy
inference_ms_mean: float # mean per-step inference latency
decision_steps: int # number of decision points in this game
error: str = ""
# ─────────────────────────────────────────────
# Eval summary
# ─────────────────────────────────────────────
@dataclass
class EvalSummary:
run_id: str
checkpoint_path: str
num_games: int
wins: int
losses: int
win_rate: float
win_rate_ci95_low: float
win_rate_ci95_high: float
avg_turns: float
avg_life_delta: float
avg_commander_damage: float
avg_illegal_actions: float
avg_entropy: float
avg_inference_ms: float
total_decision_steps: int
elapsed_wall_s: float
games_per_sec: float
error_count: int
result_file: str
def __str__(self) -> str:
return (
f"EvalSummary run={self.run_id}\n"
f" checkpoint : {self.checkpoint_path}\n"
f" games : {self.num_games} ({self.wins}W / {self.losses}L)\n"
f" win rate : {self.win_rate:.1%} "
f"[{self.win_rate_ci95_low:.1%}, {self.win_rate_ci95_high:.1%}] 95% CI\n"
f" avg turns : {self.avg_turns:.1f}\n"
f" avg life Δ : {self.avg_life_delta:+.1f}\n"
f" avg cmd dmg: {self.avg_commander_damage:.1f}\n"
f" illegal : {self.avg_illegal_actions:.2f}/game\n"
f" entropy : {self.avg_entropy:.4f}\n"
f" latency : {self.avg_inference_ms:.1f} ms/step\n"
f" throughput : {self.games_per_sec:.2f} games/sec\n"
f" result file: {self.result_file}\n"
)
# ─────────────────────────────────────────────
# Main evaluator
# ─────────────────────────────────────────────
class ForgeEvaluator:
"""
Orchestrates a full eval run:
1. Spin up PolicyInferenceService on cfg.policy_server_url port
2. Run cfg.num_games Forge games via MultiThreadBatchRunner
3. Parse Forge JSONL output for per-game metrics
4. Write results to {results_dir}/eval-{run_id}.json
"""
def __init__(self, cfg: EvalConfig):
self.cfg = cfg
self._records: List[GameRecord] = []
self._result_path = os.path.join(
cfg.results_dir, f"eval-{cfg.run_id}.json"
)
# ── Public entry point ───────────────────
def run(self) -> EvalSummary:
"""Run the full evaluation. Returns an EvalSummary."""
t0 = time.time()
os.makedirs(self.cfg.results_dir, exist_ok=True)
logger.info("[Eval %s] Starting %d-game eval run", self.cfg.run_id, self.cfg.num_games)
logger.info("[Eval %s] Checkpoint: %s", self.cfg.run_id, self.cfg.checkpoint_path)
# Load policy service
service = self._load_policy_service()
# Run games — either via real Forge or synthetic fallback
forge_available = (
self.cfg.forge_jar
and Path(self.cfg.forge_jar).exists()
and len(self.cfg.deck_files) >= 3
)
if forge_available:
self._records = self._run_forge_games(service)
else:
logger.warning(
"[Eval %s] Forge JAR or decks not found — running synthetic eval.",
self.cfg.run_id
)
self._records = self._run_synthetic_eval(service)
elapsed = time.time() - t0
summary = self._build_summary(elapsed)
self._write_results(summary)
logger.info("[Eval %s] Done. %s", self.cfg.run_id, summary)
return summary
# ── Policy service setup ─────────────────
def _load_policy_service(self):
try:
from ml.serving.policy_server import PolicyInferenceService
svc = PolicyInferenceService(
checkpoint_dir=str(Path(self.cfg.checkpoint_path).parent),
embeddings_dir=self.cfg.embeddings_dir,
device=self.cfg.device,
)
svc.load()
ok = svc.reload(self.cfg.checkpoint_path)
if not ok:
logger.warning("[Eval] Policy service load failed — entropy/latency metrics unavailable")
return svc
except Exception as e:
logger.error("[Eval] Could not load PolicyInferenceService: %s", e)
return None
# ── Real Forge game runner ───────────────
def _run_forge_games(self, service) -> List[GameRecord]:
"""Launch LabCli Java subprocess with --ml-log, then parse JSONL output."""
logger.info("[Eval %s] Launching Forge CLI subprocess for %d games", self.cfg.run_id, self.cfg.num_games)
deck_args = []
for i, df in enumerate(self.cfg.deck_files[:4]):
deck_args.extend([f"--deck{i+1}", Path(df).stem])
# Resolve Java 17
java_exe = self.cfg.java_path
for c in [r"C:\Program Files\Eclipse Adoptium\jdk-17.0.18.8-hotspot\bin\java.exe",
r"C:\Program Files\Java\jdk-17\bin\java.exe"]:
if Path(c).exists():
java_exe = c
break
lab_jar = os.path.join(project_root, "target", "commander-ai-lab-1.0.0-SNAPSHOT.jar")
if not Path(lab_jar).exists():
logger.error("[Eval] Lab JAR not found at %s -- run mvn package -DskipTests first", lab_jar)
return self._run_synthetic_eval(service)
cmd = [
java_exe, "-jar", lab_jar,
"--forge-jar", self.cfg.forge_jar,
"--forge-dir", self.cfg.forge_work_dir,
*deck_args,
"--games", str(self.cfg.num_games),
"--threads", str(self.cfg.num_threads),
"--clock", str(self.cfg.clock_seconds),
"--ml-log",
"--output", os.path.join(self.cfg.results_dir, f"eval-{self.cfg.run_id}-batch.json"),
]
if self.cfg.seed is not None:
cmd.extend(["--seed", str(self.cfg.seed)])
logger.info("[Eval %s] CMD: %s", self.cfg.run_id, " ".join(cmd))
try:
result = subprocess.run(cmd, capture_output=True, text=True,
timeout=self.cfg.clock_seconds * self.cfg.num_games + 300)
if result.returncode != 0:
logger.warning("[Eval %s] Forge CLI exit %d: %s", self.cfg.run_id, result.returncode, result.stderr[:500])
except (subprocess.TimeoutExpired, FileNotFoundError) as e:
logger.error("[Eval %s] Forge CLI failed: %s -- falling back to synthetic", self.cfg.run_id, e)
return self._run_synthetic_eval(service)
return self._parse_batch_json(service)
def _parse_batch_json(self, service) -> List[GameRecord]:
"""Parse the batch JSON output from LabCli Java subprocess."""
batch_path = os.path.join(
self.cfg.results_dir, f"eval-{self.cfg.run_id}-batch.json"
)
if not Path(batch_path).exists():
logger.warning("[Eval] Batch JSON not found at %s — falling back to synthetic", batch_path)
return self._run_synthetic_eval(service)
with open(batch_path) as f:
batch = json.load(f)
games_list = batch.get("games", [])
if not games_list:
logger.warning("[Eval] No games in batch JSON — falling back to synthetic")
return self._run_synthetic_eval(service)
records = []
for idx, g in enumerate(games_list):
won = g.get("winningSeat", -1) == 0
turns = g.get("totalTurns", 0)
pr = g.get("playerResults", [])
lp = pr[0].get("finalLife", 40) if len(pr) > 0 else 40
lo = pr[1].get("finalLife", 40) if len(pr) > 1 else 40
records.append(GameRecord(
game_index=idx,
won=won,
turns=turns,
life_final_policy=lp,
life_final_opponent=lo,
life_delta=lp - lo,
commander_damage_dealt=0,
illegal_actions=0,
entropy_mean=0.0,
inference_ms_mean=0.0,
decision_steps=turns,
))
if len(records) >= self.cfg.num_games:
break
logger.info("[Eval %s] Parsed %d games from batch JSON", self.cfg.run_id, len(records))
return records
def _run_forge_via_subprocess(self, service) -> List[GameRecord]:
"""Parse JSONL files produced by a separately-launched Forge batch."""
records = []
results_path = Path(self.cfg.results_dir)
pattern = f"ml-decisions-forge-eval-{self.cfg.run_id}*.jsonl"
all_files = sorted(results_path.glob(pattern))
if not all_files:
logger.warning("[Eval] No JSONL files found matching %s — falling back to synthetic", pattern)
return self._run_synthetic_eval(service)
from ml.training.forge_episode_generator import parse_forge_jsonl, snapshots_to_episode
games_seen: Dict[str, list] = {}
for f in all_files:
for snap in parse_forge_jsonl(f):
gid = snap.get("game_id", "g0")
games_seen.setdefault(gid, []).append(snap)
for idx, (gid, snaps) in enumerate(games_seen.items()):
records.append(self._snaps_to_record(idx, gid, snaps, service))
if len(records) >= self.cfg.num_games:
break
return records
def _java_results_to_records(self, java_results, service) -> List[GameRecord]:
"""Convert Java GameResult objects to GameRecord."""
records = []
for jr in java_results:
try:
won = getattr(jr, "winningSeat", 0) == 0 # seat 0 = policy
records.append(GameRecord(
game_index=getattr(jr, "gameIndex", len(records)),
won=won,
turns=getattr(jr, "turns", 0),
life_final_policy=getattr(jr, "lifeAtEnd", [40])[0] if hasattr(jr, "lifeAtEnd") else 40,
life_final_opponent=getattr(jr, "lifeAtEnd", [40, 40])[1] if hasattr(jr, "lifeAtEnd") else 40,
life_delta=0,
commander_damage_dealt=0,
illegal_actions=getattr(jr, "illegalActions", 0),
entropy_mean=0.0,
inference_ms_mean=0.0,
decision_steps=0,
))
except Exception as e:
logger.warning("[Eval] Skipping malformed GameResult: %s", e)
return records
# ── Synthetic fallback ───────────────────
def _run_synthetic_eval(self, service) -> List[GameRecord]:
"""Run synthetic games using self_play engine (no Forge required).
Used when Forge JAR is missing or deck files aren't configured.
Metrics like illegal_actions and commander_damage are 0 in this mode.
"""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
records = []
try:
from ml.training.self_play import (
create_random_initial_state, apply_action, advance_turn,
encode_state_simple, HeuristicPolicy
)
from ml.config.scope import NUM_ACTIONS, IDX_TO_ACTION
import torch
import torch.nn.functional as F
except ImportError as e:
logger.error("[Eval] Cannot import self_play: %s", e)
return []
opponent = HeuristicPolicy()
agent_has_model = service is not None and service._loaded
for game_idx in range(self.cfg.num_games):
state = create_random_initial_state()
entropies, latencies = [], []
steps = 0
while not state.game_over and steps < 60:
sv = encode_state_simple(state, 0, "midrange")
t0 = time.time()
if agent_has_model:
snap = state.to_snapshot()
result = service.predict(
snap, playstyle="midrange",
greedy=self.cfg.greedy,
temperature=self.cfg.temperature,
)
action_idx = result.get("action_index", 7)
probs_map = result.get("probabilities", {})
probs = np.array(list(probs_map.values())) if probs_map else np.ones(NUM_ACTIONS) / NUM_ACTIONS
else:
action_idx = np.random.randint(NUM_ACTIONS)
probs = np.ones(NUM_ACTIONS) / NUM_ACTIONS
latencies.append((time.time() - t0) * 1000)
p = np.clip(probs, 1e-9, 1.0)
p /= p.sum()
entropies.append(float(-np.sum(p * np.log(p))))
state = apply_action(state, action_idx, 0)
if not state.game_over:
opp_sv = encode_state_simple(state, 1, "midrange")
opp_a, _ = opponent.select_action(opp_sv)
state = apply_action(state, opp_a, 1)
if not state.game_over:
state = advance_turn(state)
steps += 1
won = state.winner == 0
lp = state.players[0].life_total
lo = state.players[1].life_total
records.append(GameRecord(
game_index=game_idx,
won=won,
turns=state.turn,
life_final_policy=lp,
life_final_opponent=lo,
life_delta=lp - lo,
commander_damage_dealt=0,
illegal_actions=0,
entropy_mean=float(np.mean(entropies)) if entropies else 0.0,
inference_ms_mean=float(np.mean(latencies)) if latencies else 0.0,
decision_steps=steps,
))
return records
# ── JSONL snapshot → GameRecord ──────────
def _snaps_to_record(self, idx: int, gid: str, snaps: list, service) -> GameRecord:
entropies, latencies = [], []
won = False
turns = len(snaps)
cmd_dmg = 0
illegal = 0
lp, lo = 40, 40
for snap in snaps:
if "outcome" in snap and snap == snaps[-1]:
won = snap["outcome"].get("winner_seat", -1) == 0
lp = snap["outcome"].get("life_remaining", 40)
lo = snap["outcome"].get("opp_life_remaining", 0)
cmd_dmg = snap["outcome"].get("commander_damage_dealt", 0)
if service and service._loaded and "state_vector" in snap:
t0 = time.time()
result = service.predict(snap, playstyle="midrange", greedy=self.cfg.greedy)
latencies.append((time.time() - t0) * 1000)
probs_map = result.get("probabilities", {})
if probs_map:
p = np.clip(list(probs_map.values()), 1e-9, 1.0)
p /= p.sum()
entropies.append(float(-np.sum(p * np.log(p))))
return GameRecord(
game_index=idx,
won=won,
turns=turns,
life_final_policy=lp,
life_final_opponent=lo,
life_delta=lp - lo,
commander_damage_dealt=cmd_dmg,
illegal_actions=illegal,
entropy_mean=float(np.mean(entropies)) if entropies else 0.0,
inference_ms_mean=float(np.mean(latencies)) if latencies else 0.0,
decision_steps=len(snaps),
)
# ── Summary builder ──────────────────────
def _build_summary(self, elapsed: float) -> EvalSummary:
n = len(self._records)
if n == 0:
return EvalSummary(
run_id=self.cfg.run_id,
checkpoint_path=self.cfg.checkpoint_path,
num_games=0, wins=0, losses=0,
win_rate=0.0, win_rate_ci95_low=0.0, win_rate_ci95_high=0.0,
avg_turns=0.0, avg_life_delta=0.0, avg_commander_damage=0.0,
avg_illegal_actions=0.0, avg_entropy=0.0, avg_inference_ms=0.0,
total_decision_steps=0, elapsed_wall_s=elapsed, games_per_sec=0.0,
error_count=0, result_file=self._result_path,
)
wins = sum(1 for r in self._records if r.won)
losses = n - wins
wr = wins / n
# Wilson score 95% CI
z = 1.96
denom = 1 + z**2 / n
centre = (wr + z**2 / (2 * n)) / denom
margin = (z * (wr * (1 - wr) / n + z**2 / (4 * n**2)) ** 0.5) / denom
ci_lo = max(0.0, centre - margin)
ci_hi = min(1.0, centre + margin)
errors = sum(1 for r in self._records if r.error)
return EvalSummary(
run_id=self.cfg.run_id,
checkpoint_path=self.cfg.checkpoint_path,
num_games=n,
wins=wins,
losses=losses,
win_rate=wr,
win_rate_ci95_low=ci_lo,
win_rate_ci95_high=ci_hi,
avg_turns=float(np.mean([r.turns for r in self._records])),
avg_life_delta=float(np.mean([r.life_delta for r in self._records])),
avg_commander_damage=float(np.mean([r.commander_damage_dealt for r in self._records])),
avg_illegal_actions=float(np.mean([r.illegal_actions for r in self._records])),
avg_entropy=float(np.mean([r.entropy_mean for r in self._records])),
avg_inference_ms=float(np.mean([r.inference_ms_mean for r in self._records])),
total_decision_steps=sum(r.decision_steps for r in self._records),
elapsed_wall_s=elapsed,
games_per_sec=n / max(elapsed, 0.001),
error_count=errors,
result_file=self._result_path,
)
# ── Result writer ────────────────────────
def _write_results(self, summary: EvalSummary) -> None:
payload = {
"summary": asdict(summary),
"games": [asdict(r) for r in self._records],
}
os.makedirs(self.cfg.results_dir, exist_ok=True)
with open(self._result_path, "w") as f:
json.dump(payload, f, indent=2)
logger.info("[Eval %s] Results written to %s", self.cfg.run_id, self._result_path)
+2 -1
View File
@@ -223,7 +223,8 @@ def load_checkpoint(path: str, device: str = "cpu"):
num_layers=config.get("num_layers", TRAINING_CONFIG.num_layers),
dropout=config.get("dropout", TRAINING_CONFIG.dropout),
)
model.load_state_dict(checkpoint["model_state_dict"])
state = {k: v for k, v in checkpoint["model_state_dict"].items() if not k.startswith("value_head")}
model.load_state_dict(state, strict=False)
model.to(device)
return model, checkpoint
+285 -78
View File
@@ -9,22 +9,27 @@ Proximal Policy Optimization (Clip) with:
- Self-play episode collection between updates
- Periodic checkpointing and evaluation
- Win-rate tracking against opponent policies
- Forge-based episode source via --data-source forge|mixed
Reference: Schulman et al., "Proximal Policy Optimization Algorithms", 2017
Usage:
python -m ml.training.ppo_trainer --iterations 100 --episodes-per-iter 64
python -m ml.training.ppo_trainer --load-supervised ml/models/checkpoints/best_policy.pt
python -m ml.training.ppo_trainer --data-source forge --forge-results-dir results --iterations 50
python -m ml.training.ppo_trainer --data-source mixed --forge-weight 0.7 --iterations 100
"""
import argparse
import asyncio
import json
import logging
import os
import sys
import threading
import time
from pathlib import Path
from typing import Dict, Optional
from typing import Dict, List, Optional
import numpy as np
@@ -51,6 +56,114 @@ from ml.training.self_play import (
logger = logging.getLogger("ml.ppo")
# ══════════════════════════════════════════════════════════
# Forge data-source integration
# ══════════════════════════════════════════════════════════
def episodes_to_buffer(episodes, buffer: RolloutBuffer) -> int:
"""Drain a list of ForgeEpisodes into an existing RolloutBuffer.
Returns the number of transitions added.
"""
added = 0
for ep in episodes:
for t in ep.transitions:
if buffer.size >= buffer.buffer_size:
break
buffer.add(
state=t.state,
action=t.action,
reward=t.reward,
log_prob=t.log_prob,
value=t.value,
done=t.done,
)
added += 1
return added
class ForgeDataSource:
"""Runs the Forge producer pipeline in a background thread and
exposes a synchronous drain() method for the PPO training loop.
Usage::
source = ForgeDataSource(results_dir="results", batch_id="run1", num_games=64)
source.start()
# ... in training loop ...
episodes = source.drain()
source.stop()
"""
def __init__(
self,
results_dir: str = "results",
batch_id: str = "forge-batch",
num_games: int = 256,
reward_config: Optional[RewardConfig] = None,
queue_size: int = 256,
):
self.results_dir = results_dir
self.batch_id = batch_id
self.num_games = num_games
self.reward_config = reward_config or RewardConfig()
self.queue_size = queue_size
self._episodes: List = []
self._lock = threading.Lock()
self._thread: Optional[threading.Thread] = None
self._stop_event = threading.Event()
def start(self):
"""Launch the background producer thread."""
self._thread = threading.Thread(target=self._run, daemon=True)
self._thread.start()
logger.info("[ForgeDataSource] Producer started (results_dir=%s, batch_id=%s, num_games=%d)",
self.results_dir, self.batch_id, self.num_games)
def _run(self):
from ml.training.forge_episode_generator import ForgeEpisodeQueue, forge_producer
async def _produce():
queue = ForgeEpisodeQueue(max_size=self.queue_size)
produced = await forge_producer(
queue=queue,
results_dir=self.results_dir,
batch_id=self.batch_id,
num_games=self.num_games,
reward_config=self.reward_config,
)
queue.close()
# Drain remaining items
while not queue.is_closed:
ep = await queue.get(timeout=1.0)
if ep is not None:
with self._lock:
self._episodes.append(ep)
logger.info("[ForgeDataSource] Producer finished: %d episodes", produced)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(_produce())
finally:
loop.close()
def drain(self) -> List:
"""Return and clear all buffered episodes (thread-safe)."""
with self._lock:
eps = list(self._episodes)
self._episodes.clear()
return eps
def stop(self):
self._stop_event.set()
if self._thread:
self._thread.join(timeout=5.0)
# ══════════════════════════════════════════════════════════
# PPO Config
# ══════════════════════════════════════════════════════════
class PPOConfig:
"""PPO hyperparameters."""
@@ -91,6 +204,13 @@ class PPOConfig:
# Supervised preload
load_supervised: Optional[str] = None,
# Data source
data_source: str = "synthetic", # synthetic | forge | mixed
forge_results_dir: str = "results",
forge_batch_id: str = "forge-batch",
forge_num_games: int = 256,
forge_weight: float = 0.7, # fraction of buffer from Forge when mixed
):
self.iterations = iterations
self.episodes_per_iter = episodes_per_iter
@@ -114,10 +234,19 @@ class PPOConfig:
self.eval_every = eval_every
self.eval_episodes = eval_episodes
self.load_supervised = load_supervised
self.data_source = data_source
self.forge_results_dir = forge_results_dir
self.forge_batch_id = forge_batch_id
self.forge_num_games = forge_num_games
self.forge_weight = max(0.0, min(1.0, forge_weight))
# ══════════════════════════════════════════════════════════
# PPO Trainer
# ══════════════════════════════════════════════════════════
class PPOTrainer:
"""Proximal Policy Optimization trainer with self-play."""
"""Proximal Policy Optimization trainer with self-play and optional Forge data source."""
def __init__(self, config: PPOConfig):
if not TORCH_AVAILABLE:
@@ -126,28 +255,24 @@ class PPOTrainer:
self.config = config
self.device = self._detect_device()
# Create actor-critic model
from ml.training.policy_network import PolicyValueNetwork
self.model = PolicyValueNetwork(
input_dim=STATE_DIMS.total_state_dim,
hidden_dim=TRAINING_CONFIG.hidden_dim,
num_actions=NUM_ACTIONS,
num_layers=TRAINING_CONFIG.num_layers,
dropout=0.0, # No dropout during RL
dropout=0.0,
).to(self.device)
# Optionally load from supervised checkpoint
if config.load_supervised and os.path.exists(config.load_supervised):
self._load_supervised_weights(config.load_supervised)
# Optimizer
self.optimizer = torch.optim.Adam(
self.model.parameters(),
lr=config.learning_rate,
eps=1e-5,
)
# Learning rate scheduler
self.scheduler = None
if config.lr_schedule == "linear":
self.scheduler = torch.optim.lr_scheduler.LinearLR(
@@ -163,7 +288,6 @@ class PPOTrainer:
eta_min=config.min_lr,
)
# Rollout buffer
self.buffer = RolloutBuffer(
buffer_size=config.buffer_size,
state_dim=STATE_DIMS.total_state_dim,
@@ -171,16 +295,23 @@ class PPOTrainer:
gae_lambda=config.gae_lambda,
)
# Reward config
self.reward_config = RewardConfig(
gamma=config.gamma,
gae_lambda=config.gae_lambda,
)
# Opponent policy
self.opponent = self._create_opponent(config.opponent)
# Tracking
# Forge data source (started lazily in train())
self._forge_source: Optional[ForgeDataSource] = None
if config.data_source in ("forge", "mixed"):
self._forge_source = ForgeDataSource(
results_dir=config.forge_results_dir,
batch_id=config.forge_batch_id,
num_games=config.forge_num_games,
reward_config=self.reward_config,
)
self.iteration = 0
self.total_steps = 0
self.best_win_rate = 0.0
@@ -196,13 +327,9 @@ class PPOTrainer:
return "cpu"
def _load_supervised_weights(self, path: str):
"""Load weights from a supervised PolicyNetwork checkpoint into the
PolicyValueNetwork (ignoring the value head, which is new)."""
logger.info("Loading supervised weights from %s", path)
checkpoint = torch.load(path, map_location=self.device, weights_only=False)
state_dict = checkpoint.get("model_state_dict", checkpoint)
# Filter out keys that don't match (e.g., value_head)
model_dict = self.model.state_dict()
compatible = {k: v for k, v in state_dict.items() if k in model_dict and v.shape == model_dict[k].shape}
model_dict.update(compatible)
@@ -216,14 +343,10 @@ class PPOTrainer:
elif opponent_type == "heuristic":
return HeuristicPolicy()
elif opponent_type == "self":
# Self-play: opponent uses a snapshot of the current policy
return self._create_self_play_opponent()
else:
return HeuristicPolicy()
def _create_self_play_opponent(self):
"""Create an opponent that uses a frozen copy of the current policy."""
class SelfPlayPolicy:
def __init__(self, model, device):
self.model = model
@@ -241,11 +364,99 @@ class PPOTrainer:
return SelfPlayPolicy(self.model, self.device)
def ppo_update(self) -> Dict:
"""Run PPO update epochs on the collected rollout buffer.
def _collect_forge_rollouts(self) -> Dict:
"""Drain buffered Forge episodes into the RolloutBuffer.
Returns dict of loss metrics.
Returns stats dict compatible with collect_rollouts() output.
"""
episodes = self._forge_source.drain()
if not episodes:
logger.warning("[Forge] No episodes available yet — waiting for JSONL output in %s",
self.config.forge_results_dir)
return {"steps": 0, "win_rate": 0.0, "avg_reward_per_ep": 0.0,
"wins": 0, "losses": 0, "draws": 0, "source": "forge"}
added = episodes_to_buffer(episodes, self.buffer)
wins = sum(1 for ep in episodes if ep.winner == 0)
win_rate = wins / max(len(episodes), 1)
avg_reward = sum(ep.total_reward for ep in episodes) / max(len(episodes), 1)
logger.info("[Forge] Drained %d episodes (%d transitions) into buffer — WR=%.0f%% AvgR=%.3f",
len(episodes), added, win_rate * 100, avg_reward)
return {
"steps": added,
"win_rate": win_rate,
"avg_reward_per_ep": avg_reward,
"wins": wins,
"losses": len(episodes) - wins,
"draws": 0,
"source": "forge",
}
def _collect_rollouts_for_iter(self) -> Dict:
"""Collect rollouts according to configured data_source.
- synthetic: pure self_play.collect_rollouts (original behaviour)
- forge: pure Forge episode drain
- mixed: Forge fills forge_weight fraction of buffer, synthetic fills remainder
"""
cfg = self.config
self.buffer.reset()
self.model.eval()
if cfg.data_source == "forge":
stats = self._collect_forge_rollouts()
elif cfg.data_source == "mixed":
forge_cap = int(cfg.buffer_size * cfg.forge_weight)
synth_cap = cfg.buffer_size - forge_cap
# Temporarily shrink buffer for Forge slice
original_size = self.buffer.buffer_size
self.buffer.buffer_size = forge_cap
forge_stats = self._collect_forge_rollouts()
self.buffer.buffer_size = original_size
synth_eps = max(1, int(cfg.episodes_per_iter * (1.0 - cfg.forge_weight)))
synth_stats = collect_rollouts(
agent_model=self.model,
opponent_policy=self.opponent,
buffer=self.buffer,
num_episodes=synth_eps,
playstyle=cfg.playstyle,
reward_config=self.reward_config,
)
stats = {
"steps": forge_stats["steps"] + synth_stats["steps"],
"win_rate": (
forge_stats["win_rate"] * cfg.forge_weight
+ synth_stats["win_rate"] * (1.0 - cfg.forge_weight)
),
"avg_reward_per_ep": (
forge_stats["avg_reward_per_ep"] * cfg.forge_weight
+ synth_stats["avg_reward_per_ep"] * (1.0 - cfg.forge_weight)
),
"wins": forge_stats["wins"] + synth_stats["wins"],
"losses": forge_stats["losses"] + synth_stats["losses"],
"draws": synth_stats.get("draws", 0),
"source": "mixed",
}
else: # synthetic (default — unchanged behaviour)
stats = collect_rollouts(
agent_model=self.model,
opponent_policy=self.opponent,
buffer=self.buffer,
num_episodes=cfg.episodes_per_iter,
playstyle=cfg.playstyle,
reward_config=self.reward_config,
)
stats["source"] = "synthetic"
self.model.train()
return stats
def ppo_update(self) -> Dict:
"""Run PPO update epochs on the collected rollout buffer."""
cfg = self.config
total_policy_loss = 0.0
total_value_loss = 0.0
@@ -258,56 +469,42 @@ class PPOTrainer:
for batch in self.buffer.get_batches(cfg.batch_size):
states, actions, old_log_probs, advantages, returns = batch
# To tensors
states_t = torch.from_numpy(states).to(self.device)
actions_t = torch.from_numpy(actions).to(self.device)
old_log_probs_t = torch.from_numpy(old_log_probs).to(self.device)
advantages_t = torch.from_numpy(advantages).to(self.device)
returns_t = torch.from_numpy(returns).to(self.device)
# Forward pass
logits, values = self.model.forward_with_value(states_t)
values = values.squeeze(-1)
# Action distribution
probs = F.softmax(logits, dim=-1)
dist = torch.distributions.Categorical(probs)
new_log_probs = dist.log_prob(actions_t)
entropy = dist.entropy().mean()
# --- Policy Loss (clipped surrogate) ---
ratio = torch.exp(new_log_probs - old_log_probs_t)
surr1 = ratio * advantages_t
surr2 = torch.clamp(ratio, 1.0 - cfg.clip_epsilon, 1.0 + cfg.clip_epsilon) * advantages_t
policy_loss = -torch.min(surr1, surr2).mean()
# --- Value Loss (clipped) ---
if cfg.value_clip > 0:
# Get old values from buffer for clipping
old_values_t = torch.from_numpy(
self.buffer.values[:self.buffer.size]
).to(self.device)
# Reindex to match batch
# Since we use shuffled indices in get_batches, we clip against returns
value_loss_unclipped = (values - returns_t) ** 2
value_loss = 0.5 * value_loss_unclipped.mean()
else:
value_loss = 0.5 * ((values - returns_t) ** 2).mean()
# --- Total Loss ---
loss = (
policy_loss
+ cfg.value_loss_coeff * value_loss
- cfg.entropy_coeff * entropy
)
# Optimize
self.optimizer.zero_grad()
loss.backward()
nn.utils.clip_grad_norm_(self.model.parameters(), cfg.max_grad_norm)
self.optimizer.step()
# Track metrics
with torch.no_grad():
approx_kl = ((ratio - 1) - torch.log(ratio)).mean().item()
clip_frac = ((ratio - 1.0).abs() > cfg.clip_epsilon).float().mean().item()
@@ -330,12 +527,8 @@ class PPOTrainer:
}
def evaluate(self, num_episodes: int = 50) -> Dict:
"""Evaluate the current policy against opponents.
Returns win rates and metrics.
"""
"""Evaluate the current policy against heuristic and random opponents."""
self.model.eval()
results = {}
for opp_name, opp_policy in [
("heuristic", HeuristicPolicy()),
@@ -363,18 +556,15 @@ class PPOTrainer:
"avg_reward": stats["avg_reward_per_ep"],
"avg_steps": stats["avg_steps_per_ep"],
}
self.model.train()
return results
def save_checkpoint(self, path: str = None, metrics: dict = None):
"""Save PPO checkpoint."""
if path is None:
path = os.path.join(
self.config.checkpoint_dir,
f"ppo_iter_{self.iteration:04d}.pt",
)
os.makedirs(os.path.dirname(path), exist_ok=True)
torch.save({
"iteration": self.iteration,
@@ -388,6 +578,7 @@ class PPOTrainer:
"learning_rate": self.config.learning_rate,
"opponent": self.config.opponent,
"playstyle": self.config.playstyle,
"data_source": self.config.data_source,
},
"best_win_rate": self.best_win_rate,
"model_config": {
@@ -401,14 +592,7 @@ class PPOTrainer:
logger.info("Checkpoint saved: %s", path)
def train(self, progress_callback=None) -> Dict:
"""Run the full PPO training loop.
Args:
progress_callback: Optional callable(iteration, metrics) for progress reporting
Returns:
Training summary dict
"""
"""Run the full PPO training loop."""
cfg = self.config
logger.info("=" * 60)
logger.info("PPO Training — Commander AI Lab")
@@ -423,52 +607,45 @@ class PPOTrainer:
logger.info(" Opponent: %s", cfg.opponent)
logger.info(" Device: %s", self.device)
logger.info(" Supervised: %s", cfg.load_supervised or "None")
logger.info(" Data source: %s", cfg.data_source)
if cfg.data_source in ("forge", "mixed"):
logger.info(" Forge results: %s / %s", cfg.forge_results_dir, cfg.forge_batch_id)
logger.info(" Forge weight: %.0f%%", cfg.forge_weight * 100)
logger.info("")
# Start Forge producer background thread if needed
if self._forge_source:
self._forge_source.start()
t_start = time.time()
try:
for iteration in range(1, cfg.iterations + 1):
self.iteration = iteration
t_iter = time.time()
# --- Collect rollouts ---
self.model.eval()
rollout_stats = collect_rollouts(
agent_model=self.model,
opponent_policy=self.opponent,
buffer=self.buffer,
num_episodes=cfg.episodes_per_iter,
playstyle=cfg.playstyle,
reward_config=self.reward_config,
)
rollout_stats = self._collect_rollouts_for_iter()
self.total_steps += rollout_stats["steps"]
self.model.train()
# --- PPO Update ---
update_metrics = self.ppo_update()
# --- LR Schedule ---
current_lr = self.optimizer.param_groups[0]["lr"]
if self.scheduler:
self.scheduler.step()
# --- Evaluate ---
eval_results = None
if iteration % cfg.eval_every == 0 or iteration == 1:
eval_results = self.evaluate(cfg.eval_episodes)
heuristic_wr = eval_results.get("heuristic", {}).get("win_rate", 0)
if heuristic_wr > self.best_win_rate:
self.best_win_rate = heuristic_wr
best_path = os.path.join(cfg.checkpoint_dir, "best_ppo.pt")
self.save_checkpoint(best_path, {"eval": eval_results})
logger.info(" ★ New best win rate: %.1f%%", heuristic_wr * 100)
# --- Save periodic checkpoint ---
if iteration % cfg.save_every == 0:
self.save_checkpoint()
# --- Log ---
iter_time = time.time() - t_iter
metrics = {
"iteration": iteration,
@@ -483,20 +660,21 @@ class PPOTrainer:
"clip_fraction": update_metrics["clip_fraction"],
"lr": current_lr,
"iter_time_s": round(iter_time, 1),
"data_source": rollout_stats.get("source", cfg.data_source),
"eval": eval_results,
}
self.history.append(metrics)
# Pretty log
eval_str = ""
if eval_results:
h_wr = eval_results.get("heuristic", {}).get("win_rate", 0)
r_wr = eval_results.get("random", {}).get("win_rate", 0)
eval_str = f" | eval: h={h_wr:.0%} r={r_wr:.0%}"
src_tag = f"[{rollout_stats.get('source', '?')[:3]}]"
logger.info(
" [%3d/%d] wr=%.0f%% rew=%.3f ploss=%.4f vloss=%.4f ent=%.3f kl=%.4f%s (%.1fs)",
iteration, cfg.iterations,
" [%3d/%d]%s wr=%.0f%% rew=%.3f ploss=%.4f vloss=%.4f ent=%.3f kl=%.4f%s (%.1fs)",
iteration, cfg.iterations, src_tag,
rollout_stats["win_rate"] * 100,
rollout_stats["avg_reward_per_ep"],
update_metrics["policy_loss"],
@@ -510,11 +688,13 @@ class PPOTrainer:
if progress_callback:
progress_callback(iteration, metrics)
# --- Final Save ---
finally:
if self._forge_source:
self._forge_source.stop()
total_time = time.time() - t_start
self.save_checkpoint()
# Save training history
history_path = os.path.join(cfg.checkpoint_dir, "ppo_history.json")
with open(history_path, "w") as f:
json.dump(self.history, f, indent=2, default=str)
@@ -526,6 +706,7 @@ class PPOTrainer:
"final_win_rate": self.history[-1]["win_rate"] if self.history else 0,
"total_time_s": round(total_time, 1),
"device": self.device,
"data_source": cfg.data_source,
"checkpoint_dir": cfg.checkpoint_dir,
"history_path": history_path,
}
@@ -536,6 +717,7 @@ class PPOTrainer:
logger.info(" Total time: %.1f s", total_time)
logger.info(" Total steps: %d", self.total_steps)
logger.info(" Best win rate: %.1f%%", self.best_win_rate * 100)
logger.info(" Data source: %s", cfg.data_source)
logger.info(" History: %s", history_path)
logger.info("=" * 60)
@@ -578,12 +760,32 @@ def main():
parser.add_argument("--load-supervised", default=None,
help="Path to supervised checkpoint to initialize from")
# Data source
parser.add_argument(
"--data-source", default="synthetic",
choices=["synthetic", "forge", "mixed"],
help="Episode source: synthetic (self-play), forge (Forge JSONL), mixed (blend)",
)
parser.add_argument(
"--forge-results-dir", default="results",
help="Directory where Forge writes ml-decisions-forge-*.jsonl files",
)
parser.add_argument(
"--forge-batch-id", default="forge-batch",
help="Batch ID prefix for Forge JSONL files (matches BatchRunner --batchId)",
)
parser.add_argument(
"--forge-num-games", type=int, default=256,
help="Total Forge games to consume before stopping the producer",
)
parser.add_argument(
"--forge-weight", type=float, default=0.7,
help="Fraction of RolloutBuffer filled from Forge when --data-source mixed (0.0–1.0)",
)
args = parser.parse_args()
logging.basicConfig(
level=logging.INFO,
format="%(message)s",
)
logging.basicConfig(level=logging.INFO, format="%(message)s")
config = PPOConfig(
iterations=args.iterations,
@@ -605,6 +807,11 @@ def main():
eval_every=args.eval_every,
eval_episodes=args.eval_episodes,
load_supervised=args.load_supervised,
data_source=args.data_source,
forge_results_dir=args.forge_results_dir,
forge_batch_id=args.forge_batch_id,
forge_num_games=args.forge_num_games,
forge_weight=args.forge_weight,
)
trainer = PPOTrainer(config)
+2 -1
View File
@@ -9,7 +9,7 @@ class StartRequest(BaseModel):
numGames: int = 100
threads: int = 4
seed: Optional[int] = None
clock: int = 300
clock: int = 5000
deckSources: Optional[list[Optional[dict]]] = None
useLearnedPolicy: bool = False
policyStyle: str = "midrange"
@@ -170,6 +170,7 @@ class CoachGoalsRequest(BaseModel):
meta_focus: Optional[str] = None # aggro, control, combo, midrange, stax
budget: Optional[str] = None # budget, medium, no-limit
focus_areas: list[str] = [] # e.g., ["ramp", "card draw"]
class DeckGenV3SubstituteRequest(BaseModel):
card_name: str
substitute_name: str
+37
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import json
import logging
import os
import subprocess
from datetime import datetime
from pathlib import Path
@@ -25,10 +26,44 @@ class Config:
port: int = 8080
ximilar_api_key: str = ""
pplx_api_key: str = ""
anthropic_api_key: str = ""
CFG = Config()
# Pre-populate from environment so uvicorn/ASGI mode works without CLI args.
# CLI args (lab_api.py main()) will override these if provided.
CFG.anthropic_api_key = os.environ.get("ANTHROPIC_API_KEY", "")
CFG.ximilar_api_key = os.environ.get("XIMILAR_API_KEY", "")
CFG.pplx_api_key = os.environ.get("PPLX_API_KEY", "")
CFG.forge_jar = os.environ.get("FORGE_JAR", "")
CFG.forge_dir = os.environ.get("FORGE_DIR", "")
CFG.lab_jar = os.environ.get("LAB_JAR", "")
# PRECON_DIR / PRECON_DECKS_DIR are aliases -- prefer PRECON_DIR
CFG.precon_dir = os.environ.get("PRECON_DIR") or os.environ.get("PRECON_DECKS_DIR", "")
CFG.forge_decks_dir = os.environ.get("FORGE_DECKS_DIR", "")
def validate_forge_config() -> None:
"""Warn clearly at startup if FORGE_DIR is misconfigured."""
if not CFG.forge_dir:
log.warning("FORGE_DIR not set -- Forge card enrichment disabled. "
"Set FORGE_DIR to your forge-repo root or forge-gui directory.")
return
base = Path(CFG.forge_dir)
a = base / "forge-gui" / "res" / "cardsfolder"
b = base / "res" / "cardsfolder"
if not a.is_dir() and not b.is_dir():
log.warning(
"FORGE_DIR=%s is set but cardsfolder not found at:\n"
" %s\n %s\n"
"Check that FORGE_DIR points to forge-repo root or forge-gui/.",
CFG.forge_dir, a, b
)
else:
resolved = a if a.is_dir() else b
log.info("Forge cardsfolder confirmed at %s", resolved)
# ══════════════════════════════════════════════════════════════
# In-Memory State
@@ -49,7 +84,9 @@ class BatchState:
self.sims_per_sec: float = 0.0
import asyncio as _asyncio
active_batches: dict[str, BatchState] = {}
active_batches_lock = _asyncio.Lock()
COMMANDER_META: dict = {}
+4
View File
@@ -0,0 +1,4 @@
unzip mtg-commander-lan.zip
cd mtg-commander-lan
npm install
node server.js
+6
View File
@@ -0,0 +1,6 @@
{
"name": "MTG Commander (HTML)",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
+104
View File
@@ -0,0 +1,104 @@
[metadata]
Name=20 Ways to Win
[Commander]
1 Go-Shintai of Life's Origin
[Main]
1 Alseid of Life's Bounty
1 Approach of the Second Sun
1 Arcane Signet
1 Augur of Autumn
1 Auriok Champion
1 Austere Command
1 Azorius Guildgate
1 Baldur's Gate
1 Biovisionary
1 Black Dragon Gate
1 Boros Guildgate
1 Bountiful Promenade
1 Brushland
1 Chromatic Lantern
1 Circuitous Route
1 Citadel Gate
1 Clever Concealment
1 Cliffgate
1 Command Tower
1 Crib Swap
1 Darksteel Citadel
1 Darksteel Mutation
1 Dimir Guildgate
1 Drown in Dreams
1 Dryad of the Ilysian Grove
1 Eerie Ultimatum
1 Exotic Orchard
1 Faeburrow Elder
1 Felidar Sovereign
1 Fertile Ground
1 Forbidden Orchard
1 Forest
1 Forgotten Ancient
1 Gateway Plaza
1 Ghostly Prison
1 Ghoulish Impetus
1 Golgari Guildgate
1 Gond Gate
1 Gruul Guildgate
1 Halo Fountain
1 Hangarback Walker
1 Happily Ever After
1 Heap Gate
1 Heliod's Intervention
1 Heliod, Sun-Crowned
1 Helix Pinnacle
1 Hellkite Tyrant
1 Homeward Path
1 Izzet Guildgate
1 Kynaios and Tiro of Meletis
1 Lightning Greaves
1 Lignify
1 Liliana's Contract
1 Mangara, the Diplomat
1 Manor Gate
1 Maskwood Nexus
1 Mayael's Aria
1 Maze's End
1 Mechanized Production
1 Monologue Tax
1 Morbid Opportunist
1 Mystic Remora
1 Orzhov Guildgate
1 Overgrown Farmland
1 Pariah
1 Path of Ancestry
1 Plains
1 Plaza of Harmony
1 Rakdos Guildgate
1 Razorverge Thicket
1 Reflecting Pool
1 Revel in Riches
1 Rite of Replication
1 Sanctum Weaver
1 Sea Gate
1 Seedborn Muse
1 Selesnya Guildgate
1 Shalai, Voice of Plenty
1 Shiny Impetus
1 Simic Ascendancy
1 Simic Guildgate
1 Sol Ring
1 Soul's Attendant
1 Sphinx's Revelation
1 Sun Titan
1 Sunscorch Regent
1 Supreme Verdict
1 Taurean Mauler
1 Test of Endurance
1 The Eternal Wanderer
1 The World Tree
1 Thornglint Bridge
1 Tireless Tracker
1 Trace of Abundance
1 Tragic Arrogance
1 Triskaidekaphile
1 Twenty-Toed Toad
1 Wooded Bastion
1 Yahenni, Undying Partisan
+92
View File
@@ -0,0 +1,92 @@
[metadata]
Name=Abzan Armor
[Commander]
1 Felothar the Steadfast
[Main]
1 Betor, Ancestor's Voice
1 Protector of the Wastes
1 Reunion of the House
1 Jaws of Defeat
1 Tip the Scales
1 Will of the Abzan
1 Arbor Adherent
1 Canopy Gargantuan
1 Rampart Architect
1 Tree of Redemption
1 Ikra Shidiqi, the Usurper
1 Baldin, Century Herdmaster
1 Expel the Interlopers
1 Indomitable Ancients
1 Rhox Faithmender
1 Shalai, Voice of Plenty
1 Wakestone Gargoyle
1 Wall of Reverence
1 Welcoming Vampire
1 Zetalpa, Primal Dawn
1 Arasta of the Endless Web
1 Assault Formation
1 Hornet Nest
1 Seedborn Muse
1 Sylvan Caryatid
1 Towering Titan
1 Anguished Unmaking
1 Dragonlord Dromoka
1 Faeburrow Elder
1 Shadrix Silverquill
1 Sidar Kondo of Jamuraa
1 Colfenor's Urn
1 Staff of Compleation
1 Weathered Sentinels
1 Canopy Vista
1 Exotic Orchard
1 Fortified Village
1 Isolated Chapel
1 Overgrown Farmland
1 Sungrass Prairie
1 Sunpetal Grove
1 Temple of Malady
1 Temple of Plenty
1 Temple of Silence
1 Twilight Mire
1 Woodland Cemetery
1 Arcane Signet
1 Sol Ring
1 Command Tower
1 Nyx-Fleece Ram
1 Slaughter the Strong
1 Swords to Plowshares
1 Wall of Omens
1 Wingmantle Chaplain
1 Behind the Scenes
1 Blight Pile
1 Feed the Swarm
1 Infernal Grasp
1 Wall of Limbs
1 Arboreal Grazer
1 Axebane Guardian
1 Carven Caryatid
1 Evolving Wilds
1 Jaddi Offshoot
1 Overgrown Battlement
1 Sandsteppe Citadel
1 Tower Defense
1 Wall of Blossoms
1 Wall of Roots
1 Despark
1 Indulging Patrician
1 Crashing Drawbridge
1 Orzhov Signet
1 Selesnya Signet
1 Swiftfoot Boots
1 Walking Bulwark
1 Access Tunnel
1 Bojuka Bog
1 Deceptive Landscape
1 Path of Ancestry
1 Radiant Grove
3 Plains
3 Plains
3 Swamp
2 Swamp
4 Forest
3 Forest
+90
View File
@@ -0,0 +1,90 @@
[metadata]
Name=Adaptive Enchantment
[Commander]
1 Estrid, the Masked
[Main]
1 Bruna, Light of Alabaster
1 Eidolon of Blossoms
1 Hydra Omnivore
1 Ajani's Chosen
1 Celestial Archon
1 Silent Sentinel
1 Boon Satyr
1 Herald of the Pantheon
1 Cold-Eyed Selkie
1 Daxos of Meletis
1 Elderwood Scion
1 Archetype of Imagination
1 Whitewater Naiads
1 Aura Gnarlid
1 Reclamation Sage
1 Yavimaya Enchantress
1 Martial Coup
1 Phyrexian Rebirth
1 Winds of Rath
1 Creeping Renaissance
1 Kruphix's Insight
1 Dismantling Blow
1 Bant Charm
1 Sol Ring
1 Sigil of the Empty Throne
1 Dictate of Kruphix
1 Bear Umbra
1 Enchantress's Presence
1 Epic Proportions
1 Ground Seal
1 Spawning Grounds
1 Finest Hour
1 Righteous Authority
1 Sage's Reverie
1 Soul Snare
1 Unquestioned Authority
1 Eel Umbra
1 Vow of Flight
1 Dawn's Reflection
1 Fertile Ground
1 Overgrowth
1 Snake Umbra
1 Vow of Wildness
1 Wild Growth
1 Unflinching Courage
3 Plains
3 Plains
3 Plains
2 Island
2 Island
2 Island
3 Forest
3 Forest
2 Forest
1 Azorius Chancery
1 Blossoming Sands
1 Command Tower
1 Evolving Wilds
1 Krosan Verge
1 Meandering River
1 Mosswort Bridge
1 Seaside Citadel
1 Selesnya Sanctuary
1 Simic Growth Chamber
1 Terramorphic Expanse
1 Thornwood Falls
1 Tranquil Cove
1 Tranquil Expanse
1 Woodland Stream
1 Kestia, the Cultivator
1 Tuvasa the Sunlit
1 Empyrial Storm
1 Heavenly Blademaster
1 Estrid's Invocation
1 Ever-Watching Threshold
1 Octopus Umbra
1 Genesis Storm
1 Myth Unbound
1 Nylea's Colossus
1 Ravenous Slime
1 Arixmethes, Slumbering Isle
1 Loyal Unicorn
1 Loyal Drake
1 Loyal Guardian
1 Forge of Heroes
+95
View File
@@ -0,0 +1,95 @@
[metadata]
Name=Ahoy Mateys
[Commander]
1 Admiral Brass, Unsinkable
[Main]
1 Don Andres, the Renegade
1 The Indomitable
1 Storm Fleet Negotiator
1 Francisco, Fowl Marauder
1 The Grim Captain's Locker
1 Skeleton Crew
1 Broadside Bombardiers
1 Gemcutter Buccaneer
1 Arm-Mounted Anchor
1 Amphin Mutineer
1 Bident of Thassa
1 Corsair Captain
1 Evacuation
1 Timestream Navigator
1 Warkite Marauder
1 Black Market Connections
1 Blood Money
1 Dire Fleet Ravager
1 Fathom Fleet Captain
1 Lethal Scheme
1 Angrath's Marauders
1 Blasphemous Act
1 Captain Lannery Storm
1 Captivating Crew
1 Chaos Warp
1 Coercive Recruiter
1 Dire Fleet Daredevil
1 Kari Zev, Skyship Raider
1 Port Razer
1 Shared Animosity
1 Admiral Beckett Brass
1 Hostage Taker
1 King Narfi's Betrayal
1 Prismari Command
1 Zara, Renegade Recruiter
1 Icon of Ancestry
1 Vanquisher's Banner
1 Choked Estuary
1 Desolate Lighthouse
1 Exotic Orchard
1 Foreboding Ruins
1 Frostboil Snarl
1 Geier Reach Sanitarium
1 Nephalia Drownyard
1 Smoldering Marsh
1 Sulfur Falls
1 Sunken Hollow
1 Azure Fleet Admiral
1 Enterprising Scallywag
1 Daring Saboteur
1 Departed Deckhand
1 Distant Melody
1 Ghost of Ramirez DePietro
1 Malcolm, Keen-Eyed Navigator
1 Merchant Raiders
1 Siren Stormtamer
1 Spectral Sailor
1 Windfall
1 Feed the Swarm
1 Pitiless Plunderer
1 Breeches, Brazen Plunderer
1 Faithless Looting
1 Rakdos Charm
1 Ramirez DePietro, Pillager
1 Arcane Signet
1 Commander's Sphere
1 Dimir Signet
1 Herald's Horn
1 Izzet Signet
1 Rakdos Signet
1 Sol Ring
1 Wayfarer's Bauble
1 Command Tower
1 Crumbling Necropolis
1 Evolving Wilds
1 Path of Ancestry
1 Port of Karfell
1 Rogue's Passage
1 Secluded Courtyard
1 Terramorphic Expanse
1 Thriving Bluff
1 Thriving Isle
1 Thriving Moor
1 Unclaimed Territory
3 Island
3 Island
2 Swamp
2 Swamp
3 Mountain
2 Mountain
@@ -0,0 +1,78 @@
[metadata]
Name=Angels: They're Just Like Us but Cooler and with Wings
[Commander]
1 Gisela, the Broken Blade
[Main]
1 Bruna, the Fading Light
1 Ajani, Strength of the Pride
1 Angel of Destiny
1 Angel of Finality
1 Angel of Serenity
1 Angel of the Ruins
1 Angel of Vitality
1 Angelic Accord
1 Angelic Field Marshal
1 Arcane Signet
1 Arch of Orazca
1 Archangel of Thune
1 Archangel of Tithes
1 Arden Angel
1 Austere Command
1 Bishop of Wings
1 Bonders' Enclave
1 Breathkeeper Seraph
1 Cleansing Nova
1 Commander's Plate
1 Cosmos Elixir
1 Court of Grace
1 Dawn of Hope
1 Dawnbreak Reclaimer
1 Dismantling Wave
1 Emeria Shepherd
1 Emeria, the Sky Ruin
1 Endless Atlas
1 Entreat the Angels
1 Everflowing Chalice
1 Giada, Font of Hope
1 Griffin Aerie
1 Heirloom Blade
1 Invoke the Divine
1 Karoo
1 Keeper of the Accord
1 Kindred Boon
1 Lightning Greaves
1 Marble Diamond
1 Mazemind Tome
1 Mind Stone
1 Myriad Landscape
1 Nykthos Paragon
1 Nykthos, Shrine to Nyx
1 Oketra's Monument
1 Path of Ancestry
1 Path to Exile
1 Pearl Medallion
8 Plains
8 Plains
7 Plains
7 Plains
1 Righteous Valkyrie
1 Search for Glory
1 Sephara, Sky's Blade
1 Seraph Sanctuary
1 Serra Ascendant
1 Shattered Angel
1 Sol Ring
1 Speaker of the Heavens
1 Starnheim Aspirant
1 Sunblast Angel
1 Swiftfoot Boots
1 Sword of the Animist
1 Swords to Plowshares
1 Thalia's Lancers
1 The Book of Exalted Deeds
1 Tome of Legends
1 Urza's Incubator
1 Valkyrie Harbinger
1 Vanquisher's Banner
1 War Room
1 Well of Lost Dreams
+90
View File
@@ -0,0 +1,90 @@
[metadata]
Name=Animated Army
[Commander]
1 Bello, Bard of the Brambles
[Main]
1 Wildsear, Scouring Maw
1 Domri, Anarch of Bolas
1 Etali, Primal Storm
1 Prosperous Bandit
1 Pyreswipe Hawk
1 Alchemist's Talent
1 Berserkers' Onslaught
1 Outpost Siege
1 Rain of Riches
1 Chaos Warp
1 Sunbird's Invocation
1 Gratuitous Violence
1 Warstorm Surge
1 Starstorm
1 Lotus Cobra
1 Evercoat Ursine
1 Brightcap Badger // Fungus Frolic
1 Trailtracker Scout
1 Thickest in the Thicket
1 Ghalta, Primal Hunger
1 Esika's Chariot
1 Unnatural Growth
1 Greater Good
1 Kodama of the East Tree
1 Grothama, All-Devouring
1 Rampaging Baloths
1 Bootleggers' Stash
1 Primeval Bounty
1 Gilded Lotus
1 Spine of Ish Sah
1 Temple of Abandon
1 Karplusan Forest
1 Exotic Orchard
1 Sheltered Thicket
1 Game Trail
1 Raging Ravine
1 Copperline Gorge
1 Mossfire Valley
1 Cinder Glade
1 Rootbound Crag
1 Mosswort Bridge
1 Blasphemous Act
1 Llanowar Loamspeaker
1 Tendershoot Dryad
1 Goreclaw, Terror of Qal Sisma
1 Path of Discovery
1 Decimate
1 Rolling Hamsphere
1 Teapot Slinger
1 Explore
1 Farseek
1 Cultivate
1 Grumgully, the Generous
1 Wandertale Mentor
1 Thought Vessel
1 Arcane Signet
1 Wooded Ridgeline
1 Big Score
1 Abrade
1 Rampant Growth
1 Sakura-Tribe Elder
1 Beast Within
1 Garruk's Packleader
1 Harmonize
1 Garruk's Uprising
1 Gruul Signet
1 Burnished Hart
1 Hedron Archive
1 Fellwar Stone
1 Thran Dynamo
1 Sol Ring
1 Mind Stone
1 Talisman of Impulse
1 Terramorphic Expanse
1 Path of Ancestry
1 Gruul Turf
1 Evolving Wilds
1 Forgotten Cave
1 Tranquil Thicket
1 Command Tower
1 Reliquary Tower
4 Mountain
4 Mountain
5 Forest
5 Forest
+95
View File
@@ -0,0 +1,95 @@
[metadata]
Name=Arcane Maelstrom
[Commander]
1 Kalamax, the Stormsire
[Main]
1 Jace, Architect of Thought
1 Xyris, the Writhing Storm
1 Haldan, Avid Arcanist
1 Pako, Arcane Retriever
1 Eon Frolicker
1 Nascent Metamorph
1 Glademuse
1 Ravenous Gigantotherium
1 Lunar Mystic
1 Niblis of Frost
1 Talrand, Sky Summoner
1 Charmbreaker Devils
1 Dualcaster Mage
1 Etali, Primal Storm
1 Goblin Dark-Dwellers
1 Djinn Illuminatus
1 Melek, Izzet Paragon
1 Rashmi, Eternities Crafter
1 Wort, the Raidmother
1 Solemn Simulacrum
1 Murmuring Mystic
1 Crackling Drake
1 Surreal Memoir
1 Decoy Gambit
1 Deflecting Swat
1 Curious Herd
1 Chaos Warp
1 Comet Storm
1 Commune with Lava
1 Starstorm
1 Strength of the Tajuru
1 Artifact Mutation
1 Prophetic Bolt
1 Clash of Titans
1 Channeled Force
1 Chemister's Insight
1 Frantic Search
1 Whiplash Trap
1 Crop Rotation
1 Evolution Charm
1 Harrow
1 Hunter's Insight
1 Hunting Pack
1 Natural Connection
1 Slice in Twain
1 Tribute to the Wild
1 Growth Spiral
1 Temur Charm
1 Lavabrink Floodgates
1 Twinning Staff
1 Bonder's Ornament
1 Arcane Signet
1 Commander's Sphere
1 Lightning Greaves
1 Sol Ring
1 Swarm Intelligence
1 Primal Empathy
1 Psychic Impetus
1 Shiny Impetus
1 Predatory Impetus
1 Wilderness Reclamation
1 Cinder Glade
1 Desolate Lighthouse
1 Exotic Orchard
1 Kessig Wolf Run
1 Mossfire Valley
1 Mosswort Bridge
1 Oran-Rief, the Vastwood
1 Scavenger Grounds
1 Yavimaya Coast
1 Command Tower
1 Frontier Bivouac
1 Gruul Turf
1 Halimar Depths
1 Izzet Boilerworks
1 Myriad Landscape
1 Rupture Spire
1 Simic Growth Chamber
1 Rugged Highlands
1 Swiftwater Cliffs
1 Thornwood Falls
3 Forest
3 Forest
2 Forest
2 Island
2 Island
1 Island
2 Mountain
2 Mountain
1 Mountain
+93
View File
@@ -0,0 +1,93 @@
[metadata]
Name=Arcane Wizardry
[Commander]
1 Inalla, Archmage Ritualist
[Main]
1 Kess, Dissident Mage
1 Mairsil, the Pretender
1 Galecaster Colossus
1 Magus of the Mind
1 Portal Mage
1 Vindictive Lich
1 Izzet Chemister
1 Taigam, Sidisi's Hand
1 Havengul Lich
1 Marchesa, the Black Rose
1 Vela the Night-Clad
1 Arcanis the Omnipotent
1 Azami, Lady of Scrolls
1 Body Double
1 Harbinger of the Tides
1 Serendib Sorcerer
1 Apprentice Necromancer
1 Magus of the Abyss
1 Puppeteer Clique
1 Etherium-Horn Sorcerer
1 Mercurial Chemister
1 Nin, the Pain Artist
1 Niv-Mizzet, the Firemind
1 Shadowmage Infiltrator
1 Bloodline Necromancer
1 Archaeomancer
1 Merchant of Secrets
1 Sea Gate Oracle
1 Corpse Augur
1 Izzet Chronarch
1 Nivix Guildmage
1 Kindred Dominance
1 Clone Legion
1 Spelltwine
1 Decree of Pain
1 Necromantic Selection
1 Comet Storm
1 Polymorphist's Jest
1 Chaos Warp
1 Memory Plunder
1 Silumgar's Command
1 Into the Roil
1 Opportunity
1 Reality Shift
1 Go for the Throat
1 Cauldron Dance
1 Crosis's Charm
1 Rakdos Charm
1 Terminate
1 Nevinyrral's Disk
1 Mirror of the Forebears
1 Commander's Sphere
1 Darksteel Ingot
1 Fellwar Stone
1 Sol Ring
1 Unstable Obelisk
1 Worn Powerstone
1 Shifting Shadow
1 Curse of Verbosity
1 Curse of Disturbance
1 Curse of Opulence
1 Exotic Orchard
1 Mystifying Maze
4 Island
3 Island
3 Island
2 Swamp
2 Swamp
2 Swamp
2 Mountain
1 Mountain
1 Mountain
1 Path of Ancestry
1 Command Tower
1 Crumbling Necropolis
1 Dimir Aqueduct
1 Dismal Backwater
1 Evolving Wilds
1 Grixis Panorama
1 Izzet Boilerworks
1 Jwar Isle Refuge
1 Rakdos Carnarium
1 Swiftwater Cliffs
1 Temple of the False God
1 Terramorphic Expanse
1 Vivid Crag
1 Vivid Creek
1 Vivid Marsh
+85
View File
@@ -0,0 +1,85 @@
[metadata]
Name=Arm for Battle
[Commander]
1 Wyleth, Soul of Steel
[Main]
1 Oreskos Explorer
1 Relic Seeker
1 Sram, Senior Edificer
1 Brass Squire
1 Danitha Capashen, Paragon
1 Dualcaster Mage
1 Flickerwisp
1 Ironclad Slayer
1 Kor Cartographer
1 Odric, Lunarch Marshal
1 Tiana, Ship's Caretaker
1 Condemn
1 Expedite
1 Swords to Plowshares
1 Abrade
1 Boros Charm
1 Comet Storm
1 Dawn Charm
1 Deflecting Palm
1 Disenchant
1 Fists of Flame
1 Temur Battle Rage
1 Valorous Stance
1 Generous Gift
1 Unbreakable Formation
1 Volcanic Fallout
1 Wear // Tear
1 White Sun's Zenith
1 Master Warcraft
1 Return to Dust
1 Wild Ricochet
1 Word of Seizing
1 Response // Resurgence
1 Jaya's Immolating Inferno
1 Martial Coup
1 Relentless Assault
1 Winds of Rath
1 Bonesplitter
1 Explorer's Scope
1 Sol Ring
1 Blackblade Reforged
1 Blazing Sunsteel
1 Boros Signet
1 Hero's Blade
1 Mask of Avacyn
1 Ring of Thune
1 Ring of Valkas
1 Swiftfoot Boots
1 Fireshrieker
1 Haunted Cloak
1 Loxodon Warhammer
1 Sunforger
1 Sword of Vengeance
1 Sigarda's Aid
1 Spirit Mantle
1 Timely Ward
1 Unquestioned Authority
1 Faith Unbroken
1 On Serra's Wings
1 Boros Garrison
1 Boros Guildgate
1 Command Tower
1 Encroaching Wastes
1 Evolving Wilds
1 Forgotten Cave
1 Memorial to War
5 Mountain
4 Mountain
1 Myriad Landscape
7 Plains
7 Plains
1 Rogue's Passage
1 Rupture Spire
1 Secluded Steppe
1 Slayers' Stronghold
1 Stone Quarry
1 Sunhome, Fortress of the Legion
1 Terramorphic Expanse
1 Transguild Promenade
1 Wind-Scarred Crag
+100
View File
@@ -0,0 +1,100 @@
[metadata]
Name=Aura of Courage
[Commander]
1 Galea, Kindler of Hope
[Main]
1 Angel of Finality
1 Puresteel Paladin
1 Realm-Cloaked Giant // Cast Off
1 Sram, Senior Edificer
1 Prognostic Sphinx
1 Cold-Eyed Selkie
1 Fleecemane Lion
1 Knight of Autumn
1 Riverwise Augur
1 Acidic Slime
1 Paradise Druid
1 Winds of Rath
1 Serum Visions
1 Nature's Lore
1 Heroic Intervention
1 Valorous Stance
1 Brainstorm
1 Bant Charm
1 Argentum Armor
1 Basilisk Collar
1 Masterwork of Ingenuity
1 Moonsilver Spear
1 Sword of the Animist
1 Behemoth Sledge
1 Arcane Signet
1 Colossus Hammer
1 Explorer's Scope
1 Sol Ring
1 Swiftfoot Boots
1 Viridian Longbow
1 Imprisoned in the Moon
1 Greater Good
1 Verdant Embrace
1 Angelic Gift
1 Gryff's Boon
1 Curse of Verbosity
1 Eel Umbra
1 Psychic Impetus
1 Abundant Growth
1 Fertile Ground
1 Kenrith's Transformation
1 Rancor
1 Utopia Sprawl
1 Wild Growth
1 Shielding Plax
1 Canopy Vista
1 Exotic Orchard
1 Fortified Village
1 Lumbering Falls
1 Port Town
1 Prairie Stream
1 Skycloud Expanse
1 Sungrass Prairie
1 Evolving Wilds
1 Azorius Chancery
1 Bant Panorama
1 Command Tower
1 Flood Plain
1 Grasslands
1 Halimar Depths
1 Mishra's Factory
1 Path of Ancestry
1 Seaside Citadel
1 Simic Growth Chamber
1 Terramorphic Expanse
1 Thriving Grove
1 Thriving Heath
1 Thriving Isle
1 Vitu-Ghazi, the City-Tree
1 Island
1 Island
1 Island
1 Island
2 Forest
2 Forest
2 Forest
2 Forest
1 Plains
1 Plains
1 Storvald, Frost Giant Jarl
1 Fey Steed
1 Holy Avenger
1 Mantle of the Ancients
1 Robe of Stars
1 Valiant Endeavor
1 Diviner's Portent
1 Netherese Puzzle-Ward
1 Winged Boots
1 Belt of Giant Strength
1 Song of Inspiration
1 Catti-brie of Mithral Hall
1 Ride the Avalanche
1 Clay Golem
1 Ebony Fly
1 Sword of Hours
+96
View File
@@ -0,0 +1,96 @@
[metadata]
Name=Bedecked Brokers
[Commander]
1 Perrie, the Pulverizer
[Main]
1 Kros, Defense Contractor
1 Ajani Unyielding
1 Angelic Sleuth
1 Aven Courier
1 Aven Mimeomancer
1 Avenging Huntbonder
1 Bribe Taker
1 Crystalline Giant
1 Devoted Druid
1 Denry Klin, Editor in Chief
1 Evolution Sage
1 Fathom Mage
1 Forgotten Ancient
1 Grateful Apparition
1 Incubation Druid
1 Jenara, Asura of War
1 Luminarch Aspirant
1 Park Heights Maverick
1 Rishkar, Peema Renegade
1 Roalesk, Apex Hybrid
1 Scavenging Ooze
1 Shield Broker
1 Skyboon Evangelist
1 Skyship Plunderer
1 Slippery Bogbonder
1 Steelbane Hydra
1 Thrummingbird
1 Vorel of the Hull Clade
1 Wall of Roots
1 Wickerbough Elder
1 Wingspan Mentor
1 Declaration in Stone
1 Damning Verdict
1 Planar Outburst
1 Tezzeret's Gambit
1 Rishkar's Expertise
1 Urban Evolution
1 Bant Charm
1 Brokers Charm
1 Brokers Confluence
1 Contractual Safeguard
1 Exotic Pets
1 Generous Gift
1 Storm of Forms
1 Family's Favor
1 Hoofprints of the Stag
1 Primal Empathy
1 Resourceful Defense
1 Together Forever
1 Agent's Toolkit
1 Arcane Signet
1 Commander's Sphere
1 Everflowing Chalice
1 Fellwar Stone
1 Gavel of the Righteous
1 Midnight Clock
1 Oblivion Stone
1 Oracle's Vault
1 Power Conduit
1 Sol Ring
1 Swiftfoot Boots
1 Ash Barrens
1 Bant Panorama
1 Brokers Hideout
1 Canopy Vista
1 Command Tower
1 Exotic Orchard
1 Flooded Grove
1 Fortified Village
1 Gavony Township
1 Karn's Bastion
1 Littjara Mirrorlake
1 Llanowar Reborn
1 Myriad Landscape
1 Nesting Grounds
1 Path of Ancestry
1 Port Town
1 Prairie Stream
1 Seaside Citadel
1 Skycloud Expanse
1 Sungrass Prairie
1 Temple of Mystery
1 Vivid Creek
1 Vivid Grove
1 Vivid Meadow
3 Forest
2 Forest
2 Island
2 Island
3 Plains
2 Plains
+92
View File
@@ -0,0 +1,92 @@
[metadata]
Name=Blame Game
[Commander]
1 Nelly Borca, Impulsive Accuser
[Main]
1 Feather, Radiant Arbiter
1 Immortal Obligation
1 Otherworldly Escort
1 Redemption Arc
1 Trouble in Pairs
1 Havoc Eater
1 Hot Pursuit
1 Mob Verdict
1 Prisoner's Dilemma
1 Take the Bait
1 Ransom Note
1 Angel of the Ruins
1 Comeuppance
1 Darien, King of Kjeldor
1 Duelist's Heritage
1 Elspeth, Sun's Champion
1 Keeper of the Accord
1 Loran of the Third Path
1 Promise of Loyalty
1 Selfless Squire
1 Sevinne's Reclamation
1 Smuggler's Share
1 Stalking Leonin
1 Sun Titan
1 Windborn Muse
1 Winds of Rath
1 Agitator Ant
1 Brash Taunter
1 Disrupt Decorum
1 Etali, Primal Storm
1 Fiendish Duo
1 Frontier Warmonger
1 Kazuul, Tyrant of the Cliffs
1 Spectacular Showdown
1 Vengeful Ancestor
1 Anya, Merciless Angel
1 Boros Reckoner
1 Deflecting Palm
1 Gisela, Blade of Goldnight
1 Ancient Stone Idol
1 Solemn Simulacrum
1 Steel Hellkite
1 Tome of Legends
1 Castle Ardenvale
1 Exotic Orchard
1 Furycalm Snarl
1 Kher Keep
1 Labyrinth of Skophos
1 Needle Spires
1 Scavenger Grounds
1 Slayers' Stronghold
1 Temple of Triumph
1 Throne of the High City
1 War Room
1 Ghostly Prison
1 Gideon's Sacrifice
1 Martial Impetus
1 Orzhov Advokist
1 Seal of Cleansing
1 Soul Snare
1 Vow of Duty
1 Wall of Omens
1 Curse of Opulence
1 Rite of the Raging Storm
1 Shiny Impetus
1 Vow of Lightning
1 Arcane Signet
1 Bloodthirsty Blade
1 Fellwar Stone
1 Mind Stone
1 Sol Ring
1 Talisman of Conviction
1 Thought Vessel
1 Access Tunnel
1 Ash Barrens
1 Boros Garrison
1 Command Tower
1 Escape Tunnel
1 Myriad Landscape
1 Reliquary Tower
1 Rogue's Passage
1 Sunhome, Fortress of the Legion
1 Temple of the False God
5 Plains
4 Plains
4 Mountain
3 Mountain
+89
View File
@@ -0,0 +1,89 @@
[metadata]
Name=Blight Curse
[Commander]
1 Auntie Ool, Cursewretch
[Main]
1 The Reaper, King No More
1 Wickersmith's Tools
1 Sinister Gnarlbark
1 Eventide's Shadow
1 Aberrant Return
1 Dread Tiller
1 Grave Venerations
1 Oft-Nabbed Goat
1 Village Pillagers
1 Puca's Covenant
1 Ferrafor, Young Yew
1 Tree of Perdition
1 Vernal Fen
1 Festering Thicket
1 Necroskitter
1 Ignoble Hierarch
1 Nesting Grounds
1 Massacre Girl, Known Killer
1 Grave Titan
1 Vraska, Betrayal's Sting
1 The Scorpion God
1 Liliana, Death Wielder
1 Chimil, the Inner Sun
1 Carnifex Demon
1 Puppeteer Clique
1 Dusk Urchins
1 Black Sun's Zenith
1 Painful Truths
1 Persist
1 Midnight Banshee
1 Chain Reaction
1 Archfiend of Ifnir
1 Channeler Initiate
1 Hapatra, Vizier of Poisons
1 Glissa Sunslayer
1 Everlasting Torment
1 Assassin's Trophy
1 Grim Poppet
1 Canyon Slough
1 Sheltered Thicket
1 Exotic Orchard
1 Smoldering Marsh
1 Cinder Glade
1 Dragonskull Summit
1 Rootbound Crag
1 Woodland Cemetery
1 Lasting Tarfire
1 Burning Curiosity
1 Hoarder's Greed
1 Blowfly Infestation
1 Fire Covenant
1 Sol Ring
1 Arcane Signet
1 Command Tower
1 Cathartic Reunion
1 Skinrender
1 Soul Snuffers
1 Incremental Blight
1 Infernal Grasp
1 Evolution Sage
1 Devoted Druid
1 Flourishing Defenses
1 Harmonize
1 Putrefy
1 Binding the Old Gods
1 Kulrath Knight
1 Contagion Clasp
1 Savage Lands
1 Ifnir Deadlands
1 Night's Whisper
1 Wickerbough Elder
1 Terminate
1 Commander's Sphere
1 Cathartic Pyre
1 Evolving Wilds
1 Terramorphic Expanse
1 Path of Ancestry
1 Riveteers Overlook
1 Rakdos Carnarium
1 Gruul Turf
1 Golgari Rot Farm
8 Swamp
4 Mountain
6 Forest
+87
View File
@@ -0,0 +1,87 @@
[metadata]
Name=Blood Rites
[Commander]
1 Clavileño, First of the Blessed
[Main]
1 Carmen, Cruel Skymarcher
1 Charismatic Conqueror
1 Elenda's Hierophant
1 March of the Canonized
1 Redemption Choir
1 Dusk Legion Sergeant
1 Master of Dark Rites
1 Promise of Aclazotz // Foul Rebirth
1 Order of Sacred Dusk
1 Austere Command
1 Kindred Boon
1 Mavren Fein, Dusk Apostle
1 Radiant Destiny
1 Welcoming Vampire
1 Bloodghast
1 Bloodtracker
1 Butcher of Malakir
1 Champion of Dusk
1 Cordial Vampire
1 Crossway Troublemakers
1 Damn
1 Drana, Liberator of Malakir
1 Exquisite Blood
1 Glass-Cast Heart
1 New Blood
1 Nighthawk Scavenger
1 Olivia's Wrath
1 Pact of the Serpent
1 Patron of the Vein
1 Sanctum Seeker
1 Timothar, Baron of Bats
1 Twilight Prophet
1 Yahenni, Undying Partisan
1 Elenda, the Dusk Rose
1 Sorin, Lord of Innistrad
1 Utter End
1 Vona, Butcher of Magan
1 Blade of the Bloodchief
1 Isolated Chapel
1 Shineshadow Snarl
1 Temple of Silence
1 Vault of the Archangel
1 Voldaren Estate
1 Windbrisk Heights
1 Martyr of Dusk
1 Return to Dust
1 Swords to Plowshares
1 Blood Artist
1 Bloodline Necromancer
1 Dusk Legion Zealot
1 Falkenrath Noble
1 Indulgent Aristocrat
1 Oathsworn Vampire
1 Village Rites
1 Viscera Seer
1 Bartolomé del Presidio
1 Cruel Celebrant
1 Etchings of the Chosen
1 Legion Lieutenant
1 Arcane Signet
1 Commander's Sphere
1 Heirloom Blade
1 Mind Stone
1 Orzhov Signet
1 Sol Ring
1 Swiftfoot Boots
1 Talisman of Hierarchy
1 Wayfarer's Bauble
1 Bojuka Bog
1 Command Tower
1 Myriad Landscape
1 Orzhov Basilica
1 Path of Ancestry
1 Rogue's Passage
1 Secluded Courtyard
1 Tainted Field
1 Temple of the False God
1 Unclaimed Territory
4 Plains
4 Plains
7 Swamp
6 Swamp
+95
View File
@@ -0,0 +1,95 @@
[metadata]
Name=Breed Lethality
[Commander]
1 Atraxa, Praetors' Voice
[Main]
1 Thrummingbird
1 Festercreep
1 Scavenging Ooze
1 Abzan Falconer
1 Orzhov Advokist
1 Tuskguard Captain
1 Necroplasm
1 Champion of Lambholt
1 Reyhan, Last of the Abzan
1 Vorel of the Hull Clade
1 Crystalline Crawler
1 Custodi Soulbinders
1 Forgotten Ancient
1 Bane of the Living
1 Ishai, Ojutai Dragonspeaker
1 Corpsejack Menace
1 Fathom Mage
1 Master Biomancer
1 Elite Scaleguard
1 Reveillark
1 Deepglow Skate
1 Kalonian Hydra
1 Ikra Shidiqi, the Usurper
1 Vulturous Zombie
1 Juniper Order Ranger
1 Ghave, Guru of Spores
1 Enduring Scalelord
1 Manifold Insights
1 Languish
1 Tezzeret's Gambit
1 Migratory Route
1 Merciless Eviction
1 Spitting Image
1 Sublime Exhalation
1 Duneblast
1 Treasure Cruise
1 Disdainful Stroke
1 Solidarity of Heroes
1 Grip of Phyresis
1 Inspiring Call
1 Mortify
1 Putrefy
1 Ancient Excavation
1 Mirrorweave
1 Sylvan Reclamation
1 Sol Ring
1 Fellwar Stone
1 Golgari Signet
1 Orzhov Signet
1 Simic Signet
1 Commander's Sphere
1 Darksteel Ingot
1 Cauldron of Souls
1 Astral Cornucopia
1 Hardened Scales
1 Brave the Sands
1 Duelist's Heritage
1 Bred for the Hunt
1 Citadel Siege
1 Cathars' Crusade
1 Arcane Sanctum
1 Ash Barrens
1 Azorius Chancery
1 Command Tower
1 Darkwater Catacombs
1 Dreadship Reef
1 Evolving Wilds
1 Exotic Orchard
1 Golgari Rot Farm
1 Murmuring Bosk
1 Opal Palace
1 Opulent Palace
1 Sandsteppe Citadel
1 Seaside Citadel
1 Sungrass Prairie
1 Temple of the False God
1 Terramorphic Expanse
1 Underground River
2 Plains
2 Plains
1 Plains
2 Island
1 Island
1 Island
2 Swamp
2 Swamp
1 Swamp
3 Forest
2 Forest
2 Forest
+104
View File
@@ -0,0 +1,104 @@
[metadata]
Name=Breed Lethality
[Commander]
1 Atraxa, Praetors' Voice
[Main]
1 Thrummingbird
1 Festercreep
1 Scavenging Ooze
1 Abzan Falconer
1 Orzhov Advokist
1 Tuskguard Captain
1 Necroplasm
1 Champion of Lambholt
1 Reyhan, Last of the Abzan
1 Vorel of the Hull Clade
1 Crystalline Crawler
1 Custodi Soulbinders
1 Forgotten Ancient
1 Bane of the Living
1 Ishai, Ojutai Dragonspeaker
1 Corpsejack Menace
1 Fathom Mage
1 Master Biomancer
1 Elite Scaleguard
1 Reveillark
1 Deepglow Skate
1 Kalonian Hydra
1 Ikra Shidiqi, the Usurper
1 Vulturous Zombie
1 Juniper Order Ranger
1 Ghave, Guru of Spores
1 Enduring Scalelord
1 Manifold Insights
1 Languish
1 Tezzeret's Gambit
1 Migratory Route
1 Merciless Eviction
1 Spitting Image
1 Sublime Exhalation
1 Duneblast
1 Treasure Cruise
1 Disdainful Stroke
1 Solidarity of Heroes
1 Grip of Phyresis
1 Inspiring Call
1 Mortify
1 Putrefy
1 Ancient Excavation
1 Mirrorweave
1 Sylvan Reclamation
1 Sol Ring
1 Fellwar Stone
1 Golgari Signet
1 Orzhov Signet
1 Simic Signet
1 Commander's Sphere
1 Darksteel Ingot
1 Cauldron of Souls
1 Astral Cornucopia
1 Hardened Scales
1 Brave the Sands
1 Duelist's Heritage
1 Bred for the Hunt
1 Citadel Siege
1 Cathars' Crusade
1 Arcane Sanctum
1 Ash Barrens
1 Azorius Chancery
1 Command Tower
1 Darkwater Catacombs
1 Dreadship Reef
1 Evolving Wilds
1 Exotic Orchard
1 Golgari Rot Farm
1 Murmuring Bosk
1 Opal Palace
1 Opulent Palace
1 Sandsteppe Citadel
1 Seaside Citadel
1 Sungrass Prairie
1 Temple of the False God
1 Terramorphic Expanse
1 Underground River
1 Plains
1 Plains
1 Plains
1 Plains
1 Plains
1 Island
1 Island
1 Island
1 Island
1 Swamp
1 Swamp
1 Swamp
1 Swamp
1 Swamp
1 Forest
1 Forest
1 Forest
1 Forest
1 Forest
1 Forest
1 Forest
+78
View File
@@ -0,0 +1,78 @@
[metadata]
Name=Buckle Up
[Commander]
1 Kotori, Pilot Prodigy
[Main]
1 Shorikai, Genesis Engine
1 Jace, Architect of Thought
1 Drumbellower
1 Ironsoul Enforcer
1 Cyberdrive Awakener
1 Kappa Cannoneer
1 Katsumasa, the Animator
1 Research Thief
1 Aeronaut Admiral
1 Cataclysmic Gearhulk
1 Indomitable Archangel
1 Myrsmith
1 Sram, Senior Edificer
1 Teshar, Ancestor's Apostle
1 Emry, Lurker of the Loch
1 Etherium Sculptor
1 Master of Etherium
1 Riddlesmith
1 Sai, Master Thopterist
1 Vedalken Engineer
1 Whirler Rogue
1 Arcanist's Owl
1 Hanna, Ship's Navigator
1 Raff Capashen, Ship's Mage
1 Foundry Inspector
1 Gold Myr
1 Shimmer Myr
1 Silver Myr
1 Solemn Simulacrum
1 Organic Extinction
1 Universal Surveillance
1 Thoughtcast
1 Dance of the Manse
1 Release to Memory
1 Access Denied
1 Armed and Armored
1 Crush Contraband
1 Dispatch
1 Generous Gift
1 Swords to Plowshares
1 Reality Shift
1 Aerial Surveyor
1 Imposter Mech
1 Imperial Recovery Unit
1 Mobilizer Mech
1 Prodigy's Prototype
1 Surgehacker Mech
1 Parhelion II
1 Arcane Signet
1 Azorius Signet
1 Colossal Plow
1 Cultivator's Caravan
1 Fellwar Stone
1 Mirage Mirror
1 Peacewalker Colossus
1 Raiders' Karve
1 Skysovereign, Consul Flagship
1 Smuggler's Copter
1 Sol Ring
1 Weatherlight
1 Swift Reconfiguration
1 Thopter Spy Network
1 Command Tower
1 Exotic Orchard
1 Port Town
1 Prairie Stream
1 Skycloud Expanse
1 Spire of Industry
1 Temple of Enlightenment
8 Plains
7 Plains
8 Island
7 Island
+79
View File
@@ -0,0 +1,79 @@
[metadata]
Name=Built From Scratch
[Commander]
1 Daretti, Scrap Savant
[Main]
1 Goblin Welder
1 Epochrasite
1 Myr Retriever
1 Myr Sire
1 Bottle Gnomes
1 Cathodion
1 Junk Diver
1 Palladium Myr
1 Pilgrim's Eye
1 Tuktuk the Explorer
1 Dualcaster Mage
1 Feldon of the Third Path
1 Solemn Simulacrum
1 Flametongue Kavu
1 Beetleback Chief
1 Ingot Chewer
1 Steel Hellkite
1 Wurmcoil Engine
1 Spitebellows
1 Hoard-Smelter Dragon
1 Warmonger Hellkite
1 Myr Battlesphere
1 Pentavus
1 Tyrant's Familiar
1 Bosh, Iron Golem
1 Bogardan Hellkite
1 Faithless Looting
1 Whipflare
1 Scrap Mastery
1 Incite Rebellion
1 Blasphemous Act
1 Impact Resonance
1 Chaos Warp
1 Volcanic Offering
1 Word of Seizing
1 Magmaquake
1 Starstorm
1 Everflowing Chalice
1 Panic Spellbomb
1 Sol Ring
1 Wayfarer's Bauble
1 Fire Diamond
1 Ichor Wellspring
1 Liquimetal Coating
1 Mind Stone
1 Mycosynth Wellspring
1 Ruby Medallion
1 Swiftfoot Boots
1 Commander's Sphere
1 Jalum Tome
1 Pristine Talisman
1 Unstable Obelisk
1 Trading Post
1 Caged Sun
1 Dreamstone Hedron
1 Loreseeker's Stone
1 Spine of Ish Sah
1 Darksteel Citadel
1 Great Furnace
1 Bitter Feud
1 Arcane Lighthouse
1 Buried Ruin
1 Dormant Volcano
1 Flamekin Village
1 Forgotten Cave
1 Ghost Quarter
1 Phyrexia's Core
1 Reliquary Tower
1 Smoldering Crater
1 Temple of the False God
8 Mountain
7 Mountain
7 Mountain
7 Mountain
+81
View File
@@ -0,0 +1,81 @@
[metadata]
Name=Built From Scratch
[Commander]
1 Daretti, Scrap Savant
[Main]
1 Goblin Welder
1 Epochrasite
1 Myr Retriever
1 Myr Sire
1 Bottle Gnomes
1 Cathodion
1 Junk Diver
1 Palladium Myr
1 Pilgrim's Eye
1 Tuktuk the Explorer
1 Dualcaster Mage
1 Feldon of the Third Path
1 Solemn Simulacrum
1 Flametongue Kavu
1 Beetleback Chief
1 Ingot Chewer
1 Steel Hellkite
1 Wurmcoil Engine
1 Spitebellows
1 Hoard-Smelter Dragon
1 Warmonger Hellkite
1 Myr Battlesphere
1 Pentavus
1 Tyrant's Familiar
1 Bosh, Iron Golem
1 Bogardan Hellkite
1 Faithless Looting
1 Whipflare
1 Scrap Mastery
1 Incite Rebellion
1 Blasphemous Act
1 Impact Resonance
1 Chaos Warp
1 Volcanic Offering
1 Word of Seizing
1 Magmaquake
1 Starstorm
1 Everflowing Chalice
1 Panic Spellbomb
1 Sol Ring
1 Wayfarer's Bauble
1 Fire Diamond
1 Ichor Wellspring
1 Liquimetal Coating
1 Mind Stone
1 Mycosynth Wellspring
1 Ruby Medallion
1 Swiftfoot Boots
1 Commander's Sphere
1 Jalum Tome
1 Pristine Talisman
1 Unstable Obelisk
1 Trading Post
1 Caged Sun
1 Dreamstone Hedron
1 Loreseeker's Stone
1 Spine of Ish Sah
1 Darksteel Citadel
1 Great Furnace
1 Bitter Feud
1 Arcane Lighthouse
1 Buried Ruin
1 Dormant Volcano
1 Flamekin Village
1 Forgotten Cave
1 Ghost Quarter
1 Phyrexia's Core
1 Reliquary Tower
1 Smoldering Crater
1 Temple of the False God
5 Mountain
5 Mountain
5 Mountain
5 Mountain
5 Mountain
4 Mountain
+94
View File
@@ -0,0 +1,94 @@
[metadata]
Name=Cabaretti Cacophony
[Commander]
1 Kitt Kanto, Mayhem Diva
[Main]
1 Phabine, Boss's Confidant
1 Leafkin Druid
1 Sakura-Tribe Elder
1 Agitator Ant
1 Bess, Soul Nourisher
1 Champion of Lambholt
1 Magus of the Wheel
1 Orzhov Advokist
1 Rumor Gatherer
1 Scute Swarm
1 Selvala, Explorer Returned
1 Wood Elves
1 Zurzoth, Chaos Rider
1 Arasta of the Endless Web
1 Life of the Party
1 Master of Ceremonies
1 Rose Room Treasurer
1 Sizzling Soloist
1 Boss's Chauffeur
1 Gahiji, Honored One
1 Kazuul, Tyrant of the Cliffs
1 Thunderfoot Baloth
1 Cabaretti Confluence
1 Camaraderie
1 Cultivate
1 Fell the Mighty
1 Harmonize
1 Indulge // Excess
1 Martial Coup
1 Seize the Spotlight
1 Shamanic Revelation
1 Sylvan Offering
1 Vivien's Stampede
1 Artifact Mutation
1 Aura Mutation
1 Beast Within
1 Boros Charm
1 Cabaretti Charm
1 Crash the Party
1 Call the Coppercoats
1 Grand Crescendo
1 March of the Multitudes
1 Path to Exile
1 Sol Ring
1 Arcane Signet
1 Bloodthirsty Blade
1 Fellwar Stone
1 Idol of Oblivion
1 Commander's Sphere
1 Scepter of Celebration
1 False Floor
1 Intangible Virtue
1 Awakening Zone
1 Beastmaster Ascension
1 Duelist's Heritage
1 Killer Service
1 Prosperous Partnership
1 Felidar Retreat
1 Outpost Siege
1 Assemble the Legion
1 Sandwurm Convergence
1 Ash Barrens
1 Cabaretti Courtyard
1 Canopy Vista
1 Castle Ardenvale
1 Castle Embereth
1 Cinder Glade
1 Command Tower
1 Exotic Orchard
1 Fortified Village
1 Game Trail
1 Jungle Shrine
1 Mossfire Valley
1 Myriad Landscape
1 Naya Panorama
1 Path of Ancestry
1 Rugged Prairie
1 Sungrass Prairie
1 Temple of Triumph
1 Thriving Bluff
1 Thriving Grove
1 Thriving Heath
1 Windbrisk Heights
2 Plains
2 Plains
2 Mountain
2 Mountain
4 Forest
4 Forest
+88
View File
@@ -0,0 +1,88 @@
[metadata]
Name=Call the Spirits
[Commander]
1 Daxos the Returned
[Main]
1 Oreskos Explorer
1 Karlov of the Ghost Council
1 Underworld Coinsmith
1 Burnished Hart
1 Bastion Protector
1 Dawnglare Invoker
1 Ghostblade Eidolon
1 Kor Sanctifiers
1 Monk Idealist
1 Mesa Enchantress
1 Nighthowler
1 Corpse Augur
1 Fate Unraveler
1 Ajani's Chosen
1 Doomwake Giant
1 Dreadbringer Lampads
1 Celestial Ancient
1 Celestial Archon
1 Herald of the Host
1 Banshee of the Dread Choir
1 Thief of Blood
1 Treasury Thrull
1 Sandstone Oracle
1 Silent Sentinel
1 Teysa, Envoy of Ghosts
1 Ancient Craving
1 Gild
1 Dawn to Dusk
1 Righteous Confluence
1 Open the Vaults
1 Deadly Tempest
1 Death Grasp
1 Sol Ring
1 Wayfarer's Bauble
1 Lightning Greaves
1 Orzhov Signet
1 Thought Vessel
1 Crystal Chimes
1 Orzhov Cluestone
1 Phyrexian Reclamation
1 Seal of Cleansing
1 Grave Peril
1 Banishing Light
1 Cage of Hands
1 Karmic Justice
1 Vow of Duty
1 Fallen Ideal
1 Seal of Doom
1 Vow of Malice
1 Aura of Silence
1 Grasp of Fate
1 Shielded by Faith
1 Phyrexian Arena
1 Underworld Connections
1 Daxos's Torment
1 Marshal's Anthem
1 Dictate of Heliod
1 Sigil of the Empty Throne
1 Black Market
1 Necromancer's Covenant
1 Barren Moor
1 Command Tower
1 Evolving Wilds
1 Ghost Quarter
1 New Benalia
1 Orzhov Basilica
1 Orzhov Guildgate
1 Rogue's Passage
1 Scoured Barrens
1 Secluded Steppe
1 Tainted Field
1 Temple of the False God
1 Terramorphic Expanse
1 Vivid Marsh
1 Vivid Meadow
3 Plains
3 Plains
3 Plains
2 Plains
4 Swamp
3 Swamp
3 Swamp
3 Swamp
+83
View File
@@ -0,0 +1,83 @@
[metadata]
Name=Chaos Incarnate
[Commander]
1 Kardur, Doomscourge
[Main]
1 Archfiend of Depravity
1 Bloodgift Demon
1 Deadly Tempest
1 Dredge the Mire
1 Ob Nixilis Reignited
1 Profane Command
1 Rakshasa Debaser
1 Reign of the Pit
1 Sangromancer
1 Scythe Specter
1 Sepulchral Primordial
1 Soul Shatter
1 Titan Hunter
1 Blasphemous Act
1 Brash Taunter
1 Chaos Warp
1 Combustible Gearhulk
1 Dictate of the Twin Gods
1 Fiery Confluence
1 Geode Rager
1 Kazuul, Tyrant of the Cliffs
1 Magmatic Force
1 Sunbird's Invocation
1 Tectonic Giant
1 Wild Ricochet
1 Wildfire Devils
1 Kaervek the Merciless
1 Spiteful Visions
1 Stormfist Crusader
1 Theater of Horrors
1 Coveted Jewel
1 Solemn Simulacrum
1 Foreboding Ruins
1 Smoldering Marsh
1 Stensia Bloodhall
1 Temple of Malice
1 Ambition's Cost
1 Feed the Swarm
1 Indulgent Tormentor
1 Read the Bones
1 Sign in Blood
1 Syphon Mind
1 Vampire Nighthawk
1 Abrade
1 Explosion of Riches
1 Guttersnipe
1 Hate Mirage
1 Mana Geyser
1 Thermo-Alchemist
1 Breath of Malfegor
1 Rakdos Charm
1 Terminate
1 Unlicensed Disintegration
1 Arcane Signet
1 Burnished Hart
1 Commander's Sphere
1 Lightning Greaves
1 Nihil Spellbomb
1 Rakdos Signet
1 Sol Ring
1 Talisman of Indulgence
1 Wayfarer's Bauble
1 Worn Powerstone
1 Akoum Refuge
1 Bloodfell Caves
1 Cinder Barrens
1 Command Tower
1 Molten Slagheap
1 Myriad Landscape
1 Urborg Volcano
4 Swamp
4 Swamp
4 Swamp
3 Swamp
4 Mountain
4 Mountain
3 Mountain
3 Mountain
+87
View File
@@ -0,0 +1,87 @@
[metadata]
Name=Corrupting Influence
[Commander]
1 Ixhel, Scion of Atraxa
[Main]
1 Bilious Skulldweller
1 Blight Mamba
1 Blightbelly Rat
1 Cankerbloom
1 Contaminant Grafter
1 Evolution Sage
1 Glissa's Retriever
1 Grateful Apparition
1 Ichor Rats
1 Ichorclaw Myr
1 Mycosynth Fiend
1 Myr Convert
1 Norn's Choirmaster
1 Pestilent Syphoner
1 Phyrexian Swarmlord
1 Plague Myr
1 Plague Stinger
1 Scavenging Ooze
1 Venomous Brutalizer
1 Viridian Corrupter
1 Vishgraz, the Doomhive
1 Windborn Muse
1 Caress of Phyrexia
1 Culling Ritual
1 Cultivate
1 Expand the Sphere
1 Feed the Infection
1 Fumigate
1 Geth's Summons
1 Infectious Inquiry
1 Merciless Eviction
1 Night's Whisper
1 Noxious Assault
1 Painful Truths
1 Phyresis Outbreak
1 Phyrexian Rebirth
1 Unnatural Restoration
1 Vat Emergence
1 Wurmquake
1 Beast Within
1 Carrion Call
1 Mortify
1 Noxious Revival
1 Putrefy
1 Swords to Plowshares
1 Vraska's Fall
1 Ghostly Prison
1 Moldervine Reclamation
1 Norn's Decree
1 Arcane Signet
1 Chromatic Lantern
1 Commander's Sphere
1 Contagion Clasp
1 Fellwar Stone
1 Glistening Sphere
1 Golgari Signet
1 Grafted Exoskeleton
1 Norn's Annex
1 Phyrexian Atlas
1 Sol Ring
1 Trailblazer's Boots
1 Bojuka Bog
1 Canopy Vista
1 Command Tower
1 Exotic Orchard
1 Fortified Village
1 Karn's Bastion
1 Krosan Verge
1 Myriad Landscape
1 Necroblossom Snarl
1 Path of Ancestry
1 Sandsteppe Citadel
1 Shineshadow Snarl
1 Sungrass Prairie
1 Tainted Field
1 Tainted Wood
1 Temple of Malady
1 Temple of Plenty
1 Temple of Silence
6 Plains
6 Swamp
8 Forest
@@ -0,0 +1,104 @@
[metadata]
Name=Counter Blitz (FINAL FANTASY X)
[Commander]
1 Tidus, Yuna's Guardian
[Main]
1 Yuna, Grand Summoner
1 Auron, Venerated Guardian
1 Chocobo Knights
1 Gatta and Luzzu
1 Lord Jyscal Guado
1 Protection Magic
1 Summon: Ixion
1 Summon: Yojimbo
1 Summoner's Sending
1 Blitzball Stadium
1 Lulu, Stern Guardian
1 O'aka, Traveling Merchant
1 Rikku, Resourceful Guardian
1 Summon: Valefor
1 Maester Seymour
1 Sphere Grid
1 Summon: Magus Sisters
1 Tromell, Seymour's Butler
1 Yuna's Decision
1 Yuna's Whistle
1 Kimahri, Valiant Guardian
1 Shelinda, Yevon Acolyte
1 Sin, Unending Cataclysm
1 Wakka, Devoted Guardian
1 Collective Effort
1 Damning Verdict
1 Farewell
1 Luminous Broodmoth
1 Promise of Loyalty
1 Resourceful Defense
1 Scholar of New Horizons
1 Sunscorch Regent
1 Together Forever
1 Chasm Skulker
1 Inexorable Tide
1 Pull from Tomorrow
1 Bane of Progress
1 Fight Rigging
1 Forgotten Ancient
1 Generous Patron
1 Gyre Sage
1 Hardened Scales
1 Incubation Druid
1 Path of Discovery
1 Rampant Rejuvenator
1 Tireless Tracker
1 Altered Ego
1 Endless Detour
1 Fathom Mage
1 Walking Ballista
1 Brushland
1 Canopy Vista
1 Exotic Orchard
1 Flooded Grove
1 Fortified Village
1 Glacial Fortress
1 Hinterland Harbor
1 Overflowing Basin
1 Port Town
1 Prairie Stream
1 Skycloud Expanse
1 Sungrass Prairie
1 Sunpetal Grove
1 Temple of Enlightenment
1 Temple of Mystery
1 Temple of Plenty
1 Vineglimmer Snarl
1 Destroy Evil
1 Grateful Apparition
1 Path to Exile
1 An Offer You Can't Refuse
1 Duskshell Crawler
1 Farseek
1 Inspiring Call
1 Three Visits
1 Bred for the Hunt
1 Arcane Signet
1 Everflowing Chalice
1 Sol Ring
1 Ash Barrens
1 Evolving Wilds
1 Forge of Heroes
1 Idyllic Beachfront
1 Nesting Grounds
1 Path of Ancestry
1 Radiant Grove
1 Seaside Citadel
1 Tangled Islet
1 Temple of the False God
1 Command Tower
1 Island
1 Island
1 Island
1 Forest
1 Forest
1 Forest
1 Plains
1 Plains
1 Plains
@@ -0,0 +1,98 @@
[metadata]
Name=Counter Blitz Collector's Edition (FINAL FANTASY X)
[Commander]
1 Tidus, Yuna's Guardian
[Main]
1 Altered Ego
1 An Offer You Can't Refuse
1 Arcane Signet
1 Ash Barrens
1 Auron, Venerated Guardian
1 Bane of Progress
1 Blitzball Stadium
1 Bred for the Hunt
1 Brushland
1 Canopy Vista
1 Chasm Skulker
1 Chocobo Knights
1 Collective Effort
1 Command Tower
1 Damning Verdict
1 Destroy Evil
1 Duskshell Crawler
1 Endless Detour
1 Everflowing Chalice
1 Evolving Wilds
1 Exotic Orchard
1 Farewell
1 Farseek
1 Fathom Mage
1 Fight Rigging
1 Flooded Grove
3 Forest
1 Forge of Heroes
1 Forgotten Ancient
1 Fortified Village
1 Gatta and Luzzu
1 Generous Patron
1 Glacial Fortress
1 Grateful Apparition
1 Gyre Sage
1 Hardened Scales
1 Hinterland Harbor
1 Idyllic Beachfront
1 Incubation Druid
1 Inexorable Tide
1 Inspiring Call
3 Island
1 Kimahri, Valiant Guardian
1 Lord Jyscal Guado
1 Lulu, Stern Guardian
1 Luminous Broodmoth
1 Maester Seymour
1 Nesting Grounds
1 O'aka, Traveling Merchant
1 Overflowing Basin
1 Path of Ancestry
1 Path of Discovery
1 Path to Exile
3 Plains
1 Port Town
1 Prairie Stream
1 Promise of Loyalty
1 Protection Magic
1 Pull from Tomorrow
1 Radiant Grove
1 Rampant Rejuvenator
1 Resourceful Defense
1 Rikku, Resourceful Guardian
1 Scholar of New Horizons
1 Seaside Citadel
1 Shelinda, Yevon Acolyte
1 Sin, Unending Cataclysm
1 Skycloud Expanse
1 Sol Ring
1 Sphere Grid
1 Summon: Ixion
1 Summon: Magus Sisters
1 Summon: Valefor
1 Summon: Yojimbo
1 Summoner's Sending
1 Sungrass Prairie
1 Sunpetal Grove
1 Sunscorch Regent
1 Tangled Islet
1 Temple of Enlightenment
1 Temple of Mystery
1 Temple of Plenty
1 Temple of the False God
1 Three Visits
1 Tireless Tracker
1 Together Forever
1 Tromell, Seymour's Butler
1 Vineglimmer Snarl
1 Wakka, Devoted Guardian
1 Walking Ballista
1 Yuna, Grand Summoner
1 Yuna's Decision
1 Yuna's Whistle
+101
View File
@@ -0,0 +1,101 @@
[metadata]
Name=Counter Intelligence
[Commander]
1 Inspirit, Flagship Vessel
[Main]
1 Kilo, Apogee Mind
1 Patrolling Peacemaker
1 Insight Engine
1 Uthros Research Craft
1 Depthshaker Titan
1 Long-Range Sensor
1 Moxite Refinery
1 Solar Array
1 Surge Conductor
1 Glittering Massif
1 Radiant Summit
1 Swan Song
1 Chaos Warp
1 Cloud Key
1 Gavel of the Righteous
1 Battlefield Forge
1 Angel of the Ruins
1 Fumigate
1 Organic Extinction
1 Resourceful Defense
1 Chrome Host Seedshark
1 Cyberdrive Awakener
1 Deepglow Skate
1 Emry, Lurker of the Loch
1 Kappa Cannoneer
1 Phyrexian Metamorph
1 Pull from Tomorrow
1 Ripples of Potential
1 Tekuthal, Inquiry Dominus
1 Thought Monitor
1 Universal Surveillance
1 Chain Reaction
1 Alibou, Ancient Witness
1 Jhoira, Weatherlight Captain
1 Wake the Past
1 Astral Cornucopia
1 Crystalline Crawler
1 Darksteel Reactor
1 Empowered Autogenerator
1 Hangarback Walker
1 Lux Artillery
1 Lux Cannon
1 Steel Overseer
1 Threefold Thunderhulk
1 Titan Forge
1 Adarkar Wastes
1 Cascade Bluffs
1 Clifftop Retreat
1 Exotic Orchard
1 Glacial Fortress
1 Irrigated Farmland
1 Karn's Bastion
1 The Mycosynth Gardens
1 Rugged Prairie
1 Shivan Reef
1 Skycloud Expanse
1 Spire of Industry
1 Sulfur Falls
1 Temple of Enlightenment
1 Temple of Epiphany
1 Temple of Triumph
1 Swords to Plowshares
1 Tezzeret's Gambit
1 Thirst for Knowledge
1 Arcane Signet
1 Pentad Prism
1 Sol Ring
1 Command Tower
1 Dispatch
1 Etherium Sculptor
1 Experimental Augury
1 Thrummingbird
1 Enthusiastic Mechanaut
1 Coretapper
1 Etched Oracle
1 Everflowing Chalice
1 Golem Foundry
1 Mindless Automaton
1 Soul-Guide Lantern
1 Ancient Den
1 Buried Ruin
1 Evolving Wilds
1 Great Furnace
1 Lonely Sandbar
1 Mystic Monastery
1 Razortide Bridge
1 Rustvale Bridge
1 Seat of the Synod
1 Secluded Steppe
1 Silverbluff Bridge
2 Plains
1 Plains
2 Island
1 Island
2 Mountain
1 Mountain
+90
View File
@@ -0,0 +1,90 @@
[metadata]
Name=Counterpunch
[Commander]
1 Ghave, Guru of Spores
[Main]
1 Aquastrand Spider
1 Deadly Recluse
1 Sakura-Tribe Elder
1 Scavenging Ooze
1 Monk Realist
1 Selesnya Evangel
1 Golgari Guildmage
1 Selesnya Guildmage
1 Nantuko Husk
1 Fertilid
1 Spawnwrithe
1 Vampire Nighthawk
1 Spike Feeder
1 Yavimaya Elder
1 Squallmonger
1 Penumbra Spider
1 Sigil Captain
1 Shriekmaw
1 Dark Hatchling
1 Teneb, the Harvester
1 Hornet Queen
1 Vish Kal, Blood Arbiter
1 Symbiotic Wurm
1 Celestial Force
1 Karador, Ghost Chieftain
1 Chorus of the Conclave
1 Alliance of Arms
1 Cultivate
1 Harmonize
1 Syphon Flesh
1 Bestial Menace
1 Hex
1 Hour of Reckoning
1 Death Mutation
1 Storm Herd
1 Doom Blade
1 Tribute to the Wild
1 Footbottom Feast
1 Afterlife
1 Mortify
1 Nemesis Trap
1 Cobra Trap
1 Skullclamp
1 Sol Ring
1 Golgari Signet
1 Lightning Greaves
1 Orzhov Signet
1 Selesnya Signet
1 Darksteel Ingot
1 Acorn Catapult
1 Soul Snare
1 Fists of Ironwood
1 Necrogenesis
1 Vow of Malice
1 Awakening Zone
1 Vow of Wildness
1 Oblivion Ring
1 Vow of Duty
1 Attrition
1 Aura Shards
1 Barren Moor
1 Command Tower
1 Evolving Wilds
1 Golgari Rot Farm
1 Orzhov Basilica
1 Rupture Spire
1 Secluded Steppe
1 Selesnya Sanctuary
1 Temple of the False God
1 Tranquil Thicket
1 Vivid Grove
1 Vivid Marsh
1 Vivid Meadow
2 Swamp
2 Swamp
2 Swamp
2 Swamp
3 Forest
3 Forest
2 Forest
2 Forest
2 Plains
2 Plains
2 Plains
2 Plains
+82
View File
@@ -0,0 +1,82 @@
[metadata]
Name=Coven Counters
[Commander]
1 Leinore, Autumn Sovereign
[Main]
1 Angel of Glory's Rise
1 Bastion Protector
1 Custodi Soulbinders
1 Dearly Departed
1 Herald of War
1 Knight of the White Orchid
1 Mikaeus, the Lunarch
1 Odric, Master Tactician
1 Riders of Gavony
1 Victory's Envoy
1 Champion of Lambholt
1 Gyre Sage
1 Kessig Cagebreakers
1 Somberwald Sage
1 Verdurous Gearhulk
1 Wild Beastmaster
1 Heron's Grace Champion
1 Sigarda, Heron's Grace
1 Abzan Falconer
1 Ainok Bond-Kin
1 Elite Scaleguard
1 Orzhov Advokist
1 Avacyn's Pilgrim
1 Eternal Witness
1 Yavimaya Elder
1 Enduring Scalelord
1 Juniper Order Ranger
1 Trostani's Summoner
1 Cleansing Nova
1 Hour of Reckoning
1 Shamanic Revelation
1 Bestial Menace
1 Biogenic Upgrade
1 Growth Spasm
1 Unbreakable Formation
1 Return to Dust
1 Swords to Plowshares
1 Beast Within
1 Inspiring Call
1 Lifecrafter's Bestiary
1 Arcane Signet
1 Sol Ring
1 Swiftfoot Boots
1 Talisman of Unity
1 Citadel Siege
1 Death's Presence
1 Canopy Vista
1 Exotic Orchard
1 Fortified Village
1 Sungrass Prairie
1 Temple of Plenty
1 Blighted Woodland
1 Command Tower
1 Krosan Verge
1 Myriad Landscape
1 Path of Ancestry
1 Rogue's Passage
1 Selesnya Sanctuary
1 Temple of the False God
12 Forest
12 Plains
1 Kyler, Sigardian Emissary
1 Celestial Judgment
1 Curse of Conformity
1 Moorland Rescuer
1 Sigarda's Vanguard
1 Stalwart Pathlighter
1 Wall of Mourning
1 Celebrate the Harvest
1 Curse of Clinging Webs
1 Heronblade Elite
1 Kurbis, Harvest Celebrant
1 Ruinous Intrusion
1 Sigardian Zealot
1 Somberwald Beastmaster
1 Dawnhart Wardens
1 Moonsilver Key
+70 -67
View File
@@ -3,85 +3,88 @@ Name=Creative Energy
[Commander]
1 Satya, Aetherflux Genius
[Main]
1 Adarkar Wastes
1 Aether Hub
1 Cayth, Famed Mechanist
1 Sphinx of the Revelation
1 Filigree Racer
1 Localized Destruction
1 Aether Refinery
1 Hourglass of the Lost
1 Salvation Colossus
1 Blaster Hulk
1 Razorfield Ripper
1 Silverquill Lecturer
1 Overclocked Electromancer
1 Stone Idol Generator
1 Aurora Shifter
1 Conversion Apparatus
1 Gonti's Aether Heart
1 Lightning Runner
1 Aethertide Whale
1 Aethergeode Miner
1 Aethersphere Harvester
1 Aethersquall Ancient
1 Aetherstorm Roc
1 Aethertide Whale
1 Aetherworks Marvel
1 Akroma's Will
1 Amped Raptor
1 Angel of Invention
1 Arcane Signet
1 Aurora Shifter
1 Austere Command
1 Azorius Chancery
1 Battlefield Forge
1 Bespoke Battlewagon
1 Bident of Thassa
1 Blaster Hulk
1 Temple of Enlightenment
1 Battlefield Forge
1 Myr Battlesphere
1 Grenzo, Havoc Raiser
1 Brudiclad, Telchor Engineer
1 Burnished Hart
1 Castle Vantress
1 Cayth, Famed Mechanist
1 Coveted Jewel
1 Goldspan Dragon
1 Legion Loyalty
1 Mystic Gate
1 Akroma's Will
1 Adarkar Wastes
1 Shivan Reef
1 Coalition Relic
1 Combustible Gearhulk
1 Command Tower
1 Aetherworks Marvel
1 Angel of Invention
1 Confiscation Coup
1 Conversion Apparatus
1 Coveted Jewel
1 Decoction Module
1 Demolition Field
1 Era of Innovation
1 Farewell
1 Filigree Racer
1 Aetherstorm Roc
1 Aethersquall Ancient
1 Frostboil Snarl
1 Austere Command
1 Furycalm Snarl
1 Glimmer of Genius
1 Goldspan Dragon
1 Gonti's Aether Heart
1 Grenzo, Havoc Raiser
1 Hourglass of the Lost
1 Izzet Boilerworks
1 Izzet Generatorium
1 Jolted Awake
1 Legion Loyalty
1 Lightning Runner
1 Localized Destruction
1 Midnight Clock
1 Myr Battlesphere
1 Mystic Gate
1 Mystic Monastery
1 Overclocked Electromancer
1 Port Town
1 Prairie Stream
1 Professional Face-Breaker
1 Razorfield Ripper
1 Roil Cartographer
1 Salvation Colossus
1 Scurry of Gremlins
1 Shivan Reef
1 Silverquill Lecturer
1 Skyclave Apparition
1 Sol Ring
1 Solar Transformer
1 Solemn Simulacrum
1 Sphinx of the Revelation
1 Stone Idol Generator
1 Swords to Plowshares
1 Talisman of Conviction
1 Talisman of Creativity
1 Talisman of Progress
1 Temple of Enlightenment
1 Temple of Epiphany
1 Farewell
1 Port Town
1 Temple of Triumph
1 Tezzeret's Gambit
1 Solemn Simulacrum
1 Temple of Epiphany
1 Prairie Stream
1 Skyclave Apparition
1 Castle Vantress
1 Jolted Awake
1 Bespoke Battlewagon
1 Roil Cartographer
1 Unstable Amulet
1 Wayfarer's Bauble
1 Amped Raptor
1 Solar Transformer
1 Scurry of Gremlins
1 Izzet Generatorium
1 Talisman of Conviction
1 Tezzeret's Gambit
1 Izzet Boilerworks
1 Azorius Chancery
1 Talisman of Progress
1 Command Tower
1 Burnished Hart
1 Arcane Signet
1 Sol Ring
1 Era of Innovation
1 Aether Hub
1 Decoction Module
1 Glimmer of Genius
1 Whirler Virtuoso
7 Plains
7 Island
6 Mountain
1 Talisman of Creativity
1 Swords to Plowshares
1 Wayfarer's Bauble
1 Demolition Field
1 Mystic Monastery
5 Plains
5 Plains
3 Island
2 Island
3 Mountain
2 Mountain
@@ -0,0 +1,90 @@
[metadata]
Name=Creative Energy Collector's Edition
[Commander]
1 Satya, Aetherflux Genius
[Main]
1 Cayth, Famed Mechanist
1 Sphinx of the Revelation
1 Filigree Racer
1 Localized Destruction
1 Aether Refinery
1 Hourglass of the Lost
1 Salvation Colossus
1 Blaster Hulk
1 Razorfield Ripper
1 Silverquill Lecturer
1 Overclocked Electromancer
1 Stone Idol Generator
1 Aurora Shifter
1 Conversion Apparatus
1 Gonti's Aether Heart
1 Lightning Runner
1 Aethertide Whale
1 Aethergeode Miner
1 Aethersphere Harvester
1 Bident of Thassa
1 Temple of Enlightenment
1 Battlefield Forge
1 Myr Battlesphere
1 Grenzo, Havoc Raiser
1 Brudiclad, Telchor Engineer
1 Coveted Jewel
1 Goldspan Dragon
1 Legion Loyalty
1 Mystic Gate
1 Akroma's Will
1 Adarkar Wastes
1 Shivan Reef
1 Coalition Relic
1 Combustible Gearhulk
1 Aetherworks Marvel
1 Angel of Invention
1 Confiscation Coup
1 Aetherstorm Roc
1 Aethersquall Ancient
1 Frostboil Snarl
1 Austere Command
1 Furycalm Snarl
1 Midnight Clock
1 Professional Face-Breaker
1 Farewell
1 Port Town
1 Temple of Triumph
1 Solemn Simulacrum
1 Temple of Epiphany
1 Prairie Stream
1 Skyclave Apparition
1 Castle Vantress
1 Jolted Awake
1 Bespoke Battlewagon
1 Roil Cartographer
1 Unstable Amulet
1 Amped Raptor
1 Solar Transformer
1 Scurry of Gremlins
1 Izzet Generatorium
1 Talisman of Conviction
1 Tezzeret's Gambit
1 Izzet Boilerworks
1 Azorius Chancery
1 Talisman of Progress
1 Command Tower
1 Burnished Hart
1 Arcane Signet
1 Sol Ring
1 Era of Innovation
1 Aether Hub
1 Decoction Module
1 Glimmer of Genius
1 Whirler Virtuoso
1 Talisman of Creativity
1 Swords to Plowshares
1 Wayfarer's Bauble
1 Demolition Field
1 Mystic Monastery
5 Plains
5 Plains
3 Island
2 Island
3 Mountain
2 Mountain
+93
View File
@@ -0,0 +1,93 @@
[metadata]
Name=Dance of the Elements
[Commander]
1 Ashling, the Limitless
[Main]
1 Mass of Mysteries
1 Elemental Spectacle
1 Springleaf Parade
1 Jubilation
1 Impulsivity
1 Lamentation
1 Belonging
1 Subterfuge
1 Rain-Slicked Copse
1 Sodden Verdure
1 Abundant Countryside
1 Endurance
1 Fury
1 Haunting Voyage
1 Avenger of Zendikar
1 Cavalier of Thorns
1 Greenwarden of Murasa
1 Selvala, Heart of the Wilds
1 Titan of Industry
1 Muldrotha, the Gravetide
1 Omnath, Locus of Rage
1 Yarok, the Desecrated
1 Omnath, Locus of the Roil
1 Timeless Lotus
1 Shatter the Sky
1 Hoofprints of the Stag
1 Slithermuse
1 Descendants' Fury
1 Blasphemous Act
1 Cream of the Crop
1 Kindred Summons
1 Bane of Progress
1 Realmwalker
1 Return of the Wildspeaker
1 Faeburrow Elder
1 Vernal Sovereign
1 Horde of Notions
1 Maelstrom Wanderer
1 Jegantha, the Wellspring
1 Chromatic Lantern
1 Primal Beyond
1 Raging Ravine
1 Exotic Orchard
1 Flamekin Village
1 Path to Exile
1 Mulldrifter
1 Reality Shift
1 Shriekmaw
1 Shimmercreep
1 Flamebraider
1 Crib Swap
1 Secluded Courtyard
1 Sol Ring
1 Command Tower
1 Arcane Signet
1 Distant Melody
1 Incandescent Soulstoke
1 Eclipsed Flamekin
1 Foundation Breaker
1 Garruk's Uprising
1 Risen Reef
1 Fellwar Stone
1 Unclaimed Territory
1 Ancient Ziggurat
1 Frontier Bivouac
1 Sandsteppe Citadel
1 Savage Lands
1 Opulent Palace
1 Seaside Citadel
1 Jungle Shrine
1 Smokebraider
1 Ingot Chewer
1 Fertile Ground
1 Abundant Growth
1 Kodama's Reach
1 Cultivate
1 Path of Ancestry
1 Thriving Grove
1 Thriving Heath
1 Thriving Isle
1 Opal Palace
1 Thriving Bluff
1 Thriving Moor
2 Plains
2 Island
2 Swamp
2 Mountain
8 Forest
+99
View File
@@ -0,0 +1,99 @@
[metadata]
Name=Deadly Disguise
[Commander]
1 Kaust, Eyes of the Glade
[Main]
1 Duskana, the Rage Mother
1 True Identity
1 Unexplained Absence
1 Veiled Ascension
1 Boltbender
1 Showstopping Surprise
1 Tesak, Judith's Hellhound
1 Experiment Twelve
1 Printlifter Ooze
1 Panoptic Projektor
1 Ransom Note
1 Ugin's Mastery
1 Austere Command
1 Dusk // Dawn
1 Exalted Angel
1 Fell the Mighty
1 Hidden Dragonslayer
1 Master of Pearls
1 Mastery of the Unseen
1 Mirror Entity
1 Welcoming Vampire
1 Akroma, Angel of Fury
1 Ashcloud Phoenix
1 Chaos Warp
1 Imperial Hellkite
1 Jeska's Will
1 Neheb, the Eternal
1 Scourge of the Throne
1 Beast Whisperer
1 Deathmist Raptor
1 Den Protector
1 Hooded Hydra
1 Krosan Cloudscraper
1 Krosan Colossus
1 Obscuring Aether
1 Ohran Frostfang
1 Return of the Wildspeaker
1 Root Elemental
1 Saryth, the Viper's Fang
1 Seedborn Muse
1 Temur War Shaman
1 Thelonite Hermit
1 Toski, Bearer of Secrets
1 Trail of Mystery
1 Whisperwood Elemental
1 Yedora, Grave Gardener
1 Decimate
1 Sidar Kondo of Jamuraa
1 Lifecrafter's Bestiary
1 Scroll of Fate
1 Canopy Vista
1 Cinder Glade
1 Exotic Orchard
1 Fortified Village
1 Furycalm Snarl
1 Game Trail
1 Kessig Wolf Run
1 Mossfire Valley
1 Mosswort Bridge
1 Scattered Groves
1 Sheltered Thicket
1 Shrine of the Forsaken Gods
1 Sungrass Prairie
1 Temple of Abandon
1 Temple of Plenty
1 Temple of Triumph
1 Path to Exile
1 Ainok Survivalist
1 Broodhatch Nantuko
1 Nervous Gardener
1 Nantuko Vigilante
1 Nature's Lore
1 Sakura-Tribe Elder
1 Salt Road Ambushers
1 Three Visits
1 Wild Growth
1 Arcane Signet
1 Sol Ring
1 Boros Garrison
1 Command Tower
1 Branch of Vitu-Ghazi
1 Gruul Turf
1 Jungle Shrine
1 Krosan Verge
1 Sacred Peaks
1 Selesnya Sanctuary
1 Temple of the False God
1 Zoetic Cavern
2 Plains
2 Plains
2 Mountain
1 Mountain
2 Forest
2 Forest
+96
View File
@@ -0,0 +1,96 @@
[metadata]
Name=Deep Clue Sea
[Commander]
1 Morska, Undersea Sleuth
[Main]
1 Sophia, Dogged Detective
1 Armed with Proof
1 Merchant of Truth
1 Serene Sleuth
1 Detective of the Month
1 Follow the Bodies
1 Tangletrove Kelp
1 Innocuous Researcher
1 On the Trail
1 Knowledge Is Power
1 Ransom Note
1 Aerial Extortionist
1 Bennie Bracks, Zoologist
1 Farewell
1 Fumigate
1 Organic Extinction
1 Search the Premises
1 Alandra, Sky Dreamer
1 Confirm Suspicions
1 Ethereal Investigator
1 Finale of Revelation
1 Kappa Cannoneer
1 Mechanized Production
1 Nadir Kraken
1 Shimmer Dragon
1 Teferi's Ageless Insight
1 Tezzeret, Betrayer of Flesh
1 Thought Monitor
1 Hornet Queen
1 Jolrael, Mwonvuli Recluse
1 Killer Service
1 Tireless Tracker
1 Adrix and Nev, Twincasters
1 Chulane, Teller of Tales
1 Disorder in the Court
1 Esix, Fractal Bloom
1 Hydroid Krasis
1 Koma, Cosmos Serpent
1 Lonis, Cryptozoologist
1 Selvala, Explorer Returned
1 Academy Manufactor
1 Idol of Oblivion
1 Inspiring Statuary
1 Nettlecyst
1 Psychosis Crawler
1 Canopy Vista
1 Exotic Orchard
1 Irrigated Farmland
1 Prairie Stream
1 Scattered Groves
1 Skycloud Expanse
1 Spire of Industry
1 Sungrass Prairie
1 Temple of Enlightenment
1 Temple of Mystery
1 Temple of Plenty
1 Swords to Plowshares
1 Erdwal Illuminator
1 Junk Winder
1 Ongoing Investigation
1 Whirler Rogue
1 Graf Mole
1 Ulvenwald Mysteries
1 Wilderness Reclamation
1 Wavesifter
1 Arcane Signet
1 Azorius Signet
1 Simic Signet
1 Sol Ring
1 Talisman of Curiosity
1 Talisman of Progress
1 Talisman of Unity
1 Azorius Chancery
1 Magnifying Glass
1 Command Tower
1 Krosan Verge
1 Lonely Sandbar
1 Path of Ancestry
1 Reliquary Tower
1 Seaside Citadel
1 Secluded Steppe
1 Selesnya Sanctuary
1 Simic Growth Chamber
1 Temple of the False God
1 Tranquil Thicket
2 Plains
1 Plains
3 Island
3 Island
3 Forest
2 Forest
+93
View File
@@ -0,0 +1,93 @@
[metadata]
Name=Desert Bloom
[Commander]
1 Yuma, Proud Protector
[Main]
1 Kirri, Talented Sprout
1 Scavenger Grounds
1 Sun Titan
1 Omnath, Locus of Rage
1 Descend upon the Sinful
1 Chromatic Lantern
1 Marshal's Anthem
1 Sheltered Thicket
1 Scute Swarm
1 Hour of Promise
1 Oracle of Mul Daya
1 Ramunap Excavator
1 Scattered Groves
1 World Shaper
1 Nesting Dragon
1 Turntimber Sower
1 Sevinne's Reclamation
1 Ancient Greenwarden
1 Titania, Protector of Argoth
1 Return of the Wildspeaker
1 Perennial Behemoth
1 Avenger of Zendikar
1 Hazezon, Shaper of Sand
1 Escape to the Wilds
1 Heaven // Earth
1 Genesis Hydra
1 Sunscorched Divide
1 The Mending of Dominaria
1 Decimate
1 Sand Scout
1 Embrace the Unknown
1 Dune Chanter
1 Cataclysmic Prospecting
1 Vengeful Regrowth
1 Angel of Indemnity
1 Cactus Preserve
1 Rumbleweed
1 Terramorphic Expanse
1 Evolving Wilds
1 Swiftfoot Boots
1 Explore
1 Sol Ring
1 Satyr Wayfinder
1 Perpetual Timepiece
1 Crawling Sensation
1 Painted Bluffs
1 Command Tower
1 Magmatic Insight
1 Krosan Verge
1 Desert of the True
1 Skullwinder
1 Desert of the Indomitable
1 Jungle Shrine
1 Bitter Reunion
1 Desert of the Fervent
1 Valorous Stance
1 Dunes of the Dead
1 Shefet Dunes
1 Hashep Oasis
1 Elvish Rejuvenator
1 Winding Way
1 Springbloom Druid
1 Arcane Signet
1 Unholy Heat
1 Thrilling Discovery
1 Electric Revelation
1 Eccentric Farmer
1 Harrow
1 Ramunap Ruins
1 Path to Exile
1 Requisition Raid
1 Bovine Intervention
1 Map the Frontier
1 Conduit Pylons
1 Mirage Mesa
1 Wreck and Rebuild
1 Angel of the Ruins
1 Bristling Backwoods
1 Creosote Heath
1 Abraded Bluffs
1 Scaretiller
1 Nantuko Cultivator
3 Plains
3 Plains
2 Mountain
2 Mountain
4 Forest
3 Forest
+98
View File
@@ -0,0 +1,98 @@
[metadata]
Name=Devour for Power
[Commander]
1 The Mimeoplasm
[Main]
1 Nezumi Graverobber // Nighteyes the Desecrator
1 Skullbriar, the Walking Grave
1 Riddlekeeper
1 Fleshbag Marauder
1 Eternal Witness
1 Troll Ascetic
1 Yavimaya Elder
1 Solemn Simulacrum
1 Brawn
1 Wonder
1 Sewer Nemesis
1 Gravedigger
1 Lhurgoyf
1 Dreamborn Muse
1 Mortivore
1 Desecrator Hag
1 Mulldrifter
1 Acidic Slime
1 Vulturous Zombie
1 Dark Hatchling
1 Extractor Demon
1 Scythe Specter
1 Wrexial, the Risen Deep
1 Vorosh, the Hunter
1 Triskelavus
1 Slipstream Eel
1 Butcher of Malakir
1 Patron of the Nezumi
1 Damia, Sage of Stone
1 Szadek, Lord of Secrets
1 Avatar of Woe
1 Artisan of Kozilek
1 Minds Aglow
1 Shared Trauma
1 Sign in Blood
1 Stitch Together
1 Cultivate
1 Windfall
1 Buried Alive
1 Syphon Mind
1 Unnerve
1 Rise from the Grave
1 Syphon Flesh
1 Living Death
1 Tribute to the Wild
1 Spell Crumple
1 Fact or Fiction
1 Relic Crush
1 Sol Ring
1 Dimir Signet
1 Golgari Signet
1 Lightning Greaves
1 Simic Signet
1 Oblivion Stone
1 Vow of Wildness
1 Vow of Flight
1 Vow of Malice
1 Memory Erosion
1 Grave Pact
1 Barren Moor
1 Command Tower
1 Dimir Aqueduct
1 Dreadship Reef
1 Golgari Rot Farm
1 Jwar Isle Refuge
1 Lonely Sandbar
1 Rupture Spire
1 Simic Growth Chamber
1 Svogthos, the Restless Tomb
1 Temple of the False God
1 Terramorphic Expanse
1 Tranquil Thicket
2 Forest
1 Forest
1 Forest
1 Forest
1 Forest
1 Forest
1 Forest
2 Island
1 Island
1 Island
1 Island
1 Island
1 Island
1 Island
2 Swamp
2 Swamp
2 Swamp
2 Swamp
1 Swamp
1 Swamp
1 Swamp
+89
View File
@@ -0,0 +1,89 @@
[metadata]
Name=Devour for Power
[Commander]
1 The Mimeoplasm
[Main]
1 Nezumi Graverobber // Nighteyes the Desecrator
1 Skullbriar, the Walking Grave
1 Riddlekeeper
1 Fleshbag Marauder
1 Eternal Witness
1 Troll Ascetic
1 Yavimaya Elder
1 Solemn Simulacrum
1 Brawn
1 Wonder
1 Sewer Nemesis
1 Gravedigger
1 Lhurgoyf
1 Dreamborn Muse
1 Mortivore
1 Desecrator Hag
1 Mulldrifter
1 Acidic Slime
1 Vulturous Zombie
1 Dark Hatchling
1 Extractor Demon
1 Scythe Specter
1 Wrexial, the Risen Deep
1 Vorosh, the Hunter
1 Triskelavus
1 Slipstream Eel
1 Butcher of Malakir
1 Patron of the Nezumi
1 Damia, Sage of Stone
1 Szadek, Lord of Secrets
1 Avatar of Woe
1 Artisan of Kozilek
1 Minds Aglow
1 Shared Trauma
1 Sign in Blood
1 Stitch Together
1 Cultivate
1 Windfall
1 Buried Alive
1 Syphon Mind
1 Unnerve
1 Rise from the Grave
1 Syphon Flesh
1 Living Death
1 Tribute to the Wild
1 Spell Crumple
1 Fact or Fiction
1 Relic Crush
1 Sol Ring
1 Dimir Signet
1 Golgari Signet
1 Lightning Greaves
1 Simic Signet
1 Oblivion Stone
1 Vow of Wildness
1 Vow of Flight
1 Vow of Malice
1 Memory Erosion
1 Grave Pact
1 Barren Moor
1 Command Tower
1 Dimir Aqueduct
1 Dreadship Reef
1 Golgari Rot Farm
1 Jwar Isle Refuge
1 Lonely Sandbar
1 Rupture Spire
1 Simic Growth Chamber
1 Svogthos, the Restless Tomb
1 Temple of the False God
1 Terramorphic Expanse
1 Tranquil Thicket
2 Forest
2 Forest
2 Forest
2 Forest
2 Island
2 Island
2 Island
2 Island
3 Swamp
3 Swamp
3 Swamp
2 Swamp
+82
View File
@@ -0,0 +1,82 @@
[metadata]
Name=Draconic Destruction
[Commander]
1 Atarka, World Render
[Main]
1 Akoum Hellkite
1 Chain Reaction
1 Crucible of Fire
1 Demanding Dragon
1 Dragonkin Berserker
1 Dragonmaster Outcast
1 Drakuseth, Maw of Flames
1 Dream Pillager
1 Flameblast Dragon
1 Hoard-Smelter Dragon
1 Magmaquake
1 Mordant Dragon
1 Runehorn Hellkite
1 Sarkhan, the Dragonspeaker
1 Scourge of Valkas
1 Spit Flame
1 Sweltering Suns
1 Thunderbreak Regent
1 Thundermaw Hellkite
1 Tyrant's Familiar
1 Verix Bladewing
1 Foe-Razer Regent
1 Frontier Siege
1 Hunter's Prowess
1 Primal Might
1 Shamanic Revelation
1 Clan Defiance
1 Harbinger of the Hunt
1 Dragon's Hoard
1 Steel Hellkite
1 Cinder Glade
1 Game Trail
1 Haven of the Spirit Dragon
1 Temple of Abandon
1 Dragon Mage
1 Dragon Tempest
1 Dragonlord's Servant
1 Dragonspeaker Shaman
1 Furnace Whelp
1 Provoke the Trolls
1 Rapacious Dragon
1 Unleash Fury
1 Vandalblast
1 Beast Within
1 Blossoming Defense
1 Cultivate
1 Drumhunter
1 Elemental Bond
1 Garruk's Uprising
1 Harmonize
1 Hunter's Insight
1 Loaming Shaman
1 Return to Nature
1 Sakura-Tribe Elder
1 Draconic Disciple
1 Fires of Yavimaya
1 Savage Ventmaw
1 Arcane Signet
1 Atarka Monument
1 Commander's Sphere
1 Sol Ring
1 Swiftfoot Boots
1 Talisman of Impulse
1 Command Tower
1 Kazandu Refuge
1 Path of Ancestry
1 Rugged Highlands
1 Shivan Oasis
1 Timber Gorge
5 Mountain
5 Mountain
4 Mountain
4 Mountain
3 Forest
3 Forest
3 Forest
3 Forest
+88
View File
@@ -0,0 +1,88 @@
[metadata]
Name=Draconic Dissent
[Commander]
1 Firkraag, Cunning Instigator
[Main]
1 Will Kenrith
1 Rowan Kenrith
1 Keiga, the Tide Star
1 Pursued Whale
1 Sly Instigator
1 Agitator Ant
1 Avatar of Slaughter
1 Brash Taunter
1 Chaos Dragon
1 Drakuseth, Maw of Flames
1 Geode Rager
1 Goblin Spymaster
1 Kazuul, Tyrant of the Cliffs
1 Ryusei, the Falling Star
1 Territorial Hellkite
1 Thunder Dragon
1 Vengeful Ancestor
1 Warmonger Hellkite
1 Niv-Mizzet, Parun
1 Solemn Simulacrum
1 Steel Hellkite
1 Stuffy Doll
1 Burnished Hart
1 Sprite Dragon
1 Aether Gale
1 Curse of the Swine
1 Blasphemous Act
1 Chain Reaction
1 Disrupt Decorum
1 Compulsive Research
1 Domineering Will
1 Reins of Power
1 Chaos Warp
1 Midnight Clock
1 Dragon's Hoard
1 Arcane Signet
1 Mind Stone
1 Wayfarer's Bauble
1 Bloodthirsty Blade
1 Fellwar Stone
1 Hedron Archive
1 Izzet Signet
1 Sol Ring
1 Talisman of Creativity
1 Dissipation Field
1 The Akroan War
1 Psychic Impetus
1 Shiny Impetus
1 Curse of Verbosity
1 Propaganda
1 Curse of Opulence
1 Castle Vantress
1 Desolate Lighthouse
1 Kher Keep
1 Temple of Epiphany
1 Wandering Fumarole
1 Command Tower
1 Ash Barrens
1 Izzet Boilerworks
1 Myriad Landscape
1 Path of Ancestry
1 Prismari Campus
1 Reliquary Tower
1 Temple of the False God
1 Terrain Generator
3 Island
3 Island
3 Island
3 Island
3 Mountain
3 Mountain
3 Mountain
3 Mountain
1 Baeloth Barrityl, Entertainer
1 Clan Crafter
1 Artificer Class
1 Astral Dragon
1 Mocking Doppelganger
1 Bothersome Quasit
1 Death Kiss
1 Loot Dispute
1 Spectacular Showdown
1 Angler Turtle
+101
View File
@@ -0,0 +1,101 @@
[metadata]
Name=Draconic Domination
[Commander]
1 The Ur-Dragon
[Main]
1 O-Kagachi, Vengeful Kami
1 Ramos, Dragon Engine
1 Scalelord Reckoner
1 Boneyard Scourge
1 Territorial Hellkite
1 Taigam, Ojutai Master
1 Wasitora, Nekoru Queen
1 Ryusei, the Falling Star
1 Scourge of Valkas
1 Utvara Hellkite
1 Sunscorch Regent
1 Deathbringer Regent
1 Hellkite Charger
1 Tyrant's Familiar
1 Atarka, World Render
1 Bladewing the Risen
1 Broodmate Dragon
1 Crosis, the Purger
1 Dromoka, the Eternal
1 Intet, the Dreamer
1 Kolaghan, the Storm's Fury
1 Niv-Mizzet, Dracogenius
1 Ojutai, Soul of Winter
1 Scion of the Ur-Dragon
1 Silumgar, the Drifting Death
1 Spellbound Dragon
1 Teneb, the Harvester
1 Steel Hellkite
1 Orator of Ojutai
1 Dragonlord's Servant
1 Dragonspeaker Shaman
1 Savage Ventmaw
1 Fortunate Few
1 Fractured Identity
1 Crux of Fate
1 Painful Truths
1 Earthquake
1 Cultivate
1 Farseek
1 Kodama's Reach
1 Rain of Thorns
1 Fist of Suns
1 Herald's Horn
1 Mirror of the Forebears
1 Armillary Sphere
1 Commander's Sphere
1 Darksteel Ingot
1 Dreamstone Hedron
1 Lightning Greaves
1 Nihil Spellbomb
1 Sol Ring
1 Wayfarer's Bauble
1 Kindred Discovery
1 Monastery Siege
1 Palace Siege
1 Crucible of Fire
1 Dragon Tempest
1 Frontier Siege
1 Curse of Verbosity
1 Curse of Opulence
1 Curse of Bounty
1 Elemental Bond
1 Crucible of the Spirit Dragon
1 Haven of the Spirit Dragon
2 Mountain
2 Mountain
2 Mountain
1 Swamp
1 Swamp
1 Swamp
1 Forest
1 Forest
1 Forest
1 Plains
1 Plains
1 Plains
1 Island
1 Island
1 Island
1 Path of Ancestry
1 Arcane Sanctum
1 Command Tower
1 Crumbling Necropolis
1 Frontier Bivouac
1 Jungle Shrine
1 Mystic Monastery
1 Nomad Outpost
1 Opulent Palace
1 Sandsteppe Citadel
1 Savage Lands
1 Seaside Citadel
1 Vivid Crag
1 Vivid Creek
1 Vivid Grove
1 Vivid Marsh
1 Vivid Meadow
+85
View File
@@ -0,0 +1,85 @@
[metadata]
Name=Draconic Rage
[Commander]
1 Vrondiss, Rage of Ancients
[Main]
1 Bogardan Hellkite
1 Demanding Dragon
1 Dragonmaster Outcast
1 Hoard-Smelter Dragon
1 Opportunistic Dragon
1 Scourge of Valkas
1 Shivan Hellkite
1 Skyline Despot
1 Skyship Stalker
1 Taurean Mauler
1 Terror of Mount Velus
1 Thunderbreak Regent
1 Chameleon Colossus
1 Atarka, World Render
1 Anger
1 Dragonlord's Servant
1 Savage Ventmaw
1 Dragonspeaker Shaman
1 Chain Reaction
1 Rishkar's Expertise
1 Shamanic Revelation
1 Rile
1 Cultivate
1 Explore
1 Rampant Growth
1 Magmaquake
1 Spit Flame
1 Decree of Savagery
1 Kindred Summons
1 Return of the Wildspeaker
1 Beast Within
1 Return to Nature
1 Dragon's Hoard
1 Arcane Signet
1 Commander's Sphere
1 Gruul Signet
1 Heirloom Blade
1 Sol Ring
1 Gratuitous Violence
1 Outpost Siege
1 Warstorm Surge
1 Colossal Majesty
1 Garruk's Uprising
1 Cinder Glade
1 Crucible of the Spirit Dragon
1 Exotic Orchard
1 Game Trail
1 Haven of the Spirit Dragon
1 Mossfire Valley
1 Mosswort Bridge
1 Command Tower
1 Desert
1 Gruul Turf
1 Path of Ancestry
3 Mountain
3 Mountain
3 Mountain
3 Mountain
4 Forest
4 Forest
4 Forest
3 Forest
1 Klauth, Unrivaled Ancient
1 Berserker's Frenzy
1 Chaos Dragon
1 Maddening Hex
1 Vengeful Ancestor
1 Bag of Tricks
1 Druid of Purification
1 Indomitable Might
1 Neverwinter Hydra
1 Wild Endeavor
1 Dragonborn Champion
1 Klauth's Will
1 Wulfgar of Icewind Dale
1 Barbarian Class
1 Earth-Cult Elemental
1 Component Pouch
1 Sword of Hours
1 Underdark Rift
+97
View File
@@ -0,0 +1,97 @@
[metadata]
Name=Dungeons of Death
[Commander]
1 Sefris of the Hidden Ways
[Main]
1 Cataclysmic Gearhulk
1 Eternal Dragon
1 Karmic Guide
1 Sun Titan
1 Sunblast Angel
1 Champion of Wits
1 Curator of Mysteries
1 Phantasmal Image
1 Doomed Necromancer
1 Ashen Rider
1 Baleful Strix
1 Hostage Taker
1 Solemn Simulacrum
1 Ronom Unicorn
1 Wall of Omens
1 Merfolk Looter
1 Mulldrifter
1 Murder of Crows
1 Plaguecrafter
1 Reassembling Skeleton
1 Shriekmaw
1 Cloudblazer
1 Necrotic Sliver
1 Obsessive Stitcher
1 Burnished Hart
1 Meteor Golem
1 Necromantic Selection
1 Unburial Rites
1 Victimize
1 Utter End
1 Swords to Plowshares
1 Forbidden Alchemy
1 Despark
1 Vanish into Memory
1 Arcane Signet
1 Commander's Sphere
1 Fellwar Stone
1 Sol Ring
1 Wayfarer's Bauble
1 Lightning Greaves
1 Propaganda
1 Choked Estuary
1 Darkwater Catacombs
1 Exotic Orchard
1 Geier Reach Sanitarium
1 High Market
1 Nimbus Maze
1 Port Town
1 Prairie Stream
1 Sunken Hollow
1 Evolving Wilds
1 Arcane Sanctum
1 Azorius Chancery
1 Command Tower
1 Dimir Aqueduct
1 Esper Panorama
1 Orzhov Basilica
1 Terramorphic Expanse
1 Thriving Heath
1 Thriving Isle
1 Thriving Moor
2 Plains
2 Plains
2 Plains
1 Plains
2 Island
1 Island
1 Island
1 Island
2 Swamp
2 Swamp
2 Swamp
1 Swamp
1 Nihiloor
1 Immovable Rod
1 Radiant Solar
1 Revivify
1 Thorough Investigation
1 Arcane Endeavor
1 Minn, Wily Illusionist
1 Phantom Steed
1 Rod of Absorption
1 Grave Endeavor
1 Wand of Orcus
1 Extract Brain
1 Midnight Pathlighter
1 Minimus Containment
1 Hama Pashar, Ruin Seeker
1 Dungeon Map
1 Bucknard's Everfull Purse
1 Clay Golem
1 Component Pouch
+104
View File
@@ -0,0 +1,104 @@
[metadata]
Name=Eldrazi Incursion
[Commander]
1 Ulalek, Fused Atrocity
[Main]
1 Azlask, the Swelling Scourge
1 Ulamog's Dreadsire
1 Benthic Anomaly
1 Eldritch Immunity
1 Selective Obliteration
1 Angelic Aberration
1 Bismuth Mindrender
1 Hideous Taskmaster
1 Twins of Discord
1 Mutated Cultist
1 Eldrazi Confluence
1 Spawnbed Protector
1 Chittering Dispatcher
1 Inversion Behemoth
1 Ugin, the Ineffable
1 Eldrazi Conscription
1 Cascading Cataracts
1 Eldrazi Monument
1 Sire of Stagnation
1 Shrine of the Forsaken Gods
1 Ugin's Insight
1 Drowner of Hope
1 Return of the Wildspeaker
1 Tendo Ice Bridge
1 Battlefield Forge
1 Llanowar Wastes
1 Brushland
1 Underground River
1 Oblivion Sower
1 Adarkar Wastes
1 Karplusan Forest
1 Yavimaya Coast
1 Sulfurous Springs
1 Caves of Koilos
1 Shivan Reef
1 Imprisoned in the Moon
1 Temple of Malady
1 Temple of Silence
1 Morophon, the Boundless
1 Kozilek's Return
1 World Breaker
1 Ruins of Oran-Rief
1 Sifter of Skulls
1 Endbringer
1 Corrupted Crossroads
1 Deepfathom Skulker
1 Vile Redeemer
1 Eldrazi Displacer
1 Mystic Forge
1 All Is Dust
1 Rishkar's Expertise
1 Awakening Zone
1 Eldrazi Temple
1 Elder Deep-Fiend
1 Bonders' Enclave
1 Exotic Orchard
1 Forsaken Monument
1 Snapping Voidcraw
1 Herald's Horn
1 Everflowing Chalice
1 Glaring Fleshraker
1 Wastescape Battlemage
1 Crib Swap
1 Titans' Vanguard
1 Talisman of Resilience
1 Hedron Archive
1 Spawning Bed
1 Ulamog's Nullifier
1 Twisted Landscape
1 Tranquil Landscape
1 Tectonic Edge
1 Reliquary Tower
1 Sol Ring
1 Artisan of Kozilek
1 Opal Palace
1 Dreamstone Hedron
1 Arcane Signet
1 Tomb of the Spirit Dragon
1 Secluded Courtyard
1 Idol of Oblivion
1 Wastes
1 Talisman of Curiosity
1 Talisman of Impulse
1 Warping Wail
1 Talisman of Dominance
1 Command Tower
1 Ash Barrens
1 Ancient Stirrings
1 Ulamog's Crusher
1 Garruk's Uprising
1 Suffer the Past
1 Skittering Invasion
1 Path of Ancestry
1 Unclaimed Territory
1 Plains
1 Island
1 Swamp
1 Mountain
1 Forest
@@ -0,0 +1,104 @@
[metadata]
Name=Eldrazi Incursion Collector's Edition
[Commander]
1 Ulalek, Fused Atrocity
[Main]
1 Azlask, the Swelling Scourge
1 Ulamog's Dreadsire
1 Benthic Anomaly
1 Eldritch Immunity
1 Selective Obliteration
1 Angelic Aberration
1 Bismuth Mindrender
1 Hideous Taskmaster
1 Twins of Discord
1 Mutated Cultist
1 Eldrazi Confluence
1 Spawnbed Protector
1 Chittering Dispatcher
1 Inversion Behemoth
1 Ugin, the Ineffable
1 Eldrazi Conscription
1 Cascading Cataracts
1 Eldrazi Monument
1 Sire of Stagnation
1 Shrine of the Forsaken Gods
1 Ugin's Insight
1 Drowner of Hope
1 Return of the Wildspeaker
1 Tendo Ice Bridge
1 Battlefield Forge
1 Llanowar Wastes
1 Brushland
1 Underground River
1 Oblivion Sower
1 Adarkar Wastes
1 Karplusan Forest
1 Yavimaya Coast
1 Sulfurous Springs
1 Caves of Koilos
1 Shivan Reef
1 Imprisoned in the Moon
1 Temple of Malady
1 Temple of Silence
1 Morophon, the Boundless
1 Kozilek's Return
1 World Breaker
1 Ruins of Oran-Rief
1 Sifter of Skulls
1 Endbringer
1 Corrupted Crossroads
1 Deepfathom Skulker
1 Vile Redeemer
1 Eldrazi Displacer
1 Mystic Forge
1 All Is Dust
1 Rishkar's Expertise
1 Awakening Zone
1 Eldrazi Temple
1 Elder Deep-Fiend
1 Bonders' Enclave
1 Exotic Orchard
1 Forsaken Monument
1 Snapping Voidcraw
1 Herald's Horn
1 Everflowing Chalice
1 Glaring Fleshraker
1 Wastescape Battlemage
1 Crib Swap
1 Titans' Vanguard
1 Talisman of Resilience
1 Hedron Archive
1 Spawning Bed
1 Ulamog's Nullifier
1 Twisted Landscape
1 Tranquil Landscape
1 Tectonic Edge
1 Reliquary Tower
1 Sol Ring
1 Artisan of Kozilek
1 Opal Palace
1 Dreamstone Hedron
1 Arcane Signet
1 Tomb of the Spirit Dragon
1 Secluded Courtyard
1 Idol of Oblivion
1 Wastes
1 Talisman of Curiosity
1 Talisman of Impulse
1 Warping Wail
1 Talisman of Dominance
1 Command Tower
1 Ash Barrens
1 Ancient Stirrings
1 Ulamog's Crusher
1 Garruk's Uprising
1 Suffer the Past
1 Skittering Invasion
1 Path of Ancestry
1 Unclaimed Territory
1 Plains
1 Island
1 Swamp
1 Mountain
1 Forest
+66 -65
View File
@@ -3,88 +3,89 @@ Name=Eldrazi Unbound
[Commander]
1 Zhulodok, Void Gorger
[Main]
1 Abstruse Archaic
1 All Is Dust
1 Ancient Stone Idol
1 Arcane Lighthouse
1 Arch of Orazca
1 Artisan of Kozilek
1 Bane of Bala Ged
1 Blast Zone
1 Bonders' Enclave
1 Burnished Hart
1 Calamity of the Titans
1 Crashing Drawbridge
1 Darksteel Monolith
1 Desecrate Reality
1 Dreamstone Hedron
1 Duplicant
1 Eldrazi Temple
1 Endbringer
1 Endless Atlas
1 Endless One
1 Everflowing Chalice
1 Fireshrieker
1 Flayer of Loyalties
1 Forge of Heroes
1 Forsaken Monument
1 Geier Reach Sanitarium
1 Geode Golem
1 Guildless Commons
1 Hangarback Walker
1 Hedron Archive
1 Investigator's Journal
1 It That Betrays
1 Kaldra Compleat
1 Omarthis, Ghostfire Initiate
1 Kozilek, the Great Distortion
1 Lightning Greaves
1 Mage-Ring Network
1 Abstruse Archaic
1 Calamity of the Titans
1 Desecrate Reality
1 Flayer of Loyalties
1 Rise of the Eldrazi
1 Skittering Cicada
1 Ugin's Mastery
1 Darksteel Monolith
1 All Is Dust
1 Endbringer
1 Endless One
1 It That Betrays
1 Matter Reshaper
1 Oblivion Sower
1 Ugin, the Ineffable
1 Ancient Stone Idol
1 Duplicant
1 Endless Atlas
1 Forsaken Monument
1 Hangarback Walker
1 Investigator's Journal
1 Kaldra Compleat
1 Mazemind Tome
1 Metalwork Colossus
1 Meteor Golem
1 Mind Stone
1 Mirage Mirror
1 Mirrorpool
1 Myriad Construct
1 Mystic Forge
1 Not of This World
1 Oblivion Sower
1 Omarthis, Ghostfire Initiate
1 Ornithopter of Paradise
1 Palladium Myr
1 Perilous Vault
1 Phyrexian Triniform
1 Reliquary Tower
1 Rise of the Eldrazi
1 Rogue's Passage
1 Solemn Simulacrum
1 Soul of New Phyrexia
1 Steel Hellkite
1 Stonecoil Serpent
1 Transmogrifying Wand
1 Arch of Orazca
1 Blast Zone
1 Bonders' Enclave
1 Geier Reach Sanitarium
1 Mirrorpool
1 Ruins of Oran-Rief
1 Scaretiller
1 Scavenger Grounds
1 Sea Gate Wreckage
1 Shrine of the Forsaken Gods
1 Skittering Cicada
1 Tyrite Sanctum
1 War Room
1 Burnished Hart
1 Geode Golem
1 Lightning Greaves
1 Meteor Golem
1 Sol Ring
1 Solemn Simulacrum
1 Soul of New Phyrexia
1 Spatial Contortion
1 Steel Hellkite
1 Stonecoil Serpent
1 Suspicious Bookcase
1 Temple of the False God
1 Thought Vessel
1 Thran Dynamo
1 Titan's Presence
1 Tomb of the Spirit Dragon
1 Transmogrifying Wand
1 Tyrite Sanctum
1 Ugin's Mastery
1 Ugin, the Ineffable
1 Unstable Obelisk
1 Reliquary Tower
1 Rogue's Passage
1 Artisan of Kozilek
1 Bane of Bala Ged
1 Not of This World
1 Spatial Contortion
1 Titan's Presence
1 Warping Wail
1 Crashing Drawbridge
1 Dreamstone Hedron
1 Everflowing Chalice
1 Fireshrieker
1 Hedron Archive
1 Mind Stone
1 Ornithopter of Paradise
1 Palladium Myr
1 Scaretiller
1 Suspicious Bookcase
1 Worn Powerstone
1 Arcane Lighthouse
1 Eldrazi Temple
1 Forge of Heroes
1 Guildless Commons
1 Mage-Ring Network
1 Temple of the False God
1 Tomb of the Spirit Dragon
1 Urza's Mine
1 Urza's Power Plant
1 Urza's Tower
1 War Room
1 Warping Wail
1 Worn Powerstone
15 Wastes
8 Wastes
7 Wastes
+82
View File
@@ -0,0 +1,82 @@
[metadata]
Name=Elven Council
[Commander]
1 Galadriel, Elven-Queen
[Main]
1 Gandalf, Westward Voyager
1 Raise the Palisade
1 Trap the Trespassers
1 Arwen, Weaver of Hope
1 Galadhrim Ambush
1 Haldir, Lórien Lieutenant
1 Legolas Greenleaf
1 Mirkwood Elk
1 Travel Through Caradhras
1 Windswift Slice
1 Círdan the Shipwright
1 Elrond of the White Council
1 Erestor of the Council
1 Mirkwood Trapper
1 Radagast, Wizard of Wilds
1 Sail into the West
1 Song of Eärendil
1 Lothlórien Blade
1 Model of Unity
1 Colossal Whale
1 Devastation Tide
1 Mystic Confluence
1 Plea for Power
1 Swan Song
1 Asceticism
1 Elvish Archdruid
1 Elvish Piper
1 Elvish Warmaster
1 Genesis Wave
1 Heroic Intervention
1 Hornet Queen
1 Inscription of Abundance
1 Overwhelming Stampede
1 Realm Seekers
1 Seeds of Renewal
1 Sylvan Offering
1 Exotic Orchard
1 Flooded Grove
1 Hinterland Harbor
1 Rejuvenating Springs
1 Vineglimmer Snarl
1 Lórien Revealed
1 Celeborn the Wise
1 Elven Farsight
1 Wose Pathfinder
1 Learn from the Past
1 Opt
1 Preordain
1 Arbor Elf
1 Beast Within
1 Cultivate
1 Elvish Mystic
1 Elvish Visionary
1 Farhaven Elf
1 Mirror of Galadriel
1 Lignify
1 Paradise Druid
1 Rampant Growth
1 Reclamation Sage
1 Wood Elves
1 Growth Spiral
1 Arcane Signet
1 Commander's Sphere
1 Lightning Greaves
1 Sol Ring
1 Whispersilk Cloak
1 Ash Barrens
1 Command Tower
1 Field of Ruin
1 Lonely Sandbar
1 Thornwood Falls
1 Tranquil Thicket
1 Woodland Stream
6 Island
5 Island
8 Forest
7 Forest
+31 -32
View File
@@ -4,68 +4,38 @@ Name=Elven Empire
1 Lathril, Blade of the Elves
[Main]
1 Abomination of Llanowar
1 Ambition's Cost
1 Arcane Signet
1 Beast Whisperer
1 Binding the Old Gods
1 Bounty of Skemfar
1 Canopy Tactician
1 Casualties of War
1 Command Tower
1 Crown of Skemfar
1 Cultivator of Blades
1 Dwynen, Gilt-Leaf Daen
1 Elderfang Ritualist
1 Elderfang Venom
1 Elven Ambush
1 Elvish Archdruid
1 Elvish Mystic
1 Elvish Promenade
1 Elvish Rejuvenator
1 End-Raze Forerunners
1 Eyeblight Cullers
1 Eyeblight Massacre
1 Farhaven Elf
1 Foul Orchard
1 Golgari Findbroker
1 Golgari Guildgate
1 Golgari Rot Farm
1 Harald, King of Skemfar
1 Harvest Season
1 Imperious Perfect
1 Jagged-Scar Archers
1 Jaspera Sentinel
1 Jungle Hollow
1 Llanowar Tribe
1 Lys Alana Huntmaster
1 Lys Alana Scarblade
1 Marwyn, the Nurturer
1 Masked Admirers
1 Miara, Thorn of the Glade
1 Moldervine Reclamation
1 Myriad Landscape
1 Nullmage Shepherd
1 Numa, Joraga Chieftain
1 Pact of the Serpent
1 Path of Ancestry
1 Poison the Cup
1 Poison-Tip Archer
1 Pride of the Perfect
1 Prowess of the Fair
1 Putrefy
1 Reclamation Sage
1 Return Upon the Tide
1 Rhys the Exiled
1 Roots of Wisdom
1 Ruthless Winnower
1 Serpent's Soul-Jar
1 Shaman of the Pack
1 Skemfar Elderhall
1 Skemfar Shadowsage
1 Sol Ring
1 Springbloom Druid
1 Sylvan Messenger
1 Tergrid's Shadow
1 Timberwatch Elf
1 Twinblade Assassins
1 Voice of Many
@@ -73,6 +43,35 @@ Name=Elven Empire
1 Wirewood Channeler
1 Wolverine Riders
1 Wood Elves
1 Elven Ambush
1 Poison the Cup
1 Putrefy
1 Tergrid's Shadow
1 Ambition's Cost
1 Bounty of Skemfar
1 Casualties of War
1 Eyeblight Massacre
1 Elvish Promenade
1 Harvest Season
1 Pact of the Serpent
1 Return Upon the Tide
1 Roots of Wisdom
1 Arcane Signet
1 Serpent's Soul-Jar
1 Sol Ring
1 Binding the Old Gods
1 Crown of Skemfar
1 Elderfang Venom
1 Pride of the Perfect
1 Prowess of the Fair
1 Moldervine Reclamation
1 Command Tower
14 Swamp
14 Forest
16 Forest
1 Foul Orchard
1 Golgari Guildgate
1 Golgari Rot Farm
1 Jungle Hollow
1 Myriad Landscape
1 Path of Ancestry
1 Skemfar Elderhall
13 Swamp
+91
View File
@@ -0,0 +1,91 @@
[metadata]
Name=Enduring Enchantments
[Commander]
1 Anikthea, Hand of Erebos
[Main]
1 Narci, Fable Singer
1 Battle at the Helvault
1 Boon of the Spirit Realm
1 Ondu Spiritdancer
1 Cacophony Unleashed
1 Demon of Fate's Design
1 Ghoulish Impetus
1 Composer of Spring
1 Nyxborn Behemoth
1 Archon of Sun's Grace
1 Felidar Retreat
1 Grasp of Fate
1 Heliod, God of the Sun
1 Mesa Enchantress
1 Sigil of the Empty Throne
1 Starfield Mystic
1 Starfield of Nyx
1 Cunning Rhetoric
1 Doomwake Giant
1 Dreadhorde Invasion
1 Erebos, Bleak-Hearted
1 Abundance
1 Arasta of the Endless Web
1 Courser of Kruphix
1 Dryad of the Ilysian Grove
1 Eidolon of Blossoms
1 Enchantress's Presence
1 Herald of the Pantheon
1 The Mending of Dominaria
1 Sanctum Weaver
1 Sandwurm Convergence
1 Setessan Champion
1 Verduran Enchantress
1 Battle for Bretagard
1 Calix, Destiny's Hand
1 Culling Ritual
1 Mirari's Wake
1 Sythis, Harvest's Hand
1 Canopy Vista
1 Exotic Orchard
1 Fortified Village
1 Necroblossom Snarl
1 Shineshadow Snarl
1 Sungrass Prairie
1 Temple of Malady
1 Temple of Plenty
1 Temple of Silence
1 Path to Exile
1 Extinguish All Hope
1 Kodama's Reach
1 Arcane Signet
1 Sol Ring
1 Ash Barrens
1 Command Tower
1 Cast Out
1 Love Song of Night and Day
1 Omen of the Sun
1 Spirited Companion
1 The Eldest Reborn
1 Mindwrack Harpy
1 The Binding of the Titans
1 Destiny Spinner
1 Farseek
1 Font of Fertility
1 Greater Tanuki
1 Khalni Heart Expedition
1 Nessian Wanderer
1 Omen of the Hunt
1 Rampant Growth
1 Binding the Old Gods
1 Jukai Naturalist
1 Nyx Weaver
1 Satyr Enchanter
1 Golgari Rot Farm
1 Krosan Verge
1 Orzhov Basilica
1 Sandsteppe Citadel
1 Selesnya Sanctuary
1 Tainted Field
1 Tainted Wood
3 Plains
3 Plains
3 Swamp
2 Swamp
4 Forest
4 Forest
+94
View File
@@ -0,0 +1,94 @@
[metadata]
Name=Enhanced Evolution
[Commander]
1 Otrimi, the Ever-Playful
[Main]
1 Nissa, Steward of Elements
1 Zaxara, the Exemplary
1 Cazur, Ruthless Stalker
1 Ukkima, Stalking Shadow
1 Souvenir Snatcher
1 Tidal Barracuda
1 Boneyard Mycodrax
1 Mindleecher
1 Capricopian
1 Sawtusk Demolisher
1 Beast Whisperer
1 Genesis Hydra
1 Hungering Hydra
1 Masked Admirers
1 Predator Ooze
1 Vastwood Hydra
1 Vorapede
1 Cold-Eyed Selkie
1 Wydwen, the Biting Gale
1 Silent Arbiter
1 Archipelagore
1 Dreamtail Heron
1 Pouncing Shoreshark
1 Cavern Whisperer
1 Chittering Harvester
1 Insatiable Hemophage
1 Auspicious Starrix
1 Fertilid
1 Glowstone Recluse
1 Migratory Greathorn
1 Boneyard Lurker
1 Trumpeting Gnarr
1 Illusory Ambusher
1 Mulldrifter
1 Shriekmaw
1 Heroes' Bane
1 Reclamation Sage
1 Yavimaya Dryad
1 Trygon Predator
1 Dredge the Mire
1 Mind Spring
1 Deadly Tempest
1 Profane Command
1 Animist's Awakening
1 Find // Finality
1 Gaze of Granite
1 Villainous Wealth
1 Migration Path
1 Kodama's Reach
1 Deadly Rollick
1 Beast Within
1 Krosan Grip
1 Putrefy
1 Manascape Refractor
1 Lifecrafter's Bestiary
1 Bonder's Ornament
1 Arcane Signet
1 Sol Ring
1 Psychic Impetus
1 Parasitic Impetus
1 Predatory Impetus
1 Propaganda
1 Darkwater Catacombs
1 Endless Sands
1 Exotic Orchard
1 Llanowar Wastes
1 Sunken Hollow
1 Blighted Woodland
1 Command Tower
1 Dimir Aqueduct
1 Golgari Rot Farm
1 Mortuary Mire
1 Myriad Landscape
1 Opulent Palace
1 Rogue's Passage
1 Simic Growth Chamber
1 Soaring Seacliff
1 Temple of the False God
1 Dismal Backwater
1 Jungle Hollow
1 Thornwood Falls
2 Swamp
2 Swamp
1 Swamp
4 Forest
4 Forest
3 Forest
1 Island
1 Island
+96
View File
@@ -0,0 +1,96 @@
[metadata]
Name=Entropic Uprising
[Commander]
1 Yidris, Maelstrom Wielder
[Main]
1 Satyr Wayfinder
1 Wall of Blossoms
1 Coiling Oracle
1 Thrasios, Triton Hero
1 Goblin Spymaster
1 Spellheart Chimera
1 Vial Smasher the Fierce
1 Academy Elite
1 Gamekeeper
1 Sangromancer
1 Bloodbraid Elf
1 Horizon Chimera
1 Kydele, Chosen of Kruphix
1 Glint-Eye Nephilim
1 Aeon Chronicler
1 Guiltfeeder
1 Consuming Aberration
1 Nath of the Gilt-Leaf
1 Runehorn Hellkite
1 Etherium-Horn Sorcerer
1 Dragon Mage
1 Blood Tyrant
1 Wheel of Fate
1 Windfall
1 Parting Thoughts
1 Far Wanderings
1 Past in Flames
1 Whispering Madness
1 Decimate
1 Devastation Tide
1 Reforge the Soul
1 Worm Harvest
1 Spelltwine
1 Whims of the Fates
1 Grave Upheaval
1 Cruel Entertainment
1 Ghastly Conscription
1 Volcanic Vision
1 Treasure Cruise
1 Treacherous Terrain
1 Army of the Damned
1 In Garruk's Wake
1 Chain of Vapor
1 Rakdos Charm
1 Chaos Warp
1 Ancient Excavation
1 Evacuation
1 Bituminous Blast
1 Curtains' Call
1 Sol Ring
1 Fellwar Stone
1 Rakdos Signet
1 Simic Signet
1 Chromatic Lantern
1 Commander's Sphere
1 Whispersilk Cloak
1 Boompile
1 Burgeoning
1 Waste Not
1 Frenzied Fugue
1 Ash Barrens
1 Command Tower
1 Crumbling Necropolis
1 Dismal Backwater
1 Evolving Wilds
1 Exotic Orchard
1 Frontier Bivouac
1 Jungle Hollow
1 Opulent Palace
1 Rakdos Carnarium
1 Reliquary Tower
1 Rugged Highlands
1 Rupture Spire
1 Savage Lands
1 Shadowblood Ridge
1 Simic Growth Chamber
1 Swiftwater Cliffs
1 Terramorphic Expanse
1 Thornwood Falls
2 Island
2 Island
1 Island
2 Swamp
2 Swamp
1 Swamp
2 Mountain
2 Mountain
1 Mountain
2 Forest
2 Forest
1 Forest
+92
View File
@@ -0,0 +1,92 @@
[metadata]
Name=Eternal Bargain
[Commander]
1 Oloro, Ageless Ascetic
[Main]
1 Ajani's Pridemate
1 Augury Adept
1 Azorius Herald
1 Disciple of Griselbrand
1 Diviner Spirit
1 Divinity of Pride
1 Filigree Angel
1 Hooded Horror
1 Kongming, "Sleeping Dragon"
1 Marrow Bats
1 Myr Battlesphere
1 Phyrexian Delver
1 Phyrexian Gargantua
1 Raven Familiar
1 Razor Hippogriff
1 Serene Master
1 Serra Avatar
1 Sharding Sphinx
1 Sharuum the Hegemon
1 Sphinx of the Steel Wind
1 Stormscape Battlemage
1 Sydri, Galvanic Genius
1 Tidal Force
1 Tidehollow Strix
1 Tower Gargoyle
1 Vizkopa Guildmage
1 Wall of Reverence
1 Brilliant Plan
1 Death Grasp
1 Deep Analysis
1 Famine
1 Order of Succession
1 Survival Cache
1 Tempt with Immortality
1 Toxic Deluge
1 Dromar's Charm
1 Lim-Dûl's Vault
1 Reckless Spite
1 Spinal Embrace
1 Crawlspace
1 Nevinyrral's Disk
1 Nihil Spellbomb
1 Obelisk of Esper
1 Pristine Talisman
1 Sol Ring
1 Sun Droplet
1 Swiftfoot Boots
1 Thopter Foundry
1 Well of Lost Dreams
1 Act of Authority
1 Cradle of Vitality
1 Curse of Inertia
1 Curse of Shallow Graves
1 Curse of the Forsaken
1 Darksteel Mutation
1 Greed
1 Phyrexian Reclamation
1 Sanguine Bond
1 Arcane Sanctum
1 Azorius Chancery
1 Azorius Guildgate
1 Barren Moor
1 Command Tower
1 Dimir Guildgate
1 Esper Panorama
1 Evolving Wilds
2 Island
2 Island
1 Island
1 Island
1 Jwar Isle Refuge
1 Lonely Sandbar
1 Opal Palace
1 Orzhov Basilica
1 Orzhov Guildgate
3 Plains
2 Plains
2 Plains
2 Plains
1 Rupture Spire
1 Springjack Pasture
3 Swamp
2 Swamp
2 Swamp
2 Swamp
1 Temple of the False God
1 Transguild Promenade
+93
View File
@@ -0,0 +1,93 @@
[metadata]
Name=Eternal Might
[Commander]
1 Temmet, Naktamun's Will
[Main]
1 Hashaton, Scarab's Fist
1 On Wings of Gold
1 Priest of the Crossing
1 Renewed Solidarity
1 Wizened Mentor
1 Prophet of the Scarab
1 Rhet-Tomb Mystic
1 Lost Monarch of Ifnir
1 Accursed Duneyard
1 Commence the Endgame
1 Cryptbreaker
1 Grave Titan
1 Gravecrawler
1 Midnight Reaper
1 Murderous Rider // Swift End
1 Zombie Master
1 Angel of Sanctions
1 Dusk // Dawn
1 God-Eternal Oketra
1 Timeless Dragon
1 Champion of Wits
1 Forgotten Creation
1 Pull from Tomorrow
1 Vizier of Many Faces
1 Archfiend of Ifnir
1 Cemetery Reaper
1 Crowded Crypt
1 Damn
1 Dread Summons
1 Dreadhorde Invasion
1 Liliana, Death's Majesty
1 Never // Return
1 Plague Belcher
1 Rot Hulk
1 The Scarab God
1 God-Pharaoh's Gift
1 Maskwood Nexus
1 Adarkar Wastes
1 Caves of Koilos
1 Drowned Catacomb
1 Exotic Orchard
1 Fetid Pools
1 Glacial Fortress
1 Irrigated Farmland
1 Isolated Chapel
1 Prairie Stream
1 Sunken Hollow
1 Temple of Deceit
1 Temple of Silence
1 Underground River
1 Unholy Grotto
1 Swords to Plowshares
1 Corpse Augur
1 Corpse Knight
1 Arcane Signet
1 Sol Ring
1 Command Tower
1 Path of Ancestry
1 Binding Mummy
1 Cast Out
1 Eternal Skylord
1 Fleshbag Marauder
1 Gempalm Polluter
1 Lord of the Accursed
1 Twisted Abomination
1 Undead Augur
1 Despark
1 Gleaming Overseer
1 Lazotep Chancellor
1 Wayward Servant
1 Bontu's Monument
1 Commander's Sphere
1 Dimir Signet
1 Gate to the Afterlife
1 Orzhov Signet
1 Talisman of Dominance
1 Talisman of Hierarchy
1 Arcane Sanctum
1 Ash Barrens
1 Desert of the Glorified
1 Desert of the Mindful
1 Desert of the True
1 Evolving Wilds
1 Orzhov Basilica
1 Terramorphic Expanse
5 Plains
4 Island
5 Swamp
+96
View File
@@ -0,0 +1,96 @@
[metadata]
Name=Evasive Maneuvers
[Commander]
1 Derevi, Empyrial Tactician
[Main]
1 Acidic Slime
1 Aerie Mystics
1 Angel of Finality
1 Azami, Lady of Scrolls
1 Bane of Progress
1 Deceiver Exarch
1 Diviner Spirit
1 Djinn of Infinite Deceits
1 Dungeon Geists
1 Farhaven Elf
1 Fiend Hunter
1 Flickerwisp
1 Hada Spy Patrol
1 Karmic Guide
1 Kazandu Tuskcaller
1 Lu Xun, Scholar General
1 Mirror Entity
1 Mistmeadow Witch
1 Murkfiend Liege
1 Phantom Nantuko
1 Pilgrim's Eye
1 Roon of the Hidden Realm
1 Rubinia Soulsinger
1 Selesnya Guildmage
1 Skyward Eye Prophets
1 Stonecloaker
1 Thornwind Faeries
1 Winged Coatl
1 Wonder
1 Borrowing 100,000 Arrows
1 Kirtar's Wrath
1 Restore
1 Tempt with Glory
1 Wash Out
1 Aethermage's Touch
1 Arcane Denial
1 Blue Sun's Zenith
1 Krosan Grip
1 Selesnya Charm
1 Unexpectedly Absent
1 Azorius Keyrune
1 Basalt Monolith
1 Conjurer's Closet
1 Darksteel Ingot
1 Leonin Bladetrap
1 Selesnya Signet
1 Simic Signet
1 Sol Ring
1 Surveyor's Scope
1 Swiftfoot Boots
1 Sword of the Paruns
1 Thousand-Year Elixir
1 Thunderstaff
1 Control Magic
1 Curse of Inertia
1 Curse of Predation
1 Curse of the Forsaken
1 Darksteel Mutation
1 Flickerform
1 Leafdrake Roost
1 Presence of Gond
1 Azorius Chancery
1 Azorius Guildgate
1 Bant Panorama
1 Command Tower
1 Evolving Wilds
1 Faerie Conclave
2 Forest
2 Forest
1 Forest
1 Forest
2 Island
2 Island
2 Island
1 Island
1 Opal Palace
2 Plains
2 Plains
2 Plains
1 Plains
1 Rupture Spire
1 Saltcrusted Steppe
1 Seaside Citadel
1 Secluded Steppe
1 Sejiri Refuge
1 Selesnya Guildgate
1 Selesnya Sanctuary
1 Simic Guildgate
1 Temple of the False God
1 Terramorphic Expanse
1 Transguild Promenade
+101
View File
@@ -0,0 +1,101 @@
[metadata]
Name=Evasive Maneuvers
[Commander]
1 Derevi, Empyrial Tactician
[Main]
1 Acidic Slime
1 Aerie Mystics
1 Angel of Finality
1 Azami, Lady of Scrolls
1 Bane of Progress
1 Deceiver Exarch
1 Diviner Spirit
1 Djinn of Infinite Deceits
1 Dungeon Geists
1 Farhaven Elf
1 Fiend Hunter
1 Flickerwisp
1 Hada Spy Patrol
1 Karmic Guide
1 Kazandu Tuskcaller
1 Lu Xun, Scholar General
1 Mirror Entity
1 Mistmeadow Witch
1 Murkfiend Liege
1 Phantom Nantuko
1 Pilgrim's Eye
1 Roon of the Hidden Realm
1 Rubinia Soulsinger
1 Selesnya Guildmage
1 Skyward Eye Prophets
1 Stonecloaker
1 Thornwind Faeries
1 Winged Coatl
1 Wonder
1 Borrowing 100,000 Arrows
1 Kirtar's Wrath
1 Restore
1 Tempt with Glory
1 Wash Out
1 Aethermage's Touch
1 Arcane Denial
1 Blue Sun's Zenith
1 Krosan Grip
1 Selesnya Charm
1 Unexpectedly Absent
1 Azorius Keyrune
1 Basalt Monolith
1 Conjurer's Closet
1 Darksteel Ingot
1 Leonin Bladetrap
1 Selesnya Signet
1 Simic Signet
1 Sol Ring
1 Surveyor's Scope
1 Swiftfoot Boots
1 Sword of the Paruns
1 Thousand-Year Elixir
1 Thunderstaff
1 Control Magic
1 Curse of Inertia
1 Curse of Predation
1 Curse of the Forsaken
1 Darksteel Mutation
1 Flickerform
1 Leafdrake Roost
1 Presence of Gond
1 Azorius Chancery
1 Azorius Guildgate
1 Bant Panorama
1 Command Tower
1 Evolving Wilds
1 Faerie Conclave
1 Forest
1 Forest
1 Forest
1 Forest
1 Forest
1 Forest
2 Island
2 Island
2 Island
1 Island
1 Opal Palace
1 Plains
1 Plains
1 Plains
1 Plains
1 Plains
1 Plains
1 Plains
1 Rupture Spire
1 Saltcrusted Steppe
1 Seaside Citadel
1 Secluded Steppe
1 Sejiri Refuge
1 Selesnya Guildgate
1 Selesnya Sanctuary
1 Simic Guildgate
1 Temple of the False God
1 Terramorphic Expanse
1 Transguild Promenade
+104
View File
@@ -0,0 +1,104 @@
[metadata]
Name=Everyone's Invited!
[Commander]
1 Morophon, the Boundless
[Main]
1 Raise the Palisade
1 Bitterblossom
1 Taurean Mauler
1 Avenger of Zendikar
1 Coat of Arms
1 Kindred Summons
1 Maskwood Nexus
1 Sol Ring
1 Tendershoot Dryad
1 Adarkar Wastes
1 Amoeboid Changeling
1 Ancient Amphitheater
1 Arcane Adaptation
1 Arcane Denial
1 Arcane Sanctum
1 Arcane Signet
1 Atla Palani, Nest Tender
1 Beast Within
1 Black Market Connections
1 Bloodline Pretender
1 Brenard, Ginger Sculptor
1 Bruse Tarl, Roving Rancher
1 Brushland
1 Chameleon Colossus
1 Cloudshredder Sliver
1 Command Tower
1 Crib Swap
1 Cultivate
1 Darkwater Catacombs
1 Distant Melody
1 Double Down
1 Exotic Orchard
1 Fabled Passage
1 Farseek
1 Feline Sovereign
1 Fire-Belly Changeling
1 Frontier Bivouac
1 Gemhide Sliver
1 Gilt-Leaf Palace
1 Graveshifter
1 Guardian Gladewalker
1 Harabaz Druid
1 Harper Recruiter
1 Impostor of the Sixth Pride
1 Jungle Shrine
1 Karplusan Forest
1 Kindred Discovery
1 Kindred Dominance
1 Kinsbaile Cavalier
1 Kirri, Talented Sprout
1 Kodama's Reach
1 Llanowar Wastes
1 Magda, Brazen Outlaw
1 Manaweft Sliver
1 Masked Vandal
1 Mirror Entity
1 Moritte of the Frost
1 Mossfire Valley
1 Mothdust Changeling
1 Murmuring Bosk
1 Mutavault
1 Nameless Inversion
1 Opulent Palace
1 Overflowing Basin
1 Path of Ancestry
1 Pongify
1 Realmbreaker, the Invasion Tree
1 Realmwalker
1 Rin and Seri, Inseparable
1 Risen Reef
1 Rukarumel, Biologist
1 Seaside Citadel
1 Secluded Glen
1 Shapesharer
1 Shields of Velis Vel
1 Skeletal Changeling
1 Sophia, Dogged Detective
1 Spoils of Adventure
1 Stick Together
1 Sungrass Prairie
1 Tazri, Beacon of Unity
1 The Bears of Littjara
1 The World Tree
1 Universal Automaton
1 Unsettled Mariner
1 Wanderwine Hub
1 Yavimaya Coast
1 Forest
1 Forest
1 Forest
1 Plains
1 Plains
1 Plains
1 Mountain
1 Mountain
1 Island
1 Island
1 Island
1 Swamp
+89
View File
@@ -0,0 +1,89 @@
[metadata]
Name=Exit from Exile
[Commander]
1 Faldorn, Dread Wolf Herald
[Main]
1 Vivien, Champion of the Wilds
1 Xenagos, the Reveler
1 Aurora Phoenix
1 Bonecrusher Giant // Stomp
1 Dire Fleet Daredevil
1 Dream Pillager
1 Etali, Primal Storm
1 Greater Gargadon
1 Izzet Chemister
1 Laelia, the Blade Reforged
1 Tectonic Giant
1 Urabrask the Hidden
1 Wild-Magic Sorcerer
1 Arasta of the Endless Web
1 Battle Mammoth
1 End-Raze Forerunners
1 Hornet Queen
1 Lovestruck Beast // Heart's Desire
1 Managorger Hydra
1 Sweet-Gum Recluse
1 Embereth Shieldbreaker // Battle Display
1 Sakura-Tribe Elder
1 Bloodbraid Elf
1 Grumgully, the Generous
1 Ignite the Future
1 Jeska's Will
1 Mizzium Mortars
1 Ezuri's Predation
1 Escape to the Wilds
1 Nature's Lore
1 Light Up the Stage
1 Beanstalk Giant // Fertile Footsteps
1 Cultivate
1 Explore
1 Kodama's Reach
1 Search for Tomorrow
1 Terramorph
1 Three Visits
1 Return of the Wildspeaker
1 Demon Bolt
1 Beast Within
1 Natural Reclamation
1 Chaos Wand
1 Arcane Signet
1 Sol Ring
1 Outpost Siege
1 Stolen Strategy
1 Warstorm Surge
1 Primeval Bounty
1 Sandwurm Convergence
1 Castle Embereth
1 Cinder Glade
1 Game Trail
1 Kessig Wolf Run
1 Mossfire Valley
1 Mosswort Bridge
1 Raging Ravine
1 Spinerock Knoll
1 Temple of Abandon
1 Command Tower
1 Ash Barrens
1 Blighted Woodland
1 Gruul Turf
1 Highland Forest
1 Myriad Landscape
1 Temple of the False God
3 Mountain
3 Mountain
3 Mountain
2 Mountain
3 Forest
3 Forest
3 Forest
3 Forest
1 Durnan of the Yawning Portal
1 Passionate Archaeologist
1 Delayed Blast Fireball
1 Nalfeshnee
1 Green Slime
1 Journey to the Lost City
1 Tlincalli Hunter // Retrieve Prey
1 Venture Forth
1 Sarevok's Tome
1 Volcanic Torrent
+77 -75
View File
@@ -3,84 +3,86 @@ Name=Explorers of the Deep
[Commander]
1 Hakbal of the Surging Soul
[Main]
1 Aetherize
1 Alchemist's Refuge
1 Arcane Signet
1 Beast Within
1 Benthic Biomancer
1 Branching Evolution
1 Bygone Marvels
1 Cold-Eyed Selkie
1 Commander's Sphere
1 Commit // Memory
1 Command Tower
1 Coralhelm Commander
1 Curse of the Swine
1 Deeproot Elite
1 Deeproot Historian
1 Deeproot Waters
1 Emperor Mihail II
1 Evolution Sage
1 Explore
1 Growth Spiral
1 Hardened Scales
1 Herald of Secret Streams
1 Hinterland Harbor
1 Inspiring Call
1 Karn's Bastion
1 Kiora's Follower
1 Kindred Discovery
1 Kodama's Reach
1 Kopala, Warden of Waves
1 Kumena, Tyrant of Orazca
1 Llanowar Reborn
1 Master of the Pearl Trident
1 Merfolk Cave-Diver
1 Merfolk Mistbinder
1 Merfolk Skydiver
1 Merfolk Sovereign
1 Merrow Reejerey
1 Metallic Mimic
1 Xolatoyac, the Smiling Flood
1 Mist Dancer
1 Mosswort Bridge
1 Myriad Landscape
1 Nicanzil, Current Conductor
1 Path of Ancestry
1 Prime Speaker Zegana
1 Quandrix Command
1 Rapid Hybridization
1 Ravenform
1 Realmwalker
1 Reflections of Littjara
1 Reliquary Tower
1 Ripples of Potential
1 Rogue's Passage
1 Ruinous Intrusion
1 Sage of Fables
1 Seafloor Oracle
1 Secluded Courtyard
1 Simic Ascendancy
1 Simic Growth Chamber
1 Simic Signet
1 Singer of Swift Rivers
1 Sol Ring
1 Stonybrook Banneret
1 Surgespanner
1 Svyelun of Sea and Sky
1 Swiftfoot Boots
1 Tatyova, Benthic Druid
1 Temple of Mystery
1 Temple of the False God
1 Thassa, God of the Sea
1 Thieving Skydiver
1 Tishana, Voice of Thunder
1 Wave Goodbye
1 Bygone Marvels
1 Deeproot Historian
1 Topography Tracker
1 Tributary Instructor
1 Unclaimed Territory
1 Vineglimmer Snarl
1 Singer of Swift Rivers
1 Benthic Biomancer
1 Commit // Memory
1 Coralhelm Commander
1 Curse of the Swine
1 Emperor Mihail II
1 Herald of Secret Streams
1 Kindred Discovery
1 Kopala, Warden of Waves
1 Master of the Pearl Trident
1 Merfolk Sovereign
1 Reflections of Littjara
1 Seafloor Oracle
1 Surgespanner
1 Svyelun of Sea and Sky
1 Thassa, God of the Sea
1 Thieving Skydiver
1 Branching Evolution
1 Deeproot Elite
1 Hardened Scales
1 Realmwalker
1 Ruinous Intrusion
1 Cold-Eyed Selkie
1 Kumena, Tyrant of Orazca
1 Prime Speaker Zegana
1 Quandrix Command
1 Simic Ascendancy
1 Tishana, Voice of Thunder
1 Vorel of the Hull Clade
1 Wave Goodbye
1 Xolatoyac, the Smiling Flood
1 Zegana, Utopian Speaker
10 Island
10 Forest
1 Metallic Mimic
1 Alchemist's Refuge
1 Hinterland Harbor
1 Karn's Bastion
1 Mosswort Bridge
1 Temple of Mystery
1 Vineglimmer Snarl
1 Merfolk Cave-Diver
1 Aetherize
1 Deeproot Waters
1 Merrow Reejerey
1 Rapid Hybridization
1 Ravenform
1 Sage of Fables
1 Stonybrook Banneret
1 Beast Within
1 Nicanzil, Current Conductor
1 Evolution Sage
1 Explore
1 Inspiring Call
1 Kodama's Reach
1 Growth Spiral
1 Kiora's Follower
1 Merfolk Mistbinder
1 Merfolk Skydiver
1 Tatyova, Benthic Druid
1 Arcane Signet
1 Commander's Sphere
1 Simic Signet
1 Sol Ring
1 Swiftfoot Boots
1 Command Tower
1 Llanowar Reborn
1 Myriad Landscape
1 Path of Ancestry
1 Reliquary Tower
1 Rogue's Passage
1 Secluded Courtyard
1 Simic Growth Chamber
1 Temple of the False God
1 Unclaimed Territory
4 Forest
3 Forest
7 Island
6 Island
+83
View File
@@ -0,0 +1,83 @@
[metadata]
Name=Exquisite Invention
[Commander]
1 Saheeli, the Gifted
[Main]
1 Maverick Thopterist
1 Soul of New Phyrexia
1 Inkwell Leviathan
1 Sharding Sphinx
1 Hellkite Igniter
1 Bosh, Iron Golem
1 Darksteel Juggernaut
1 Duplicant
1 Myr Battlesphere
1 Psychosis Crawler
1 Scuttling Doom Engine
1 Steel Hellkite
1 Thopter Assembly
1 Etherium Sculptor
1 Whirler Rogue
1 Chief of the Foundry
1 Pilgrim's Eye
1 Saheeli's Artistry
1 Blasphemous Act
1 Reverse Engineer
1 Tidings
1 Chaos Warp
1 Magmaquake
1 Into the Roil
1 Thirst for Knowledge
1 Blinkmoth Urn
1 Mimic Vat
1 Mirrorworks
1 Prototype Portal
1 Unwinding Clock
1 Commander's Sphere
1 Dreamstone Hedron
1 Hedron Archive
1 Izzet Signet
1 Magnifying Glass
1 Mind Stone
1 Prismatic Lens
1 Scrabbling Claws
1 Sol Ring
1 Swiftfoot Boots
1 Unstable Obelisk
1 Vessel of Endless Rest
1 Worn Powerstone
1 Darksteel Citadel
1 Great Furnace
1 Seat of the Synod
1 Thopter Spy Network
5 Island
5 Island
5 Island
4 Mountain
4 Mountain
4 Mountain
1 Buried Ruin
1 Command Tower
1 Foundry of the Consuls
1 Highland Lake
1 Izzet Boilerworks
1 Izzet Guildgate
1 Swiftwater Cliffs
1 Brudiclad, Telchor Engineer
1 Tawnos, Urza's Apprentice
1 Echo Storm
1 Vedalken Humiliator
1 Enchanter's Bane
1 Saheeli's Directive
1 Treasure Nabber
1 Varchild, Betrayer of Kjeldor
1 Ancient Stone Idol
1 Coveted Jewel
1 Endless Atlas
1 Retrofitter Foundry
1 Aether Gale
1 Loyal Drake
1 Loyal Apprentice
1 Geode Golem
1 Thopter Engineer
1 Forge of Heroes
+98
View File
@@ -0,0 +1,98 @@
[metadata]
Name=Faceless Menace
[Commander]
1 Kadena, Slinking Sorcerer
[Main]
1 Vraska the Unseen
1 Rayami, First of the Fallen
1 Volrath, the Shapestealer
1 Kadena's Silencer
1 Thought Sponge
1 Thieving Amalgam
1 Apex Altisaur
1 Grismold, the Dreadsower
1 Deathmist Raptor
1 Hooded Hydra
1 Chromeshell Crab
1 Ixidron
1 Kheru Spellsnatcher
1 Stratus Dancer
1 Thousand Winds
1 Vesuvan Shapeshifter
1 Bane of the Living
1 Grim Haruspex
1 Silumgar Assassin
1 Den Protector
1 Seedborn Muse
1 Thelonite Hermit
1 Sagu Mauler
1 Voice of Many
1 Scaretiller
1 Willbender
1 Skinthinner
1 Ainok Survivalist
1 Great Oak Guardian
1 Nantuko Vigilante
1 Sakura-Tribe Elder
1 Icefeather Aven
1 Road of Return
1 Ghastly Conscription
1 Hex
1 Overwhelming Stampede
1 Tempt with Discovery
1 Mire in Misery
1 Tezzeret's Gambit
1 Cultivate
1 Explore
1 Farseek
1 Urban Evolution
1 Sudden Substitution
1 Biomass Mutation
1 Leadership Vacuum
1 Echoing Truth
1 Reality Shift
1 Putrefy
1 Sultai Charm
1 Pendant of Prosperity
1 Scroll of Fate
1 Strionic Resonator
1 Sol Ring
1 Thran Dynamo
1 Gift of Doom
1 Trail of Mystery
1 Bounty of the Luxa
1 Secret Plans
1 Darkwater Catacombs
1 Exotic Orchard
1 Llanowar Wastes
1 Shrine of the Forsaken Gods
1 Sunken Hollow
1 Thespian's Stage
1 Yavimaya Coast
2 Island
2 Island
1 Island
1 Swamp
1 Swamp
1 Swamp
3 Forest
2 Forest
2 Forest
1 Ash Barrens
1 Bojuka Bog
1 Command Tower
1 Dimir Aqueduct
1 Evolving Wilds
1 Foul Orchard
1 Golgari Guildgate
1 Golgari Rot Farm
1 Jungle Hollow
1 Myriad Landscape
1 Opulent Palace
1 Reliquary Tower
1 Simic Growth Chamber
1 Simic Guildgate
1 Temple of the False God
1 Terramorphic Expanse
1 Thornwood Falls
1 Woodland Stream
+83
View File
@@ -0,0 +1,83 @@
[metadata]
Name=Fae Dominion
[Commander]
1 Tegwyll, Duke of Splendor
[Main]
1 Alela, Cunning Conqueror
1 Archmage of Echoes
1 Malleable Impostor
1 Misleading Signpost
1 Shadow Puppeteers
1 Blightwing Bandit
1 Faerie Bladecrafter
1 Nettling Nuisance
1 Tegwyll's Scouring
1 Brazen Borrower // Petty Theft
1 Dig Through Time
1 Faerie Formation
1 Glen Elendra Archmage
1 Hullbreaker Horror
1 Illusionist's Gambit
1 Midnight Clock
1 Perplexing Test
1 Reflections of Littjara
1 Scion of Oona
1 Sower of Temptation
1 Theoretical Duplication
1 Kindred Dominance
1 Nightmare Unmaking
1 Puppeteer Clique
1 Rankle, Master of Pranks
1 Thrilling Encore
1 Glen Elendra Liege
1 Nymris, Oona's Trickster
1 Oona, Queen of the Fae
1 Choked Estuary
1 Darkwater Catacombs
1 Exotic Orchard
1 Secluded Glen
1 Sunken Hollow
1 Temple of Deceit
1 Mocking Sprite
1 Picklock Prankster // Free the Fae
1 Spell Stutter
1 Obyra, Dreaming Duelist
1 Spellscorn Coven // Take It Back
1 Arcane Denial
1 Cloud of Faeries
1 Consider
1 Distant Melody
1 Fact or Fiction
1 Faerie Seer
1 Frantic Search
1 Hypnotic Sprite // Mesmeric Glare
1 Keep Watch
1 Nightveil Sprite
1 Opt
1 Quickling
1 Reality Shift
1 Reconnaissance Mission
1 Repulse
1 Run Away Together
1 Snap
1 Reckless Spite
1 Halo Forager
1 Arcane Signet
1 Dimir Signet
1 Fellwar Stone
1 Mind Stone
1 Sol Ring
1 Talisman of Dominance
1 Wayfarer's Bauble
1 Bojuka Bog
1 Command Tower
1 Dimir Aqueduct
1 Faerie Conclave
1 Myriad Landscape
1 Path of Ancestry
1 Tainted Isle
1 Temple of the False God
7 Island
6 Island
6 Swamp
6 Swamp
+97
View File
@@ -0,0 +1,97 @@
[metadata]
Name=Family Matters
[Commander]
1 Zinnia, Valley's Voice
[Main]
1 Arthur, Marigold Knight
1 Elspeth, Sun's Champion
1 Jazal Goldmane
1 Martial Coup
1 Storm of Souls
1 Selfless Spirit
1 Murmuration
1 Blade Splicer
1 Hanged Executioner
1 Loyal Warhound
1 Restoration Angel
1 Jacked Rabbit
1 Skyclave Apparition
1 Dusk // Dawn
1 Boss's Chauffeur
1 Angel of the Ruins
1 Luminous Broodmoth
1 Sun Titan
1 Pollywog Prodigy
1 Fortune Teller's Talent
1 Aether Channeler
1 Pull from Tomorrow
1 Shield Broker
1 Stolen by the Fae
1 Rapid Augmenter
1 Bident of Thassa
1 Curiosity Crafter
1 Devilish Valet
1 Siege-Gang Commander
1 Echoing Assault
1 Agate Instigator
1 Calamity of Cinders
1 Rose Room Treasurer
1 Combat Celebrant
1 Inferno Titan
1 Time Wipe
1 Solemn Simulacrum
1 Helm of the Host
1 Glacial Fortress
1 Adarkar Wastes
1 Temple of Enlightenment
1 Castle Ardenvale
1 Seachrome Coast
1 Sulfur Falls
1 Cascade Bluffs
1 Exotic Orchard
1 Clifftop Retreat
1 Shivan Reef
1 Temple of Triumph
1 Battlefield Forge
1 Skycloud Expanse
1 Temple of Epiphany
1 Ferrous Lake
1 Rugged Prairie
1 Sunscorched Divide
1 Spirited Companion
1 Inspiring Overseer
1 Cut a Deal
1 Path to Exile
1 Illusory Ambusher
1 Rowdy Research
1 Plumecreed Escort
1 Rapid Hybridization
1 Junk Winder
1 Aetherize
1 Chart a Course
1 Tetsuko Umezawa, Fugitive
1 Thopter Engineer
1 Cloudblazer
1 Arcane Signet
1 Boros Signet
1 Ornithopter of Paradise
1 Azorius Signet
1 Izzet Signet
1 Circuit Mender
1 Fellwar Stone
1 Sol Ring
1 Mind Stone
1 Terramorphic Expanse
1 Path of Ancestry
1 Thriving Heath
1 Evolving Wilds
1 Thriving Isle
1 Thriving Bluff
1 Command Tower
1 Mystic Monastery
3 Plains
2 Plains
2 Island
2 Island
2 Mountain
2 Mountain
+97
View File
@@ -0,0 +1,97 @@
[metadata]
Name=Feline Ferocity
[Commander]
1 Arahbo, Roar of the World
[Main]
1 Nazahn, Revered Bladesmith
1 Mirri, Weatherlight Duelist
1 Alms Collector
1 Balan, Wandering Knight
1 Stalking Leonin
1 Hungry Lynx
1 Qasali Slingers
1 Jazal Goldmane
1 Jareth, Leonine Titan
1 Kemba, Kha Regent
1 Leonin Arbiter
1 Leonin Shikari
1 Raksha Golden Cub
1 Seht's Tiger
1 Spirit of the Hearth
1 Jedit Ojanen of Efrava
1 Fleecemane Lion
1 Phantom Nishoba
1 Leonin Relic-Warder
1 Oreskos Explorer
1 Sunspear Shikari
1 Taj-Nar Swordsmith
1 Temur Sabertooth
1 Qasali Pridemage
1 Traverse the Outlands
1 Divine Reckoning
1 Rout
1 Hunter's Prowess
1 Soul's Majesty
1 Cultivate
1 Harmonize
1 Nissa's Pilgrimage
1 Kindred Summons
1 White Sun's Zenith
1 Condemn
1 Wing Shards
1 Crushing Vines
1 Relic Crush
1 Bloodforged Battle-Axe
1 Hammer of Nazahn
1 Argentum Armor
1 Grappling Hook
1 Quietus Spike
1 Staff of Nin
1 Sword of the Animist
1 Sword of Vengeance
1 Heirloom Blade
1 Herald's Horn
1 Behemoth Sledge
1 Dreamstone Hedron
1 Hedron Archive
1 Hero's Blade
1 Lightning Greaves
1 Loxodon Warhammer
1 Skullclamp
1 Sol Ring
1 Swiftfoot Boots
1 Mirari's Wake
1 Abundance
1 Zendikar Resurgent
1 Curse of Vitality
1 Curse of Bounty
1 Mosswort Bridge
1 Stirring Wildwood
3 Plains
2 Plains
2 Plains
2 Forest
2 Forest
2 Forest
1 Path of Ancestry
1 Blighted Woodland
1 Blossoming Sands
1 Command Tower
1 Elfhame Palace
1 Evolving Wilds
1 Grasslands
1 Graypelt Refuge
1 Krosan Verge
1 Myriad Landscape
1 Opal Palace
1 Rogue's Passage
1 Saltcrusted Steppe
1 Secluded Steppe
1 Selesnya Guildgate
1 Selesnya Sanctuary
1 Temple of the False God
1 Terramorphic Expanse
1 Tranquil Expanse
1 Tranquil Thicket
1 Vivid Meadow
1 Vivid Grove
+82
View File
@@ -0,0 +1,82 @@
[metadata]
Name=First Flight
[Commander]
1 Isperia, Supreme Judge
[Main]
1 Archon of Redemption
1 Cartographer's Hawk
1 Cleansing Nova
1 Emeria Angel
1 Gideon Jura
1 Hanged Executioner
1 Remorseful Cleric
1 Sephara, Sky's Blade
1 Steel-Plume Marshal
1 Storm Herd
1 True Conviction
1 Angler Turtle
1 Bident of Thassa
1 Diluvian Primordial
1 Ever-Watching Threshold
1 Faerie Formation
1 Gravitational Shift
1 Inspired Sphinx
1 Sharding Sphinx
1 Sphinx of Enlightenment
1 Windreader Sphinx
1 Absorb
1 Skycat Sovereign
1 Sphinx's Revelation
1 Time Wipe
1 Moorland Haunt
1 Port Town
1 Prairie Stream
1 Temple of Enlightenment
1 Aven Gagglemaster
1 Banishing Light
1 Condemn
1 Crush Contraband
1 Disenchant
1 Generous Gift
1 Kangee's Lieutenant
1 Rally of Wings
1 Soul Snare
1 Swords to Plowshares
1 Vow of Duty
1 Aetherize
1 Counterspell
1 Favorable Winds
1 Negate
1 Tide Skimmer
1 Warden of Evos Isle
1 Winged Words
1 Cloudblazer
1 Empyrean Eagle
1 Jubilant Skybonder
1 Kangee, Sky Warden
1 Migratory Route
1 Staggering Insight
1 Thunderclap Wyvern
1 Arcane Signet
1 Azorius Signet
1 Commander's Sphere
1 Hedron Archive
1 Pilgrim's Eye
1 Sky Diamond
1 Skyscanner
1 Sol Ring
1 Talisman of Progress
1 Thought Vessel
1 Coastal Tower
1 Command Tower
1 Meandering River
1 Sejiri Refuge
1 Tranquil Cove
4 Plains
4 Plains
4 Plains
3 Plains
4 Island
4 Island
4 Island
3 Island
+94
View File
@@ -0,0 +1,94 @@
[metadata]
Name=Food and Fellowship
[Commander]
1 Frodo, Adventurous Hobbit
1 Sam, Loyal Attendant
[Main]
1 Field-Tested Frying Pan
1 The Gaffer
1 Gwaihir, Greatest of the Eagles
1 Of Herbs and Stewed Rabbit
1 Gollum, Obsessed Stalker
1 Lobelia, Defender of Bag End
1 Rapacious Guest
1 Assemble the Entmoot
1 Feasting Hobbit
1 Motivated Pony
1 Prize Pig
1 Banquet Guests
1 Bilbo, Birthday Celebrant
1 Farmer Cotton
1 Merry, Warden of Isengard
1 Pippin, Warden of Isengard
1 Treebeard, Gracious Host
1 Hithlain Rope
1 Call for Unity
1 Dawn of Hope
1 Dusk // Dawn
1 Fell the Mighty
1 Fumigate
1 Mentor of the Meek
1 Sanguine Bond
1 Toxic Deluge
1 Birds of Paradise
1 Gilded Goose
1 Woodfall Primus
1 Anguished Unmaking
1 Chromatic Lantern
1 Trading Post
1 Well of Lost Dreams
1 Brushland
1 Canopy Vista
1 Exotic Orchard
1 Fortified Village
1 Isolated Chapel
1 Murmuring Bosk
1 Necroblossom Snarl
1 Scattered Groves
1 Shineshadow Snarl
1 Sunpetal Grove
1 Woodland Cemetery
1 Eagles of the North
1 Landroval, Horizon Witness
1 Rosie Cotton of South Lane
1 Shire Shirriff
1 Mirkwood Bats
1 Generous Ent
1 Path to Exile
1 Swords to Plowshares
1 Revive the Shire
1 Butterbur, Bree Innkeeper
1 Crypt Incursion
1 Go for the Throat
1 Night's Whisper
1 Cultivate
1 Essence Warden
1 Farseek
1 Great Oak Guardian
1 Harmonize
1 Orchard Strider
1 Prosperous Innkeeper
1 Shire Terrace
1 Tireless Provisioner
1 Mortify
1 Savvy Hunter
1 Arcane Signet
1 Commander's Sphere
1 Pristine Talisman
1 Sol Ring
1 Access Tunnel
1 Ash Barrens
1 Command Tower
1 Evolving Wilds
1 Ghost Quarter
1 Graypelt Refuge
1 Path of Ancestry
1 Rogue's Passage
1 Sandsteppe Citadel
1 Scoured Barrens
2 Plains
2 Plains
2 Swamp
2 Swamp
4 Forest
4 Forest
+87
View File
@@ -0,0 +1,87 @@
[metadata]
Name=Forces of the Imperium
[Commander]
1 Inquisitor Greyfax
[Main]
1 Bastion Protector
1 Collective Effort
1 Deploy to the Front
1 Fell the Mighty
1 Hour of Reckoning
1 Launch the Fleet
1 Martial Coup
1 Entrapment Maneuver
1 Utter End
1 Swords to Plowshares
1 Mortify
1 Everflowing Chalice
1 Mind Stone
1 Skullclamp
1 Sol Ring
1 Talisman of Dominance
1 Talisman of Hierarchy
1 Talisman of Progress
1 Arcane Signet
1 Commander's Sphere
1 Reconnaissance Mission
1 Choked Estuary
1 Darkwater Catacombs
1 Exotic Orchard
1 Port Town
1 Prairie Stream
1 Skycloud Expanse
1 Sunken Hollow
1 Arcane Sanctum
1 Ash Barrens
1 Memorial to Glory
1 Command Tower
1 Dismal Backwater
1 Evolving Wilds
1 Path of Ancestry
1 Scoured Barrens
1 Terramorphic Expanse
1 Tranquil Cove
5 Island
7 Swamp
8 Plains
1 Marneus Calgar
1 Celestine, the Living Saint
1 Defenders of Humanity
1 For the Emperor!
1 Space Marine Devastator
1 Triumph of Saint Katherine
1 Ultramarines Honour Guard
1 Vexilus Praetor
1 Zephyrim
1 Sister of Silence
1 Vanguard Suppressor
1 Arco-Flagellant
1 Primaris Eliminator
1 Assault Intercessor
1 Belisarius Cawl
1 Birth of the Imperium
1 Callidus Assassin
1 Commissar Severina Raine
1 Company Commander
1 Cybernetica Datasmith
1 Epistolary Librarian
1 Exterminatus
1 The Flesh Is Weak
1 Inquisitor Eisenhorn
1 Neyam Shai Murad
1 Sister Hospitaller
1 Sister Repentia
1 The Golden Throne
1 Inquisitorial Rosette
1 Knight Paladin
1 Reaver Titan
1 Redemptor Dreadnought
1 Thunderhawk Gunship
1 And They Shall Know No Fear
1 Grey Knight Paragon
1 Space Marine Scout
1 Thunderwolf Cavalry
1 Sicarian Infiltrator
1 Sanguinary Priest
1 Deny the Witch
1 Primaris Chaplain
@@ -0,0 +1,87 @@
[metadata]
Name=Forces of the Imperium Collector's Edition
[Commander]
1 Inquisitor Greyfax
[Main]
1 Bastion Protector
1 Collective Effort
1 Deploy to the Front
1 Fell the Mighty
1 Hour of Reckoning
1 Launch the Fleet
1 Martial Coup
1 Entrapment Maneuver
1 Utter End
1 Swords to Plowshares
1 Mortify
1 Everflowing Chalice
1 Mind Stone
1 Skullclamp
1 Sol Ring
1 Talisman of Dominance
1 Talisman of Hierarchy
1 Talisman of Progress
1 Arcane Signet
1 Commander's Sphere
1 Reconnaissance Mission
1 Choked Estuary
1 Darkwater Catacombs
1 Exotic Orchard
1 Port Town
1 Prairie Stream
1 Skycloud Expanse
1 Sunken Hollow
1 Arcane Sanctum
1 Ash Barrens
1 Memorial to Glory
1 Command Tower
1 Dismal Backwater
1 Evolving Wilds
1 Path of Ancestry
1 Scoured Barrens
1 Terramorphic Expanse
1 Tranquil Cove
5 Island
7 Swamp
8 Plains
1 Marneus Calgar
1 Celestine, the Living Saint
1 Defenders of Humanity
1 For the Emperor!
1 Space Marine Devastator
1 Triumph of Saint Katherine
1 Ultramarines Honour Guard
1 Vexilus Praetor
1 Zephyrim
1 Sister of Silence
1 Vanguard Suppressor
1 Arco-Flagellant
1 Primaris Eliminator
1 Assault Intercessor
1 Belisarius Cawl
1 Birth of the Imperium
1 Callidus Assassin
1 Commissar Severina Raine
1 Company Commander
1 Cybernetica Datasmith
1 Epistolary Librarian
1 Exterminatus
1 The Flesh Is Weak
1 Inquisitor Eisenhorn
1 Neyam Shai Murad
1 Sister Hospitaller
1 Sister Repentia
1 The Golden Throne
1 Inquisitorial Rosette
1 Knight Paladin
1 Reaver Titan
1 Redemptor Dreadnought
1 Thunderhawk Gunship
1 And They Shall Know No Fear
1 Grey Knight Paragon
1 Space Marine Scout
1 Thunderwolf Cavalry
1 Sicarian Infiltrator
1 Sanguinary Priest
1 Deny the Witch
1 Primaris Chaplain
+76
View File
@@ -0,0 +1,76 @@
[metadata]
Name=Forged In Stone
[Commander]
1 Nahiri, the Lithomancer
[Main]
1 Containment Priest
1 Whitemane Lion
1 Grand Abolisher
1 Kor Sanctifiers
1 Mentor of the Meek
1 Flickerwisp
1 Hallowed Spiritkeeper
1 Kemba, Kha Regent
1 Silverblade Paladin
1 Skyhunter Skirmisher
1 Angelic Field Marshal
1 Celestial Crusader
1 Jazal Goldmane
1 Geist-Honored Monk
1 Requiem Angel
1 Adarkar Valkyrie
1 Sun Titan
1 Sunblast Angel
1 Twilight Shepherd
1 Angel of the Dire Hour
1 Serra Avatar
1 Gift of Estates
1 Spectral Procession
1 Fell the Mighty
1 Nomads' Assembly
1 Deploy to the Front
1 Martial Coup
1 Decree of Justice
1 Brave the Elements
1 Condemn
1 Afterlife
1 Midnight Haunting
1 Oblation
1 Wing Shards
1 Benevolent Offering
1 Comeuppance
1 Return to Dust
1 White Sun's Zenith
1 Masterwork of Ingenuity
1 Skullclamp
1 Sol Ring
1 Marble Diamond
1 Mask of Memory
1 Pearl Medallion
1 Swiftfoot Boots
1 Commander's Sphere
1 Loxodon Warhammer
1 Strata Scythe
1 Sword of Vengeance
1 Assault Suit
1 Bonehoard
1 Moonsilver Spear
1 Argentum Armor
1 Loreseeker's Stone
1 Armistice
1 Mobilization
1 Sacred Mesa
1 Marshal's Anthem
1 Cathars' Crusade
1 True Conviction
1 Arcane Lighthouse
1 Drifting Meadow
1 Emeria, the Sky Ruin
1 Ghost Quarter
1 Karoo
1 Secluded Steppe
1 Temple of the False God
8 Plains
8 Plains
8 Plains
8 Plains
+104
View File
@@ -0,0 +1,104 @@
[metadata]
Name=From Cute to Brute
[Commander]
1 Esika, God of the Tree
[Main]
1 Archangel Avacyn
1 Bloodline Keeper
1 Nicol Bolas, the Ravager
1 Westvale Abbey
1 Putrefy
1 Rhys the Redeemed
1 Evolving Wilds
1 Exotic Orchard
1 Vivid Grove
1 Arlinn Kord
1 Voldaren Pariah
1 Guardian Project
1 Beast Whisperer
1 Chromatic Lantern
1 Emmara, Soul of the Accord
1 Elbrus, the Binding Blade
1 Sisay, Weatherlight Captain
1 Sol Ring
1 Scattered Groves
1 Sheltered Thicket
1 Harmonize
1 Altar of the Pantheon
1 Kinnan, Bonder Prodigy
1 Pongify
1 Chandra, Fire of Kaladesh
1 Jace, Vryn's Prodigy
1 Kytheon, Hero of Akros
1 Liliana, Heretical Healer
1 Nissa, Vastwood Seer
1 Opulent Palace
1 Azor's Gateway
1 Hadana's Climb
1 Journey to Eternity
1 Dowsing Dagger
1 Legion's Landing
1 Search for Azcanta
1 Thaumatic Compass
1 Treasure Map
1 Beast Within
1 Farseek
1 Garruk Relentless
1 Command Tower
1 Brightclimb Pathway
1 Branchloft Pathway
1 Cragcrown Pathway
1 Needleverge Pathway
1 Riverglide Pathway
1 Valakut Awakening
1 Bala Ged Recovery
1 Clearwater Pathway
1 Hagra Mauling
1 Ondu Inversion
1 Sandstone Oracle
1 Sphinx of the Second Sun
1 Meteor Golem
1 Terramorphic Expanse
1 Blightstep Pathway
1 Hengegate Pathway
1 Barkchannel Pathway
1 Darkbore Pathway
1 Valki, God of Lies
1 Rimewood Falls
1 Woodland Chasm
1 Kolvori, God of Kinship
1 Cosima, God of the Voyage
1 Plargg, Dean of Chaos
1 Valentin, Dean of the Vein
1 Shaile, Dean of Radiance
1 Mila, Crafty Companion
1 Triplicate Titan
1 Diluvian Primordial
1 Seaside Citadel
1 Arlinn, the Pack's Hope
1 Ludevic, Necrogenius
1 Dennick, Pious Apprentice
1 Tovolar's Huntmaster
1 Butcher of Malakir
1 Path of Ancestry
1 Savage Lands
1 Tireless Provisioner
1 Fellwar Stone
1 Urza's Ruinous Blast
1 Zetalpa, Primal Dawn
1 Commander's Sphere
1 Arcane Signet
1 Jolrael, Mwonvuli Recluse
1 Sandsteppe Citadel
1 Plains
1 Swamp
1 Forest
1 Forest
1 Forest
1 Island
1 Mountain
1 Jungle Shrine
1 Frontier Bivouac
1 Time Wipe
1 Utter End
1 The World Tree
+95
View File
@@ -0,0 +1,95 @@
[metadata]
Name=Grand Larceny
[Commander]
1 Gonti, Canny Acquisitor
[Main]
1 Felix Five-Boots
1 Thieving Skydiver
1 Sage of the Beyond
1 Ghostly Pilferer
1 Diluvian Primordial
1 Stolen Goods
1 Curse of the Swine
1 Dazzling Sphinx
1 Arcane Heist
1 Smirking Spelljacker
1 Mind's Dilation
1 Gonti, Lord of Luxury
1 Predators' Hour
1 Brainstealer Dragon
1 Cunning Rhetoric
1 Thieving Amalgam
1 Orochi Soul-Reaver
1 Thieving Varmint
1 Heartless Conscription
1 Nashi, Moon Sage's Scion
1 Baleful Mastery
1 Ohran Frostfang
1 Savvy Trader
1 Tower Winder
1 Cazur, Ruthless Stalker
1 Silent-Blade Oni
1 Hostage Taker
1 Edric, Spymaster of Trest
1 Fallen Shinobi
1 Cold-Eyed Selkie
1 Siphon Insight
1 Extract Brain
1 Shadowmage Infiltrator
1 Baleful Strix
1 Culling Ritual
1 Thief of Sanity
1 Villainous Wealth
1 The Mimeoplasm
1 Plasm Capture
1 Ukkima, Stalking Shadow
1 Bladegriff Prototype
1 Chaos Wand
1 Dream-Thief's Bandana
1 Oblivion Sower
1 Overflowing Basin
1 Drowned Catacomb
1 Temple of Deceit
1 Temple of Malady
1 Darkwater Catacombs
1 Fetid Pools
1 Yavimaya Coast
1 Viridescent Bog
1 Exotic Orchard
1 Underground River
1 Woodland Cemetery
1 Sunken Hollow
1 Temple of Mystery
1 Hinterland Harbor
1 Darkslick Shores
1 Flooded Grove
1 Twilight Mire
1 Llanowar Wastes
1 Slither Blade
1 Whirler Rogue
1 Triton Shorestalker
1 Feed the Swarm
1 Silhana Ledgewalker
1 Rampant Growth
1 Kodama's Reach
1 Void Attendant
1 Three Visits
1 Putrefy
1 Trygon Predator
1 Doc Aurlock, Grizzled Genius
1 Arcane Signet
1 Darksteel Ingot
1 Fellwar Stone
1 Prismatic Lens
1 Sol Ring
1 Command Tower
1 Reliquary Tower
1 Access Tunnel
1 Dimir Aqueduct
1 Opulent Palace
3 Island
2 Island
3 Swamp
3 Swamp
2 Forest
2 Forest
+81
View File
@@ -0,0 +1,81 @@
[metadata]
Name=Grave Danger
[Commander]
1 Gisa and Geralf
[Main]
1 Geralf's Mindcrusher
1 Laboratory Drudge
1 Army of the Damned
1 Cemetery Reaper
1 Champion of the Perished
1 Crippling Fear
1 Gravespawn Sovereign
1 Josu Vess, Lich Knight
1 Liliana, Untouched by Death
1 Liliana's Mastery
1 Liliana's Standard Bearer
1 Midnight Reaper
1 Necromantic Selection
1 Necrotic Hex
1 Open the Graves
1 Overseer of the Damned
1 Scourge of Nel Toth
1 Unbreathing Horde
1 Zombie Apocalypse
1 Enter the God-Eternals
1 Havengul Lich
1 Undermine
1 Vela the Night-Clad
1 Grimoire of the Dead
1 Choked Estuary
1 Sunken Hollow
1 Temple of Deceit
1 Deep Analysis
1 Distant Melody
1 Eternal Skylord
1 Lazotep Plating
1 Sinister Sabotage
1 Cruel Revival
1 Curse of Disturbance
1 Feed the Swarm
1 Fleshbag Marauder
1 Gray Merchant of Asphodel
1 Lazotep Reaver
1 Liliana's Devotee
1 Lord of the Accursed
1 Lotleth Giant
1 Loyal Subordinate
1 Mire Triton
1 Murder
1 Spark Reaper
1 Syphon Flesh
1 Undead Augur
1 Vampiric Rites
1 Vengeful Dead
1 Victimize
1 Vizier of the Scorpion
1 Withered Wretch
1 Diregraf Captain
1 Gleaming Overseer
1 Pilfered Plans
1 Arcane Signet
1 Commander's Sphere
1 Dimir Signet
1 Heraldic Banner
1 Sol Ring
1 Talisman of Dominance
1 Unstable Obelisk
1 Wayfarer's Bauble
1 Command Tower
1 Dismal Backwater
1 Jwar Isle Refuge
1 Salt Marsh
1 Submerged Boneyard
4 Island
3 Island
3 Island
3 Island
5 Swamp
5 Swamp
4 Swamp
4 Swamp
+98
View File
@@ -0,0 +1,98 @@
[metadata]
Name=Graveyard Overdrive
[Commander]
1 Disa the Restless
[Main]
1 Coram, the Undertaker
1 Bloodbraid Challenger
1 Broodmate Tyrant
1 Tempt with Mayhem
1 Gluttonous Hellkite
1 Pyrogoyf
1 Polygoyf
1 Barrowgoyf
1 Sawhorn Nemesis
1 Infested Thrinax
1 Final Act
1 Siege-Gang Lieutenant
1 Tarmogoyf Nest
1 Exterminator Magmarch
1 Liliana, Death's Majesty
1 Maelstrom Pulse
1 Junji, the Midnight Sky
1 Garruk, Apex Predator
1 The Reaver Cleaver
1 Temple of Malady
1 Deadbridge Chant
1 Kolaghan's Command
1 Izoni, Thousand-Eyed
1 Lhurgoyf
1 Selvala, Heart of the Wilds
1 Kessig Wolf Run
1 Archon of Cruelty
1 Maskwood Nexus
1 Grist, the Hunger Tide
1 Ignoble Hierarch
1 Necrogoyf
1 Mortivore
1 Chandra's Ignition
1 Viridescent Bog
1 Find // Finality
1 Mossfire Valley
1 Ziatora, the Incinerator
1 Canyon Slough
1 Cinder Glade
1 Exotic Orchard
1 Shadowblood Ridge
1 Sheltered Thicket
1 Smoldering Marsh
1 Temple of Abandon
1 Temple of Malice
1 Raging Ravine
1 Terminate
1 Demolition Field
1 Command Tower
1 Twisted Landscape
1 Grisly Salvage
1 Yavimaya Elder
1 Bituminous Blast
1 Bloodbraid Elf
1 Eternal Witness
1 Savage Lands
1 Tainted Wood
1 Burnished Hart
1 Deathreap Ritual
1 Arcane Signet
1 Syr Konrad, the Grim
1 Grapple with the Past
1 Dakmor Salvage
1 Accursed Marauder
1 Brawn
1 Faithless Looting
1 Rampant Growth
1 Anger
1 Tranquil Thicket
1 Stitcher's Supplier
1 Graveshifter
1 Talisman of Resilience
1 Altar of the Goyf
1 Syphon Mind
1 Talisman of Indulgence
1 Sakura-Tribe Elder
1 Terramorphic Expanse
1 Tainted Peak
1 Riveteers Overlook
1 Riveteers Charm
1 Forgotten Cave
1 Path of Ancestry
1 Lightning Greaves
1 Myriad Landscape
1 Sol Ring
1 Talisman of Impulse
1 Evolving Wilds
2 Swamp
2 Swamp
2 Mountain
1 Mountain
3 Forest
2 Forest
@@ -0,0 +1,98 @@
[metadata]
Name=Graveyard Overdrive Collector's Edition
[Commander]
1 Disa the Restless
[Main]
1 Coram, the Undertaker
1 Bloodbraid Challenger
1 Broodmate Tyrant
1 Tempt with Mayhem
1 Gluttonous Hellkite
1 Pyrogoyf
1 Polygoyf
1 Barrowgoyf
1 Sawhorn Nemesis
1 Infested Thrinax
1 Final Act
1 Siege-Gang Lieutenant
1 Tarmogoyf Nest
1 Exterminator Magmarch
1 Liliana, Death's Majesty
1 Maelstrom Pulse
1 Junji, the Midnight Sky
1 Garruk, Apex Predator
1 The Reaver Cleaver
1 Temple of Malady
1 Deadbridge Chant
1 Kolaghan's Command
1 Izoni, Thousand-Eyed
1 Lhurgoyf
1 Selvala, Heart of the Wilds
1 Kessig Wolf Run
1 Archon of Cruelty
1 Maskwood Nexus
1 Grist, the Hunger Tide
1 Ignoble Hierarch
1 Necrogoyf
1 Mortivore
1 Chandra's Ignition
1 Viridescent Bog
1 Find // Finality
1 Mossfire Valley
1 Ziatora, the Incinerator
1 Canyon Slough
1 Cinder Glade
1 Exotic Orchard
1 Shadowblood Ridge
1 Sheltered Thicket
1 Smoldering Marsh
1 Temple of Abandon
1 Temple of Malice
1 Raging Ravine
1 Terminate
1 Demolition Field
1 Command Tower
1 Twisted Landscape
1 Grisly Salvage
1 Yavimaya Elder
1 Bituminous Blast
1 Bloodbraid Elf
1 Eternal Witness
1 Savage Lands
1 Tainted Wood
1 Burnished Hart
1 Deathreap Ritual
1 Arcane Signet
1 Syr Konrad, the Grim
1 Grapple with the Past
1 Dakmor Salvage
1 Accursed Marauder
1 Brawn
1 Faithless Looting
1 Rampant Growth
1 Anger
1 Tranquil Thicket
1 Stitcher's Supplier
1 Graveshifter
1 Talisman of Resilience
1 Altar of the Goyf
1 Syphon Mind
1 Talisman of Indulgence
1 Sakura-Tribe Elder
1 Terramorphic Expanse
1 Tainted Peak
1 Riveteers Overlook
1 Riveteers Charm
1 Forgotten Cave
1 Path of Ancestry
1 Lightning Greaves
1 Myriad Landscape
1 Sol Ring
1 Talisman of Impulse
1 Evolving Wilds
2 Swamp
2 Swamp
2 Mountain
1 Mountain
3 Forest
2 Forest
+91
View File
@@ -0,0 +1,91 @@
[metadata]
Name=Guided by Nature
[Commander]
1 Freyalise, Llanowar's Fury
[Main]
1 Elvish Mystic
1 Elvish Skysweeper
1 Essence Warden
1 Joraga Warcaller
1 Llanowar Elves
1 Sylvan Safekeeper
1 Elvish Visionary
1 Priest of Titania
1 Sylvan Ranger
1 Thornweald Archer
1 Wellwisher
1 Farhaven Elf
1 Imperious Perfect
1 Reclamation Sage
1 Timberwatch Elf
1 Titania's Chosen
1 Wood Elves
1 Elvish Archdruid
1 Ezuri, Renegade Leader
1 Drove of Elves
1 Immaculate Magistrate
1 Wren's Run Packmaster
1 Lys Alana Huntmaster
1 Masked Admirers
1 Wolfbriar Elemental
1 Creeperhulk
1 Silklash Spider
1 Titania, Protector of Argoth
1 Grave Sifter
1 Primordial Sage
1 Rampaging Baloths
1 Soul of the Harvest
1 Thunderfoot Baloth
1 Siege Behemoth
1 Tornado Elemental
1 Terastodon
1 Lifeblood Hydra
1 Hunting Triad
1 Whirlwind
1 Overwhelming Stampede
1 Overrun
1 Grim Flowering
1 Collective Unconscious
1 Desert Twister
1 Wave of Vitriol
1 Praetor's Counsel
1 Sylvan Offering
1 Harrow
1 Fresh Meat
1 Skullclamp
1 Sol Ring
1 Emerald Medallion
1 Moss Diamond
1 Swiftfoot Boots
1 Commander's Sphere
1 Assault Suit
1 Seer's Sundial
1 Predator, Flagship
1 Loreseeker's Stone
1 Beastmaster Ascension
1 Song of the Dryads
1 Wolfcaller's Howl
1 Crystal Vein
1 Evolving Wilds
1 Gargoyle Castle
1 Ghost Quarter
1 Haunted Fengraf
1 Havenwood Battleground
1 Jungle Basin
1 Myriad Landscape
1 Oran-Rief, the Vastwood
1 Slippery Karst
1 Terramorphic Expanse
1 Tranquil Thicket
3 Forest
2 Forest
2 Forest
2 Forest
2 Forest
2 Forest
2 Forest
2 Forest
2 Forest
2 Forest
2 Forest
2 Forest
+96
View File
@@ -0,0 +1,96 @@
[metadata]
Name=Hail, Caesar
[Commander]
1 Caesar, Legion's Emperor
[Main]
1 Mr. House, President and CEO
1 Aradesh, the Founder
1 Battle of Hoover Dam
1 Overseer of Vault 76
1 Securitron Squadron
1 Sierra, Nuka's Biggest Fan
1 Vault 75: Middle School
1 Yes Man, Personal Securitron
1 V.A.T.S.
1 Wasteland Raider
1 Mysterious Stranger
1 Powder Ganger
1 Rose, Cutthroat Raider
1 Thrill-Kill Disciple
1 Wild Wasteland
1 Boomer Scrapper
1 Colonel Autumn
1 Desdemona, Freedom's Edge
1 Elder Arthur Maxson
1 Kellogg, Dangerous Mind
1 MacCready, Lamplight Mayor
1 The Nipton Lottery
1 Paladin Elizabeth Taggerdy
1 Vault 11: Voter's Dilemma
1 ED-E, Lonesome Eyebot
1 Desolate Mire
1 Diamond City
1 Captain of the Watch
1 Entrapment Maneuver
1 Hour of Reckoning
1 Keeper of the Accord
1 Marshal's Anthem
1 Martial Coup
1 Secure the Wastes
1 Black Market
1 Lethal Scheme
1 Stolen Strategy
1 Anguished Unmaking
1 Assemble the Legion
1 Fervent Charge
1 Ruinous Ultimatum
1 Canyon Slough
1 Clifftop Retreat
1 Dragonskull Summit
1 Isolated Chapel
1 Shadowblood Ridge
1 Smoldering Marsh
1 Temple of Malice
1 Temple of Silence
1 Temple of Triumph
1 Windbrisk Heights
1 Gary Clone
1 Butch DeLoria, Tunnel Snake
1 Ruthless Radrat
1 Craig Boone, Novac Guard
1 Legate Lanius, Caesar's Ace
1 White Glove Gourmand
1 Charisma Bobblehead
1 Luck Bobblehead
1 Survivor's Med Kit
1 Impassioned Orator
1 Intangible Virtue
1 Bastion of Remembrance
1 Deadly Dispute
1 Morbid Opportunist
1 Pitiless Plunderer
1 General's Enforcer
1 Heroic Reinforcements
1 Wear // Tear
1 Arcane Signet
1 Skullclamp
1 Sol Ring
1 Talisman of Conviction
1 Talisman of Hierarchy
1 Talisman of Indulgence
1 Ash Barrens
1 Command Tower
1 Evolving Wilds
1 Memorial to Glory
1 Myriad Landscape
1 Nomad Outpost
1 Path of Ancestry
1 Tainted Field
1 Tainted Peak
1 Terramorphic Expanse
3 Plains
2 Plains
2 Swamp
2 Swamp
3 Mountain
2 Mountain
@@ -0,0 +1,91 @@
[metadata]
Name=Heads I Win, Tails You Lose
[Commander]
1 Zndrsplt, Eye of Wisdom
1 Okaun, Eye of Chaos
[Main]
1 Daretti, Scrap Savant
1 Ral Zarek
1 Goblin Kaboomist
1 Krark, the Thumbless
1 Niv-Mizzet, Parun
1 Spark Double
1 Frenetic Sliver
1 Goblin Archaeologist
1 Karplusan Minotaur
1 Tavern Scoundrel
1 Tribute Mage
1 Bloodsworn Steward
1 Goblin Engineer
1 The Locust God
1 Yusri, Fortune's Flame
1 Mogg Assassin
1 Sakashima the Impostor
1 Stitch in Time
1 Blasphemous Act
1 Fabricate
1 Fiery Gambit
1 Chandra's Ignition
1 Gamble
1 Reshape
1 Seize the Day
1 Ponder
1 Preordain
1 Serum Visions
1 Slip Through Space
1 Squee's Revenge
1 Vandalblast
1 Negate
1 Chaos Warp
1 Whir of Invention
1 Counterspell
1 Long-Term Plans
1 Muddle the Mixture
1 Temur Battle Rage
1 Krark's Thumb
1 Izzet Signet
1 Lightning Greaves
1 Mind Stone
1 Swiftfoot Boots
1 Sword of Vengeance
1 Thought Vessel
1 Whispersilk Cloak
1 Boompile
1 Commander's Plate
1 Embercleave
1 Shadowspear
1 Arcane Signet
1 Crooked Scales
1 Sol Ring
1 Talisman of Creativity
1 Propaganda
1 Mirror March
1 Chance Encounter
1 Footfall Crater
1 Impulsive Maneuvers
1 Planar Chaos
1 Risky Move
1 Temple of Epiphany
1 Wandering Fumarole
1 Buried Ruin
1 Great Furnace
1 Izzet Boilerworks
1 Myriad Landscape
1 Path of Ancestry
1 Rogue's Passage
7 Island
8 Mountain
1 Academy Ruins
1 Cascade Bluffs
1 Desolate Lighthouse
1 Exotic Orchard
1 Flamekin Village
1 Inventors' Fair
1 Shivan Reef
1 Spinerock Knoll
1 Sulfur Falls
1 Tolaria West
1 Training Center
1 Command Tower
1 Reliquary Tower
1 Temple of the False God
+100
View File
@@ -0,0 +1,100 @@
[metadata]
Name=Heavenly Inferno
[Commander]
1 Kaalia of the Vast
[Main]
1 Akroma, Angel of Fury
1 Angel of Despair
1 Angelic Arbiter
1 Anger
1 Archangel of Strife
1 Avatar of Slaughter
1 Basandra, Battle Seraph
1 Bladewing the Risen
1 Boros Guildmage
1 Dragon Whelp
1 Dread Cacodemon
1 Duergar Hedge-Mage
1 Fallen Angel
1 Furnace Whelp
1 Gwyllion Hedge-Mage
1 Lightkeeper of Emeria
1 Malfegor
1 Mana-Charged Dragon
1 Mother of Runes
1 Oni of Wild Places
1 Oros, the Avenger
1 Orzhov Guildmage
1 Razorjaw Oni
1 Reiver Demon
1 Serra Angel
1 Shattered Angel
1 Tariel, Reckoner of Souls
1 Voice of All
1 Akroma's Vengeance
1 Death by Dragons
1 Diabolic Tutor
1 Earthquake
1 Evincar's Justice
1 Syphon Flesh
1 Syphon Mind
1 Bathe in Light
1 Cleansing Beam
1 Comet Storm
1 Congregate
1 Master Warcraft
1 Mortify
1 Orim's Thunder
1 Path to Exile
1 Return to Dust
1 Sulfurous Blast
1 Terminate
1 Wrecking Ball
1 Armillary Sphere
1 Boros Signet
1 Darksteel Ingot
1 Lightning Greaves
1 Orzhov Signet
1 Rakdos Signet
1 Sol Ring
1 Pyrohemia
1 Righteous Cause
1 Soul Snare
1 Stranglehold
1 Vow of Duty
1 Vow of Lightning
1 Vow of Malice
1 Akoum Refuge
1 Barren Moor
1 Bojuka Bog
1 Boros Garrison
1 Command Tower
1 Evolving Wilds
1 Forgotten Cave
1 Molten Slagheap
2 Mountain
2 Mountain
2 Mountain
2 Mountain
1 Orzhov Basilica
1 Plains
1 Plains
1 Plains
1 Plains
1 Plains
1 Plains
1 Plains
1 Plains
1 Rakdos Carnarium
1 Rupture Spire
1 Secluded Steppe
1 Swamp
1 Swamp
1 Swamp
1 Swamp
1 Swamp
1 Swamp
1 Swamp
1 Swamp
1 Vivid Meadow
1 Zoetic Cavern
+92
View File
@@ -0,0 +1,92 @@
[metadata]
Name=Heavenly Inferno
[Commander]
1 Kaalia of the Vast
[Main]
1 Mother of Runes
1 Orzhov Guildmage
1 Boros Guildmage
1 Gwyllion Hedge-Mage
1 Duergar Hedge-Mage
1 Lightkeeper of Emeria
1 Razorjaw Oni
1 Anger
1 Voice of All
1 Dragon Whelp
1 Furnace Whelp
1 Serra Angel
1 Shattered Angel
1 Fallen Angel
1 Basandra, Battle Seraph
1 Oni of Wild Places
1 Mana-Charged Dragon
1 Oros, the Avenger
1 Malfegor
1 Angelic Arbiter
1 Archangel of Strife
1 Tariel, Reckoner of Souls
1 Angel of Despair
1 Bladewing the Risen
1 Avatar of Slaughter
1 Akroma, Angel of Fury
1 Reiver Demon
1 Dread Cacodemon
1 Syphon Mind
1 Diabolic Tutor
1 Evincar's Justice
1 Syphon Flesh
1 Akroma's Vengeance
1 Death by Dragons
1 Earthquake
1 Path to Exile
1 Bathe in Light
1 Terminate
1 Orim's Thunder
1 Mortify
1 Congregate
1 Return to Dust
1 Sulfurous Blast
1 Wrecking Ball
1 Master Warcraft
1 Cleansing Beam
1 Comet Storm
1 Sol Ring
1 Armillary Sphere
1 Boros Signet
1 Lightning Greaves
1 Orzhov Signet
1 Rakdos Signet
1 Darksteel Ingot
1 Soul Snare
1 Vow of Duty
1 Vow of Malice
1 Vow of Lightning
1 Stranglehold
1 Pyrohemia
1 Righteous Cause
1 Akoum Refuge
1 Barren Moor
1 Bojuka Bog
1 Boros Garrison
1 Command Tower
1 Evolving Wilds
1 Forgotten Cave
1 Molten Slagheap
1 Orzhov Basilica
1 Rakdos Carnarium
1 Rupture Spire
1 Secluded Steppe
1 Vivid Meadow
1 Zoetic Cavern
2 Plains
2 Plains
2 Plains
2 Plains
2 Swamp
2 Swamp
2 Swamp
2 Swamp
2 Mountain
2 Mountain
2 Mountain
2 Mountain
+98
View File
@@ -0,0 +1,98 @@
[metadata]
Name=Invent Superiority
[Commander]
1 Breya, Etherium Shaper
[Main]
1 Daretti, Scrap Savant
1 Myr Retriever
1 Chief Engineer
1 Etherium Sculptor
1 Vedalken Engineer
1 Slobad, Goblin Tinkerer
1 Baleful Strix
1 Akiri, Line-Slinger
1 Armory Automaton
1 Shimmer Myr
1 Master of Etherium
1 Trinket Mage
1 Magus of the Will
1 Hanna, Ship's Navigator
1 Silas Renn, Seeker Adept
1 Sydri, Galvanic Genius
1 Etched Oracle
1 Solemn Simulacrum
1 Sanctum Gargoyle
1 Faerie Artisans
1 Bruse Tarl, Boorish Herder
1 Ethersworn Adjudicator
1 Sphinx Summoner
1 Jor Kadeen, the Prevailer
1 Soul of New Phyrexia
1 Godo, Bandit Warlord
1 Hellkite Tyrant
1 Sharuum the Hegemon
1 Myr Battlesphere
1 Hellkite Igniter
1 Filigree Angel
1 Whipflare
1 Parting Thoughts
1 Trash for Treasure
1 Beacon of Unrest
1 Migratory Route
1 Open the Vaults
1 Phyrexian Rebirth
1 Grave Upheaval
1 Coastal Breach
1 Grip of Phyresis
1 Ancient Excavation
1 Read the Runes
1 Trial // Error
1 Everflowing Chalice
1 Skullclamp
1 Sol Ring
1 Dispeller's Capsule
1 Executioner's Capsule
1 Cranial Plating
1 Fellwar Stone
1 Ichor Wellspring
1 Mycosynth Wellspring
1 Swiftfoot Boots
1 Thopter Foundry
1 Commander's Sphere
1 Loxodon Warhammer
1 Bonehoard
1 Nevinyrral's Disk
1 Trading Post
1 Blinkmoth Urn
1 Darksteel Citadel
1 Seat of the Synod
1 Curse of Vengeance
1 Arcane Sanctum
1 Ash Barrens
1 Azorius Chancery
1 Boros Garrison
1 Buried Ruin
1 Command Tower
1 Crumbling Necropolis
1 Dimir Aqueduct
1 Evolving Wilds
1 Exotic Orchard
1 Mystic Monastery
1 Nomad Outpost
1 Rakdos Carnarium
1 Rupture Spire
1 Temple of the False God
1 Terramorphic Expanse
1 Transguild Promenade
2 Plains
2 Plains
1 Plains
2 Island
2 Island
1 Island
2 Swamp
1 Swamp
1 Swamp
2 Mountain
1 Mountain
1 Mountain
Loaded 100 of 254 files, more files were not shown because too many files have changed in this diff. Show more