mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-09-21 20:40:47 +02:00
release: 2.2.6 – refresh bracket list JSONs; finalize brackets compliance docs and UI polish
This commit is contained in:
parent
4e03997923
commit
375349e56e
11 changed files with 734 additions and 16 deletions
21
CHANGELOG.md
21
CHANGELOG.md
|
@ -12,6 +12,8 @@ This format follows Keep a Changelog principles and aims for Semantic Versioning
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [2.2.6] - 2025-09-04
|
||||
|
||||
### Added
|
||||
- Bracket policy enforcement: global pool-level prune for disallowed categories when limits are 0 (e.g., Game Changers in Brackets 1–2). Applies to both Web and headless runs.
|
||||
- Inline enforcement UI: violations surface before the summary; Continue/Rerun disabled until you replace or remove flagged cards. Alternatives are role-consistent and exclude commander/locked/in-deck cards.
|
||||
|
@ -20,10 +22,29 @@ This format follows Keep a Changelog principles and aims for Semantic Versioning
|
|||
### Changed
|
||||
- Spells and creatures phases apply bracket-aware pre-filters to reduce violations proactively.
|
||||
- Compliance detection for Game Changers falls back to in-code constants when `config/card_lists/game_changers.json` is empty.
|
||||
- Data refresh: updated static lists used by bracket compliance/enforcement with current card names and metadata:
|
||||
- `config/card_lists/extra_turns.json`
|
||||
- `config/card_lists/game_changers.json`
|
||||
- `config/card_lists/mass_land_denial.json`
|
||||
- `config/card_lists/tutors_nonland.json`
|
||||
Each list includes `list_version: "manual-2025-09-04"` and `generated_at`.
|
||||
|
||||
### Fixed
|
||||
- Summary/export mismatch in headless JSON runs where disallowed cards could be pruned from exports but appear in summaries; global prune ensures consistent state across phases and reports.
|
||||
|
||||
### Notes
|
||||
- These lists underpin the bracket enforcement feature introduced in 2.2.5; shipping them as a follow-up release ensures consistent results across Web and headless runs.
|
||||
|
||||
## [2.2.5] - 2025-09-03
|
||||
|
||||
### Added
|
||||
- Bracket WARN thresholds: `config/brackets.yml` supports optional `<category>_warn` keys (e.g., `tutors_nonland_warn`, `extra_turns_warn`). Compliance now returns PASS/WARN/FAIL; low brackets (1–2) conservatively WARN on presence of tutors/extra_turns when thresholds aren’t provided.
|
||||
- Web UI compliance polish: the panel auto-opens on non-compliance (WARN/FAIL) and shows a colored overall status chip (green/WARN amber/red). WARN items now render as tiles with a subtle amber style and a WARN badge; tiles and enforcement actions remain FAIL-only.
|
||||
- Tests: added coverage to ensure WARN thresholds from YAML are applied and that fallback WARN behavior appears for low brackets.
|
||||
|
||||
### Changed
|
||||
- Web: flagged metadata now includes WARN categories with a `severity` field to support softer UI rendering for advisory cases.
|
||||
|
||||
## [2.2.4] - 2025-09-02
|
||||
|
||||
### Added
|
||||
|
|
BIN
README.md
BIN
README.md
Binary file not shown.
|
@ -100,10 +100,19 @@ def _canonicalize(name: str | None) -> str:
|
|||
return s.casefold()
|
||||
|
||||
|
||||
def _status_for(count: int, limit: Optional[int]) -> str:
|
||||
def _status_for(count: int, limit: Optional[int], warn: Optional[int] = None) -> str:
|
||||
# Unlimited hard limit -> always PASS (no WARN semantics without a cap)
|
||||
if limit is None:
|
||||
return "PASS"
|
||||
return "PASS" if count <= int(limit) else "FAIL"
|
||||
if count > int(limit):
|
||||
return "FAIL"
|
||||
# Soft guidance: if warn threshold provided and met, surface WARN
|
||||
try:
|
||||
if warn is not None and int(warn) > 0 and count >= int(warn):
|
||||
return "WARN"
|
||||
except Exception:
|
||||
pass
|
||||
return "PASS"
|
||||
|
||||
|
||||
def evaluate_deck(
|
||||
|
@ -155,7 +164,13 @@ def evaluate_deck(
|
|||
flagged_names_disp = sorted({deck_canon_to_display.get(cn, cn) for cn in flagged_set})
|
||||
c = len(flagged_set)
|
||||
lim = limits.get(key)
|
||||
status = _status_for(c, lim)
|
||||
# Optional warn thresholds live alongside limits as "<key>_warn"
|
||||
try:
|
||||
warn_key = f"{key}_warn"
|
||||
warn_val = limits.get(warn_key)
|
||||
except Exception:
|
||||
warn_val = None
|
||||
status = _status_for(c, lim, warn=warn_val)
|
||||
cat: CategoryFinding = {
|
||||
"count": c,
|
||||
"limit": lim,
|
||||
|
@ -166,12 +181,27 @@ def evaluate_deck(
|
|||
categories[key] = cat
|
||||
if status == "FAIL":
|
||||
messages.append(f"{key.replace('_',' ').title()}: {c} exceeds limit {lim}")
|
||||
elif status == "WARN":
|
||||
try:
|
||||
if warn_val is not None:
|
||||
messages.append(f"{key.replace('_',' ').title()}: {c} present (discouraged for this bracket)")
|
||||
except Exception:
|
||||
pass
|
||||
# Conservative fallback: for low brackets (levels 1–2), tutors/extra-turns should WARN when present
|
||||
# even if a warn threshold was not provided in YAML.
|
||||
if status == "PASS" and level in (1, 2) and key in ("tutors_nonland", "extra_turns"):
|
||||
try:
|
||||
if (warn_val is None) and (lim is not None) and c > 0 and c <= int(lim):
|
||||
categories[key]["status"] = "WARN"
|
||||
messages.append(f"{key.replace('_',' ').title()}: {c} present (discouraged for this bracket)")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Two-card combos detection
|
||||
combos = detect_combos(deck_cards.keys(), combos_path=combos_path)
|
||||
cheap_early_pairs = [p for p in combos if p.cheap_early]
|
||||
c_limit = limits.get("two_card_combos")
|
||||
combos_status = _status_for(len(cheap_early_pairs), c_limit)
|
||||
combos_status = _status_for(len(cheap_early_pairs), c_limit, warn=None)
|
||||
categories["two_card_combos"] = {
|
||||
"count": len(cheap_early_pairs),
|
||||
"limit": c_limit,
|
||||
|
|
|
@ -51,3 +51,33 @@ def test_two_card_combination_detection_respects_cheap_early():
|
|||
rep2 = evaluate_deck(deck, commander_name=None, bracket="optimized")
|
||||
assert rep2["categories"]["two_card_combos"]["limit"] is None
|
||||
assert rep2["overall"] == "PASS"
|
||||
|
||||
|
||||
def test_warn_thresholds_in_yaml_are_applied():
|
||||
# Exhibition: tutors_nonland_warn=1 -> WARN when a single tutor present (hard limit 3)
|
||||
deck1 = {
|
||||
# Use a non-"Game Changer" tutor to avoid hard fail in Exhibition
|
||||
"Solve the Equation": _mk_card(["Bracket:TutorNonland"]),
|
||||
"Cultivate": _mk_card([]),
|
||||
}
|
||||
rep1 = evaluate_deck(deck1, commander_name=None, bracket="exhibition")
|
||||
assert rep1["level"] == 1
|
||||
assert rep1["categories"]["tutors_nonland"]["status"] == "WARN"
|
||||
assert rep1["overall"] == "WARN"
|
||||
|
||||
# Core: extra_turns_warn=1 -> WARN at 1, PASS at 0, FAIL above hard limit 3
|
||||
deck2 = {
|
||||
"Time Warp": _mk_card(["Bracket:ExtraTurn"]),
|
||||
"Explore": _mk_card([]),
|
||||
}
|
||||
rep2 = evaluate_deck(deck2, commander_name=None, bracket="core")
|
||||
assert rep2["level"] == 2
|
||||
assert rep2["categories"]["extra_turns"]["limit"] == 3
|
||||
assert rep2["categories"]["extra_turns"]["status"] in {"WARN", "PASS"}
|
||||
# With two extra turns, still <= limit, but should at least WARN
|
||||
deck3 = {
|
||||
"Time Warp": _mk_card(["Bracket:ExtraTurn"]),
|
||||
"Temporal Manipulation": _mk_card(["Bracket:ExtraTurn"]),
|
||||
}
|
||||
rep3 = evaluate_deck(deck3, commander_name=None, bracket="core")
|
||||
assert rep3["categories"]["extra_turns"]["status"] == "WARN"
|
||||
|
|
|
@ -2273,12 +2273,12 @@ async def build_compliance_panel(request: Request) -> HTMLResponse:
|
|||
seen_lower: set[str] = set()
|
||||
for key, cat in cats.items():
|
||||
try:
|
||||
lim = cat.get('limit')
|
||||
cnt = int(cat.get('count', 0) or 0)
|
||||
if lim is None or cnt <= int(lim):
|
||||
status = str(cat.get('status') or '').upper()
|
||||
# Only surface tiles for WARN and FAIL
|
||||
if status not in {"WARN", "FAIL"}:
|
||||
continue
|
||||
# For two-card combos, split pairs into individual cards and skip commander
|
||||
if key == 'two_card_combos':
|
||||
if key == 'two_card_combos' and status == 'FAIL':
|
||||
# Prefer the structured combos list to ensure we only expand counted pairs
|
||||
pairs = []
|
||||
try:
|
||||
|
@ -2316,6 +2316,7 @@ async def build_compliance_panel(request: Request) -> HTMLResponse:
|
|||
'category': labels.get(key, key.replace('_',' ').title()),
|
||||
'role': role,
|
||||
'owned': (nm_l in owned_lower),
|
||||
'severity': status,
|
||||
})
|
||||
continue
|
||||
# Default handling for list/tag categories
|
||||
|
@ -2332,6 +2333,7 @@ async def build_compliance_panel(request: Request) -> HTMLResponse:
|
|||
'category': labels.get(key, key.replace('_',' ').title()),
|
||||
'role': role,
|
||||
'owned': (nm_l in owned_lower),
|
||||
'severity': status,
|
||||
})
|
||||
except Exception:
|
||||
continue
|
||||
|
|
|
@ -1,7 +1,18 @@
|
|||
{% if compliance %}
|
||||
<details id="compliance-panel" style="margin-top:.75rem;">
|
||||
{% set non_compliant = compliance.overall is defined and (compliance.overall|string|lower != 'pass') %}
|
||||
<details id="compliance-panel" style="margin-top:.75rem;" {% if non_compliant %}open{% endif %}>
|
||||
<summary>Bracket compliance</summary>
|
||||
<div class="muted" style="margin:.35rem 0;">Overall: <strong>{{ compliance.overall }}</strong> (Bracket: {{ compliance.bracket|title }}{{ ' #' ~ compliance.level if compliance.level is defined }})</div>
|
||||
{% set ov = compliance.overall|string|lower %}
|
||||
<div class="muted" style="margin:.35rem 0;">Overall:
|
||||
{% if ov == 'fail' %}
|
||||
<span class="chip" title="Overall bracket status"><span class="dot" style="background: var(--red-main);"></span> FAIL</span>
|
||||
{% elif ov == 'warn' %}
|
||||
<span class="chip" title="Overall bracket status"><span class="dot" style="background: var(--orange-main);"></span> WARN</span>
|
||||
{% else %}
|
||||
<span class="chip" title="Overall bracket status"><span class="dot" style="background: var(--green-main);"></span> PASS</span>
|
||||
{% endif %}
|
||||
(Bracket: {{ compliance.bracket|title }}{{ ' #' ~ compliance.level if compliance.level is defined }})
|
||||
</div>
|
||||
{% if compliance.messages and compliance.messages|length > 0 %}
|
||||
<ul style="margin:.25rem 0; padding-left:1.25rem;">
|
||||
{% for m in compliance.messages %}
|
||||
|
@ -15,7 +26,8 @@
|
|||
<h5 style="margin:.75rem 0 .35rem 0;">Flagged cards</h5>
|
||||
<div class="card-grid">
|
||||
{% for f in flagged_meta %}
|
||||
<div class="card-tile" data-card-name="{{ f.name }}" data-role="{{ f.role or '' }}">
|
||||
{% set sev = (f.severity or 'FAIL')|upper %}
|
||||
<div class="card-tile" data-card-name="{{ f.name }}" data-role="{{ f.role or '' }}" {% if sev == 'FAIL' %}style="border-color: var(--red-main);"{% elif sev == 'WARN' %}style="border-color: var(--orange-main);"{% endif %}>
|
||||
<a href="https://scryfall.com/search?q={{ f.name|urlencode }}" target="_blank" rel="noopener" class="img-btn" title="{{ f.name }}">
|
||||
<img class="card-thumb" src="https://api.scryfall.com/cards/named?fuzzy={{ f.name|urlencode }}&format=image&version=normal" alt="{{ f.name }} image" width="160" loading="lazy" decoding="async" data-lqip="1"
|
||||
srcset="https://api.scryfall.com/cards/named?fuzzy={{ f.name|urlencode }}&format=image&version=small 160w, https://api.scryfall.com/cards/named?fuzzy={{ f.name|urlencode }}&format=image&version=normal 488w, https://api.scryfall.com/cards/named?fuzzy={{ f.name|urlencode }}&format=image&version=large 672w"
|
||||
|
@ -23,7 +35,14 @@
|
|||
</a>
|
||||
<div class="owned-badge" title="{{ 'Owned' if f.owned else 'Not owned' }}" aria-label="{{ 'Owned' if f.owned else 'Not owned' }}">{% if f.owned %}✔{% else %}✖{% endif %}</div>
|
||||
<div class="name">{{ f.name }}</div>
|
||||
<div class="muted" style="text-align:center; font-size:12px;">{{ f.category }}{% if f.role %} • {{ f.role }}{% endif %}</div>
|
||||
<div class="muted" style="text-align:center; font-size:12px; display:flex; gap:.35rem; justify-content:center; align-items:center; flex-wrap:wrap;">
|
||||
<span>{{ f.category }}{% if f.role %} • {{ f.role }}{% endif %}</span>
|
||||
{% if sev == 'FAIL' %}
|
||||
<span class="chip" title="Severity: FAIL"><span class="dot" style="background: var(--red-main);"></span> FAIL</span>
|
||||
{% elif sev == 'WARN' %}
|
||||
<span class="chip" title="Severity: WARN"><span class="dot" style="background: var(--orange-main);"></span> WARN</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="display:flex; justify-content:center; margin-top:.25rem;">
|
||||
{# Role-aware alternatives: pass the flagged name; server will infer role and exclude in-deck/locked #}
|
||||
<button type="button" class="btn" hx-get="/build/alternatives" hx-vals='{"name": "{{ f.name }}"}' hx-target="#alts-flag-{{ loop.index0 }}" hx-swap="innerHTML" title="Suggest role-consistent replacements">Pick replacement…</button>
|
||||
|
|
|
@ -9,6 +9,9 @@ exhibition:
|
|||
extra_turns: 0
|
||||
tutors_nonland: 3
|
||||
two_card_combos: 0
|
||||
# Soft-warning guidance (does not affect FAIL threshold):
|
||||
# Tutors are discouraged at this level; warn if any are present.
|
||||
tutors_nonland_warn: 1
|
||||
core:
|
||||
level: 2
|
||||
name: Core
|
||||
|
@ -18,6 +21,10 @@ core:
|
|||
extra_turns: 3
|
||||
tutors_nonland: 3
|
||||
two_card_combos: 0
|
||||
# Soft-warning guidance: extra turns and tutors are allowed sparsely.
|
||||
# Warn when any appear, fail only when exceeding the hard limit above.
|
||||
extra_turns_warn: 1
|
||||
tutors_nonland_warn: 1
|
||||
upgraded:
|
||||
level: 3
|
||||
name: Upgraded
|
||||
|
|
|
@ -1 +1,56 @@
|
|||
{"source_url": "test", "generated_at": "now", "cards": ["Time Warp"]}
|
||||
{
|
||||
"cards": [
|
||||
"Alchemist's Gambit",
|
||||
"Alrund's Epiphany",
|
||||
"Beacon of Tomorrows",
|
||||
"Capture of Jingzhou",
|
||||
"Chance for Glory",
|
||||
"Expropriate",
|
||||
"Final Fortune",
|
||||
"Gonti's Aether Heart",
|
||||
"Ichormoon Gauntlet",
|
||||
"Karn's Temporal Sundering",
|
||||
"Last Chance",
|
||||
"Lighthouse Chronologist",
|
||||
"Lost Isle Calling",
|
||||
"Magistrate's Scepter",
|
||||
"Magosi, the Waterveil",
|
||||
"Medomai the Ageless",
|
||||
"Mu Yanling",
|
||||
"Nexus of Fate",
|
||||
"Notorious Throng",
|
||||
"Part the Waterveil",
|
||||
"Plea for Power",
|
||||
"Ral Zarek",
|
||||
"Regenerations Restored",
|
||||
"Rise of the Eldrazi",
|
||||
"Sage of Hours",
|
||||
"Savor the Moment",
|
||||
"Search the City",
|
||||
"Second Chance",
|
||||
"Seedtime",
|
||||
"Stitch in Time",
|
||||
"Teferi, Master of Time",
|
||||
"Teferi, Timebender",
|
||||
"Temporal Extortion",
|
||||
"Temporal Manipulation",
|
||||
"Temporal Mastery",
|
||||
"Temporal Trespass",
|
||||
"Time Sieve",
|
||||
"Time Stretch",
|
||||
"Time Warp",
|
||||
"Timesifter",
|
||||
"Timestream Navigator",
|
||||
"Twice Upon a Time // Unlikely Meeting",
|
||||
"Twice Upon a TimeUnlikely Meeting",
|
||||
"Ugin's Nexus",
|
||||
"Ultimecia, Time Sorceress",
|
||||
"Ultimecia, Time Sorceress // Ultimecia, Omnipotent",
|
||||
"Walk the Aeons",
|
||||
"Wanderwine Prophets",
|
||||
"Warrior's Oath",
|
||||
"Wormfang Manta"
|
||||
],
|
||||
"list_version": "v1.0",
|
||||
"generated_at": "2025-09-04"
|
||||
}
|
||||
|
|
|
@ -1 +1,68 @@
|
|||
{"source_url": "test", "generated_at": "now", "cards": []}
|
||||
{
|
||||
"cards": [
|
||||
"Ad Nauseam",
|
||||
"Ancient Tomb",
|
||||
"Aura Shards",
|
||||
"Bolas's Citadel",
|
||||
"Braids, Cabal Minion",
|
||||
"Chrome Mox",
|
||||
"Coalition Victory",
|
||||
"Consecrated Sphinx",
|
||||
"Crop Rotation",
|
||||
"Cyclonic Rift",
|
||||
"Deflecting Swat",
|
||||
"Demonic Tutor",
|
||||
"Drannith Magistrate",
|
||||
"Enlightened Tutor",
|
||||
"Expropriate",
|
||||
"Field of the Dead",
|
||||
"Fierce Guardianship",
|
||||
"Food Chain",
|
||||
"Force of Will",
|
||||
"Gaea's Cradle",
|
||||
"Gamble",
|
||||
"Gifts Ungiven",
|
||||
"Glacial Chasm",
|
||||
"Grand Arbiter Augustin IV",
|
||||
"Grim Monolith",
|
||||
"Humility",
|
||||
"Imperial Seal",
|
||||
"Intuition",
|
||||
"Jeska's Will",
|
||||
"Jin-Gitaxias, Core Augur",
|
||||
"Kinnan, Bonder Prodigy",
|
||||
"Lion's Eye Diamond",
|
||||
"Mana Vault",
|
||||
"Mishra's Workshop",
|
||||
"Mox Diamond",
|
||||
"Mystical Tutor",
|
||||
"Narset, Parter of Veils",
|
||||
"Natural Order",
|
||||
"Necropotence",
|
||||
"Notion Thief",
|
||||
"Opposition Agent",
|
||||
"Orcish Bowmasters",
|
||||
"Panoptic Mirror",
|
||||
"Rhystic Study",
|
||||
"Seedborn Muse",
|
||||
"Serra's Sanctum",
|
||||
"Smothering Tithe",
|
||||
"Survival of the Fittest",
|
||||
"Sway of the Stars",
|
||||
"Teferi's Protection",
|
||||
"Tergrid, God of Fright",
|
||||
"Tergrid, God of Fright // Tergrid's Lantern",
|
||||
"Thassa's Oracle",
|
||||
"The One Ring",
|
||||
"The Tabernacle at Pendrell Vale",
|
||||
"Underworld Breach",
|
||||
"Urza, Lord High Artificer",
|
||||
"Vampiric Tutor",
|
||||
"Vorinclex, Voice of Hunger",
|
||||
"Winota, Joiner of Forces",
|
||||
"Worldly Tutor",
|
||||
"Yuriko, the Tiger's Shadow"
|
||||
],
|
||||
"list_version": "v1.0",
|
||||
"generated_at": "2025-09-04"
|
||||
}
|
|
@ -1 +1,79 @@
|
|||
{"source_url": "test", "generated_at": "now", "cards": ["Armageddon"]}
|
||||
{
|
||||
"cards": [
|
||||
"Acid Rain",
|
||||
"Apocalypse",
|
||||
"Armageddon",
|
||||
"Back to Basics",
|
||||
"Bearer of the Heavens",
|
||||
"Bend or Break",
|
||||
"Blood Moon",
|
||||
"Boil",
|
||||
"Boiling Seas",
|
||||
"Boom // Bust",
|
||||
"BoomBust",
|
||||
"Break the Ice",
|
||||
"Burning of Xinye",
|
||||
"Cataclysm",
|
||||
"Catastrophe",
|
||||
"Choke",
|
||||
"Cleansing",
|
||||
"Contamination",
|
||||
"Conversion",
|
||||
"Curse of Marit Lage",
|
||||
"Death Cloud",
|
||||
"Decree of Annihilation",
|
||||
"Desolation Angel",
|
||||
"Destructive Force",
|
||||
"Devastating Dreams",
|
||||
"Devastation",
|
||||
"Dimensional Breach",
|
||||
"Disciple of Caelus Nin",
|
||||
"Epicenter",
|
||||
"Fall of the Thran",
|
||||
"Flashfires",
|
||||
"Gilt-Leaf Archdruid",
|
||||
"Glaciers",
|
||||
"Global Ruin",
|
||||
"Hall of Gemstone",
|
||||
"Harbinger of the Seas",
|
||||
"Hokori, Dust Drinker",
|
||||
"Impending Disaster",
|
||||
"Infernal Darkness",
|
||||
"Jokulhaups",
|
||||
"Keldon Firebombers",
|
||||
"Land Equilibrium",
|
||||
"Magus of the Balance",
|
||||
"Magus of the Moon",
|
||||
"Myojin of Infinite Rage",
|
||||
"Naked Singularity",
|
||||
"Natural Balance",
|
||||
"Obliterate",
|
||||
"Omen of Fire",
|
||||
"Raiding Party",
|
||||
"Ravages of War",
|
||||
"Razia's Purification",
|
||||
"Reality Twist",
|
||||
"Realm Razer",
|
||||
"Restore Balance",
|
||||
"Rising Waters",
|
||||
"Ritual of Subdual",
|
||||
"Ruination",
|
||||
"Soulscour",
|
||||
"Stasis",
|
||||
"Static Orb",
|
||||
"Storm Cauldron",
|
||||
"Sunder",
|
||||
"Sway of the Stars",
|
||||
"Tectonic Break",
|
||||
"Thoughts of Ruin",
|
||||
"Tsunami",
|
||||
"Wildfire",
|
||||
"Winter Moon",
|
||||
"Winter Orb",
|
||||
"Worldfire",
|
||||
"Worldpurge",
|
||||
"Worldslayer"
|
||||
],
|
||||
"list_version": "v1.0",
|
||||
"generated_at": "2025-09-04"
|
||||
}
|
|
@ -1 +1,410 @@
|
|||
{"source_url": "test", "generated_at": "now", "cards": ["Demonic Tutor"]}
|
||||
{
|
||||
"cards": [
|
||||
"Academy Rector",
|
||||
"Aether Searcher",
|
||||
"Altar of Bone",
|
||||
"Amrou Scout",
|
||||
"Analyze the Pollen",
|
||||
"Anchor to Reality",
|
||||
"Archdruid's Charm",
|
||||
"Archmage Ascension",
|
||||
"Arcum Dagsson",
|
||||
"Arena Rector",
|
||||
"Artificer's Intuition",
|
||||
"Assembly Hall",
|
||||
"Auratouched Mage",
|
||||
"Aurochs Herd",
|
||||
"Axgard Armory",
|
||||
"Ayara's Oathsworn",
|
||||
"Begin the Invasion",
|
||||
"Behold the Beyond",
|
||||
"Beseech the Mirror",
|
||||
"Beseech the Queen",
|
||||
"Bifurcate",
|
||||
"Bilbo, Birthday Celebrant",
|
||||
"Birthing Pod",
|
||||
"Bitterheart Witch",
|
||||
"Blightspeaker",
|
||||
"Blood Speaker",
|
||||
"Boggart Harbinger",
|
||||
"Bog Glider",
|
||||
"Boonweaver Giant",
|
||||
"Brainspoil",
|
||||
"Brightglass Gearhulk",
|
||||
"Bringer of the Black Dawn",
|
||||
"Bring to Light",
|
||||
"Brutalizer Exarch",
|
||||
"Buried Alive",
|
||||
"Burning-Rune Demon",
|
||||
"Call the Gatewatch",
|
||||
"Captain Sisay",
|
||||
"Caradora, Heart of Alacria",
|
||||
"Case of the Stashed Skeleton",
|
||||
"Cateran Brute",
|
||||
"Cateran Enforcer",
|
||||
"Cateran Kidnappers",
|
||||
"Cateran Overlord",
|
||||
"Cateran Persuader",
|
||||
"Cateran Slaver",
|
||||
"Cateran Summons",
|
||||
"Central ElevatorPromising Stairs",
|
||||
"Central Elevator // Promising Stairs",
|
||||
"Chandra, Heart of Fire",
|
||||
"Chord of Calling",
|
||||
"Citanul Flute",
|
||||
"Clarion Ultimatum",
|
||||
"Cloud, Midgar Mercenary",
|
||||
"Clutch of the Undercity",
|
||||
"Conduit of Ruin",
|
||||
"Conflux",
|
||||
"Congregation at Dawn",
|
||||
"Corpse Connoisseur",
|
||||
"Corpse Harvester",
|
||||
"Coveted Prize",
|
||||
"Cruel Tutor",
|
||||
"Curse of Misfortunes",
|
||||
"Cynical Loner",
|
||||
"Dark Petition",
|
||||
"Deadeye Quartermaster",
|
||||
"Deathbellow War Cry",
|
||||
"Defense of the Heart",
|
||||
"Defiant Falcon",
|
||||
"Defiant Vanguard",
|
||||
"Delivery Moogle",
|
||||
"Demonic Bargain",
|
||||
"Demonic Collusion",
|
||||
"Demonic Consultation",
|
||||
"Demonic Counsel",
|
||||
"Demonic Tutor",
|
||||
"Diabolic Intent",
|
||||
"Diabolic Revelation",
|
||||
"Diabolic Tutor",
|
||||
"Dig Up",
|
||||
"Dimir House Guard",
|
||||
"Dimir Infiltrator",
|
||||
"Dimir Machinations",
|
||||
"Disciples of Gix",
|
||||
"Distant Memories",
|
||||
"Dizzy Spell",
|
||||
"Djeru, With Eyes Open",
|
||||
"Doomsday",
|
||||
"Doubling Chant",
|
||||
"Draconic Muralists",
|
||||
"Dragon's Approach",
|
||||
"Dragonstorm",
|
||||
"Drift of Phantasms",
|
||||
"Dwarven Recruiter",
|
||||
"Ecological Appreciation",
|
||||
"Eerie Procession",
|
||||
"Eladamri's Call",
|
||||
"Eldritch Evolution",
|
||||
"Elvish Harbinger",
|
||||
"Emergent Ultimatum",
|
||||
"Enduring Ideal",
|
||||
"Enigmatic Incarnation",
|
||||
"Enlightened Tutor",
|
||||
"Entomb",
|
||||
"Ethereal Usher",
|
||||
"Evolving Door",
|
||||
"Eye of Ugin",
|
||||
"Fabricate",
|
||||
"Faerie Harbinger",
|
||||
"Fang-Druid Summoner",
|
||||
"Fauna Shaman",
|
||||
"Fervent Mastery",
|
||||
"Fiend Artisan",
|
||||
"Fierce Empath",
|
||||
"Fighter Class",
|
||||
"Finale of Devastation",
|
||||
"Final Parting",
|
||||
"Firemind's Foresight",
|
||||
"Flamekin Harbinger",
|
||||
"Fleshwrither",
|
||||
"Forerunner of the Coalition",
|
||||
"Forerunner of the Empire",
|
||||
"Forerunner of the Heralds",
|
||||
"Forerunner of the Legion",
|
||||
"Forging the Tyrite Sword",
|
||||
"From Beyond",
|
||||
"From Father to Son",
|
||||
"Frostpyre Arcanist",
|
||||
"Fugitive of the Judoon",
|
||||
"Gamble",
|
||||
"Garruk, Caller of Beasts",
|
||||
"Garruk Relentless",
|
||||
"Garruk Relentless // Garruk, the Veil-Cursed",
|
||||
"Garruk, Unleashed",
|
||||
"General Tazri",
|
||||
"Giant Harbinger",
|
||||
"Gifts Ungiven",
|
||||
"Goblin Engineer",
|
||||
"Goblin Matron",
|
||||
"Goblin Recruiter",
|
||||
"Godo, Bandit Warlord",
|
||||
"Gravebreaker Lamia",
|
||||
"Green Sun's Zenith",
|
||||
"Grim Servant",
|
||||
"Grim Tutor",
|
||||
"Grozoth",
|
||||
"Guardian Sunmare",
|
||||
"Guidelight Pathmaker",
|
||||
"Heliod's Pilgrim",
|
||||
"Hibernation's End",
|
||||
"Higure, the Still Wind",
|
||||
"Hoarding Broodlord",
|
||||
"Hoarding Dragon",
|
||||
"Homing Sliver",
|
||||
"Honored Knight-Captain",
|
||||
"Hour of Victory",
|
||||
"Huatli, Poet of Unity",
|
||||
"Huatli, Poet of Unity // Roar of the Fifth People",
|
||||
"Idyllic Tutor",
|
||||
"Ignite the Beacon",
|
||||
"Illicit Shipment",
|
||||
"Imperial Hellkite",
|
||||
"Imperial Recruiter",
|
||||
"Imperial Seal",
|
||||
"Iname as One",
|
||||
"Iname, Death Aspect",
|
||||
"Increasing Ambition",
|
||||
"Infernal Tutor",
|
||||
"Insatiable Avarice",
|
||||
"Insidious Dreams",
|
||||
"Instrument of the Bards",
|
||||
"Intuition",
|
||||
"Invasion of Arcavios",
|
||||
"Invasion of Arcavios // Invocation of the Founders",
|
||||
"Invasion of Ikoria",
|
||||
"Invasion of Ikoria // Zilortha, Apex of Ikoria",
|
||||
"Invasion of Theros",
|
||||
"Invasion of Theros // Ephara, Ever-Sheltering",
|
||||
"Inventors' Fair",
|
||||
"InvertInvent",
|
||||
"Invert // Invent",
|
||||
"Iron Man, Titan of Innovation",
|
||||
"Isperia the Inscrutable",
|
||||
"Jarad's Orders",
|
||||
"Kaho, Minamo Historian",
|
||||
"Kaito Shizuki",
|
||||
"Kasmina, Enigma Sage",
|
||||
"Kellan, the Fae-BloodedBirthright Boon",
|
||||
"Kellan, the Fae-Blooded // Birthright Boon",
|
||||
"Kithkin Harbinger",
|
||||
"Kuldotha Forgemaster",
|
||||
"Lagomos, Hand of Hatred",
|
||||
"Library of Lat-Nam",
|
||||
"Lifespinner",
|
||||
"Light-Paws, Emperor's Voice",
|
||||
"Liliana Vess",
|
||||
"Lim-Dûl's Vault",
|
||||
"Lin Sivvi, Defiant Hero",
|
||||
"Lively Dirge",
|
||||
"Long-Term Plans",
|
||||
"Lost Auramancers",
|
||||
"Lotuslight Dancers",
|
||||
"Loyal Inventor",
|
||||
"Maelstrom of the Spirit Dragon",
|
||||
"Magda, Brazen Outlaw",
|
||||
"Magus of the Order",
|
||||
"Mangara's Tome",
|
||||
"Maralen of the Mornsong",
|
||||
"March of Burgeoning Life",
|
||||
"Mask of the Mimic",
|
||||
"Mastermind's Acquisition",
|
||||
"Mausoleum Secrets",
|
||||
"Merchant Scroll",
|
||||
"Merrow Harbinger",
|
||||
"Micromancer",
|
||||
"Mimeofacture",
|
||||
"Mishra, Artificer Prodigy",
|
||||
"Moggcatcher",
|
||||
"Momir Vig, Simic Visionary",
|
||||
"Moon-Blessed Cleric",
|
||||
"Moonsilver Key",
|
||||
"Muddle the Mixture",
|
||||
"Mwonvuli Beast Tracker",
|
||||
"Myr Kinsmith",
|
||||
"Myr Turbine",
|
||||
"Mystical Teachings",
|
||||
"Mystical Tutor",
|
||||
"Mythos of Brokkos",
|
||||
"Nahiri, the Harbinger",
|
||||
"Natural Order",
|
||||
"Nature's Rhythm",
|
||||
"Nazahn, Revered Bladesmith",
|
||||
"Neoform",
|
||||
"Netherborn Phalanx",
|
||||
"Night Dealings",
|
||||
"Nissa Revane",
|
||||
"Noble Benefactor",
|
||||
"Open the Armory",
|
||||
"Opposition Agent",
|
||||
"Oriq Loremage",
|
||||
"Oswald Fiddlebender",
|
||||
"Pack Hunt",
|
||||
"Parallel Thoughts",
|
||||
"Pattern Matcher",
|
||||
"Pattern of Rebirth",
|
||||
"Perplex",
|
||||
"Personal Tutor",
|
||||
"Phantom Carriage",
|
||||
"Planar Bridge",
|
||||
"Planar Portal",
|
||||
"Plea for Guidance",
|
||||
"Priest of the Wakening Sun",
|
||||
"Primal Command",
|
||||
"Prime Speaker Vannifar",
|
||||
"Profane Tutor",
|
||||
"Protean Hulk",
|
||||
"Pyre of Heroes",
|
||||
"Quest for the Holy Relic",
|
||||
"Quiet Speculation",
|
||||
"Ramosian Captain",
|
||||
"Ramosian Commander",
|
||||
"Ramosian Lieutenant",
|
||||
"Ramosian Sergeant",
|
||||
"Ramosian Sky Marshal",
|
||||
"Ranger-Captain of Eos",
|
||||
"Ranger of Eos",
|
||||
"Ratcatcher",
|
||||
"Rathi Assassin",
|
||||
"Rathi Fiend",
|
||||
"Rathi Intimidator",
|
||||
"Razaketh's Rite",
|
||||
"Razaketh, the Foulblooded",
|
||||
"Reckless Handling",
|
||||
"Recruiter of the Guard",
|
||||
"Relic Seeker",
|
||||
"Remembrance",
|
||||
"Repurposing Bay",
|
||||
"Reshape",
|
||||
"Rhystic Tutor",
|
||||
"Ring of Three Wishes",
|
||||
"Ringsight",
|
||||
"Rocco, Cabaretti Caterer",
|
||||
"Rootless Yew",
|
||||
"Runed Crown",
|
||||
"Runeforge Champion",
|
||||
"Rune-Scarred Demon",
|
||||
"Rushed Rebirth",
|
||||
"Saheeli Rai",
|
||||
"Samut, the Tested",
|
||||
"Sanctum of All",
|
||||
"Sanctum of Ugin",
|
||||
"Sarkhan, Dragonsoul",
|
||||
"Sarkhan's Triumph",
|
||||
"Sarkhan Unbroken",
|
||||
"Savage Order",
|
||||
"Sazh Katzroy",
|
||||
"Scheming Symmetry",
|
||||
"Scion of the Ur-Dragon",
|
||||
"Scour for Scrap",
|
||||
"Scrapyard Recombiner",
|
||||
"Seahunter",
|
||||
"Search for Glory",
|
||||
"Secret Salvage",
|
||||
"Self-Assembler",
|
||||
"Servant of the Stinger",
|
||||
"Shadowborn Apostle",
|
||||
"Shadow-Rite Priest",
|
||||
"Shared Summons",
|
||||
"Shield-Wall Sentinel",
|
||||
"Shred Memory",
|
||||
"Shrine Steward",
|
||||
"Sidisi, Undead Vizier",
|
||||
"Signal the Clans",
|
||||
"Sisay, Weatherlight Captain",
|
||||
"Sivitri, Dragon Master",
|
||||
"Skyship Weatherlight",
|
||||
"Skyshroud Poacher",
|
||||
"Sliver Overlord",
|
||||
"Solve the Equation",
|
||||
"Sovereigns of Lost Alara",
|
||||
"Spellseeker",
|
||||
"Sphinx Ambassador",
|
||||
"Sphinx Summoner",
|
||||
"Starfield Shepherd",
|
||||
"Steelshaper Apprentice",
|
||||
"Steelshaper's Gift",
|
||||
"Step Through",
|
||||
"Sterling Grove",
|
||||
"Stoneforge Mystic",
|
||||
"Stonehewer Giant",
|
||||
"Summoner's Pact",
|
||||
"Sunforger",
|
||||
"SupplyDemand",
|
||||
"Supply // Demand",
|
||||
"Survival of the Fittest",
|
||||
"Sylvan Tutor",
|
||||
"Tainted Pact",
|
||||
"Taj-Nar Swordsmith",
|
||||
"Tallowisp",
|
||||
"Tamiyo's Journal",
|
||||
"Tempest Hawk",
|
||||
"Templar Knight",
|
||||
"Tezzeret, Artifice Master",
|
||||
"Tezzeret, Cruel Captain",
|
||||
"Tezzeret the Seeker",
|
||||
"Thalia's Lancers",
|
||||
"The Caves of Androzani",
|
||||
"The Creation of Avacyn",
|
||||
"The Cruelty of Gix",
|
||||
"The Eleventh Hour",
|
||||
"The Five Doctors",
|
||||
"The Hunger Tide Rises",
|
||||
"The Huntsman's Redemption",
|
||||
"The Seriema",
|
||||
"Thornvault Forager",
|
||||
"Threats Undetected",
|
||||
"Three Dreams",
|
||||
"Tiamat",
|
||||
"Time of Need",
|
||||
"Tolaria West",
|
||||
"Tooth and Nail",
|
||||
"Totem-Guide Hartebeest",
|
||||
"Transit Mage",
|
||||
"Transmutation Font",
|
||||
"Transmute Artifact",
|
||||
"Trapmaker's Snare",
|
||||
"Traverse the Ulvenwald",
|
||||
"Treasure Chest",
|
||||
"Treasure Mage",
|
||||
"Treefolk Harbinger",
|
||||
"Tribute Mage",
|
||||
"Trinket Mage",
|
||||
"Trophy Mage",
|
||||
"Twice Upon a TimeUnlikely Meeting",
|
||||
"Twice Upon a Time // Unlikely Meeting",
|
||||
"Ugin, Eye of the Storms",
|
||||
"Uncage the Menagerie",
|
||||
"Unmarked Grave",
|
||||
"Urza's Saga",
|
||||
"Urza's Sylex",
|
||||
"Vampiric Tutor",
|
||||
"Varragoth, Bloodsky Sire",
|
||||
"Vedalken Aethermage",
|
||||
"Verdant Succession",
|
||||
"Vexing Puzzlebox",
|
||||
"Vile Entomber",
|
||||
"Vivien, Monsters' Advocate",
|
||||
"Vivien on the Hunt",
|
||||
"Vizier of the Anointed",
|
||||
"Wargate",
|
||||
"War of the Last Alliance",
|
||||
"Waterlogged Teachings",
|
||||
"Waterlogged Teachings // Inundated Archive",
|
||||
"Weird Harvest",
|
||||
"Whir of Invention",
|
||||
"Wild Pair",
|
||||
"Wild Research",
|
||||
"Wirewood Herald",
|
||||
"Wishclaw Talisman",
|
||||
"Woodland Bellower",
|
||||
"Worldly Tutor",
|
||||
"Yisan, the Wanderer Bard",
|
||||
"Zirilan of the Claw",
|
||||
"Zur the Enchanter"
|
||||
],
|
||||
"list_version": "v1.0",
|
||||
"generated_at": "2025-09-04"
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue