From 06d8796316a6249e6652dfa524efe1cf08d69ef5 Mon Sep 17 00:00:00 2001 From: matt Date: Wed, 8 Oct 2025 20:59:51 -0700 Subject: [PATCH] feat: add keyword normalization and protection grant detection, fix template syntax and polling issues --- CHANGELOG.md | 19 +- README.md | 2 +- RELEASE_NOTES_TEMPLATE.md | 29 +- code/scripts/audit_protection_full_v2.py | 203 ++++ code/settings.py | 16 +- code/tagging/protection_grant_detection.py | 493 ++++++++++ code/tagging/tag_constants.py | 85 ++ code/tagging/tag_utils.py | 75 +- code/tagging/tagger.py | 92 +- code/tests/test_keyword_normalization.py | 182 ++++ code/tests/test_protection_grant_detection.py | 169 ++++ code/web/routes/build.py | 2 +- code/web/services/orchestrator.py | 12 +- code/web/templates/base.html | 3 +- code/web/templates/build/_step5.html | 5 +- code/web/templates/setup/running.html | 3 +- config/themes/theme_list.json | 913 +++++++----------- 17 files changed, 1692 insertions(+), 611 deletions(-) create mode 100644 code/scripts/audit_protection_full_v2.py create mode 100644 code/tagging/protection_grant_detection.py create mode 100644 code/tests/test_keyword_normalization.py create mode 100644 code/tests/test_protection_grant_detection.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 954e339..b5f4ce0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,16 +9,27 @@ This format follows Keep a Changelog principles and aims for Semantic Versioning ## [Unreleased] ### Summary -- _No unreleased changes yet_ +- Keyword normalization reduces specialty keyword noise by 96% while maintaining theme catalog quality +- Protection tag now focuses on cards that grant shields to others, not just those with inherent protection +- Web UI improvements: faster polling, fixed progress display, and theme refresh stability ### Added -- _None_ +- Keyword normalization system with smart filtering of one-off specialty mechanics +- Allowlist preserves important keywords like Flying, Myriad, and Transform +- Protection grant detection identifies cards that give Hexproof, Ward, or Indestructible to other permanents +- Automatic tagging for creature-type-specific protection (e.g., "Knights Gain Protection") ### Changed -- _None_ +- Keywords now consolidate variants (e.g., "Commander ninjutsu" becomes "Ninjutsu") +- Setup progress polling reduced from 3s to 5-10s intervals for better performance +- Theme catalog streamlined from 753 to 736 themes (-2.3%) with improved quality +- Protection tag refined to focus on 329 cards that grant shields (down from 1,166 with inherent effects) ### Fixed -- _None_ +- Setup progress now shows 100% completion instead of getting stuck at 99% +- Theme catalog no longer continuously regenerates after setup completes +- Health indicator polling optimized to reduce server load +- Protection detection now correctly excludes creatures with only inherent keywords ## [2.5.2] - 2025-10-08 ### Summary diff --git a/README.md b/README.md index ee62409..7ad729e 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ Execute saved configs without manual input. ### Initial Setup Refresh data and caches when formats shift. -- Runs card downloads, CSV regeneration, tagging, and commander catalog rebuilds. +- Runs card downloads, CSV regeneration, smart tagging (keywords + protection grants), and commander catalog rebuilds. - Controlled by `SHOW_SETUP=1` (on by default in compose). - Force a rebuild manually: ```powershell diff --git a/RELEASE_NOTES_TEMPLATE.md b/RELEASE_NOTES_TEMPLATE.md index 1c99b55..d35313a 100644 --- a/RELEASE_NOTES_TEMPLATE.md +++ b/RELEASE_NOTES_TEMPLATE.md @@ -1,10 +1,29 @@ -# MTG Python Deckbuilder ${VERSION} +# MTG Pyt### Added +- Keywo### Changed +- Keywords consolidate variants (e.g., "Commander ninjutsu" → "Ninjutsu") for consistent theme matching +- Protection tag refined to focus on shield-granting cards (329 cards vs 1,166 previously) +- Theme catalog streamlined with improved quality (736 themes, down 2.3%) +- Commander search and theme picker now share an intelligent debounce to prevent redundant requests while typing +- Card grids adopt modern containment rules to minimize layout recalculations on large decks +- Include/exclude buttons respond immediately with optimistic updates, reconciling gracefully if the server disagrees +- Frequently accessed views, like the commander catalog default, now pull from an in-memory cache for sub-200 ms reloads +- Deck review loads in focused chunks, keeping the initial page lean while analytics stream progressively +- Chart hover zones expand to full column width for easier interactionnup filters out one-off specialty mechanics (like set-specific ability words) while keeping evergreen abilities +- Protection grant detection identifies cards that give Hexproof, Ward, or other shields to your permanents +- Creature-type-specific protection automatically tagged (e.g., "Knights Gain Protection" for tribal strategies) +- Skeleton placeholders accept `data-skeleton-label` microcopy and only surface after ~400 ms across the build wizard, stage navigator, and alternatives panel +- Must-have toggle API (`/build/must-haves/toggle`), telemetry ingestion route (`/telemetry/events`), and structured logging helpers capture include/exclude beacons +- Commander catalog results wrap in a deferred skeleton list while commander art lazy-loads via a new `IntersectionObserver` helper in `code/web/static/app.js` +- Collapsible accordions for Mana Overview and Test Hand sections defer heavy analytics until they are expanded +- Click-to-pin chart tooltips keep comparisons anchored and add copy-friendly working buttons +- Virtualized card lists automatically render only visible items once 12+ cards are presentkbuilder ${VERSION} ### Summary -- Builder responsiveness upgrades: smarter HTMX caching, shared debounce helpers, and virtualization hints keep long card lists responsive. -- Commander catalog now ships skeleton placeholders, lazy commander art loading, and cached default results for faster repeat visits. -- Deck summary streams via an HTMX fragment while virtualization powers summary lists without loading every row up front. -- Mana analytics load on demand with collapsible sections and interactive chart tooltips that support click-to-pin comparisons. +- Smarter card tagging: Keywords are cleaner (96% noise reduction) and Protection now highlights cards that actually grant shields to your board +- Builder responsiveness upgrades: smarter HTMX caching, shared debounce helpers, and virtualization hints keep long card lists responsive +- Commander catalog now ships skeleton placeholders, lazy commander art loading, and cached default results for faster repeat visits +- Deck summary streams via an HTMX fragment while virtualization powers summary lists without loading every row up front +- Mana analytics load on demand with collapsible sections and interactive chart tooltips that support click-to-pin comparisons ### Added - Skeleton placeholders accept `data-skeleton-label` microcopy and only surface after ~400 ms across the build wizard, stage navigator, and alternatives panel. diff --git a/code/scripts/audit_protection_full_v2.py b/code/scripts/audit_protection_full_v2.py new file mode 100644 index 0000000..a10d415 --- /dev/null +++ b/code/scripts/audit_protection_full_v2.py @@ -0,0 +1,203 @@ +""" +Full audit of Protection-tagged cards with kindred metadata support (M2 Phase 2). + +Created: October 8, 2025 +Purpose: Audit and validate Protection tag precision after implementing grant detection. + Can be re-run periodically to check tagging quality. + +This script audits ALL Protection-tagged cards and categorizes them: +- Grant: Gives broad protection to other permanents YOU control +- Kindred: Gives protection to specific creature types (metadata tags) +- Mixed: Both broad and kindred/inherent +- Inherent: Only has protection itself +- ConditionalSelf: Only conditionally grants to itself +- Opponent: Grants to opponent's permanents +- Neither: False positive + +Outputs: +- m2_audit_v2.json: Full analysis with summary +- m2_audit_v2_grant.csv: Cards for main Protection tag +- m2_audit_v2_kindred.csv: Cards for kindred metadata tags +- m2_audit_v2_mixed.csv: Cards with both broad and kindred grants +- m2_audit_v2_conditional.csv: Conditional self-grants (exclude) +- m2_audit_v2_inherent.csv: Inherent protection only (exclude) +- m2_audit_v2_opponent.csv: Opponent grants (exclude) +- m2_audit_v2_neither.csv: False positives (exclude) +- m2_audit_v2_all.csv: All cards combined +""" + +import sys +from pathlib import Path +import pandas as pd +import json + +# Add project root to path +project_root = Path(__file__).parent.parent.parent +sys.path.insert(0, str(project_root)) + +from code.tagging.protection_grant_detection import ( + categorize_protection_card, + get_kindred_protection_tags, + is_granting_protection, +) + +def load_all_cards(): + """Load all cards from color/identity CSV files.""" + csv_dir = project_root / 'csv_files' + + # Get all color/identity CSVs (not the raw cards.csv) + csv_files = list(csv_dir.glob('*_cards.csv')) + csv_files = [f for f in csv_files if f.stem not in ['cards', 'testdata']] + + all_cards = [] + for csv_file in csv_files: + try: + df = pd.read_csv(csv_file) + all_cards.append(df) + except Exception as e: + print(f"Warning: Could not load {csv_file.name}: {e}") + + # Combine all DataFrames + combined = pd.concat(all_cards, ignore_index=True) + + # Drop duplicates (cards appear in multiple color files) + combined = combined.drop_duplicates(subset=['name'], keep='first') + + return combined + +def audit_all_protection_cards(): + """Audit all Protection-tagged cards.""" + print("Loading all cards...") + df = load_all_cards() + + print(f"Total cards loaded: {len(df)}") + + # Filter to Protection-tagged cards (column is 'themeTags' in color CSVs) + df_prot = df[df['themeTags'].str.contains('Protection', case=False, na=False)].copy() + + print(f"Protection-tagged cards: {len(df_prot)}") + + # Categorize each card + categories = [] + grants_list = [] + kindred_tags_list = [] + + for idx, row in df_prot.iterrows(): + name = row['name'] + text = str(row.get('text', '')).replace('\\n', '\n') # Convert escaped newlines to real newlines + keywords = str(row.get('keywords', '')) + card_type = str(row.get('type', '')) + + # Categorize with kindred exclusion enabled + category = categorize_protection_card(name, text, keywords, card_type, exclude_kindred=True) + + # Check if it grants broadly + grants_broad = is_granting_protection(text, keywords, exclude_kindred=True) + + # Get kindred tags + kindred_tags = get_kindred_protection_tags(text) + + categories.append(category) + grants_list.append(grants_broad) + kindred_tags_list.append(', '.join(sorted(kindred_tags)) if kindred_tags else '') + + df_prot['category'] = categories + df_prot['grants_broad'] = grants_list + df_prot['kindred_tags'] = kindred_tags_list + + # Generate summary (convert numpy types to native Python for JSON serialization) + summary = { + 'total': int(len(df_prot)), + 'categories': {k: int(v) for k, v in df_prot['category'].value_counts().to_dict().items()}, + 'grants_broad_count': int(df_prot['grants_broad'].sum()), + 'kindred_cards_count': int((df_prot['kindred_tags'] != '').sum()), + } + + # Calculate keep vs remove + keep_categories = {'Grant', 'Mixed'} + kindred_only = df_prot[df_prot['category'] == 'Kindred'] + keep_count = len(df_prot[df_prot['category'].isin(keep_categories)]) + remove_count = len(df_prot[~df_prot['category'].isin(keep_categories | {'Kindred'})]) + + summary['keep_main_tag'] = keep_count + summary['kindred_metadata'] = len(kindred_only) + summary['remove'] = remove_count + summary['precision_estimate'] = round((keep_count / len(df_prot)) * 100, 1) if len(df_prot) > 0 else 0 + + # Print summary + print(f"\n{'='*60}") + print("AUDIT SUMMARY") + print(f"{'='*60}") + print(f"Total Protection-tagged cards: {summary['total']}") + print(f"\nCategories:") + for cat, count in sorted(summary['categories'].items()): + pct = (count / summary['total']) * 100 + print(f" {cat:20s} {count:4d} ({pct:5.1f}%)") + + print(f"\n{'='*60}") + print(f"Main Protection tag: {keep_count:4d} ({keep_count/len(df_prot)*100:5.1f}%)") + print(f"Kindred metadata only: {len(kindred_only):4d} ({len(kindred_only)/len(df_prot)*100:5.1f}%)") + print(f"Remove: {remove_count:4d} ({remove_count/len(df_prot)*100:5.1f}%)") + print(f"{'='*60}") + print(f"Precision estimate: {summary['precision_estimate']}%") + print(f"{'='*60}\n") + + # Export results + output_dir = project_root / 'logs' / 'roadmaps' / 'source' / 'tagging_refinement' + output_dir.mkdir(parents=True, exist_ok=True) + + # Export JSON summary + with open(output_dir / 'm2_audit_v2.json', 'w') as f: + json.dump({ + 'summary': summary, + 'cards': df_prot[['name', 'type', 'category', 'grants_broad', 'kindred_tags', 'keywords', 'text']].to_dict(orient='records') + }, f, indent=2) + + # Export CSVs by category + export_cols = ['name', 'type', 'category', 'grants_broad', 'kindred_tags', 'keywords', 'text'] + + # Grant category + df_grant = df_prot[df_prot['category'] == 'Grant'] + df_grant[export_cols].to_csv(output_dir / 'm2_audit_v2_grant.csv', index=False) + print(f"Exported {len(df_grant)} Grant cards to m2_audit_v2_grant.csv") + + # Kindred category + df_kindred = df_prot[df_prot['category'] == 'Kindred'] + df_kindred[export_cols].to_csv(output_dir / 'm2_audit_v2_kindred.csv', index=False) + print(f"Exported {len(df_kindred)} Kindred cards to m2_audit_v2_kindred.csv") + + # Mixed category + df_mixed = df_prot[df_prot['category'] == 'Mixed'] + df_mixed[export_cols].to_csv(output_dir / 'm2_audit_v2_mixed.csv', index=False) + print(f"Exported {len(df_mixed)} Mixed cards to m2_audit_v2_mixed.csv") + + # ConditionalSelf category + df_conditional = df_prot[df_prot['category'] == 'ConditionalSelf'] + df_conditional[export_cols].to_csv(output_dir / 'm2_audit_v2_conditional.csv', index=False) + print(f"Exported {len(df_conditional)} ConditionalSelf cards to m2_audit_v2_conditional.csv") + + # Inherent category + df_inherent = df_prot[df_prot['category'] == 'Inherent'] + df_inherent[export_cols].to_csv(output_dir / 'm2_audit_v2_inherent.csv', index=False) + print(f"Exported {len(df_inherent)} Inherent cards to m2_audit_v2_inherent.csv") + + # Opponent category + df_opponent = df_prot[df_prot['category'] == 'Opponent'] + df_opponent[export_cols].to_csv(output_dir / 'm2_audit_v2_opponent.csv', index=False) + print(f"Exported {len(df_opponent)} Opponent cards to m2_audit_v2_opponent.csv") + + # Neither category + df_neither = df_prot[df_prot['category'] == 'Neither'] + df_neither[export_cols].to_csv(output_dir / 'm2_audit_v2_neither.csv', index=False) + print(f"Exported {len(df_neither)} Neither cards to m2_audit_v2_neither.csv") + + # All cards + df_prot[export_cols].to_csv(output_dir / 'm2_audit_v2_all.csv', index=False) + print(f"Exported {len(df_prot)} total cards to m2_audit_v2_all.csv") + + print(f"\nAll files saved to: {output_dir}") + + return df_prot, summary + +if __name__ == '__main__': + df_results, summary = audit_all_protection_cards() diff --git a/code/settings.py b/code/settings.py index 0807378..5731031 100644 --- a/code/settings.py +++ b/code/settings.py @@ -1,6 +1,7 @@ from __future__ import annotations # Standard library imports +import os from typing import Dict, List, Optional # ---------------------------------------------------------------------------------- @@ -98,4 +99,17 @@ CSV_DIRECTORY: str = 'csv_files' FILL_NA_COLUMNS: Dict[str, Optional[str]] = { 'colorIdentity': 'Colorless', # Default color identity for cards without one 'faceName': None # Use card's name column value when face name is not available -} \ No newline at end of file +} + +# ---------------------------------------------------------------------------------- +# TAGGING REFINEMENT FEATURE FLAGS (M1-M3) +# ---------------------------------------------------------------------------------- + +# M1: Enable keyword normalization and singleton pruning +TAG_NORMALIZE_KEYWORDS = os.getenv('TAG_NORMALIZE_KEYWORDS', '1').lower() not in ('0', 'false', 'off', 'disabled') + +# M2: Enable protection grant detection (planned) +TAG_PROTECTION_GRANTS = os.getenv('TAG_PROTECT ION_GRANTS', '0').lower() not in ('0', 'false', 'off', 'disabled') + +# M3: Enable metadata/theme partition (planned) +TAG_METADATA_SPLIT = os.getenv('TAG_METADATA_SPLIT', '0').lower() not in ('0', 'false', 'off', 'disabled') \ No newline at end of file diff --git a/code/tagging/protection_grant_detection.py b/code/tagging/protection_grant_detection.py new file mode 100644 index 0000000..dca37b4 --- /dev/null +++ b/code/tagging/protection_grant_detection.py @@ -0,0 +1,493 @@ +""" +Protection grant detection implementation for M2. + +This module provides helpers to distinguish cards that grant protection effects +from cards that have inherent protection effects. + +Usage in tagger.py: + from code.tagging.protection_grant_detection import is_granting_protection + + if is_granting_protection(text, keywords): + # Tag as Protection +""" + +import re +from typing import Set, List, Pattern + +from code.tagging.tag_constants import CREATURE_TYPES + + +# Pre-compile kindred detection patterns at module load for performance +# Pattern: (compiled_regex, tag_name_template) +KINDRED_PATTERNS: List[tuple[Pattern, str]] = [] + +def _init_kindred_patterns(): + """Initialize pre-compiled kindred patterns for all creature types.""" + global KINDRED_PATTERNS + if KINDRED_PATTERNS: + return # Already initialized + + for creature_type in CREATURE_TYPES: + creature_lower = creature_type.lower() + creature_escaped = re.escape(creature_lower) + tag_name = f"{creature_type}s Gain Protection" + + # Create 3 patterns per type + patterns_to_compile = [ + (rf'\bother {creature_escaped}s?\b.*\b(have|gain)\b', tag_name), + (rf'\b{creature_escaped} creatures?\b.*\b(have|gain)\b', tag_name), + (rf'\btarget {creature_escaped}\b.*\bgains?\b', tag_name), + ] + + for pattern_str, tag in patterns_to_compile: + try: + compiled = re.compile(pattern_str, re.IGNORECASE) + KINDRED_PATTERNS.append((compiled, tag)) + except re.error: + # Skip patterns that fail to compile + pass + + +# Grant verb patterns - cards that give protection to other permanents +# These patterns look for grant verbs that affect OTHER permanents, not self +GRANT_VERB_PATTERNS = [ + r'\bgain[s]?\b.*\b(hexproof|shroud|indestructible|ward|protection)\b', + r'\bgive[s]?\b.*\b(hexproof|shroud|indestructible|ward|protection)\b', + r'\bgrant[s]?\b.*\b(hexproof|shroud|indestructible|ward|protection)\b', + r'\bget[s]?\b.*\+.*\b(hexproof|shroud|indestructible|ward|protection)\b', # "gets +X/+X and has" pattern +] + +# Self-reference patterns that should NOT count as granting +# Reminder text and keyword lines only +SELF_REFERENCE_PATTERNS = [ + r'^\s*(hexproof|shroud|indestructible|ward|protection)', # Start of text (keyword ability) + r'\([^)]*\b(hexproof|shroud|indestructible|ward|protection)[^)]*\)', # Reminder text in parens +] + +# Conditional self-grant patterns - activated/triggered abilities that grant to self +CONDITIONAL_SELF_GRANT_PATTERNS = [ + # Activated abilities + r'\{[^}]*\}.*:.*\bthis (creature|permanent|artifact|enchantment)\b.*\bgain[s]?\b.*\b(hexproof|shroud|indestructible|ward|protection)\b', + r'discard.*:.*\bthis (creature|permanent|artifact|enchantment)\b.*\bgain[s]?\b', + r'\{t\}.*:.*\bthis (creature|permanent|artifact|enchantment)\b.*\bgain[s]?\b', + r'sacrifice.*:.*\bthis (creature|permanent|artifact|enchantment)\b.*\bgain[s]?\b', + r'pay.*life.*:.*\bthis (creature|permanent|artifact|enchantment)\b.*\bgain[s]?\b', + # Triggered abilities that grant to self only + r'whenever.*\b(this creature|this permanent|it)\b.*\bgain[s]?\b.*\b(hexproof|shroud|indestructible|ward|protection)\b', + r'whenever you (cast|play|attack|cycle|discard|commit).*\b(this creature|this permanent|it)\b.*\bgain[s]?\b.*\b(hexproof|shroud|indestructible|ward|protection)\b', + r'at the beginning.*\b(this creature|this permanent|it)\b.*\bgain[s]?\b.*\b(hexproof|shroud|indestructible|ward|protection)\b', + r'whenever.*\b(this creature|this permanent)\b (attacks|enters|becomes).*\b(this creature|this permanent|it)\b.*\bgain[s]?\b', + # Named self-references (e.g., "Pristine Skywise gains") + r'whenever you cast.*[A-Z][a-z]+.*gains.*\b(hexproof|shroud|indestructible|ward|protection)\b', + r'whenever you.*[A-Z][a-z]+.*gains.*\b(hexproof|shroud|indestructible|ward|protection)\b', + # Static conditional abilities (as long as, if you control X) + r'as long as.*\b(this creature|this permanent|it|has)\b.*(has|gains?).*\b(hexproof|shroud|indestructible|ward|protection)\b', +] + +# Mass grant patterns - affects multiple creatures YOU control +MASS_GRANT_PATTERNS = [ + r'creatures you control (have|gain|get)', + r'other .* you control (have|gain|get)', + r'(artifacts?|enchantments?|permanents?) you control (have|gain|get)', # Artifacts you control have... + r'other (creatures?|artifacts?|enchantments?) (have|gain|get)', # Other creatures have... + r'all (creatures?|slivers?|permanents?) (have|gain|get)', # All creatures/slivers have... +] + +# Targeted grant patterns - must specify "you control" +TARGETED_GRANT_PATTERNS = [ + r'target .* you control (gains?|gets?|has)', + r'equipped creature (gains?|gets?|has)', + r'enchanted creature (gains?|gets?|has)', +] + +# Exclusion patterns - cards that remove or prevent protection +EXCLUSION_PATTERNS = [ + r"can't have (hexproof|indestructible|ward|shroud)", + r"lose[s]? (hexproof|indestructible|ward|shroud|protection)", + r"without (hexproof|indestructible|ward|shroud)", + r"protection from.*can't", +] + +# Opponent grant patterns - grants to opponent's permanents (EXCLUDE these) +OPPONENT_GRANT_PATTERNS = [ + r'target opponent', + r'each opponent', + r'all creatures', # "all creatures" without "you control" + r'all permanents', # "all permanents" without "you control" + r'each player', + r'each creature', # "each creature" without "you control" +] + +# Kindred-specific grant patterns for metadata tagging +KINDRED_GRANT_PATTERNS = { + 'Knights Gain Protection': [ + r'knight[s]? you control.*\b(hexproof|shroud|indestructible|ward|protection)\b', + r'other knight[s]?.*\b(hexproof|shroud|indestructible|ward|protection)\b', + ], + 'Merfolk Gain Protection': [ + r'merfolk you control.*\b(hexproof|shroud|indestructible|ward|protection)\b', + r'other merfolk.*\b(hexproof|shroud|indestructible|ward|protection)\b', + ], + 'Zombies Gain Protection': [ + r'zombie[s]? you control.*\b(hexproof|shroud|indestructible|ward|protection)\b', + r'other zombie[s]?.*\b(hexproof|shroud|indestructible|ward|protection)\b', + r'target.*zombie.*\bgain[s]?\b.*\b(hexproof|shroud|indestructible|ward|protection)\b', + ], + 'Vampires Gain Protection': [ + r'vampire[s]? you control.*\b(hexproof|shroud|indestructible|ward|protection)\b', + r'other vampire[s]?.*\b(hexproof|shroud|indestructible|ward|protection)\b', + ], + 'Elves Gain Protection': [ + r'el(f|ves) you control.*\b(hexproof|shroud|indestructible|ward|protection)\b', + r'other el(f|ves).*\b(hexproof|shroud|indestructible|ward|protection)\b', + ], + 'Dragons Gain Protection': [ + r'dragon[s]? you control.*\b(hexproof|shroud|indestructible|ward|protection)\b', + r'other dragon[s]?.*\b(hexproof|shroud|indestructible|ward|protection)\b', + ], + 'Goblins Gain Protection': [ + r'goblin[s]? you control.*\b(hexproof|shroud|indestructible|ward|protection)\b', + r'other goblin[s]?.*\b(hexproof|shroud|indestructible|ward|protection)\b', + ], + 'Slivers Gain Protection': [ + r'sliver[s]? you control.*\b(hexproof|shroud|indestructible|ward|protection)\b', + r'all sliver[s]?.*\b(hexproof|shroud|indestructible|ward|protection)\b', + r'other sliver[s]?.*\b(hexproof|shroud|indestructible|ward|protection)\b', + ], + 'Artifacts Gain Protection': [ + r'artifact[s]? you control (have|gain).*\b(hexproof|shroud|indestructible|ward|protection)\b', + r'other artifact[s]? (have|gain).*\b(hexproof|shroud|indestructible|ward|protection)\b', + ], + 'Enchantments Gain Protection': [ + r'enchantment[s]? you control (have|gain).*\b(hexproof|shroud|indestructible|ward|protection)\b', + r'other enchantment[s]? (have|gain).*\b(hexproof|shroud|indestructible|ward|protection)\b', + ], +} + +# Protection keyword patterns for inherent check +PROTECTION_KEYWORDS = { + 'hexproof', + 'shroud', + 'indestructible', + 'ward', + 'protection from', + 'protection', +} + + +def get_kindred_protection_tags(text: str) -> Set[str]: + """ + Identify kindred-specific protection grants for metadata tagging. + + Returns a set of metadata tag names like "Knights Gain Protection". + + Uses both predefined patterns and dynamic creature type detection. + """ + if not text: + return set() + + # Initialize pre-compiled patterns if needed + _init_kindred_patterns() + + text_lower = text.lower() + tags = set() + + # Check predefined patterns (specific kindred types we track) + for tag_name, patterns in KINDRED_GRANT_PATTERNS.items(): + for pattern in patterns: + if re.search(pattern, text_lower, re.IGNORECASE): + tags.add(tag_name) + break # Found match for this kindred type, move to next + + # Only check dynamic patterns if protection keywords present (performance optimization) + if not any(keyword in text_lower for keyword in ['hexproof', 'shroud', 'indestructible', 'ward', 'protection']): + return tags + + # Use pre-compiled patterns for all creature types + for compiled_pattern, tag_name in KINDRED_PATTERNS: + if compiled_pattern.search(text_lower): + tags.add(tag_name) + # Don't break - a card could grant to multiple creature types + + return tags + + +def is_opponent_grant(text: str) -> bool: + """ + Check if card grants protection to opponent's permanents or all permanents. + + Returns True if this grants to opponents (should be excluded from Protection tag). + """ + if not text: + return False + + text_lower = text.lower() + + # Check for opponent grant patterns + for pattern in OPPONENT_GRANT_PATTERNS: + if re.search(pattern, text_lower, re.IGNORECASE): + # Make sure it's not "target opponent" for a different effect + # Must be in context of granting protection + if any(prot in text_lower for prot in ['hexproof', 'shroud', 'indestructible', 'ward', 'protection']): + # Check if "you control" appears in same sentence + if 'you control' not in text_lower.split('.')[0]: + return True + + return False + + +def has_conditional_self_grant(text: str) -> bool: + """ + Check if card has any conditional self-grant patterns. + This does NOT check if it ALSO grants to others. + """ + if not text: + return False + + text_lower = text.lower() + + # Check for conditional self-grant patterns (activated/triggered abilities) + for pattern in CONDITIONAL_SELF_GRANT_PATTERNS: + if re.search(pattern, text_lower, re.IGNORECASE): + return True + + return False + + +def is_conditional_self_grant(text: str) -> bool: + """ + Check if card only conditionally grants protection to itself. + + Examples: + - "{B}, Discard a card: This creature gains hexproof until end of turn." + - "Whenever you cast a noncreature spell, untap this creature. It gains protection..." + - "Whenever this creature attacks, it gains indestructible until end of turn." + + These should be excluded as they don't provide protection to OTHER permanents. + """ + if not text: + return False + + text_lower = text.lower() + + # Check if it has conditional self-grant patterns + found_conditional_self = has_conditional_self_grant(text) + + if not found_conditional_self: + return False + + # If we found a conditional self-grant, check if there's ALSO a grant to others + # Look for patterns that grant to creatures besides itself + has_other_grant = any(re.search(pattern, text_lower, re.IGNORECASE) for pattern in [ + r'other creatures', + r'creatures you control (have|gain)', + r'target (creature|permanent) you control gains', + r'another target (creature|permanent)', + r'equipped creature (has|gains)', + r'enchanted creature (has|gains)', + r'target legendary', + r'permanents you control gain', + ]) + + # Return True only if it's ONLY conditional self-grants (no other grants) + return not has_other_grant + + +def is_granting_protection(text: str, keywords: str, exclude_kindred: bool = False) -> bool: + """ + Determine if a card grants protection effects to other permanents. + + Returns True if the card gives/grants protection to other cards unconditionally. + Returns False if: + - Card only has inherent protection + - Card only conditionally grants to itself + - Card grants to opponent's permanents + - Card grants only to specific kindred types (when exclude_kindred=True) + - Card creates tokens with protection (not granting to existing permanents) + - Card only modifies non-protection stats of other permanents + + Args: + text: Card text to analyze + keywords: Card keywords (comma-separated) + exclude_kindred: If True, exclude kindred-specific grants + + Returns: + True if card grants broad protection, False otherwise + """ + if not text: + return False + + text_lower = text.lower() + + # EXCLUDE: Opponent grants + if is_opponent_grant(text): + return False + + # EXCLUDE: Conditional self-grants only + if is_conditional_self_grant(text): + return False + + # EXCLUDE: Cards that remove protection + for pattern in EXCLUSION_PATTERNS: + if re.search(pattern, text_lower, re.IGNORECASE): + return False + + # EXCLUDE: Token creation with protection (not granting to existing permanents) + if re.search(r'create.*token.*with.*(hexproof|shroud|indestructible|ward|protection)', text_lower, re.IGNORECASE): + # Check if there's ALSO granting to other permanents + has_grant_to_others = any(re.search(pattern, text_lower, re.IGNORECASE) for pattern in MASS_GRANT_PATTERNS) + if not has_grant_to_others: + return False + + # EXCLUDE: Kindred-specific grants if requested + if exclude_kindred: + kindred_tags = get_kindred_protection_tags(text) + if kindred_tags: + # If we detected kindred tags, check if there's ALSO a non-kindred grant + # Look for grant patterns that explicitly grant to ALL creatures/permanents broadly + has_broad_grant = False + + # Patterns that indicate truly broad grants (not type-specific) + broad_only_patterns = [ + r'\bcreatures you control (have|gain)\b(?!.*(knight|merfolk|zombie|elf|dragon|goblin|sliver))', # Only if not followed by type + r'\bpermanents you control (have|gain)\b', + r'\beach (creature|permanent) you control', + r'\ball (creatures?|permanents?)', + ] + + for pattern in broad_only_patterns: + if re.search(pattern, text_lower, re.IGNORECASE): + has_broad_grant = True + break + + if not has_broad_grant: + return False # Only kindred grants, exclude + + # Check if card has inherent protection keywords + has_inherent = False + if keywords: + keywords_lower = keywords.lower() + has_inherent = any(k in keywords_lower for k in PROTECTION_KEYWORDS) + + # Check for explicit grants with protection keywords + found_grant = False + + # Mass grant patterns (creatures you control have/gain) + for pattern in MASS_GRANT_PATTERNS: + match = re.search(pattern, text_lower, re.IGNORECASE) + if match: + # Check if protection keyword appears in the same sentence or nearby (within 70 chars AFTER the match) + # This ensures we're looking at "creatures you control HAVE hexproof" not just having both phrases + context_start = match.start() + context_end = min(len(text_lower), match.end() + 70) + context = text_lower[context_start:context_end] + + if any(prot in context for prot in PROTECTION_KEYWORDS): + found_grant = True + break + + # Targeted grant patterns (target creature gains) + if not found_grant: + for pattern in TARGETED_GRANT_PATTERNS: + match = re.search(pattern, text_lower, re.IGNORECASE) + if match: + # Check if protection keyword appears after the grant verb (within 70 chars) + context_start = match.start() + context_end = min(len(text_lower), match.end() + 70) + context = text_lower[context_start:context_end] + + if any(prot in context for prot in PROTECTION_KEYWORDS): + found_grant = True + break + + # Grant verb patterns (creature gains/gets hexproof) + if not found_grant: + for pattern in GRANT_VERB_PATTERNS: + if re.search(pattern, text_lower, re.IGNORECASE): + found_grant = True + break + + # If we have inherent protection and the ONLY text is about stats (no grant words), exclude + if has_inherent and not found_grant: + # Check if text only talks about other stats (power/toughness, +X/+X) + has_stat_only = bool(re.search(r'(get[s]?|gain[s]?)\s+[+\-][0-9X]+/[+\-][0-9X]+', text_lower)) + # Check if text mentions "other" without protection keywords + mentions_other_without_prot = 'other' in text_lower and not any(prot in text_lower for prot in PROTECTION_KEYWORDS if prot in text_lower[text_lower.find('other'):]) + + if has_stat_only or mentions_other_without_prot: + return False + + return found_grant + + +def categorize_protection_card(name: str, text: str, keywords: str, card_type: str, exclude_kindred: bool = False) -> str: + """ + Categorize a Protection-tagged card for audit purposes. + + Args: + name: Card name + text: Card text + keywords: Card keywords + card_type: Card type line + exclude_kindred: If True, kindred-specific grants are categorized as metadata, not Grant + + Returns: + 'Grant' - gives broad protection to others + 'Kindred' - gives kindred-specific protection (metadata tag) + 'Inherent' - has protection itself + 'ConditionalSelf' - only conditionally grants to itself + 'Opponent' - grants to opponent's permanents + 'Neither' - false positive + """ + keywords_lower = keywords.lower() if keywords else '' + + # Check for opponent grants first + if is_opponent_grant(text): + return 'Opponent' + + # Check for conditional self-grants (ONLY self, no other grants) + if is_conditional_self_grant(text): + return 'ConditionalSelf' + + # Check if it has conditional self-grant (may also have other grants) + has_cond_self = has_conditional_self_grant(text) + + # Check if it has inherent protection + has_inherent = any(k in keywords_lower for k in PROTECTION_KEYWORDS) + + # Check for kindred-specific grants + kindred_tags = get_kindred_protection_tags(text) + if kindred_tags and exclude_kindred: + # Check if there's ALSO a broad grant (excluding kindred) + grants_broad = is_granting_protection(text, keywords, exclude_kindred=True) + + if grants_broad and has_inherent: + # Has inherent + kindred + broad grants + return 'Mixed' + elif grants_broad: + # Has kindred + broad grants (but no inherent) + # This is just Grant with kindred metadata tags + return 'Grant' + elif has_inherent: + # Has inherent + kindred only (not broad) + # This is still just Kindred category (inherent is separate from granting) + return 'Kindred' + else: + # Only kindred grants, no inherent or broad + return 'Kindred' + + # Check if it grants protection broadly (not kindred-specific) + grants_protection = is_granting_protection(text, keywords, exclude_kindred=exclude_kindred) + + # Categorize based on what it does + if grants_protection and has_cond_self: + # Has conditional self-grant + grants to others = Mixed + return 'Mixed' + elif grants_protection and has_inherent: + return 'Mixed' # Has inherent + grants broadly + elif grants_protection: + return 'Grant' # Only grants broadly + elif has_inherent: + return 'Inherent' # Only has inherent + else: + return 'Neither' # False positive diff --git a/code/tagging/tag_constants.py b/code/tagging/tag_constants.py index 30d70dc..6e5f3c4 100644 --- a/code/tagging/tag_constants.py +++ b/code/tagging/tag_constants.py @@ -849,4 +849,89 @@ TOPDECK_EXCLUSION_PATTERNS: List[str] = [ 'from the top of their library', 'look at the top card of target player\'s library', 'reveal the top card of target player\'s library' +] + +# ============================================================================== +# Keyword Normalization (M1 - Tagging Refinement) +# ============================================================================== + +# Keyword normalization map: variant -> canonical +# Maps Commander-specific and variant keywords to their canonical forms +KEYWORD_NORMALIZATION_MAP: Dict[str, str] = { + # Commander variants + 'Commander ninjutsu': 'Ninjutsu', + 'Commander Ninjutsu': 'Ninjutsu', + + # Partner variants (already excluded but mapped for reference) + 'Partner with': 'Partner', + 'Choose a Background': 'Choose a Background', # Keep distinct + "Doctor's Companion": "Doctor's Companion", # Keep distinct + + # Case normalization for common keywords (most are already correct) + 'flying': 'Flying', + 'trample': 'Trample', + 'vigilance': 'Vigilance', + 'haste': 'Haste', + 'deathtouch': 'Deathtouch', + 'lifelink': 'Lifelink', + 'menace': 'Menace', + 'reach': 'Reach', +} + +# Keywords that should never appear in theme tags +# Already excluded during keyword tagging, but documented here +KEYWORD_EXCLUSION_SET: set[str] = { + 'partner', # Already excluded in tag_for_keywords +} + +# Keyword allowlist - keywords that should survive singleton pruning +# Seeded from top keywords and theme whitelist +KEYWORD_ALLOWLIST: set[str] = { + # Evergreen keywords (top 50 from baseline) + 'Flying', 'Enchant', 'Trample', 'Vigilance', 'Haste', 'Equip', 'Flash', + 'Mill', 'Scry', 'Transform', 'Cycling', 'First strike', 'Reach', 'Menace', + 'Lifelink', 'Treasure', 'Defender', 'Deathtouch', 'Kicker', 'Flashback', + 'Protection', 'Surveil', 'Landfall', 'Crew', 'Ward', 'Morph', 'Devoid', + 'Investigate', 'Fight', 'Food', 'Partner', 'Double strike', 'Indestructible', + 'Threshold', 'Proliferate', 'Convoke', 'Hexproof', 'Cumulative upkeep', + 'Goad', 'Delirium', 'Prowess', 'Suspend', 'Affinity', 'Madness', 'Manifest', + 'Amass', 'Domain', 'Unearth', 'Explore', 'Changeling', + + # Additional important mechanics + 'Myriad', 'Cascade', 'Storm', 'Dredge', 'Delve', 'Escape', 'Mutate', + 'Ninjutsu', 'Overload', 'Rebound', 'Retrace', 'Bloodrush', 'Cipher', + 'Extort', 'Evolve', 'Undying', 'Persist', 'Wither', 'Infect', 'Annihilator', + 'Exalted', 'Phasing', 'Shadow', 'Horsemanship', 'Banding', 'Rampage', + 'Shroud', 'Split second', 'Totem armor', 'Living weapon', 'Undaunted', + 'Improvise', 'Surge', 'Emerge', 'Escalate', 'Meld', 'Partner', 'Afflict', + 'Aftermath', 'Embalm', 'Eternalize', 'Exert', 'Fabricate', 'Improvise', + 'Assist', 'Jump-start', 'Mentor', 'Riot', 'Spectacle', 'Addendum', + 'Afterlife', 'Adapt', 'Enrage', 'Ascend', 'Learn', 'Boast', 'Foretell', + 'Squad', 'Encore', 'Daybound', 'Nightbound', 'Disturb', 'Cleave', 'Training', + 'Reconfigure', 'Blitz', 'Casualty', 'Connive', 'Hideaway', 'Prototype', + 'Read ahead', 'Living metal', 'More than meets the eye', 'Ravenous', + 'Squad', 'Toxic', 'For Mirrodin!', 'Backup', 'Bargain', 'Craft', 'Freerunning', + 'Plot', 'Spree', 'Offspring', 'Bestow', 'Monstrosity', 'Tribute', + + # Partner mechanics (distinct types) + 'Choose a Background', "Doctor's Companion", + + # Token types (frequently used) + 'Blood', 'Clue', 'Food', 'Gold', 'Treasure', 'Powerstone', + + # Common ability words + 'Landfall', 'Raid', 'Revolt', 'Threshold', 'Metalcraft', 'Morbid', + 'Bloodthirst', 'Battalion', 'Channel', 'Grandeur', 'Kinship', 'Sweep', + 'Radiance', 'Join forces', 'Fateful hour', 'Inspired', 'Heroic', + 'Constellation', 'Strive', 'Prowess', 'Ferocious', 'Formidable', 'Renown', + 'Tempting offer', 'Will of the council', 'Parley', 'Adamant', 'Devotion', +} + +# Metadata tag prefixes (for M3 - metadata partition) +# Tags matching these patterns should be classified as metadata, not themes +METADATA_TAG_PREFIXES: List[str] = [ + 'Applied:', + 'Bracket:', + 'Diagnostic:', + 'Internal:', ] \ No newline at end of file diff --git a/code/tagging/tag_utils.py b/code/tagging/tag_utils.py index 156f0f5..e731f07 100644 --- a/code/tagging/tag_utils.py +++ b/code/tagging/tag_utils.py @@ -509,4 +509,77 @@ def create_mass_damage_mask(df: pd.DataFrame) -> pd.Series[bool]: damage_mask = create_text_mask(df, number_patterns) target_mask = create_text_mask(df, target_patterns) - return damage_mask & target_mask \ No newline at end of file + return damage_mask & target_mask + + +# ============================================================================== +# Keyword Normalization (M1 - Tagging Refinement) +# ============================================================================== + +def normalize_keywords( + raw: Union[List[str], Set[str], Tuple[str, ...]], + allowlist: Set[str], + frequency_map: dict[str, int] +) -> list[str]: + """Normalize keyword strings for theme tagging. + + Applies normalization rules: + 1. Case normalization (via normalization map) + 2. Canonical mapping (e.g., "Commander Ninjutsu" -> "Ninjutsu") + 3. Singleton pruning (unless allowlisted) + 4. Deduplication + 5. Exclusion of blacklisted keywords + + Args: + raw: Iterable of raw keyword strings + allowlist: Set of keywords that should survive singleton pruning + frequency_map: Dict mapping keywords to their occurrence count + + Returns: + Deduplicated list of normalized keywords + + Raises: + ValueError: If raw is not iterable + + Examples: + >>> normalize_keywords( + ... ['Commander Ninjutsu', 'Flying', 'Allons-y!'], + ... {'Flying', 'Ninjutsu'}, + ... {'Commander Ninjutsu': 2, 'Flying': 100, 'Allons-y!': 1} + ... ) + ['Ninjutsu', 'Flying'] # 'Allons-y!' pruned as singleton + """ + if not hasattr(raw, '__iter__') or isinstance(raw, (str, bytes)): + raise ValueError(f"raw must be iterable, got {type(raw)}") + + normalized_keywords: set[str] = set() + + for keyword in raw: + # Skip non-string entries + if not isinstance(keyword, str): + continue + + # Skip empty strings + keyword = keyword.strip() + if not keyword: + continue + + # Skip excluded keywords + if keyword.lower() in tag_constants.KEYWORD_EXCLUSION_SET: + continue + + # Apply normalization map + normalized = tag_constants.KEYWORD_NORMALIZATION_MAP.get(keyword, keyword) + + # Check if singleton (unless allowlisted) + frequency = frequency_map.get(keyword, 0) + is_singleton = frequency == 1 + is_allowlisted = normalized in allowlist or keyword in allowlist + + # Prune singletons that aren't allowlisted + if is_singleton and not is_allowlisted: + continue + + normalized_keywords.add(normalized) + + return sorted(list(normalized_keywords)) \ No newline at end of file diff --git a/code/tagging/tagger.py b/code/tagging/tagger.py index 6d3c21e..b2b3f0b 100644 --- a/code/tagging/tagger.py +++ b/code/tagging/tagger.py @@ -580,6 +580,11 @@ def add_creatures_to_tags(df: pd.DataFrame, color: str) -> None: ## Add keywords to theme tags def tag_for_keywords(df: pd.DataFrame, color: str) -> None: """Tag cards based on their keywords using vectorized operations. + + When TAG_NORMALIZE_KEYWORDS is enabled, applies normalization: + - Canonical mapping (e.g., "Commander Ninjutsu" -> "Ninjutsu") + - Singleton pruning (unless allowlisted) + - Case normalization Args: df: DataFrame containing card data @@ -589,6 +594,20 @@ def tag_for_keywords(df: pd.DataFrame, color: str) -> None: start_time = pd.Timestamp.now() try: + from settings import TAG_NORMALIZE_KEYWORDS + + # Load frequency map if normalization is enabled + frequency_map: dict[str, int] = {} + if TAG_NORMALIZE_KEYWORDS: + freq_map_path = Path(__file__).parent / 'keyword_frequency_map.json' + if freq_map_path.exists(): + with open(freq_map_path, 'r', encoding='utf-8') as f: + frequency_map = json.load(f) + logger.info('Loaded keyword frequency map with %d entries', len(frequency_map)) + else: + logger.warning('Keyword frequency map not found, normalization disabled for this run') + TAG_NORMALIZE_KEYWORDS = False + # Create mask for valid keywords has_keywords = pd.notna(df['keywords']) @@ -608,17 +627,29 @@ def tag_for_keywords(df: pd.DataFrame, color: str) -> None: else: keywords_iterable = [] - filtered_keywords = [ - kw for kw in keywords_iterable - if kw and kw.lower() not in exclusion_keywords - ] - - return sorted(list(set(base_tags + filtered_keywords))) + # Apply normalization if enabled + if TAG_NORMALIZE_KEYWORDS and frequency_map: + normalized_keywords = tag_utils.normalize_keywords( + keywords_iterable, + tag_constants.KEYWORD_ALLOWLIST, + frequency_map + ) + return sorted(list(set(base_tags + normalized_keywords))) + else: + # Legacy behavior: simple exclusion filter + filtered_keywords = [ + kw for kw in keywords_iterable + if kw and kw.lower() not in exclusion_keywords + ] + return sorted(list(set(base_tags + filtered_keywords))) df.loc[has_keywords, 'themeTags'] = keywords_df.apply(_merge_keywords, axis=1) duration = (pd.Timestamp.now() - start_time).total_seconds() logger.info('Tagged %d cards with keywords in %.2f seconds', has_keywords.sum(), duration) + + if TAG_NORMALIZE_KEYWORDS: + logger.info('Keyword normalization enabled for %s', color) except Exception as e: logger.error('Error tagging keywords: %s', str(e)) @@ -7000,6 +7031,9 @@ def tag_for_protection(df: pd.DataFrame, color: str) -> None: - Ward - Phase out + With TAG_PROTECTION_GRANTS=1, only tags cards that grant protection to other + permanents, filtering out cards with inherent protection. + The function uses helper functions to identify different types of protection and applies tags consistently using vectorized operations. @@ -7025,13 +7059,47 @@ def tag_for_protection(df: pd.DataFrame, color: str) -> None: required_cols = {'text', 'themeTags', 'keywords'} tag_utils.validate_dataframe_columns(df, required_cols) - # Create masks for different protection patterns - text_mask = create_protection_text_mask(df) - keyword_mask = create_protection_keyword_mask(df) - exclusion_mask = create_protection_exclusion_mask(df) + # Check if grant detection is enabled (M2 feature flag) + use_grant_detection = os.getenv('TAG_PROTECTION_GRANTS', '1').lower() in ('1', 'true', 'yes') - # Combine masks - final_mask = (text_mask | keyword_mask) & ~exclusion_mask + if use_grant_detection: + # M2: Use grant detection to filter out inherent-only protection + from code.tagging.protection_grant_detection import is_granting_protection, get_kindred_protection_tags + + # Create a grant detection mask + grant_mask = df.apply( + lambda row: is_granting_protection( + str(row.get('text', '')), + str(row.get('keywords', '')) + ), + axis=1 + ) + + final_mask = grant_mask + logger.info(f'Using M2 grant detection (TAG_PROTECTION_GRANTS=1)') + + # Apply kindred metadata tags for creature-type-specific grants + kindred_count = 0 + for idx, row in df[final_mask].iterrows(): + text = str(row.get('text', '')) + kindred_tags = get_kindred_protection_tags(text) + + if kindred_tags: + # Add kindred-specific metadata tags + current_tags = str(row.get('metadataTags', '')) + existing = set(t.strip() for t in current_tags.split(',') if t.strip()) + existing.update(kindred_tags) + df.at[idx, 'metadataTags'] = ', '.join(sorted(existing)) + kindred_count += 1 + + if kindred_count > 0: + logger.info(f'Applied kindred metadata tags to {kindred_count} cards') + else: + # Legacy: Use original text/keyword patterns + text_mask = create_protection_text_mask(df) + keyword_mask = create_protection_keyword_mask(df) + exclusion_mask = create_protection_exclusion_mask(df) + final_mask = (text_mask | keyword_mask) & ~exclusion_mask # Apply tags via rules engine tag_utils.apply_rules(df, rules=[ diff --git a/code/tests/test_keyword_normalization.py b/code/tests/test_keyword_normalization.py new file mode 100644 index 0000000..002adc8 --- /dev/null +++ b/code/tests/test_keyword_normalization.py @@ -0,0 +1,182 @@ +"""Tests for keyword normalization (M1 - Tagging Refinement).""" +from __future__ import annotations + +import pytest + +from code.tagging import tag_utils, tag_constants + + +class TestKeywordNormalization: + """Test suite for normalize_keywords function.""" + + def test_canonical_mappings(self): + """Test that variant keywords map to canonical forms.""" + raw = ['Commander Ninjutsu', 'Flying', 'Trample'] + allowlist = tag_constants.KEYWORD_ALLOWLIST + frequency_map = { + 'Commander Ninjutsu': 2, + 'Flying': 100, + 'Trample': 50 + } + + result = tag_utils.normalize_keywords(raw, allowlist, frequency_map) + + assert 'Ninjutsu' in result + assert 'Flying' in result + assert 'Trample' in result + assert 'Commander Ninjutsu' not in result + + def test_singleton_pruning(self): + """Test that singleton keywords are pruned unless allowlisted.""" + raw = ['Allons-y!', 'Flying', 'Take 59 Flights of Stairs'] + allowlist = {'Flying'} # Only Flying is allowlisted + frequency_map = { + 'Allons-y!': 1, + 'Flying': 100, + 'Take 59 Flights of Stairs': 1 + } + + result = tag_utils.normalize_keywords(raw, allowlist, frequency_map) + + assert 'Flying' in result + assert 'Allons-y!' not in result + assert 'Take 59 Flights of Stairs' not in result + + def test_case_normalization(self): + """Test that keywords are normalized to proper case.""" + raw = ['flying', 'TRAMPLE', 'vigilance'] + allowlist = {'Flying', 'Trample', 'Vigilance'} + frequency_map = { + 'flying': 100, + 'TRAMPLE': 50, + 'vigilance': 75 + } + + result = tag_utils.normalize_keywords(raw, allowlist, frequency_map) + + # Case normalization happens via the map + # If not in map, original case is preserved + assert len(result) == 3 + + def test_partner_exclusion(self): + """Test that partner keywords remain excluded.""" + raw = ['Partner', 'Flying', 'Trample'] + allowlist = {'Flying', 'Trample'} + frequency_map = { + 'Partner': 50, + 'Flying': 100, + 'Trample': 50 + } + + result = tag_utils.normalize_keywords(raw, allowlist, frequency_map) + + assert 'Flying' in result + assert 'Trample' in result + assert 'Partner' not in result # Excluded + assert 'partner' not in result + + def test_empty_input(self): + """Test that empty input returns empty list.""" + result = tag_utils.normalize_keywords([], set(), {}) + assert result == [] + + def test_whitespace_handling(self): + """Test that whitespace is properly stripped.""" + raw = [' Flying ', 'Trample ', ' Vigilance'] + allowlist = {'Flying', 'Trample', 'Vigilance'} + frequency_map = { + 'Flying': 100, + 'Trample': 50, + 'Vigilance': 75 + } + + result = tag_utils.normalize_keywords(raw, allowlist, frequency_map) + + assert 'Flying' in result + assert 'Trample' in result + assert 'Vigilance' in result + + def test_deduplication(self): + """Test that duplicate keywords are deduplicated.""" + raw = ['Flying', 'Flying', 'Trample', 'Flying'] + allowlist = {'Flying', 'Trample'} + frequency_map = { + 'Flying': 100, + 'Trample': 50 + } + + result = tag_utils.normalize_keywords(raw, allowlist, frequency_map) + + assert result.count('Flying') == 1 + assert result.count('Trample') == 1 + + def test_non_string_entries_skipped(self): + """Test that non-string entries are safely skipped.""" + raw = ['Flying', None, 123, 'Trample', ''] + allowlist = {'Flying', 'Trample'} + frequency_map = { + 'Flying': 100, + 'Trample': 50 + } + + result = tag_utils.normalize_keywords(raw, allowlist, frequency_map) + + assert 'Flying' in result + assert 'Trample' in result + assert len(result) == 2 + + def test_invalid_input_raises_error(self): + """Test that non-iterable input raises ValueError.""" + with pytest.raises(ValueError, match="raw must be iterable"): + tag_utils.normalize_keywords("not-a-list", set(), {}) + + def test_allowlist_preserves_singletons(self): + """Test that allowlisted keywords survive even if they're singletons.""" + raw = ['Myriad', 'Flying', 'Cascade'] + allowlist = {'Flying', 'Myriad', 'Cascade'} # All allowlisted + frequency_map = { + 'Myriad': 1, # Singleton + 'Flying': 100, + 'Cascade': 1 # Singleton + } + + result = tag_utils.normalize_keywords(raw, allowlist, frequency_map) + + assert 'Myriad' in result # Preserved despite being singleton + assert 'Flying' in result + assert 'Cascade' in result # Preserved despite being singleton + + +class TestKeywordIntegration: + """Integration tests for keyword normalization in tagging flow.""" + + def test_normalization_preserves_evergreen_keywords(self): + """Test that common evergreen keywords are always preserved.""" + evergreen = ['Flying', 'Trample', 'Vigilance', 'Haste', 'Deathtouch', 'Lifelink'] + allowlist = tag_constants.KEYWORD_ALLOWLIST + frequency_map = {kw: 100 for kw in evergreen} # All common + + result = tag_utils.normalize_keywords(evergreen, allowlist, frequency_map) + + for kw in evergreen: + assert kw in result + + def test_crossover_keywords_pruned(self): + """Test that crossover-specific singletons are pruned.""" + crossover_singletons = [ + 'Gae Bolg', # Final Fantasy + 'Psychic Defense', # Warhammer 40K + 'Allons-y!', # Doctor Who + 'Flying' # Evergreen (control) + ] + allowlist = {'Flying'} # Only Flying allowed + frequency_map = { + 'Gae Bolg': 1, + 'Psychic Defense': 1, + 'Allons-y!': 1, + 'Flying': 100 + } + + result = tag_utils.normalize_keywords(crossover_singletons, allowlist, frequency_map) + + assert result == ['Flying'] # Only evergreen survived diff --git a/code/tests/test_protection_grant_detection.py b/code/tests/test_protection_grant_detection.py new file mode 100644 index 0000000..435377b --- /dev/null +++ b/code/tests/test_protection_grant_detection.py @@ -0,0 +1,169 @@ +""" +Tests for protection grant detection (M2). + +Tests the ability to distinguish between cards that grant protection +and cards that have inherent protection. +""" + +import pytest +from code.tagging.protection_grant_detection import ( + is_granting_protection, + categorize_protection_card +) + + +class TestGrantDetection: + """Test grant verb detection.""" + + def test_gains_hexproof(self): + """Cards with 'gains hexproof' should be detected as granting.""" + text = "Target creature gains hexproof until end of turn." + assert is_granting_protection(text, "") + + def test_gives_indestructible(self): + """Cards with 'gives indestructible' should be detected as granting.""" + text = "This creature gives target creature indestructible." + assert is_granting_protection(text, "") + + def test_creatures_you_control_have(self): + """Mass grant pattern should be detected.""" + text = "Creatures you control have hexproof." + assert is_granting_protection(text, "") + + def test_equipped_creature_gets(self): + """Equipment grant pattern should be detected.""" + text = "Equipped creature gets +2/+2 and has indestructible." + assert is_granting_protection(text, "") + + +class TestInherentDetection: + """Test inherent protection detection.""" + + def test_creature_with_hexproof_keyword(self): + """Creature with hexproof keyword should not be detected as granting.""" + text = "Hexproof (This creature can't be the target of spells or abilities.)" + keywords = "Hexproof" + assert not is_granting_protection(text, keywords) + + def test_indestructible_artifact(self): + """Artifact with indestructible keyword should not be detected as granting.""" + text = "Indestructible" + keywords = "Indestructible" + assert not is_granting_protection(text, keywords) + + def test_ward_creature(self): + """Creature with Ward should not be detected as granting (unless it grants to others).""" + text = "Ward {2}" + keywords = "Ward" + assert not is_granting_protection(text, keywords) + + +class TestMixedCases: + """Test cards that both grant and have protection.""" + + def test_creature_with_self_grant(self): + """Creature that grants itself protection should be detected.""" + text = "This creature gains indestructible until end of turn." + keywords = "" + assert is_granting_protection(text, keywords) + + def test_equipment_with_inherent_and_grant(self): + """Equipment with indestructible that grants protection.""" + text = "Indestructible. Equipped creature has hexproof." + keywords = "Indestructible" + # Should be detected as granting because of "has hexproof" + assert is_granting_protection(text, keywords) + + +class TestExclusions: + """Test exclusion patterns.""" + + def test_cant_have_hexproof(self): + """Cards that prevent protection should not be tagged.""" + text = "Creatures your opponents control can't have hexproof." + assert not is_granting_protection(text, "") + + def test_loses_indestructible(self): + """Cards that remove protection should not be tagged.""" + text = "Target creature loses indestructible until end of turn." + assert not is_granting_protection(text, "") + + +class TestEdgeCases: + """Test edge cases and special patterns.""" + + def test_protection_from_color(self): + """Protection from [quality] in keywords without grant text.""" + text = "Protection from red" + keywords = "Protection from red" + assert not is_granting_protection(text, keywords) + + def test_empty_text(self): + """Empty text should return False.""" + assert not is_granting_protection("", "") + + def test_none_text(self): + """None text should return False.""" + assert not is_granting_protection(None, "") + + +class TestCategorization: + """Test full card categorization.""" + + def test_shell_shield_is_grant(self): + """Shell Shield grants hexproof - should be Grant.""" + text = "Target creature gets +0/+3 and gains hexproof until end of turn." + cat = categorize_protection_card("Shell Shield", text, "", "Instant") + assert cat == "Grant" + + def test_geist_of_saint_traft_is_mixed(self): + """Geist has hexproof and creates tokens - Mixed.""" + text = "Hexproof. Whenever this attacks, create a token." + keywords = "Hexproof" + cat = categorize_protection_card("Geist", text, keywords, "Creature") + # Has hexproof keyword, so inherent + assert cat in ("Inherent", "Mixed") + + def test_darksteel_brute_is_inherent(self): + """Darksteel Brute has indestructible - should be Inherent.""" + text = "Indestructible" + keywords = "Indestructible" + cat = categorize_protection_card("Darksteel Brute", text, keywords, "Artifact") + assert cat == "Inherent" + + def test_scion_of_oona_is_grant(self): + """Scion of Oona grants shroud to other faeries - should be Grant.""" + text = "Other Faeries you control have shroud." + keywords = "Flying, Flash" + cat = categorize_protection_card("Scion of Oona", text, keywords, "Creature") + assert cat == "Grant" + + +class TestRealWorldCards: + """Test against actual card samples from baseline audit.""" + + def test_bulwark_ox(self): + """Bulwark Ox - grants hexproof and indestructible.""" + text = "Sacrifice: Creatures you control with counters gain hexproof and indestructible" + assert is_granting_protection(text, "") + + def test_bloodsworn_squire(self): + """Bloodsworn Squire - grants itself indestructible.""" + text = "This creature gains indestructible until end of turn" + assert is_granting_protection(text, "") + + def test_kaldra_compleat(self): + """Kaldra Compleat - equipment with indestructible that grants.""" + text = "Indestructible. Equipped creature gets +5/+5 and has indestructible" + keywords = "Indestructible" + assert is_granting_protection(text, keywords) + + def test_ward_sliver(self): + """Ward Sliver - grants protection to all slivers.""" + text = "All Slivers have protection from the chosen color" + assert is_granting_protection(text, "") + + def test_rebbec(self): + """Rebbec - grants protection to artifacts.""" + text = "Artifacts you control have protection from each mana value" + assert is_granting_protection(text, "") diff --git a/code/web/routes/build.py b/code/web/routes/build.py index e058ed6..676ae71 100644 --- a/code/web/routes/build.py +++ b/code/web/routes/build.py @@ -170,7 +170,7 @@ def _step5_summary_placeholder_html(token: int, *, message: str | None = None) - return ( f'
' + 'hx-trigger="step5:refresh from:body" hx-swap="outerHTML">' f'
{_esc(text)}
' '
' ) diff --git a/code/web/services/orchestrator.py b/code/web/services/orchestrator.py index 422a355..9b9f8b4 100644 --- a/code/web/services/orchestrator.py +++ b/code/web/services/orchestrator.py @@ -1181,6 +1181,9 @@ def _ensure_setup_ready(out, force: bool = False) -> None: # Only flip phase if previous run finished if st.get('phase') in {'themes','themes-fast'}: st['phase'] = 'done' + # Also ensure percent is 100 when done + if st.get('finished_at'): + st['percent'] = 100 with open(status_path, 'w', encoding='utf-8') as _wf: json.dump(st, _wf) except Exception: @@ -1463,16 +1466,17 @@ def _ensure_setup_ready(out, force: bool = False) -> None: except Exception: pass - # Unconditional fallback: if (for any reason) no theme export ran above, perform a fast-path export now. - # This guarantees that clicking Run Setup/Tagging always leaves themes current even when tagging wasn't needed. + # Conditional fallback: only run theme export if refresh_needed was True but somehow no export performed. + # This avoids repeated exports when setup is already complete and _ensure_setup_ready is called again. try: - if not theme_export_performed: + if not theme_export_performed and refresh_needed: _refresh_theme_catalog(out, force=False, fast_path=True) except Exception: pass else: # If export just ran (either earlier or via fallback), ensure enrichment ran (safety double-call guard inside helper) try: - _run_theme_metadata_enrichment(out) + if theme_export_performed or refresh_needed: + _run_theme_metadata_enrichment(out) except Exception: pass diff --git a/code/web/templates/base.html b/code/web/templates/base.html index 576d9af..050d57c 100644 --- a/code/web/templates/base.html +++ b/code/web/templates/base.html @@ -309,7 +309,8 @@ .catch(function(){ /* noop */ }); } catch(e) {} } - setInterval(pollStatus, 3000); + // Poll every 10 seconds instead of 3 to reduce server load (only for header indicator) + setInterval(pollStatus, 10000); pollStatus(); // Health indicator poller diff --git a/code/web/templates/build/_step5.html b/code/web/templates/build/_step5.html index 9757ff0..b1d4b88 100644 --- a/code/web/templates/build/_step5.html +++ b/code/web/templates/build/_step5.html @@ -462,11 +462,12 @@ {% if allow_must_haves %} - {% include "partials/include_exclude_summary.html" with oob=False %} + {% set oob = False %} + {% include "partials/include_exclude_summary.html" %} {% endif %}
{% if summary_ready %}Loading deck summary…{% else %}Deck summary will appear after the build completes.{% endif %} diff --git a/code/web/templates/setup/running.html b/code/web/templates/setup/running.html index eb94c8a..e272a3a 100644 --- a/code/web/templates/setup/running.html +++ b/code/web/templates/setup/running.html @@ -127,7 +127,8 @@ .then(update) .catch(function(){}); } - setInterval(poll, 3000); + // Poll every 5 seconds instead of 3 to reduce server load + setInterval(poll, 5000); poll(); })(); diff --git a/config/themes/theme_list.json b/config/themes/theme_list.json index 1047f02..ba5fa1d 100644 --- a/config/themes/theme_list.json +++ b/config/themes/theme_list.json @@ -47,9 +47,9 @@ "primary_color": "Black", "secondary_color": "Blue", "example_commanders": [ - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)", "Rishkar, Peema Renegade - Synergy (Counters Matter)", - "Krenko, Tin Street Kingpin - Synergy (Counters Matter)" + "Krenko, Tin Street Kingpin - Synergy (Counters Matter)", + "Yawgmoth, Thran Physician - Synergy (Counters Matter)" ], "example_cards": [ "Wall of Roots", @@ -118,9 +118,6 @@ "Yawgmoth, Thran Physician", "Tezzeret's Gambit" ], - "synergy_commanders": [ - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Infect)" - ], "popularity_bucket": "Common", "editorial_quality": "draft", "description": "Spreads -1/-1 counters for removal, attrition, and loop engines leveraging death & sacrifice triggers. Synergies like Proliferate and Counters Matter reinforce the plan." @@ -155,7 +152,7 @@ "Silverflame Ritual" ], "synergy_commanders": [ - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)" + "Yawgmoth, Thran Physician - Synergy (Counters Matter)" ], "popularity_bucket": "Rare", "editorial_quality": "draft", @@ -191,8 +188,8 @@ "Jetfire, Ingenious Scientist // Jetfire, Air Guardian" ], "synergy_commanders": [ - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)", "Yahenni, Undying Partisan - Synergy (Counters Matter)", + "Heliod, Sun-Crowned - Synergy (Counters Matter)", "Sram, Senior Edificer - Synergy (Voltron)" ], "popularity_bucket": "Rare", @@ -211,8 +208,8 @@ "secondary_color": "Blue", "example_commanders": [ "Syr Konrad, the Grim - Synergy (Interaction)", - "Toski, Bearer of Secrets - Synergy (Interaction)", "Purphoros, God of the Forge - Synergy (Interaction)", + "Boromir, Warden of the Tower - Synergy (Interaction)", "Lotho, Corrupt Shirriff - Synergy (Spells Matter)", "Birgi, God of Storytelling // Harnfel, Horn of Bounty - Synergy (Spells Matter)" ], @@ -490,7 +487,7 @@ "Cosima, God of the Voyage // The Omenkeel" ], "synergy_commanders": [ - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)" + "Rishkar, Peema Renegade - Synergy (Counters Matter)" ], "popularity_bucket": "Niche", "editorial_quality": "draft", @@ -560,11 +557,11 @@ "id": "alien-kindred", "theme": "Alien Kindred", "synergies": [ + "Clones", "Horror Kindred", "Exile Matters", "Trample", - "Protection", - "Counters Matter" + "Soldier Kindred" ], "primary_color": "Blue", "secondary_color": "Green", @@ -586,12 +583,12 @@ "Time Beetle" ], "synergy_commanders": [ - "Mondrak, Glory Dominus - Synergy (Horror Kindred)", + "Mondrak, Glory Dominus - Synergy (Clones)", + "Kiki-Jiki, Mirror Breaker - Synergy (Clones)", + "Sakashima of a Thousand Faces - Synergy (Clones)", "Solphim, Mayhem Dominus - Synergy (Horror Kindred)", "Zopandrel, Hunger Dominus - Synergy (Horror Kindred)", - "Etali, Primal Storm - Synergy (Exile Matters)", - "Ragavan, Nimble Pilferer - Synergy (Exile Matters)", - "Ghalta, Primal Hunger - Synergy (Trample)" + "Etali, Primal Storm - Synergy (Exile Matters)" ], "popularity_bucket": "Rare", "editorial_quality": "draft", @@ -725,8 +722,8 @@ "Rishkar, Peema Renegade - Synergy (+1/+1 Counters)", "Krenko, Tin Street Kingpin - Synergy (+1/+1 Counters)", "Yawgmoth, Thran Physician - Synergy (+1/+1 Counters)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)", - "Yahenni, Undying Partisan - Synergy (Counters Matter)" + "Yahenni, Undying Partisan - Synergy (Counters Matter)", + "Heliod, Sun-Crowned - Synergy (Counters Matter)" ], "example_cards": [ "Kilnmouth Dragon", @@ -848,7 +845,7 @@ "Artifacts Matter", "Big Mana", "Toughness Matters", - "Aggro" + "Interaction" ], "primary_color": "Green", "secondary_color": "Red", @@ -2435,7 +2432,7 @@ "Ziatora's Envoy", "Mezzio Mugger", "Sabin, Master Monk", - "Night Clubber" + "Riveteers Requisitioner" ], "synergy_commanders": [ "Kutzil, Malamet Exemplar - Synergy (Warrior Kindred)", @@ -2459,21 +2456,21 @@ "primary_color": "Red", "secondary_color": "Black", "example_commanders": [ - "Edgar, Charmed Groom // Edgar Markov's Coffin", "Old Rutstein", "Kamber, the Plunderer", "Strefan, Maurer Progenitor", - "Anje, Maid of Dishonor" + "Anje, Maid of Dishonor", + "Shilgengar, Sire of Famine" ], "example_cards": [ "Voldaren Estate", "Blood for the Blood God!", - "Edgar, Charmed Groom // Edgar Markov's Coffin", "Old Rutstein", "Transmutation Font", "Glass-Cast Heart", "Voldaren Epicure", - "Font of Agonies" + "Font of Agonies", + "Exsanguinator Cavalry" ], "synergy_commanders": [ "Indoraptor, the Perfect Hybrid - Synergy (Bloodthirst)", @@ -2494,9 +2491,9 @@ "primary_color": "Red", "secondary_color": "Green", "example_commanders": [ - "Edgar, Charmed Groom // Edgar Markov's Coffin - Synergy (Blood Token)", "Old Rutstein - Synergy (Blood Token)", "Kamber, the Plunderer - Synergy (Blood Token)", + "Strefan, Maurer Progenitor - Synergy (Blood Token)", "Etali, Primal Storm - Synergy (Aggro)", "Ragavan, Nimble Pilferer - Synergy (Aggro)" ], @@ -2531,9 +2528,9 @@ "secondary_color": "Green", "example_commanders": [ "Indoraptor, the Perfect Hybrid", - "Edgar, Charmed Groom // Edgar Markov's Coffin - Synergy (Blood Token)", "Old Rutstein - Synergy (Blood Token)", "Kamber, the Plunderer - Synergy (Blood Token)", + "Strefan, Maurer Progenitor - Synergy (Blood Token)", "Rishkar, Peema Renegade - Synergy (+1/+1 Counters)" ], "example_cards": [ @@ -2626,8 +2623,7 @@ "synergy_commanders": [ "Hokori, Dust Drinker - Synergy (Bracket:MassLandDenial)", "Myojin of Infinite Rage - Synergy (Bracket:MassLandDenial)", - "Elas il-Kor, Sadistic Pilgrim - Synergy (Pingers)", - "Toski, Bearer of Secrets - Synergy (Interaction)" + "Elas il-Kor, Sadistic Pilgrim - Synergy (Pingers)" ], "popularity_bucket": "Very Common", "editorial_quality": "draft", @@ -2705,7 +2701,7 @@ "Yawgmoth, Thran Physician - Synergy (+1/+1 Counters)", "Samut, Voice of Dissent - Synergy (Combat Tricks)", "Naru Meha, Master Wizard - Synergy (Combat Tricks)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)" + "Yahenni, Undying Partisan - Synergy (Counters Matter)" ], "popularity_bucket": "Rare", "editorial_quality": "draft", @@ -2841,8 +2837,8 @@ "Static Orb" ], "synergy_commanders": [ - "Toski, Bearer of Secrets - Synergy (Interaction)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Interaction)", + "Boromir, Warden of the Tower - Synergy (Interaction)", + "Avacyn, Angel of Hope - Synergy (Interaction)", "Lotho, Corrupt Shirriff - Synergy (Spells Matter)" ], "popularity_bucket": "Niche", @@ -3197,7 +3193,6 @@ "Seasoned Dungeoneer" ], "synergy_commanders": [ - "Kellan, Daring Traveler // Journey On - Synergy (Map Token)", "Selvala, Heart of the Wilds - Synergy (Scout Kindred)" ], "popularity_bucket": "Niche", @@ -3354,8 +3349,8 @@ "Kutzil, Malamet Exemplar", "Felidar Retreat", "Displacer Kitten", - "Temur Sabertooth", "Enduring Curiosity", + "Temur Sabertooth", "Lion Sash", "Ocelot Pride", "Felidar Guardian" @@ -3580,7 +3575,7 @@ "synergy_commanders": [ "Codsworth, Handy Helper - Synergy (Mana Rock)", "Karn, Legacy Reforged - Synergy (Mana Rock)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)" + "Rishkar, Peema Renegade - Synergy (Counters Matter)" ], "popularity_bucket": "Niche", "editorial_quality": "draft", @@ -4478,8 +4473,8 @@ "id": "convoke", "theme": "Convoke", "synergies": [ - "Knight Kindred", "Big Mana", + "Knight Kindred", "Toolbox", "Combat Tricks", "Removal" @@ -4491,7 +4486,7 @@ "The Wandering Rescuer", "Hogaak, Arisen Necropolis", "Kasla, the Broken Halo", - "Syr Konrad, the Grim - Synergy (Knight Kindred)" + "Syr Konrad, the Grim - Synergy (Big Mana)" ], "example_cards": [ "Chord of Calling", @@ -4504,10 +4499,10 @@ "March of the Multitudes" ], "synergy_commanders": [ - "Adeline, Resplendent Cathar - Synergy (Knight Kindred)", - "Danitha Capashen, Paragon - Synergy (Knight Kindred)", "Etali, Primal Storm - Synergy (Big Mana)", "Tatyova, Benthic Druid - Synergy (Big Mana)", + "Adeline, Resplendent Cathar - Synergy (Knight Kindred)", + "Danitha Capashen, Paragon - Synergy (Knight Kindred)", "Junji, the Midnight Sky - Synergy (Toolbox)" ], "popularity_bucket": "Niche", @@ -4546,9 +4541,9 @@ "secondary_color": "Green", "example_commanders": [ "Ixhel, Scion of Atraxa", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Poison Counters)", "Skrelv, Defector Mite - Synergy (Poison Counters)", "Skithiryx, the Blight Dragon - Synergy (Poison Counters)", + "Fynn, the Fangbearer - Synergy (Poison Counters)", "Yawgmoth, Thran Physician - Synergy (Infect)" ], "example_cards": [ @@ -4687,11 +4682,11 @@ "primary_color": "Green", "secondary_color": "White", "example_commanders": [ - "Etali, Primal Conqueror // Etali, Primal Sickness", "Rishkar, Peema Renegade", "Krenko, Tin Street Kingpin", "Yawgmoth, Thran Physician", - "Yahenni, Undying Partisan" + "Yahenni, Undying Partisan", + "Heliod, Sun-Crowned" ], "example_cards": [ "The One Ring", @@ -4993,9 +4988,9 @@ "example_commanders": [ "The Pride of Hull Clade", "Kalakscion, Hunger Tyrant", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)", "Rishkar, Peema Renegade - Synergy (Counters Matter)", - "Krenko, Tin Street Kingpin - Synergy (Counters Matter)" + "Krenko, Tin Street Kingpin - Synergy (Counters Matter)", + "Yawgmoth, Thran Physician - Synergy (Counters Matter)" ], "example_cards": [ "The Pride of Hull Clade", @@ -5008,8 +5003,8 @@ "Algae Gharial" ], "synergy_commanders": [ - "Yawgmoth, Thran Physician - Synergy (+1/+1 Counters)", "Yahenni, Undying Partisan - Synergy (+1/+1 Counters)", + "Heliod, Sun-Crowned - Synergy (+1/+1 Counters)", "Azusa, Lost but Seeking - Synergy (Toughness Matters)" ], "popularity_bucket": "Rare", @@ -5046,8 +5041,8 @@ "Elephant Grass" ], "synergy_commanders": [ - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)", "Rishkar, Peema Renegade - Synergy (Counters Matter)", + "Krenko, Tin Street Kingpin - Synergy (Counters Matter)", "Sram, Senior Edificer - Synergy (Enchantments Matter)" ], "popularity_bucket": "Niche", @@ -5590,8 +5585,8 @@ "Azusa, Lost but Seeking - Synergy (Lands Matter)", "Tatyova, Benthic Druid - Synergy (Lands Matter)", "Sheoldred, Whispering One - Synergy (Lands Matter)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)", - "Rishkar, Peema Renegade - Synergy (Counters Matter)" + "Rishkar, Peema Renegade - Synergy (Counters Matter)", + "Krenko, Tin Street Kingpin - Synergy (Counters Matter)" ], "example_cards": [ "Sandstone Needle", @@ -5745,7 +5740,7 @@ "Rishkar, Peema Renegade - Synergy (+1/+1 Counters)", "Krenko, Tin Street Kingpin - Synergy (+1/+1 Counters)", "Yawgmoth, Thran Physician - Synergy (+1/+1 Counters)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)" + "Yahenni, Undying Partisan - Synergy (Counters Matter)" ], "example_cards": [ "Scourge of the Throne", @@ -5758,7 +5753,7 @@ "Marchesa's Emissary" ], "synergy_commanders": [ - "Yahenni, Undying Partisan - Synergy (Counters Matter)", + "Heliod, Sun-Crowned - Synergy (Counters Matter)", "Sram, Senior Edificer - Synergy (Voltron)" ], "popularity_bucket": "Rare", @@ -5867,7 +5862,7 @@ "Rishkar, Peema Renegade - Synergy (+1/+1 Counters)", "Krenko, Tin Street Kingpin - Synergy (+1/+1 Counters)", "Yawgmoth, Thran Physician - Synergy (+1/+1 Counters)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)" + "Yahenni, Undying Partisan - Synergy (Counters Matter)" ], "example_cards": [ "Mycoloth", @@ -5880,7 +5875,7 @@ "Voracious Dragon" ], "synergy_commanders": [ - "Yahenni, Undying Partisan - Synergy (Counters Matter)", + "Heliod, Sun-Crowned - Synergy (Counters Matter)", "Sram, Senior Edificer - Synergy (Voltron)" ], "popularity_bucket": "Rare", @@ -6086,10 +6081,8 @@ "id": "divinity-counters", "theme": "Divinity Counters", "synergies": [ - "Protection", "Spirit Kindred", "Counters Matter", - "Interaction", "Big Mana" ], "primary_color": "White", @@ -6111,12 +6104,12 @@ "Myojin of Infinite Rage" ], "synergy_commanders": [ - "Toski, Bearer of Secrets - Synergy (Protection)", - "Purphoros, God of the Forge - Synergy (Protection)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Protection)", "Kodama of the West Tree - Synergy (Spirit Kindred)", "Kodama of the East Tree - Synergy (Spirit Kindred)", - "Rishkar, Peema Renegade - Synergy (Counters Matter)" + "Junji, the Midnight Sky - Synergy (Spirit Kindred)", + "Rishkar, Peema Renegade - Synergy (Counters Matter)", + "Krenko, Tin Street Kingpin - Synergy (Counters Matter)", + "Syr Konrad, the Grim - Synergy (Big Mana)" ], "popularity_bucket": "Rare", "editorial_quality": "draft", @@ -6783,7 +6776,6 @@ "synergies": [ "Flying", "Burn", - "Interaction", "Little Fellas" ], "primary_color": "Red", @@ -6793,7 +6785,7 @@ "Plargg and Nassari", "Yusri, Fortune's Flame", "Najal, the Storm Runner", - "Uvilda, Dean of Perfection // Nassari, Dean of Expression" + "Niv-Mizzet, Parun - Synergy (Flying)" ], "example_cards": [ "Veyran, Voice of Duality", @@ -6802,16 +6794,15 @@ "Najal, the Storm Runner", "Frenetic Efreet", "Efreet Flamepainter", - "Uvilda, Dean of Perfection // Nassari, Dean of Expression", - "Emissary of Grudges" + "Emissary of Grudges", + "Capricious Efreet" ], "synergy_commanders": [ - "Niv-Mizzet, Parun - Synergy (Flying)", "Avacyn, Angel of Hope - Synergy (Flying)", "Old Gnawbone - Synergy (Flying)", "Syr Konrad, the Grim - Synergy (Burn)", "Braids, Arisen Nightmare - Synergy (Burn)", - "Toski, Bearer of Secrets - Synergy (Interaction)" + "Ragavan, Nimble Pilferer - Synergy (Little Fellas)" ], "popularity_bucket": "Rare", "editorial_quality": "draft", @@ -7568,7 +7559,7 @@ "Combat Tricks", "Spells Matter", "Spellslinger", - "Removal" + "Interaction" ], "primary_color": "Green", "secondary_color": "Black", @@ -7757,8 +7748,8 @@ "secondary_color": "White", "example_commanders": [ "Syr Konrad, the Grim - Synergy (Interaction)", - "Toski, Bearer of Secrets - Synergy (Interaction)", "Purphoros, God of the Forge - Synergy (Interaction)", + "Boromir, Warden of the Tower - Synergy (Interaction)", "Lotho, Corrupt Shirriff - Synergy (Spells Matter)", "Birgi, God of Storytelling // Harnfel, Horn of Bounty - Synergy (Spells Matter)" ], @@ -7904,7 +7895,7 @@ "Rishkar, Peema Renegade - Synergy (+1/+1 Counters)", "Krenko, Tin Street Kingpin - Synergy (+1/+1 Counters)", "Yawgmoth, Thran Physician - Synergy (+1/+1 Counters)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)" + "Yahenni, Undying Partisan - Synergy (Counters Matter)" ], "example_cards": [ "Gyre Sage", @@ -7917,7 +7908,7 @@ "Experiment One" ], "synergy_commanders": [ - "Yahenni, Undying Partisan - Synergy (Counters Matter)", + "Heliod, Sun-Crowned - Synergy (Counters Matter)", "Sram, Senior Edificer - Synergy (Voltron)" ], "popularity_bucket": "Rare", @@ -8029,7 +8020,7 @@ "The Indomitable - Synergy (Vehicles)", "Rishkar, Peema Renegade - Synergy (+1/+1 Counters)", "Krenko, Tin Street Kingpin - Synergy (+1/+1 Counters)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)" + "Yawgmoth, Thran Physician - Synergy (Counters Matter)" ], "popularity_bucket": "Rare", "editorial_quality": "draft", @@ -8168,7 +8159,6 @@ "Nicanzil, Current Conductor" ], "synergy_commanders": [ - "Kellan, Daring Traveler // Journey On - Synergy (Map Token)", "Selvala, Heart of the Wilds - Synergy (Scout Kindred)" ], "popularity_bucket": "Niche", @@ -8290,14 +8280,13 @@ "synergies": [ "Fading", "Counters Matter", - "Enchantments Matter", - "Interaction" + "Enchantments Matter" ], "primary_color": "Green", "secondary_color": "Black", "example_commanders": [ - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)", "Rishkar, Peema Renegade - Synergy (Counters Matter)", + "Krenko, Tin Street Kingpin - Synergy (Counters Matter)", "Sram, Senior Edificer - Synergy (Enchantments Matter)" ], "example_cards": [ @@ -8320,14 +8309,13 @@ "synergies": [ "Fade Counters", "Counters Matter", - "Enchantments Matter", - "Interaction" + "Enchantments Matter" ], "primary_color": "Green", "secondary_color": "Black", "example_commanders": [ - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)", "Rishkar, Peema Renegade - Synergy (Counters Matter)", + "Krenko, Tin Street Kingpin - Synergy (Counters Matter)", "Sram, Senior Edificer - Synergy (Enchantments Matter)" ], "example_cards": [ @@ -8490,8 +8478,7 @@ "synergies": [ "Big Mana", "Spells Matter", - "Spellslinger", - "Interaction" + "Spellslinger" ], "primary_color": "Green", "secondary_color": "Red", @@ -8604,8 +8591,8 @@ "Syr Konrad, the Grim - Synergy (Mill)", "Emry, Lurker of the Loch - Synergy (Mill)", "Sheoldred, Whispering One - Synergy (Mill)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)", "Rishkar, Peema Renegade - Synergy (Counters Matter)", + "Krenko, Tin Street Kingpin - Synergy (Counters Matter)", "Six - Synergy (Reanimate)" ], "popularity_bucket": "Rare", @@ -8658,8 +8645,8 @@ "Banding", "Kithkin Kindred", "Knight Kindred", - "Minotaur Kindred", - "Angel Kindred" + "Partner", + "Minotaur Kindred" ], "primary_color": "White", "secondary_color": "Red", @@ -8940,7 +8927,6 @@ ], "synergy_commanders": [ "Otharri, Suns' Glory - Synergy (Phoenix Kindred)", - "Joshua, Phoenix's Dominant // Phoenix, Warden of Fire - Synergy (Phoenix Kindred)", "Syrix, Carrier of the Flame - Synergy (Phoenix Kindred)", "Ezrim, Agency Chief - Synergy (Archon Kindred)", "Krond the Dawn-Clad - Synergy (Archon Kindred)", @@ -9346,8 +9332,8 @@ ], "synergy_commanders": [ "Yawgmoth, Thran Physician - Synergy (+1/+1 Counters)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)", "Yahenni, Undying Partisan - Synergy (Counters Matter)", + "Heliod, Sun-Crowned - Synergy (Counters Matter)", "Sram, Senior Edificer - Synergy (Voltron)" ], "popularity_bucket": "Rare", @@ -9902,10 +9888,10 @@ "theme": "God Kindred", "synergies": [ "Indestructible", - "Protection", "Transform", "Midrange", - "Exile Matters" + "Exile Matters", + "Sacrifice Matters" ], "primary_color": "Black", "secondary_color": "White", @@ -9929,8 +9915,8 @@ "synergy_commanders": [ "Toski, Bearer of Secrets - Synergy (Indestructible)", "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Indestructible)", - "Boromir, Warden of the Tower - Synergy (Protection)", - "Avacyn, Angel of Hope - Synergy (Protection)" + "Veyran, Voice of Duality - Synergy (Transform)", + "Rishkar, Peema Renegade - Synergy (Midrange)" ], "popularity_bucket": "Niche", "editorial_quality": "draft", @@ -10088,7 +10074,7 @@ "Novijen Sages" ], "synergy_commanders": [ - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)" + "Yawgmoth, Thran Physician - Synergy (Counters Matter)" ], "popularity_bucket": "Rare", "editorial_quality": "draft", @@ -10486,7 +10472,7 @@ "Ulasht, the Hate Seed - Synergy (Hellion Kindred)", "Thromok the Insatiable - Synergy (Hellion Kindred)", "Otharri, Suns' Glory - Synergy (Phoenix Kindred)", - "Joshua, Phoenix's Dominant // Phoenix, Warden of Fire - Synergy (Phoenix Kindred)" + "Syrix, Carrier of the Flame - Synergy (Phoenix Kindred)" ], "popularity_bucket": "Common", "editorial_quality": "draft", @@ -10714,10 +10700,10 @@ "theme": "Hexproof", "synergies": [ "Hexproof from", - "Protection", "Stax", - "Interaction", - "Beast Kindred" + "Beast Kindred", + "Elemental Kindred", + "Outlaw Kindred" ], "primary_color": "Green", "secondary_color": "Blue", @@ -10740,9 +10726,9 @@ ], "synergy_commanders": [ "Niv-Mizzet, Guildpact - Synergy (Hexproof from)", - "Toski, Bearer of Secrets - Synergy (Protection)", - "Purphoros, God of the Forge - Synergy (Protection)", - "Kutzil, Malamet Exemplar - Synergy (Stax)" + "Kutzil, Malamet Exemplar - Synergy (Stax)", + "Lotho, Corrupt Shirriff - Synergy (Stax)", + "Loot, Exuberant Explorer - Synergy (Beast Kindred)" ], "popularity_bucket": "Niche", "editorial_quality": "draft", @@ -10752,9 +10738,7 @@ "id": "hexproof-from", "theme": "Hexproof from", "synergies": [ - "Hexproof", - "Protection", - "Interaction" + "Hexproof" ], "primary_color": "Black", "secondary_color": "Green", @@ -10776,10 +10760,7 @@ "Niv-Mizzet, Supreme" ], "synergy_commanders": [ - "Silumgar, the Drifting Death - Synergy (Hexproof)", - "Toski, Bearer of Secrets - Synergy (Protection)", - "Purphoros, God of the Forge - Synergy (Protection)", - "Syr Konrad, the Grim - Synergy (Interaction)" + "Silumgar, the Drifting Death - Synergy (Hexproof)" ], "popularity_bucket": "Rare", "editorial_quality": "draft", @@ -10875,9 +10856,9 @@ "synergies": [ "Legends Matter", "Backgrounds Matter", - "Partner", "Choose a background", - "Doctor's companion" + "Doctor's companion", + "Shrines Matter" ], "primary_color": "White", "secondary_color": "Black", @@ -10901,7 +10882,7 @@ "synergy_commanders": [ "Jaheira, Friend of the Forest - Synergy (Backgrounds Matter)", "Karlach, Fury of Avernus - Synergy (Backgrounds Matter)", - "Kodama of the East Tree - Synergy (Partner)" + "Lae'zel, Vlaakith's Champion - Synergy (Choose a background)" ], "popularity_bucket": "Very Common", "editorial_quality": "draft", @@ -10976,7 +10957,7 @@ "Curious Homunculus // Voracious Reader", "Filigree Attendant", "Riddlekeeper", - "Bonded Fetch" + "Zndrsplt, Eye of Wisdom" ], "synergy_commanders": [ "Ragavan, Nimble Pilferer - Synergy (Little Fellas)", @@ -11093,8 +11074,8 @@ "Xiahou Dun, the One-Eyed", "Wu Scout", "Wei Scout", - "Lu Bu, Master-at-Arms", "Wu Light Cavalry", + "Lu Bu, Master-at-Arms", "Guan Yu, Sainted Warrior" ], "synergy_commanders": [ @@ -11234,9 +11215,9 @@ "primary_color": "Blue", "secondary_color": "Black", "example_commanders": [ - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)", "Rishkar, Peema Renegade - Synergy (Counters Matter)", - "Krenko, Tin Street Kingpin - Synergy (Counters Matter)" + "Krenko, Tin Street Kingpin - Synergy (Counters Matter)", + "Yawgmoth, Thran Physician - Synergy (Counters Matter)" ], "example_cards": [ "Dark Depths", @@ -11570,9 +11551,9 @@ "synergies": [ "God Kindred", "Protection", - "Interaction", "Lifegain", - "Life Matters" + "Life Matters", + "Stax" ], "primary_color": "White", "secondary_color": "Black", @@ -11596,7 +11577,8 @@ "synergy_commanders": [ "Birgi, God of Storytelling // Harnfel, Horn of Bounty - Synergy (God Kindred)", "Ojer Taq, Deepest Foundation // Temple of Civilization - Synergy (God Kindred)", - "Syr Konrad, the Grim - Synergy (Interaction)" + "Boromir, Warden of the Tower - Synergy (Protection)", + "Tatyova, Benthic Druid - Synergy (Lifegain)" ], "popularity_bucket": "Rare", "editorial_quality": "draft", @@ -11615,25 +11597,25 @@ "primary_color": "Green", "secondary_color": "Black", "example_commanders": [ - "Etali, Primal Conqueror // Etali, Primal Sickness", "Yawgmoth, Thran Physician", "Skrelv, Defector Mite", "Vorinclex, Monstrous Raider", - "Lae'zel, Vlaakith's Champion" + "Lae'zel, Vlaakith's Champion", + "Tekuthal, Inquiry Dominus" ], "example_cards": [ "Karn's Bastion", "Doubling Season", "Evolution Sage", "Cankerbloom", - "Etali, Primal Conqueror // Etali, Primal Sickness", "Thrummingbird", "Yawgmoth, Thran Physician", - "Tezzeret's Gambit" + "Tezzeret's Gambit", + "Innkeeper's Talent" ], "synergy_commanders": [ "Skithiryx, the Blight Dragon - Synergy (Poison Counters)", - "Tekuthal, Inquiry Dominus - Synergy (Proliferate)", + "Fynn, the Fangbearer - Synergy (Poison Counters)", "Karumonix, the Rat King - Synergy (Toxic)" ], "popularity_bucket": "Uncommon", @@ -11744,8 +11726,8 @@ "Tatyova, Benthic Druid - Synergy (Landfall)", "Aesi, Tyrant of Gyre Strait - Synergy (Landfall)", "Bristly Bill, Spine Sower - Synergy (Landfall)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Poison Counters)", "Skrelv, Defector Mite - Synergy (Poison Counters)", + "Skithiryx, the Blight Dragon - Synergy (Poison Counters)", "Rishkar, Peema Renegade - Synergy (Druid Kindred)" ], "popularity_bucket": "Common", @@ -11795,28 +11777,28 @@ "synergies": [ "Removal", "Combat Tricks", - "Protection", "Board Wipes", - "Counterspells" + "Counterspells", + "Soulshift" ], "primary_color": "White", "secondary_color": "Blue", "example_commanders": [ "Syr Konrad, the Grim", - "Toski, Bearer of Secrets", "Purphoros, God of the Forge", - "Etali, Primal Conqueror // Etali, Primal Sickness", - "Boromir, Warden of the Tower" + "Boromir, Warden of the Tower", + "Avacyn, Angel of Hope", + "Padeem, Consul of Innovation" ], "example_cards": [ "Swords to Plowshares", + "Swiftfoot Boots", + "Lightning Greaves", "Path to Exile", "Counterspell", "Blasphemous Act", "Beast Within", - "Bojuka Bog", - "Heroic Intervention", - "Cyclonic Rift" + "Bojuka Bog" ], "synergy_commanders": [ "Ulamog, the Infinite Gyre - Synergy (Removal)", @@ -12326,8 +12308,8 @@ "Kodama of the West Tree - Synergy (Spirit Kindred)", "Kodama of the East Tree - Synergy (Spirit Kindred)", "Junji, the Midnight Sky - Synergy (Spirit Kindred)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)", - "Rishkar, Peema Renegade - Synergy (Counters Matter)" + "Rishkar, Peema Renegade - Synergy (Counters Matter)", + "Krenko, Tin Street Kingpin - Synergy (Counters Matter)" ], "example_cards": [ "Petalmane Baku", @@ -12622,9 +12604,9 @@ "synergies": [ "Draw Triggers", "Wheels", - "Protection", "Creature Tokens", - "Stax" + "Stax", + "Big Mana" ], "primary_color": "Blue", "secondary_color": "Black", @@ -12651,7 +12633,7 @@ "Sheoldred, the Apocalypse - Synergy (Draw Triggers)", "Selvala, Heart of the Wilds - Synergy (Wheels)", "Niv-Mizzet, Parun - Synergy (Wheels)", - "Toski, Bearer of Secrets - Synergy (Protection)" + "Adeline, Resplendent Cathar - Synergy (Creature Tokens)" ], "popularity_bucket": "Rare", "editorial_quality": "draft", @@ -12978,9 +12960,9 @@ "synergies": [ "Historics Matter", "Backgrounds Matter", - "Partner", "Choose a background", - "Doctor's companion" + "Doctor's companion", + "Shrines Matter" ], "primary_color": "White", "secondary_color": "Black", @@ -13004,7 +12986,7 @@ "synergy_commanders": [ "Jaheira, Friend of the Forest - Synergy (Backgrounds Matter)", "Karlach, Fury of Avernus - Synergy (Backgrounds Matter)", - "Kodama of the East Tree - Synergy (Partner)" + "Lae'zel, Vlaakith's Champion - Synergy (Choose a background)" ], "popularity_bucket": "Very Common", "editorial_quality": "draft", @@ -13023,8 +13005,8 @@ "primary_color": "Blue", "secondary_color": "White", "example_commanders": [ - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)", "Rishkar, Peema Renegade - Synergy (Counters Matter)", + "Krenko, Tin Street Kingpin - Synergy (Counters Matter)", "Emry, Lurker of the Loch - Synergy (Wizard Kindred)" ], "example_cards": [ @@ -13054,8 +13036,8 @@ "primary_color": "Blue", "secondary_color": "White", "example_commanders": [ - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)", "Rishkar, Peema Renegade - Synergy (Counters Matter)", + "Krenko, Tin Street Kingpin - Synergy (Counters Matter)", "Kutzil, Malamet Exemplar - Synergy (Warrior Kindred)" ], "example_cards": [ @@ -13709,10 +13691,10 @@ "secondary_color": "Black", "example_commanders": [ "Jeska, Thrice Reborn", - "Ral, Monsoon Mage // Ral, Leyline Prodigy", "Commodore Guff", "Mila, Crafty Companion // Lukka, Wayward Bonder", - "Heart of Kiran" + "Heart of Kiran", + "Adeline, Resplendent Cathar - Synergy (Planeswalkers)" ], "example_cards": [ "Spark Double", @@ -13725,12 +13707,11 @@ "Ral, Crackling Wit" ], "synergy_commanders": [ - "Adeline, Resplendent Cathar - Synergy (Planeswalkers)", "Yawgmoth, Thran Physician - Synergy (Planeswalkers)", "Vorinclex, Monstrous Raider - Synergy (Planeswalkers)", "Lae'zel, Vlaakith's Champion - Synergy (Superfriends)", "Tekuthal, Inquiry Dominus - Synergy (Superfriends)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)" + "Rishkar, Peema Renegade - Synergy (Counters Matter)" ], "popularity_bucket": "Rare", "editorial_quality": "draft", @@ -14013,11 +13994,11 @@ "primary_color": "Blue", "secondary_color": "Green", "example_commanders": [ - "Kellan, Daring Traveler // Journey On", "Hakbal of the Surging Soul - Synergy (Explore)", "Amalia Benavides Aguirre - Synergy (Explore)", "Nicanzil, Current Conductor - Synergy (Explore)", - "Astrid Peth - Synergy (Card Selection)" + "Astrid Peth - Synergy (Card Selection)", + "Francisco, Fowl Marauder - Synergy (Card Selection)" ], "example_cards": [ "Get Lost", @@ -14030,7 +14011,6 @@ "Spyglass Siren" ], "synergy_commanders": [ - "Francisco, Fowl Marauder - Synergy (Card Selection)", "Ragavan, Nimble Pilferer - Synergy (Artifact Tokens)" ], "popularity_bucket": "Rare", @@ -14245,8 +14225,8 @@ "Saryth, the Viper's Fang - Synergy (Warlock Kindred)", "Honest Rutstein - Synergy (Warlock Kindred)", "Breena, the Demagogue - Synergy (Warlock Kindred)", - "Edgar, Charmed Groom // Edgar Markov's Coffin - Synergy (Blood Token)", "Old Rutstein - Synergy (Blood Token)", + "Kamber, the Plunderer - Synergy (Blood Token)", "Ragavan, Nimble Pilferer - Synergy (Pirate Kindred)" ], "popularity_bucket": "Common", @@ -14287,7 +14267,7 @@ "Thalia, Heretic Cathar - Synergy (Soldier Kindred)", "Rishkar, Peema Renegade - Synergy (+1/+1 Counters)", "Krenko, Tin Street Kingpin - Synergy (+1/+1 Counters)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)" + "Yawgmoth, Thran Physician - Synergy (Counters Matter)" ], "popularity_bucket": "Rare", "editorial_quality": "draft", @@ -14647,8 +14627,8 @@ "Skrelv, Defector Mite", "Vishgraz, the Doomhive", "Ria Ivor, Bane of Bladehold", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Poison Counters)", - "Skithiryx, the Blight Dragon - Synergy (Poison Counters)" + "Skithiryx, the Blight Dragon - Synergy (Poison Counters)", + "Fynn, the Fangbearer - Synergy (Poison Counters)" ], "example_cards": [ "Skrelv, Defector Mite", @@ -14991,7 +14971,7 @@ "Bonny Pall, Clearcutter - Synergy (Giant Kindred)", "Rishkar, Peema Renegade - Synergy (+1/+1 Counters)", "Krenko, Tin Street Kingpin - Synergy (+1/+1 Counters)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)" + "Yawgmoth, Thran Physician - Synergy (Counters Matter)" ], "popularity_bucket": "Rare", "editorial_quality": "draft", @@ -15053,8 +15033,8 @@ "Rishkar, Peema Renegade - Synergy (+1/+1 Counters)", "Krenko, Tin Street Kingpin - Synergy (+1/+1 Counters)", "Yawgmoth, Thran Physician - Synergy (+1/+1 Counters)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)", - "Yahenni, Undying Partisan - Synergy (Counters Matter)" + "Yahenni, Undying Partisan - Synergy (Counters Matter)", + "Heliod, Sun-Crowned - Synergy (Counters Matter)" ], "example_cards": [ "Tragic Slip", @@ -15306,7 +15286,7 @@ "Rishkar, Peema Renegade - Synergy (+1/+1 Counters)", "Krenko, Tin Street Kingpin - Synergy (+1/+1 Counters)", "Yawgmoth, Thran Physician - Synergy (+1/+1 Counters)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)" + "Yahenni, Undying Partisan - Synergy (Counters Matter)" ], "example_cards": [ "Everflowing Chalice", @@ -15319,7 +15299,7 @@ "Bloodhusk Ritualist" ], "synergy_commanders": [ - "Yahenni, Undying Partisan - Synergy (Counters Matter)", + "Heliod, Sun-Crowned - Synergy (Counters Matter)", "Selvala, Heart of the Wilds - Synergy (Blink)" ], "popularity_bucket": "Rare", @@ -15735,7 +15715,6 @@ "Ingenious Infiltrator" ], "synergy_commanders": [ - "Higure, the Still Wind - Synergy (Ninjutsu)", "Lord Skitter, Sewer King - Synergy (Rat Kindred)", "Marrow-Gnawer - Synergy (Rat Kindred)", "Syr Konrad, the Grim - Synergy (Human Kindred)" @@ -15758,20 +15737,20 @@ "secondary_color": "Blue", "example_commanders": [ "Nashi, Moon Sage's Scion", + "Yuriko, the Tiger's Shadow", "Ink-Eyes, Servant of Oni", "Higure, the Still Wind", - "Yuffie, Materia Hunter", - "Yuriko, the Tiger's Shadow - Synergy (Ninja Kindred)" + "Yuffie, Materia Hunter" ], "example_cards": [ "Nashi, Moon Sage's Scion", "Fallen Shinobi", "Prosperous Thief", "Thousand-Faced Shadow", + "Yuriko, the Tiger's Shadow", "Silver-Fur Master", "Silent-Blade Oni", - "Ingenious Infiltrator", - "Ink-Eyes, Servant of Oni" + "Ingenious Infiltrator" ], "synergy_commanders": [ "Lord Skitter, Sewer King - Synergy (Rat Kindred)", @@ -16066,7 +16045,7 @@ "Mondrak, Glory Dominus - Synergy (Phyrexian Kindred)", "Sheoldred, the Apocalypse - Synergy (Phyrexian Kindred)", "Elas il-Kor, Sadistic Pilgrim - Synergy (Phyrexian Kindred)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)" + "Rishkar, Peema Renegade - Synergy (Counters Matter)" ], "example_cards": [ "Vat of Rebirth", @@ -16079,7 +16058,7 @@ "Sawblade Scamp" ], "synergy_commanders": [ - "Rishkar, Peema Renegade - Synergy (Counters Matter)", + "Krenko, Tin Street Kingpin - Synergy (Counters Matter)", "Ragavan, Nimble Pilferer - Synergy (Artifacts Matter)" ], "popularity_bucket": "Rare", @@ -16136,7 +16115,7 @@ "Sakashima of a Thousand Faces - Synergy (Clones)", "Rishkar, Peema Renegade - Synergy (+1/+1 Counters)", "Krenko, Tin Street Kingpin - Synergy (+1/+1 Counters)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)" + "Yawgmoth, Thran Physician - Synergy (Counters Matter)" ], "popularity_bucket": "Niche", "editorial_quality": "draft", @@ -16368,8 +16347,8 @@ "Rishkar, Peema Renegade - Synergy (+1/+1 Counters)", "Krenko, Tin Street Kingpin - Synergy (+1/+1 Counters)", "Yawgmoth, Thran Physician - Synergy (+1/+1 Counters)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)", - "Yahenni, Undying Partisan - Synergy (Counters Matter)" + "Yahenni, Undying Partisan - Synergy (Counters Matter)", + "Heliod, Sun-Crowned - Synergy (Counters Matter)" ], "example_cards": [ "Abzan Falconer", @@ -16638,14 +16617,14 @@ "id": "partner", "theme": "Partner", "synergies": [ + "Partner with", "Pirate Kindred", "Artificer Kindred", - "Outlaw Kindred", - "+1/+1 Counters", - "Toughness Matters" + "First strike", + "Elf Kindred" ], - "primary_color": "White", - "secondary_color": "Black", + "primary_color": "Blue", + "secondary_color": "White", "example_commanders": [ "Kodama of the East Tree", "Sakashima of a Thousand Faces", @@ -16664,16 +16643,16 @@ "Prava of the Steel Legion" ], "synergy_commanders": [ + "Pippin, Warden of Isengard - Synergy (Partner with)", + "Merry, Warden of Isengard - Synergy (Partner with)", + "Pir, Imaginative Rascal - Synergy (Partner with)", "Ragavan, Nimble Pilferer - Synergy (Pirate Kindred)", "Captain Lannery Storm - Synergy (Pirate Kindred)", - "Malcolm, Alluring Scoundrel - Synergy (Pirate Kindred)", - "Loran of the Third Path - Synergy (Artificer Kindred)", - "Sai, Master Thopterist - Synergy (Artificer Kindred)", - "Lotho, Corrupt Shirriff - Synergy (Outlaw Kindred)" + "Loran of the Third Path - Synergy (Artificer Kindred)" ], - "popularity_bucket": "Rare", + "popularity_bucket": "Niche", "editorial_quality": "draft", - "description": "Builds around Partner leveraging synergies with Pirate Kindred and Artificer Kindred." + "description": "Builds around Partner leveraging synergies with Partner with and Pirate Kindred." }, { "id": "partner-father-son", @@ -16695,11 +16674,11 @@ "id": "partner-with", "theme": "Partner with", "synergies": [ + "Partner", "Blink", "Enter the Battlefield", "Leave the Battlefield", - "Warrior Kindred", - "Outlaw Kindred" + "Conditional Draw" ], "primary_color": "Blue", "secondary_color": "Red", @@ -16721,16 +16700,16 @@ "Sam, Loyal Attendant" ], "synergy_commanders": [ + "Kodama of the East Tree - Synergy (Partner)", + "Sakashima of a Thousand Faces - Synergy (Partner)", + "Kediss, Emberclaw Familiar - Synergy (Partner)", "Selvala, Heart of the Wilds - Synergy (Blink)", "Sheoldred, Whispering One - Synergy (Blink)", - "Ojer Taq, Deepest Foundation // Temple of Civilization - Synergy (Blink)", - "Elesh Norn, Mother of Machines - Synergy (Enter the Battlefield)", - "Kodama of the East Tree - Synergy (Enter the Battlefield)", - "Nezahal, Primal Tide - Synergy (Leave the Battlefield)" + "Ojer Taq, Deepest Foundation // Temple of Civilization - Synergy (Enter the Battlefield)" ], "popularity_bucket": "Rare", "editorial_quality": "draft", - "description": "Builds around Partner with leveraging synergies with Blink and Enter the Battlefield." + "description": "Builds around Partner with leveraging synergies with Partner and Blink." }, { "id": "peasant-kindred", @@ -16967,23 +16946,22 @@ "primary_color": "Red", "example_commanders": [ "Otharri, Suns' Glory", - "Joshua, Phoenix's Dominant // Phoenix, Warden of Fire", "Syrix, Carrier of the Flame", "Aurelia, the Warleader - Synergy (Haste)", - "Yahenni, Undying Partisan - Synergy (Haste)" + "Yahenni, Undying Partisan - Synergy (Haste)", + "Kiki-Jiki, Mirror Breaker - Synergy (Haste)" ], "example_cards": [ "Otharri, Suns' Glory", "Aurora Phoenix", "Phoenix Chick", "Jaya's Phoenix", - "Joshua, Phoenix's Dominant // Phoenix, Warden of Fire", "Detective's Phoenix", "Flamewake Phoenix", - "Everquill Phoenix" + "Everquill Phoenix", + "Ashcloud Phoenix" ], "synergy_commanders": [ - "Kiki-Jiki, Mirror Breaker - Synergy (Haste)", "Niv-Mizzet, Parun - Synergy (Flying)", "Avacyn, Angel of Hope - Synergy (Flying)", "Rishkar, Peema Renegade - Synergy (Midrange)" @@ -17009,7 +16987,7 @@ "Sheoldred, the Apocalypse", "Elas il-Kor, Sadistic Pilgrim", "Sheoldred, Whispering One", - "Etali, Primal Conqueror // Etali, Primal Sickness" + "Elesh Norn, Grand Cenobite" ], "example_cards": [ "Phyrexian Metamorph", @@ -17044,9 +17022,9 @@ "example_commanders": [ "Baird, Steward of Argive", "Teysa, Envoy of Ghosts", - "Tamiyo, Inquisitive Student // Tamiyo, Seasoned Scholar", "Sivitri, Dragon Master", - "Isperia, Supreme Judge" + "Isperia, Supreme Judge", + "Thantis, the Warweaver" ], "example_cards": [ "Propaganda", @@ -17096,8 +17074,8 @@ "Cid, Freeflier Pilot", "Kotori, Pilot Prodigy", "Tannuk, Memorial Ensign", - "Prodigy's Prototype", - "Defend the Rider" + "Defend the Rider", + "Prodigy's Prototype" ], "synergy_commanders": [ "Sram, Senior Edificer - Synergy (Vehicles)", @@ -17118,7 +17096,7 @@ "Devil Kindred", "Offspring", "Burn", - "Board Wipes" + "Role token" ], "primary_color": "Red", "secondary_color": "Black", @@ -17155,8 +17133,8 @@ "Siren Kindred", "Raid", "Encore", - "Explore", - "Outlaw Kindred" + "Outlaw Kindred", + "Explore" ], "primary_color": "Blue", "secondary_color": "Red", @@ -17382,24 +17360,23 @@ "primary_color": "Black", "secondary_color": "Green", "example_commanders": [ - "Etali, Primal Conqueror // Etali, Primal Sickness", "Skrelv, Defector Mite", "Skithiryx, the Blight Dragon", "Fynn, the Fangbearer", - "Karumonix, the Rat King" + "Karumonix, the Rat King", + "Ixhel, Scion of Atraxa" ], "example_cards": [ - "Etali, Primal Conqueror // Etali, Primal Sickness", "Skrelv, Defector Mite", "Triumph of the Hordes", "Vraska, Betrayal's Sting", "White Sun's Twilight", "Skrelv's Hive", "Plague Myr", - "Grafted Exoskeleton" + "Grafted Exoskeleton", + "Vraska's Fall" ], "synergy_commanders": [ - "Ixhel, Scion of Atraxa - Synergy (Toxic)", "Vishgraz, the Doomhive - Synergy (Mite Kindred)" ], "popularity_bucket": "Uncommon", @@ -17670,7 +17647,7 @@ "Krenko, Tin Street Kingpin - Synergy (+1/+1 Counters)", "Adeline, Resplendent Cathar - Synergy (Planeswalkers)", "Vorinclex, Monstrous Raider - Synergy (Planeswalkers)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)" + "Yahenni, Undying Partisan - Synergy (Counters Matter)" ], "popularity_bucket": "Niche", "editorial_quality": "draft", @@ -17680,37 +17657,37 @@ "id": "protection", "theme": "Protection", "synergies": [ - "Ward", - "Hexproof", "Indestructible", - "Shroud", - "Divinity Counters" + "Angel Kindred", + "Interaction", + "Knight Kindred", + "Combat Tricks" ], "primary_color": "White", "secondary_color": "Green", "example_commanders": [ - "Toski, Bearer of Secrets", - "Purphoros, God of the Forge", - "Etali, Primal Conqueror // Etali, Primal Sickness", "Boromir, Warden of the Tower", - "Avacyn, Angel of Hope" + "Avacyn, Angel of Hope", + "Yawgmoth, Thran Physician", + "Padeem, Consul of Innovation", + "Yahenni, Undying Partisan" ], "example_cards": [ + "Swiftfoot Boots", + "Lightning Greaves", "Heroic Intervention", "The One Ring", - "Teferi's Protection", - "Roaming Throne", "Boros Charm", "Flawless Maneuver", "Akroma's Will", "Mithril Coat" ], "synergy_commanders": [ - "Adrix and Nev, Twincasters - Synergy (Ward)", - "Miirym, Sentinel Wyrm - Synergy (Ward)", - "Ulamog, the Defiler - Synergy (Ward)", - "General Ferrous Rokiric - Synergy (Hexproof)", - "Silumgar, the Drifting Death - Synergy (Hexproof)" + "Toski, Bearer of Secrets - Synergy (Indestructible)", + "Purphoros, God of the Forge - Synergy (Indestructible)", + "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Indestructible)", + "Aurelia, the Warleader - Synergy (Angel Kindred)", + "Syr Konrad, the Grim - Synergy (Interaction)" ], "popularity_bucket": "Very Common", "editorial_quality": "draft", @@ -17894,8 +17871,8 @@ "Tatyova, Benthic Druid - Synergy (Landfall)", "Aesi, Tyrant of Gyre Strait - Synergy (Landfall)", "Bristly Bill, Spine Sower - Synergy (Landfall)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)", "Rishkar, Peema Renegade - Synergy (Counters Matter)", + "Krenko, Tin Street Kingpin - Synergy (Counters Matter)", "Sram, Senior Edificer - Synergy (Enchantments Matter)" ], "popularity_bucket": "Rare", @@ -18263,9 +18240,9 @@ "Swarmyard Massacre" ], "synergy_commanders": [ - "Higure, the Still Wind - Synergy (Ninjutsu)", - "Yuriko, the Tiger's Shadow - Synergy (Ninja Kindred)", + "Yuriko, the Tiger's Shadow - Synergy (Ninjutsu)", "Satoru, the Infiltrator - Synergy (Ninja Kindred)", + "Satoru Umezawa - Synergy (Ninja Kindred)", "Kiora, the Rising Tide - Synergy (Threshold)" ], "popularity_bucket": "Uncommon", @@ -18407,8 +18384,8 @@ "Reanimate", "Faithless Looting", "Victimize", - "Takenuma, Abandoned Mire", "Animate Dead", + "Takenuma, Abandoned Mire", "Syr Konrad, the Grim", "Gray Merchant of Asphodel", "Guardian Project" @@ -18562,7 +18539,7 @@ "Krovikan Rot" ], "synergy_commanders": [ - "Toski, Bearer of Secrets - Synergy (Interaction)" + "Purphoros, God of the Forge - Synergy (Interaction)" ], "popularity_bucket": "Rare", "editorial_quality": "draft", @@ -18604,8 +18581,8 @@ "Rishkar, Peema Renegade - Synergy (+1/+1 Counters)", "Krenko, Tin Street Kingpin - Synergy (+1/+1 Counters)", "Yawgmoth, Thran Physician - Synergy (+1/+1 Counters)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)", - "Yahenni, Undying Partisan - Synergy (Counters Matter)" + "Yahenni, Undying Partisan - Synergy (Counters Matter)", + "Heliod, Sun-Crowned - Synergy (Counters Matter)" ], "example_cards": [ "Wren's Run Hydra", @@ -18656,7 +18633,7 @@ "synergy_commanders": [ "He Who Hungers - Synergy (Soulshift)", "Syr Konrad, the Grim - Synergy (Interaction)", - "Toski, Bearer of Secrets - Synergy (Interaction)", + "Purphoros, God of the Forge - Synergy (Interaction)", "Lotho, Corrupt Shirriff - Synergy (Control)" ], "popularity_bucket": "Very Common", @@ -18693,7 +18670,7 @@ "Alchemist's Assistant" ], "synergy_commanders": [ - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)" + "Yawgmoth, Thran Physician - Synergy (Counters Matter)" ], "popularity_bucket": "Rare", "editorial_quality": "draft", @@ -18729,7 +18706,7 @@ "Valeron Wardens" ], "synergy_commanders": [ - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)" + "Yahenni, Undying Partisan - Synergy (Counters Matter)" ], "popularity_bucket": "Rare", "editorial_quality": "draft", @@ -18975,7 +18952,7 @@ "Rishkar, Peema Renegade - Synergy (+1/+1 Counters)", "Krenko, Tin Street Kingpin - Synergy (+1/+1 Counters)", "Yawgmoth, Thran Physician - Synergy (+1/+1 Counters)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)" + "Yahenni, Undying Partisan - Synergy (Counters Matter)" ], "example_cards": [ "Spider-Punk", @@ -18988,7 +18965,7 @@ "Wrecking Beast" ], "synergy_commanders": [ - "Yahenni, Undying Partisan - Synergy (Counters Matter)", + "Heliod, Sun-Crowned - Synergy (Counters Matter)", "Sram, Senior Edificer - Synergy (Voltron)" ], "popularity_bucket": "Rare", @@ -19254,7 +19231,7 @@ "Piper Wright, Publick Reporter - Synergy (Clue Token)", "Tivit, Seller of Secrets - Synergy (Investigate)", "Kellan, Inquisitive Prodigy // Tail the Suspect - Synergy (Investigate)", - "Edgar, Charmed Groom // Edgar Markov's Coffin - Synergy (Blood Token)" + "Old Rutstein - Synergy (Blood Token)" ], "popularity_bucket": "Common", "editorial_quality": "draft", @@ -19366,7 +19343,7 @@ "synergy_commanders": [ "Syr Konrad, the Grim - Synergy (Mill)", "Emry, Lurker of the Loch - Synergy (Mill)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)" + "Rishkar, Peema Renegade - Synergy (Counters Matter)" ], "popularity_bucket": "Rare", "editorial_quality": "draft", @@ -19563,7 +19540,7 @@ "Sewer Shambler" ], "synergy_commanders": [ - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)" + "Yahenni, Undying Partisan - Synergy (Counters Matter)" ], "popularity_bucket": "Rare", "editorial_quality": "draft", @@ -20005,7 +19982,7 @@ "Clones", "Flash", "Little Fellas", - "Protection" + "Outlaw Kindred" ], "primary_color": "Blue", "secondary_color": "Green", @@ -20126,8 +20103,8 @@ "synergy_commanders": [ "Anim Pakal, Thousandth Moon - Synergy (Soldier Kindred)", "Thalia, Heretic Cathar - Synergy (Soldier Kindred)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)", "Rishkar, Peema Renegade - Synergy (Counters Matter)", + "Krenko, Tin Street Kingpin - Synergy (Counters Matter)", "Tatyova, Benthic Druid - Synergy (Lifegain)" ], "popularity_bucket": "Rare", @@ -20172,11 +20149,10 @@ "id": "shroud", "theme": "Shroud", "synergies": [ - "Protection", - "Interaction", "Toughness Matters", "Big Mana", - "Counters Matter" + "Counters Matter", + "Little Fellas" ], "primary_color": "Green", "secondary_color": "Blue", @@ -20184,8 +20160,8 @@ "Multani, Maro-Sorcerer", "Kodama of the North Tree", "Autumn Willow", - "Toski, Bearer of Secrets - Synergy (Protection)", - "Purphoros, God of the Forge - Synergy (Protection)" + "Azusa, Lost but Seeking - Synergy (Toughness Matters)", + "Sheoldred, the Apocalypse - Synergy (Toughness Matters)" ], "example_cards": [ "Argothian Enchantress", @@ -20198,10 +20174,10 @@ "Neurok Commando" ], "synergy_commanders": [ - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Protection)", - "Syr Konrad, the Grim - Synergy (Interaction)", - "Boromir, Warden of the Tower - Synergy (Interaction)", - "Azusa, Lost but Seeking - Synergy (Toughness Matters)" + "Vito, Thorn of the Dusk Rose - Synergy (Toughness Matters)", + "Syr Konrad, the Grim - Synergy (Big Mana)", + "Etali, Primal Storm - Synergy (Big Mana)", + "Rishkar, Peema Renegade - Synergy (Counters Matter)" ], "popularity_bucket": "Rare", "editorial_quality": "draft", @@ -20215,7 +20191,7 @@ "Outlaw Kindred", "Flying", "Artifacts Matter", - "Little Fellas" + "Toughness Matters" ], "primary_color": "Blue", "example_commanders": [ @@ -20232,8 +20208,8 @@ "Spyglass Siren", "Storm Fleet Negotiator", "Malcolm, the Eyes", - "Oaken Siren", - "Hypnotic Siren" + "Zephyr Singer", + "Oaken Siren" ], "synergy_commanders": [ "Captain Lannery Storm - Synergy (Pirate Kindred)", @@ -20368,8 +20344,8 @@ "Rishkar, Peema Renegade - Synergy (+1/+1 Counters)", "Krenko, Tin Street Kingpin - Synergy (+1/+1 Counters)", "Yawgmoth, Thran Physician - Synergy (+1/+1 Counters)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)", - "Yahenni, Undying Partisan - Synergy (Counters Matter)" + "Yahenni, Undying Partisan - Synergy (Counters Matter)", + "Heliod, Sun-Crowned - Synergy (Counters Matter)" ], "example_cards": [ "Arcbound Slith", @@ -21088,8 +21064,8 @@ "Rishkar, Peema Renegade - Synergy (+1/+1 Counters)", "Krenko, Tin Street Kingpin - Synergy (+1/+1 Counters)", "Yawgmoth, Thran Physician - Synergy (+1/+1 Counters)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)", - "Yahenni, Undying Partisan - Synergy (Counters Matter)" + "Yahenni, Undying Partisan - Synergy (Counters Matter)", + "Heliod, Sun-Crowned - Synergy (Counters Matter)" ], "example_cards": [ "Spike Feeder", @@ -21575,7 +21551,7 @@ "Bottomless Vault" ], "synergy_commanders": [ - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)" + "Rishkar, Peema Renegade - Synergy (Counters Matter)" ], "popularity_bucket": "Rare", "editorial_quality": "draft", @@ -21679,9 +21655,9 @@ "Lulu, Stern Guardian" ], "synergy_commanders": [ - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)", "Rishkar, Peema Renegade - Synergy (Counters Matter)", "Krenko, Tin Street Kingpin - Synergy (Counters Matter)", + "Yawgmoth, Thran Physician - Synergy (Counters Matter)", "Kutzil, Malamet Exemplar - Synergy (Stax)", "Lotho, Corrupt Shirriff - Synergy (Stax)", "Baral, Chief of Compliance - Synergy (Loot)" @@ -21757,7 +21733,7 @@ "Shoulder to Shoulder" ], "synergy_commanders": [ - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)" + "Heliod, Sun-Crowned - Synergy (Counters Matter)" ], "popularity_bucket": "Rare", "editorial_quality": "draft", @@ -22288,7 +22264,7 @@ "Kiora, the Rising Tide", "Far Wanderings", "Barbarian Ring", - "Cabal Pit" + "Putrid Imp" ], "popularity_bucket": "Niche", "editorial_quality": "draft", @@ -22442,8 +22418,8 @@ ], "example_cards": [ "The Tenth Doctor", - "Wibbly-wobbly, Timey-wimey", "Time Beetle", + "Wibbly-wobbly, Timey-wimey", "Rotating Fireplace", "The Parting of the Ways", "All of History, All at Once", @@ -22718,8 +22694,8 @@ "Karumonix, the Rat King" ], "synergy_commanders": [ - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Poison Counters)", "Skithiryx, the Blight Dragon - Synergy (Poison Counters)", + "Fynn, the Fangbearer - Synergy (Poison Counters)", "Yawgmoth, Thran Physician - Synergy (Infect)", "Vorinclex, Monstrous Raider - Synergy (Infect)", "Mondrak, Glory Dominus - Synergy (Phyrexian Kindred)" @@ -22795,7 +22771,7 @@ "Yawgmoth, Thran Physician - Synergy (+1/+1 Counters)", "Syr Konrad, the Grim - Synergy (Human Kindred)", "Azusa, Lost but Seeking - Synergy (Human Kindred)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)" + "Yahenni, Undying Partisan - Synergy (Counters Matter)" ], "popularity_bucket": "Rare", "editorial_quality": "draft", @@ -23092,8 +23068,8 @@ "Shaman Kindred", "Trample", "Warrior Kindred", - "Protection", - "+1/+1 Counters" + "+1/+1 Counters", + "Counters Matter" ], "primary_color": "Green", "secondary_color": "Black", @@ -23131,10 +23107,10 @@ "theme": "Turtle Kindred", "synergies": [ "Ward", - "Protection", "Toughness Matters", "Stax", - "Interaction" + "Little Fellas", + "Big Mana" ], "primary_color": "Blue", "secondary_color": "Green", @@ -23159,9 +23135,9 @@ "Adrix and Nev, Twincasters - Synergy (Ward)", "Miirym, Sentinel Wyrm - Synergy (Ward)", "Ulamog, the Defiler - Synergy (Ward)", - "Toski, Bearer of Secrets - Synergy (Protection)", - "Purphoros, God of the Forge - Synergy (Protection)", - "Azusa, Lost but Seeking - Synergy (Toughness Matters)" + "Azusa, Lost but Seeking - Synergy (Toughness Matters)", + "Sheoldred, the Apocalypse - Synergy (Toughness Matters)", + "Kutzil, Malamet Exemplar - Synergy (Stax)" ], "popularity_bucket": "Rare", "editorial_quality": "draft", @@ -23492,8 +23468,8 @@ "Hellhole Flailer" ], "synergy_commanders": [ - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)", "Yahenni, Undying Partisan - Synergy (Counters Matter)", + "Heliod, Sun-Crowned - Synergy (Counters Matter)", "Sram, Senior Edificer - Synergy (Voltron)" ], "popularity_bucket": "Rare", @@ -23557,9 +23533,9 @@ "Twilight Prophet" ], "synergy_commanders": [ - "Edgar, Charmed Groom // Edgar Markov's Coffin - Synergy (Blood Token)", "Old Rutstein - Synergy (Blood Token)", "Kamber, the Plunderer - Synergy (Blood Token)", + "Strefan, Maurer Progenitor - Synergy (Blood Token)", "Heliod, Sun-Crowned - Synergy (Lifegain Triggers)", "Emrakul, the World Anew - Synergy (Madness)" ], @@ -23582,7 +23558,7 @@ "Ojer Pakpatiq, Deepest Epoch // Temple of Cyclical Time - Synergy (Time Counters)", "The Tenth Doctor - Synergy (Time Counters)", "Jhoira of the Ghitu - Synergy (Time Counters)", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)" + "Rishkar, Peema Renegade - Synergy (Counters Matter)" ], "example_cards": [ "Dreamtide Whale", @@ -23595,7 +23571,7 @@ "Chronozoa" ], "synergy_commanders": [ - "Rishkar, Peema Renegade - Synergy (Counters Matter)", + "Krenko, Tin Street Kingpin - Synergy (Counters Matter)", "Sram, Senior Edificer - Synergy (Enchantments Matter)" ], "popularity_bucket": "Rare", @@ -23746,9 +23722,9 @@ "secondary_color": "Green", "example_commanders": [ "Yisan, the Wanderer Bard", - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)", "Rishkar, Peema Renegade - Synergy (Counters Matter)", "Krenko, Tin Street Kingpin - Synergy (Counters Matter)", + "Yawgmoth, Thran Physician - Synergy (Counters Matter)", "Sram, Senior Edificer - Synergy (Enchantments Matter)" ], "example_cards": [ @@ -23936,10 +23912,10 @@ "theme": "Ward", "synergies": [ "Turtle Kindred", - "Protection", "Dinosaur Kindred", "Dragon Kindred", - "Interaction" + "Stax", + "Reach" ], "primary_color": "Blue", "secondary_color": "Green", @@ -23956,17 +23932,17 @@ "Adrix and Nev, Twincasters", "Miirym, Sentinel Wyrm", "Bronze Guardian", - "Hulking Raptor", "Ulamog, the Defiler", + "Hulking Raptor", "Valgavoth, Terror Eater" ], "synergy_commanders": [ "Kogla and Yidaro - Synergy (Turtle Kindred)", "The Pride of Hull Clade - Synergy (Turtle Kindred)", "Archelos, Lagoon Mystic - Synergy (Turtle Kindred)", - "Toski, Bearer of Secrets - Synergy (Protection)", - "Purphoros, God of the Forge - Synergy (Protection)", - "Etali, Primal Storm - Synergy (Dinosaur Kindred)" + "Etali, Primal Storm - Synergy (Dinosaur Kindred)", + "Ghalta, Primal Hunger - Synergy (Dinosaur Kindred)", + "Niv-Mizzet, Parun - Synergy (Dragon Kindred)" ], "popularity_bucket": "Niche", "editorial_quality": "draft", @@ -24631,8 +24607,8 @@ "Minas Morgul, Dark Fortress", "Witch-king of Angmar", "Nazgûl", - "Lord of the Nazgûl", "Accursed Duneyard", + "Lord of the Nazgûl", "In the Darkness Bind Them", "Street Wraith", "Sauron, the Necromancer" @@ -24777,8 +24753,8 @@ "Gray Merchant of Asphodel", "Field of the Dead", "Fanatic of Rhonas", - "Carrion Feeder", "Warren Soultrader", + "Carrion Feeder", "Accursed Marauder", "Stitcher's Supplier", "Fleshbag Marauder" @@ -24834,9 +24810,9 @@ "primary_color": "White", "secondary_color": "Blue", "example_commanders": [ - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)", "Rishkar, Peema Renegade - Synergy (Counters Matter)", - "Krenko, Tin Street Kingpin - Synergy (Counters Matter)" + "Krenko, Tin Street Kingpin - Synergy (Counters Matter)", + "Yawgmoth, Thran Physician - Synergy (Counters Matter)" ], "example_cards": [ "Dwarven Armorer", @@ -24860,9 +24836,9 @@ "primary_color": "Red", "secondary_color": "Black", "example_commanders": [ - "Etali, Primal Conqueror // Etali, Primal Sickness - Synergy (Counters Matter)", "Rishkar, Peema Renegade - Synergy (Counters Matter)", - "Krenko, Tin Street Kingpin - Synergy (Counters Matter)" + "Krenko, Tin Street Kingpin - Synergy (Counters Matter)", + "Yawgmoth, Thran Physician - Synergy (Counters Matter)" ], "example_cards": [ "Dwarven Armorer", @@ -24872,7 +24848,7 @@ "Clockwork Beast", "Clockwork Avian", "Consuming Ferocity", - "Clockwork Swarm" + "Balduvian Hydra" ], "popularity_bucket": "Rare", "editorial_quality": "draft", @@ -24893,9 +24869,9 @@ "Dwarven Armory", "Tin-Wing Chimera", "Brass-Talon Chimera", - "Fungus Elemental", "Iron-Heart Chimera", - "Lead-Belly Chimera" + "Lead-Belly Chimera", + "Fungus Elemental" ], "popularity_bucket": "Rare", "editorial_quality": "draft", @@ -24930,7 +24906,7 @@ "Cantrips": 88, "Card Draw": 309, "Combat Tricks": 214, - "Interaction": 1056, + "Interaction": 904, "Unconditional Draw": 133, "Bending": 5, "Cost Reduction": 68, @@ -24956,7 +24932,7 @@ "Sloth Kindred": 3, "Lands Matter": 169, "Gargoyle Kindred": 11, - "Protection": 331, + "Protection": 231, "Griffin Kindred": 43, "Cleric Kindred": 365, "Backgrounds Matter": 11, @@ -25020,8 +24996,6 @@ "Lifegain Triggers": 37, "Hero Kindred": 24, "Stun Counters": 5, - "Take 59 Flights of Stairs": 1, - "Take the Elevator": 1, "Pilot Kindred": 18, "Artificer Kindred": 49, "Energy": 21, @@ -25046,7 +25020,7 @@ "Loot": 71, "Haste": 1, "Trample": 15, - "Partner": 9, + "Partner": 16, "Dragon Kindred": 27, "Land Types Matter": 40, "Phyrexian Kindred": 64, @@ -25142,7 +25116,6 @@ "Noble Kindred": 23, "Spell Copy": 10, "Storm": 3, - "Brand-new Sky": 1, "Card Selection": 7, "Explore": 7, "Eye Kindred": 4, @@ -25169,12 +25142,10 @@ "Shapeshifter Kindred": 9, "Boast": 4, "Detain": 5, - "Wind Walk": 1, "Miracle": 6, "Doctor Kindred": 10, "Doctor's Companion": 8, "Doctor's companion": 8, - "History Teacher": 1, "Thopter Kindred": 3, "Ox Kindred": 13, "Extort": 4, @@ -25184,12 +25155,7 @@ "Myriad": 5, "Treasure": 11, "Treasure Token": 13, - "Ability": 1, - "Attack": 1, - "Item": 1, - "Magic": 1, "Finality Counters": 2, - "Lure the Unwary": 1, "Insect Kindred": 6, "Bat Kindred": 11, "Enrage": 3, @@ -25241,7 +25207,6 @@ "Detective Kindred": 17, "Bestow": 11, "Omen Counters": 1, - "Healing Tears": 1, "Retrace": 1, "Champion": 2, "Sweep": 2, @@ -25263,7 +25228,6 @@ "Cumulative upkeep": 13, "Hideaway": 3, "Inkling Kindred": 1, - "Crash Landing": 1, "Impulse": 3, "Junk Token": 1, "Junk Tokens": 2, @@ -25287,7 +25251,6 @@ "Evoke": 7, "Demigod Kindred": 1, "Chimera Kindred": 1, - "Mold Earth": 1, "Fade Counters": 2, "Fading": 2, "Astartes Kindred": 6, @@ -25295,14 +25258,12 @@ "God Kindred": 11, "Delay Counters": 1, "Exert": 7, - "Dragonfire Dive": 1, "Jackal Kindred": 1, "Freerunning": 1, "Intervention Counters": 1, "Toy Kindred": 4, "Sculpture Kindred": 1, "Prowess": 5, - "Gae Bolg": 1, "Bracket:GameChanger": 6, "Coyote Kindred": 1, "Aftermath": 1, @@ -25339,13 +25300,11 @@ "Storage Counters": 2, "Madness": 2, "Healing Counters": 2, - "The Allagan Eye": 1, "Squad": 5, "Map Token": 1, "Spell mastery": 3, "Meld": 1, "Gith Kindred": 2, - "Psychic Defense": 1, "Basic landcycling": 2, "Landcycling": 2, "For Mirrodin!": 5, @@ -25357,7 +25316,6 @@ "Plainswalk": 2, "Powerstone Token": 4, "Demon Kindred": 3, - "Rites of Banishment": 1, "Training": 5, "Horsemanship": 7, "Snake Kindred": 1, @@ -25375,11 +25333,9 @@ "Hoofprint Counters": 1, "Monstrosity": 4, "Soulshift": 5, - "Science Teacher": 1, "Scientist Kindred": 2, "Javelin Counters": 1, "Credit Counters": 1, - "Protection Fighting Style": 1, "Tiefling Kindred": 1, "Connive": 2, "Ascend": 6, @@ -25410,7 +25366,6 @@ "Aegis Counters": 1, "Read Ahead": 2, "Quest Counters": 6, - "Machina": 1, "Reprieve Counters": 1, "Germ Kindred": 1, "Living weapon": 1, @@ -25429,7 +25384,6 @@ "Carrion Counters": 1, "Behold": 1, "Impending": 1, - "First Contact": 1, "Synth Kindred": 1, "Forecast": 5, "Fungus Kindred": 1, @@ -25448,21 +25402,14 @@ "Study Counters": 1, "Isolation Counters": 1, "Coward Kindred": 1, - "Natural Shelter": 1, - "Cura": 1, - "Curaga": 1, - "Cure": 1, "Egg Kindred": 1, - "Bad Wolf": 1, "Wolf Kindred": 2, "Parley": 1, "\\+0/\\+1 Counters": 3, - "Keen Sight": 1, "Training Counters": 1, "Verse Counters": 2, "Shade Kindred": 1, "Shaman Kindred": 1, - "The Nuka-Cola Challenge": 1, "Blood Token": 1, "Zubera Kindred": 1, "Illusion Kindred": 2, @@ -25471,8 +25418,6 @@ "Soltari Kindred": 9, "Echo Counters": 1, "Feather Counters": 1, - "Grav-cannon": 1, - "Concealed Position": 1, "Intimidate": 1, "Reflection Kindred": 1, "Story Counters": 1, @@ -25481,66 +25426,55 @@ "Harpy Kindred": 1, "Recover": 1, "Ripple": 1, - "Brave Heart": 1, "Tempest Hawk": 1, "Tempting offer": 2, "Collect evidence": 1, "Enlightened Counters": 1, "Time Travel": 2, - "Crushing Teeth": 1, "Currency Counters": 1, "Trap Counters": 1, "Companion": 1, - "Praesidium Protectiva": 1, "Hyena Kindred": 1, "Cloak": 2, "Manifest dread": 1, "Bear Kindred": 1, - "Blessing of Light": 1, - "Aegis of the Emperor": 1, "Custodes Kindred": 1, "Berserker Kindred": 1, "Invitation Counters": 1, - "Look to the Stars": 1, "Monger Kindred": 1, - "Ice Counters": 1, - "Wild Card": 1, - "Call for Aid": 1, - "Stall for Time": 1, - "Pray for Protection": 1, - "Strike a Deal": 1 + "Ice Counters": 1 }, "blue": { - "Blink": 569, - "Enter the Battlefield": 569, + "Blink": 573, + "Enter the Battlefield": 573, "Guest Kindred": 3, - "Human Kindred": 540, - "Leave the Battlefield": 569, - "Little Fellas": 1430, - "Outlaw Kindred": 217, - "Rogue Kindred": 150, + "Human Kindred": 546, + "Leave the Battlefield": 573, + "Little Fellas": 1439, + "Outlaw Kindred": 219, + "Rogue Kindred": 151, "Casualty": 5, "Spell Copy": 78, - "Spells Matter": 1722, - "Spellslinger": 1722, + "Spells Matter": 1726, + "Spellslinger": 1726, "Topdeck": 414, "Bird Kindred": 148, - "Flying": 767, - "Toughness Matters": 905, - "Aggro": 894, + "Flying": 771, + "Toughness Matters": 908, + "Aggro": 898, "Aristocrats": 119, - "Auras": 346, - "Combat Matters": 894, - "Enchant": 303, - "Enchantments Matter": 733, + "Auras": 347, + "Combat Matters": 898, + "Enchant": 305, + "Enchantments Matter": 735, "Midrange": 54, "Sacrifice Matters": 110, "Theft": 114, - "Voltron": 595, - "Big Mana": 1217, + "Voltron": 598, + "Big Mana": 1224, "Elf Kindred": 11, - "Mill": 562, - "Reanimate": 493, + "Mill": 564, + "Reanimate": 495, "Shaman Kindred": 11, "Horror Kindred": 48, "Insect Kindred": 7, @@ -25550,16 +25484,15 @@ "Manifest dread": 9, "Control": 666, "Counterspells": 348, - "Interaction": 896, - "Stax": 914, + "Interaction": 793, + "Stax": 915, "Fish Kindred": 43, - "Flash": 167, - "Probing Telepathy": 1, - "Protection": 158, + "Flash": 169, "Ward": 39, + "Protection": 64, "Threshold": 9, - "Historics Matter": 289, - "Legends Matter": 289, + "Historics Matter": 292, + "Legends Matter": 292, "Noble Kindred": 13, "Octopus Kindred": 42, "Removal": 249, @@ -25570,28 +25503,28 @@ "Scion Kindred": 6, "Token Creation": 271, "Tokens Matter": 272, - "+1/+1 Counters": 221, - "Counters Matter": 475, + "+1/+1 Counters": 223, + "Counters Matter": 478, "Drake Kindred": 75, "Kicker": 29, - "Card Draw": 1046, - "Discard Matters": 325, - "Loot": 245, - "Wizard Kindred": 525, + "Card Draw": 1050, + "Discard Matters": 326, + "Loot": 246, + "Wizard Kindred": 526, "Cost Reduction": 144, - "Artifacts Matter": 620, + "Artifacts Matter": 621, "Equipment Matters": 90, "Lands Matter": 198, - "Conditional Draw": 194, + "Conditional Draw": 196, "Defender": 69, - "Draw Triggers": 170, + "Draw Triggers": 171, "Wall Kindred": 41, - "Wheels": 210, + "Wheels": 211, "Artifact Tokens": 107, "Thopter Kindred": 17, - "Cantrips": 191, - "Unconditional Draw": 448, - "Board Wipes": 55, + "Cantrips": 192, + "Unconditional Draw": 449, + "Board Wipes": 56, "Bracket:MassLandDenial": 8, "Equipment": 25, "Reconfigure": 3, @@ -25602,17 +25535,16 @@ "Doctor Kindred": 9, "Doctor's Companion": 7, "Doctor's companion": 6, - "Ultimate Sacrifice": 1, "Drone Kindred": 22, "Zombie Kindred": 83, "Turtle Kindred": 21, "Avatar Kindred": 14, - "Exile Matters": 140, + "Exile Matters": 141, "Suspend": 24, "Time Counters": 32, "Impulse": 11, - "Soldier Kindred": 80, - "Combat Tricks": 129, + "Soldier Kindred": 83, + "Combat Tricks": 131, "Strive": 4, "Cleric Kindred": 24, "Enchantment Tokens": 11, @@ -25620,7 +25552,7 @@ "Life Matters": 38, "Lifegain": 38, "Beast Kindred": 47, - "Elemental Kindred": 109, + "Elemental Kindred": 110, "Toolbox": 70, "Energy": 24, "Energy Counters": 22, @@ -25630,34 +25562,35 @@ "Politics": 43, "Servo Kindred": 1, "Vedalken Kindred": 55, - "Burn": 78, + "Burn": 79, "Max speed": 4, "Start your engines!": 4, "Scry": 138, "X Spells": 109, - "Shapeshifter Kindred": 57, + "Shapeshifter Kindred": 58, "Evoke": 6, "Leviathan Kindred": 21, "Whale Kindred": 17, "Detective Kindred": 20, "Sphinx Kindred": 61, "Renew": 3, - "Advisor Kindred": 31, + "Advisor Kindred": 32, "Merfolk Kindred": 215, "Robot Kindred": 20, - "Stun Counters": 45, + "Stun Counters": 46, "Cleave": 4, "Spellshaper Kindred": 11, "Reflection Kindred": 2, "Storm": 9, "Time Travel": 3, "Domain": 6, - "Siren Kindred": 19, + "Siren Kindred": 20, "Backgrounds Matter": 13, "Choose a background": 7, "Halfling Kindred": 1, - "Partner with": 8, - "Vigilance": 49, + "Partner": 17, + "Partner with": 9, + "Vigilance": 50, "Bracket:ExtraTurn": 29, "Foretell": 13, "God Kindred": 8, @@ -25666,7 +25599,7 @@ "Frog Kindred": 20, "Salamander Kindred": 8, "Encore": 4, - "Pirate Kindred": 67, + "Pirate Kindred": 68, "Warrior Kindred": 44, "Treasure": 13, "Treasure Token": 15, @@ -25680,17 +25613,16 @@ "Dragon Kindred": 45, "Elder Kindred": 4, "Hexproof": 21, - "Faerie Kindred": 80, + "Faerie Kindred": 81, "Mana Dork": 47, "Morph": 43, - "Pingers": 22, + "Pingers": 23, "Flood Counters": 3, "Manifestation Counters": 1, - "Clones": 143, + "Clones": 145, "Cipher": 7, "Prototype": 4, "Learn": 4, - "Aura Swap": 1, "Mutate": 5, "Monarch": 8, "Quest Counters": 4, @@ -25701,9 +25633,9 @@ "Metalcraft": 8, "Addendum": 3, "Heroic": 10, - "Convoke": 10, + "Convoke": 11, "Angel Kindred": 3, - "Spirit Kindred": 148, + "Spirit Kindred": 149, "Nightmare Kindred": 17, "Role token": 6, "Infect": 34, @@ -25713,11 +25645,10 @@ "Incubate": 4, "Incubator Token": 4, "Phyrexian Kindred": 51, - "Project Image": 1, "Hero Kindred": 7, "Job select": 4, "Oil Counters": 12, - "Alien Kindred": 7, + "Alien Kindred": 8, "Planeswalkers": 72, "Superfriends": 72, "Amass": 13, @@ -25758,7 +25689,7 @@ "Mana Rock": 22, "Overload": 6, "Haste": 2, - "Homunculus Kindred": 20, + "Homunculus Kindred": 21, "Rooms Matter": 12, "Card Selection": 10, "Explore": 10, @@ -25770,7 +25701,6 @@ "Phasing": 10, "Converge": 4, "Hag Kindred": 2, - "Partner": 8, "Corrupted": 2, "Clash": 7, "Madness": 7, @@ -25812,7 +25742,7 @@ "Awaken": 5, "Undaunted": 1, "Kavu Kindred": 2, - "Golem Kindred": 4, + "Golem Kindred": 5, "Warp": 7, "Lhurgoyf Kindred": 1, "Pillowfort": 4, @@ -25865,7 +25795,6 @@ "Bargain": 5, "Warlock Kindred": 8, "Behold": 1, - "Avoidance": 1, "Exploit": 8, "Transmute": 6, "Plot": 10, @@ -25881,8 +25810,7 @@ "Trilobite Kindred": 3, "Freerunning": 2, "Tiefling Kindred": 2, - "Two-Headed Coin": 1, - "Monk Kindred": 19, + "Monk Kindred": 20, "Pilot Kindred": 7, "Multikicker": 3, "Glimmer Kindred": 2, @@ -25911,10 +25839,7 @@ "Exalted": 2, "Hippogriff Kindred": 2, "Assist": 4, - "Neurotraumal Rod": 1, "Tyranid Kindred": 2, - "Children of the Cult": 1, - "Genestealer's Kiss": 1, "Infection Counters": 1, "Powerstone Token": 6, "Undying": 4, @@ -25930,7 +25855,6 @@ "Nymph Kindred": 4, "Forecast": 3, "Crocodile Kindred": 3, - "Aberrant Tinkering": 1, "Germ Kindred": 1, "Samurai Kindred": 1, "Incarnation Kindred": 3, @@ -25938,17 +25862,13 @@ "Efreet Kindred": 4, "Horsemanship": 7, "Demon Kindred": 2, - "Discover": 2, + "Discover": 3, "Tide Counters": 2, "Camarid Kindred": 1, "Weird Kindred": 4, "Ooze Kindred": 2, - "Blizzaga": 1, - "Blizzara": 1, - "Blizzard": 1, "Ice Counters": 3, "Lizard Kindred": 4, - "Ceremorphosis": 1, "First strike": 3, "Split second": 5, "Detain": 3, @@ -25960,22 +25880,17 @@ "Graveyard Matters": 5, "Loyalty Counters": 7, "Compleated": 1, - "Replacement Draw": 2, + "Replacement Draw": 3, "Cost Scaling": 5, "Modal": 5, "Spree": 5, - "Come Fly With Me": 1, "Convert": 1, "Living metal": 1, "More Than Meets the Eye": 1, "Praetor Kindred": 3, - "Confounding Clouds": 1, - "Affirmative": 1, - "Negative": 1, "Experience Counters": 1, "Exhaust": 6, "Indestructible": 3, - "Homunculus Servant": 1, "Kithkin Kindred": 1, "Flanking": 1, "Minotaur Kindred": 1, @@ -25983,7 +25898,6 @@ "Treasure Counters": 1, "Verse Counters": 3, "Grandeur": 1, - "Architect of Deception": 1, "Lieutenant": 2, "Hatchling Counters": 1, "Werewolf Kindred": 1, @@ -25994,7 +25908,6 @@ "Lifegain Triggers": 1, "Lifeloss": 1, "Lifeloss Triggers": 1, - "Woman Who Walked the Earth": 1, "Basic landcycling": 2, "Fateseal": 2, "Rabbit Kindred": 2, @@ -26010,10 +25923,8 @@ "Divinity Counters": 1, "Tentacle Kindred": 2, "Synth Kindred": 2, - "Bigby's Hand": 1, "Fox Kindred": 1, "Annihilator": 1, - "Sonic Booster": 1, "Foreshadow Counters": 1, "Paradox": 2, "Impending": 1, @@ -26030,12 +25941,10 @@ "Ape Kindred": 1, "Page Counters": 1, "Constellation": 6, - "Blue Magic": 1, "Ranger Kindred": 3, "Echo": 1, "Demonstrate": 1, "Dwarf Kindred": 1, - "Hagneia": 1, "Backup": 1, "Monger Kindred": 1, "Storage Counters": 2, @@ -26045,11 +25954,9 @@ "Troll Kindred": 1, "Lifelink": 1, "Hideaway": 3, - "Benediction of the Omnissiah": 1, "Squad": 2, "Starfish Kindred": 2, "Tribute": 1, - "Psychic Abomination": 1, "Slith Kindred": 1, "Slime Counters": 1, "Elk Kindred": 2, @@ -26075,28 +25982,23 @@ "Tempting offer": 1, "Juggernaut Kindred": 1, "Thalakos Kindred": 7, - "Water Always Wins": 1, "Knowledge Counters": 1, "Sponge Kindred": 2, "Minion Kindred": 1, - "Parallel Universe": 1, "Rejection Counters": 1, "Secret council": 1, "Adamant": 3, - "Sleight of Hand": 1, "Toy Kindred": 1, "Toxic": 1, "Harmonize": 3, "Possession Counters": 1, "Astartes Kindred": 1, - "Suppressing Fire": 1, "Sleep Counters": 1, "Hexproof from": 1, "Menace": 1, - "Gust of Wind": 1, "Coin Counters": 1, "Archer Kindred": 1, - "Hive Mind": 1 + "Body-print": 1 }, "black": { "Blink": 757, @@ -26128,7 +26030,7 @@ "Token Creation": 415, "Tokens Matter": 416, "Combat Tricks": 174, - "Interaction": 873, + "Interaction": 805, "Midrange": 69, "Horror Kindred": 184, "Basic landcycling": 2, @@ -26157,7 +26059,7 @@ "Trample": 54, "Specter Kindred": 21, "Centaur Kindred": 3, - "Protection": 93, + "Protection": 51, "Warrior Kindred": 168, "Intimidate": 13, "Spirit Kindred": 145, @@ -26249,7 +26151,6 @@ "Corrupted": 7, "Infect": 59, "Poison Counters": 48, - "Lord of the Pyrrhian Legions": 1, "Necron Kindred": 25, "Beast Kindred": 37, "Frog Kindred": 8, @@ -26265,16 +26166,14 @@ "Oil Counters": 3, "Archon Kindred": 1, "Backup": 4, - "Endurant": 1, "Squad": 3, "Noble Kindred": 31, - "Starscourge": 1, "Blood Token": 27, "Life to Draw": 8, "Planeswalkers": 58, "Superfriends": 58, "Golem Kindred": 5, - "Partner": 8, + "Partner": 15, "Thrull Kindred": 22, "\\+1/\\+2 Counters": 1, "Flashback": 22, @@ -26344,9 +26243,6 @@ "Equip": 32, "Equipment": 35, "Job select": 4, - "Buy Information": 1, - "Hire a Mercenary": 1, - "Sell Contraband": 1, "Treasure": 47, "Treasure Token": 49, "Treefolk Kindred": 6, @@ -26408,7 +26304,6 @@ "Freerunning": 6, "Buyback": 9, "Choose a background": 6, - "Tunnel Snakes Rule!": 1, "Undying": 8, "Flanking": 4, "Changeling": 8, @@ -26431,12 +26326,10 @@ "Nymph Kindred": 3, "Mutate": 5, "Hideaway": 2, - "Animate Chains": 1, "Finality Counters": 11, "Suspend": 11, "Time Counters": 14, "Escape": 10, - "Atomic Transmutation": 1, "Fathomless descent": 3, "Wither": 6, "Goat Kindred": 3, @@ -26472,9 +26365,7 @@ "Earthbending": 1, "Dredge": 6, "Dalek Kindred": 4, - "Exterminate!": 1, "Spell mastery": 4, - "Chaosbringer": 1, "Offspring": 4, "Dauthi Kindred": 11, "Shadow": 15, @@ -26555,9 +26446,7 @@ "Devour": 3, "Forage": 1, "Exploit": 12, - "Flesh Flayer": 1, "Gremlin Kindred": 2, - "Transfigure": 1, " Blood Counters": 1, "Investigate": 8, "Inspired": 5, @@ -26567,8 +26456,6 @@ "Max speed": 6, "Start your engines!": 8, "Manifest": 7, - "Death Ray": 1, - "Disintegration Ray": 1, "Vigilance": 1, "Channel": 3, "Gold Token": 2, @@ -26588,7 +26475,6 @@ "Meld": 1, "Lamia Kindred": 2, "Scout Kindred": 9, - "Reverberating Summons": 1, "-0/-2 Counters": 2, "Evoke": 5, "Dinosaur Kindred": 8, @@ -26597,16 +26483,12 @@ "Level Counters": 4, "Level Up": 4, "Ritual Counters": 1, - "Multi-threat Eliminator": 1, "Discover": 2, "Ki Counters": 2, "Boar Kindred": 3, "Exhaust": 1, "Soul Counters": 4, "Monstrosity": 3, - "Secrets of the Soul": 1, - "Grand Strategist": 1, - "Phaeron": 1, "Demonstrate": 1, "Kirin Kindred": 1, "Manifest dread": 2, @@ -26614,7 +26496,6 @@ "Modal": 4, "Spree": 4, "Body Thief": 1, - "Devour Intellect": 1, "Battles Matter": 4, "Efreet Kindred": 1, "Jump": 1, @@ -26627,11 +26508,8 @@ "Hippo Kindred": 1, "Myr Kindred": 2, "Persist": 4, - "Enmitic Exterminator": 1, "Undergrowth": 4, - "Guardian Protocols": 1, "Mannequin Counters": 1, - "Bad Breath": 1, "Plant Kindred": 2, "Manticore Kindred": 1, "Hit Counters": 2, @@ -26639,26 +26517,19 @@ "Hour Counters": 1, "Processor Kindred": 2, "Awaken": 3, - "Mold Harvest": 1, "Nautilus Kindred": 1, "Rigger Kindred": 1, "Astartes Kindred": 4, "Primarch Kindred": 1, - "Primarch of the Death Guard": 1, "Divinity Counters": 1, - "Psychic Blades": 1, "Feeding Counters": 1, "Multiple Copies": 4, "Nazgûl": 1, "Atog Kindred": 1, - "Synaptic Disintegrator": 1, - "Relentless March": 1, "Aftermath": 1, "Epic": 1, "Kinship": 2, "Revival Counters": 1, - "Mutsunokami": 1, - "Weird Insight": 1, "Weird Kindred": 1, "Scarecrow Kindred": 3, "Eon Counters": 1, @@ -26671,15 +26542,9 @@ "Offering": 1, "Depletion Counters": 1, "Carrier Kindred": 5, - "Rot Fly": 1, - "Dynastic Advisor": 1, - "Curse of the Walking Pox": 1, - "Executioner Round": 1, - "Hyperfrag Round": 1, "Mayhem": 3, "Magecraft": 2, "Populate": 1, - "Harbinger of Despair": 1, "Octopus Kindred": 2, "Starfish Kindred": 2, "Kithkin Kindred": 1, @@ -26687,73 +26552,43 @@ "Retrace": 2, "Mole Kindred": 1, "Relentless Rats": 1, - "Decayed": 1, "Kraken Kindred": 1, "Blight Counters": 1, - "Phalanx Commander": 1, - "Blood Chalice": 1, - "Elite Troops": 1, "Monger Kindred": 1, "Coward Kindred": 1, "Serf Kindred": 1, - "Super Nova": 1, "Shadowborn Apostle": 1, "C'tan Kindred": 2, - "Drain Life": 1, - "Matter Absorption": 1, - "Spear of the Void Dragon": 1, "Join forces": 1, "Surrakar Kindred": 2, "Tribute": 1, "Ape Kindred": 2, "Sweep": 1, - "Hyperphase Threshers": 1, - "Command Protocols": 1, "Snail Kindred": 1, "Cascade": 1, - "Jolly Gutpipes": 1, "Spike Kindred": 1, "Mite Kindred": 1, - "Blood Drain": 1, "Ripple": 1, - "My Will Be Done": 1, - "The Seven-fold Chant": 1, "Bracket:ExtraTurn": 1, "Tempting offer": 1, "Prey Counters": 1, "Firebending": 1, "Necrodermis Counters": 1, "Varmint Kindred": 1, - "Consume Anomaly": 1, "Stash Counters": 1, "Pegasus Kindred": 1, - "Chef's Knife": 1, "Stun Counters": 2, "Plague Counters": 2, - "Prismatic Gallery": 1, - "Dynastic Codes": 1, - "Targeting Relay": 1, "Demigod Kindred": 1, - "Horrific Symbiosis": 1, "Chroma": 1, "Barbarian Kindred": 2, - "Rat Tail": 1, - "Devourer of Souls": 1, - "Spiked Retribution": 1, - "Death Gigas": 1, - "Galian Beast": 1, - "Hellmasker": 1, - "Deal with the Black Guardian": 1, "Doctor Kindred": 1, "Doctor's Companion": 1, "Doctor's companion": 1, "Compleated": 1, - "Toxic Spores": 1, "Wish Counters": 1, "Camel Kindred": 1, - "Petrification Counters": 1, - "Burning Chains": 1, - "My First Friend": 1 + "Petrification Counters": 1 }, "red": { "Burn": 1537, @@ -26770,7 +26605,7 @@ "Combat Matters": 1406, "Combat Tricks": 160, "Discard Matters": 303, - "Interaction": 652, + "Interaction": 631, "Madness": 18, "Mill": 341, "Reanimate": 261, @@ -26792,7 +26627,6 @@ "Warrior Kindred": 363, "Cantrips": 79, "Draw Triggers": 54, - "Heavy Rock Cutter": 1, "Tyranid Kindred": 4, "Wheels": 58, "+1/+1 Counters": 248, @@ -26857,7 +26691,7 @@ "Equipment Matters": 141, "Samurai Kindred": 20, "Shaman Kindred": 175, - "Protection": 31, + "Protection": 20, "Conditional Draw": 42, "Phyrexian Kindred": 44, "Ally Kindred": 19, @@ -26877,7 +26711,7 @@ "Wizard Kindred": 94, "Treasure": 108, "Treasure Token": 111, - "Partner": 7, + "Partner": 15, "-1/-1 Counters": 27, "Infect": 7, "Ore Counters": 33, @@ -26931,7 +26765,6 @@ "Flash": 30, "Astartes Kindred": 5, "Demon Kindred": 15, - "Ruinous Ascension": 1, "Amass": 11, "Army Kindred": 10, "Robot Kindred": 18, @@ -26986,7 +26819,6 @@ "Horror Kindred": 13, "Celebration": 5, "Wurm Kindred": 4, - "Scorching Ray": 1, "God Kindred": 10, "Metalcraft": 6, "Hellbent": 7, @@ -26997,15 +26829,12 @@ "Offering": 2, "Flanking": 6, "Knight Kindred": 54, - "Blow Up": 1, "Strive": 4, "Construct Kindred": 13, "Prototype": 4, "Fight": 16, "Bloodthirst": 8, - "Crown of Madness": 1, "Delirium": 12, - "Devastating Charge": 1, "Unleash": 5, "Ooze Kindred": 4, "Wolverine Kindred": 7, @@ -27081,7 +26910,6 @@ "Shapeshifter Kindred": 5, "Harmonize": 3, "Imp Kindred": 2, - "Lord of Chaos": 1, "Fury Counters": 1, "Peasant Kindred": 6, "Rat Kindred": 8, @@ -27118,7 +26946,6 @@ "Coyote Kindred": 1, "Gold Token": 2, "Hero Kindred": 11, - "Gift of Chaos": 1, "Warlock Kindred": 9, "Beholder Kindred": 1, "Monstrosity": 7, @@ -27149,7 +26976,6 @@ "Dragon's Approach": 1, "Multiple Copies": 2, "Surveil": 2, - "Hunters for Hire": 1, "Quest Counters": 5, "\\+0/\\+1 Counters": 1, "\\+2/\\+2 Counters": 1, @@ -27172,7 +26998,6 @@ "Dethrone": 4, "Escape": 5, "Powerstone Token": 5, - "Bio-plasmic Barrage": 1, "Ravenous": 1, "Cloak": 1, "Spell mastery": 3, @@ -27189,10 +27014,6 @@ "Manifest": 4, "Chroma": 3, "Bracket:ExtraTurn": 3, - "Enthralling Performance": 1, - "Fira": 1, - "Firaga": 1, - "Fire": 1, "Bending": 5, "Firebending": 5, "Snake Kindred": 1, @@ -27230,10 +27051,8 @@ "Wither": 6, "Embalm": 1, "Pressure Counters": 1, - "Locus of Slaanesh": 1, "Emerge": 1, "Annihilator": 1, - "Slivercycling": 1, "Hyena Kindred": 2, "Recover": 1, "Doom Counters": 2, @@ -27242,35 +27061,26 @@ "Eerie": 1, "Clue Token": 3, "Investigate": 3, - "Vicious Mockery": 1, "Imprint": 1, "Battles Matter": 5, "Alien Kindred": 3, "Blitz": 8, "Converge": 2, "Void": 3, - "Symphony of Pain": 1, "Vanishing": 2, - "Berzerker": 1, - "Sigil of Corruption": 1, - "The Betrayer": 1, "Venture into the dungeon": 2, "Amplify": 1, - "Frenzied Rampage": 1, "Rhino Kindred": 2, "Forestwalk": 1, "Serpent Kindred": 2, "Assist": 2, "Spectacle": 3, - "Loud Ruckus": 1, "Lieutenant": 3, "Scorpion Kindred": 2, "Stun Counters": 1, "Delve": 1, "Join forces": 1, "Illusion Kindred": 1, - "Detonate": 1, - "Disarm": 1, "Worm Kindred": 2, "Mine Counters": 1, "Performer Kindred": 3, @@ -27282,7 +27092,6 @@ "Kinship": 3, "Divinity Counters": 1, "Banding": 1, - "Sonic Blaster": 1, "Elephant Kindred": 2, "Pangolin Kindred": 1, "Impending": 1, @@ -27290,7 +27099,6 @@ "Squad": 2, "Support": 1, "Plant Kindred": 2, - "Selfie Shot": 1, "Bloodrush": 6, "Replicate": 4, "Porcupine Kindred": 1, @@ -27305,18 +27113,10 @@ "Badger Kindred": 2, "Wage Counters": 1, "Leech Kindred": 1, - "Murasame": 1, "Depletion Counters": 1, - "Bio-Plasmic Scream": 1, - "Family Gathering": 1, - "Family gathering": 1, - "Allure of Slaanesh": 1, - "Fire Cross": 1, "Seven Dwarves": 1, "Dredge": 1, "Mobilize": 3, - "Temporal Foresight": 1, - "Double Overdrive": 1, "Split second": 4, "Grandeur": 2, "Kirin Kindred": 1, @@ -27327,19 +27127,14 @@ "Slith Kindred": 1, "Ember Counters": 1, "Hideaway": 1, - "Mantle of Inspiration": 1, "Ascend": 2, "Ripple": 1, "Synth Kindred": 1, "Vigilance": 2, "Tempting offer": 2, "Read Ahead": 2, - "Advanced Species": 1, "Summon": 1, "Slug Kindred": 1, - "Thundaga": 1, - "Thundara": 1, - "Thunder": 1, "Manifest dread": 2, "Contested Counters": 1, "Epic": 1, @@ -27351,8 +27146,6 @@ "Centaur Kindred": 1, "Token Modification": 1, "Turtle Kindred": 1, - "Bribe the Guards": 1, - "Threaten the Merchant": 1, "Ninja Kindred": 1, "Ninjutsu": 1 }, @@ -27377,8 +27170,7 @@ "Token Creation": 520, "Tokens Matter": 529, "Artifacts Matter": 449, - "Heavy Power Hammer": 1, - "Interaction": 662, + "Interaction": 538, "Little Fellas": 1380, "Mutant Kindred": 27, "Ravenous": 7, @@ -27416,7 +27208,6 @@ "Fight": 74, "Historics Matter": 263, "Legends Matter": 263, - "Nitro-9": 1, "Rebel Kindred": 3, "Equipment Matters": 79, "Reach": 219, @@ -27469,11 +27260,11 @@ "Cycling": 52, "Discard Matters": 87, "Loot": 52, + "Protection": 96, "Vehicles": 25, "Revolt": 6, "Scout Kindred": 97, "Stax": 271, - "Protection": 193, "Faerie Kindred": 13, "Soldier Kindred": 37, "Mount Kindred": 14, @@ -27499,7 +27290,7 @@ "Swampwalk": 10, "Bracket:TutorNonland": 65, "Collect evidence": 6, - "Partner": 8, + "Partner": 13, "Treasure": 26, "Treasure Token": 25, "Turtle Kindred": 12, @@ -27611,7 +27402,6 @@ "Quest Counters": 4, "Delve": 2, "Intimidate": 2, - "Genomic Enhancement": 1, "Wizard Kindred": 22, "Morph": 26, "Drone Kindred": 13, @@ -27652,7 +27442,6 @@ "Provoke": 3, "Sliver Kindred": 18, "Warp": 8, - "Brood Telepathy": 1, "Cleric Kindred": 23, "Ki Counters": 2, "Hippo Kindred": 5, @@ -27690,10 +27479,7 @@ "Acorn Counters": 1, "Bracket:MassLandDenial": 6, "Backup": 6, - "Natural Recovery": 1, - "Proclamator Hailer": 1, "Fateful hour": 2, - "Gathered Swarm": 1, "Cockatrice Kindred": 1, "Pupa Counters": 1, "Ninja Kindred": 4, @@ -27735,7 +27521,6 @@ "Wolverine Kindred": 4, "Pilot Kindred": 4, "Sand Kindred": 2, - "Immune": 1, "Egg Kindred": 2, "Soulbond": 8, "Employee Kindred": 3, @@ -27746,7 +27531,6 @@ "Rabbit Kindred": 10, "Pillowfort": 6, "Nymph Kindred": 4, - "Nonbasic landwalk": 1, "Choose a background": 6, "Endure": 3, "Awaken": 1, @@ -27806,8 +27590,6 @@ "Riot": 3, "Kithkin Kindred": 3, "Slime Counters": 1, - "Devouring Monster": 1, - "Rapacious Hunger": 1, "Replicate": 1, "Demonstrate": 1, "Samurai Kindred": 5, @@ -27815,13 +27597,10 @@ "Mite Kindred": 1, "Depletion Counters": 1, "Cloak": 1, - "Frenzied Metabolism": 1, - "Titanic": 1, "Storage Counters": 2, "Renown": 6, "Embalm": 1, "Boast": 1, - "Endless Swarm": 1, "Undying": 4, "Rat Kindred": 1, "Efreet Kindred": 2, @@ -27836,8 +27615,6 @@ "Graveyard Matters": 2, "Flanking": 1, "Ferret Kindred": 1, - "000 Needles": 1, - "10": 1, "Wither": 3, "Yeti Kindred": 3, "Phasing": 1, @@ -27847,7 +27624,6 @@ "Horsemanship": 1, "Kinship": 3, "Lhurgoyf Kindred": 5, - "Pheromone Trail": 1, "Awakening Counters": 1, "Construct Kindred": 6, "Vitality Counters": 1, @@ -27863,21 +27639,17 @@ "Growth Counters": 2, "Horse Kindred": 9, "Aftermath": 1, - "Infesting Spores": 1, "Divinity Counters": 1, "Harmonize": 3, "Tribute": 3, - "Strategic Coordinator": 1, "Compleated": 1, "Unicorn Kindred": 2, "Nomad Kindred": 1, "Licid Kindred": 2, - "Fast Healing": 1, "Council's dilemma": 3, "Basic landcycling": 3, "Landcycling": 3, "Impending": 1, - "Mama's Coming": 1, "Dethrone": 1, "Will of the Planeswalkers": 1, "Offering": 1, @@ -27889,7 +27661,6 @@ "Rebound": 3, "Ribbon Counters": 1, "Scientist Kindred": 2, - "Vanguard Species": 1, "Camel Kindred": 1, "Wombat Kindred": 1, "Possum Kindred": 2, @@ -27902,20 +27673,15 @@ "Undaunted": 1, "Bracket:ExtraTurn": 1, "Map Token": 2, - "Conjure": 1, - "Conjure Elemental": 1, "Multiple Copies": 1, "Slime Against Humanity": 1, "Slith Kindred": 1, - "Animal May-Ham": 1, "Web-slinging": 2, "Spike Kindred": 10, "Armadillo Kindred": 1, - "Spore Chimney": 1, "Monger Kindred": 1, "Mouse Kindred": 1, "Supply Counters": 1, - "Abraxas": 1, "Ripple": 1, "Replacement Draw": 1, "For Mirrodin!": 1, @@ -27924,23 +27690,16 @@ "Mystic Kindred": 2, "Tempting offer": 1, "Ascend": 2, - "Death Frenzy": 1, "Hatching Counters": 1, "Gold Token": 1, "Read Ahead": 2, - "Bear Witness": 1, - "Final Heaven": 1, - "Meteor Strikes": 1, - "Somersault": 1, "Banding": 1, "Meld": 1, "Velocity Counters": 1, - "Hypertoxic Miasma": 1, "Dash": 1, "Mentor": 1, "Nest Counters": 1, "Toy Kindred": 1, - "Shieldwall": 1, "Freerunning": 1, "Menace": 1, "Processor Kindred": 1, @@ -27948,20 +27707,18 @@ "Praetor Kindred": 3, "-0/-1 Counters": 1, "Scarecrow Kindred": 1, - "Gather Your Courage": 1, - "Run and Hide": 1, "Plainswalk": 1 } }, "generated_from": "merge (analytics + curated YAML + whitelist)", "metadata_info": { "mode": "merge", - "generated_at": "2025-10-07T18:22:00", + "generated_at": "2025-10-09T00:37:26", "curated_yaml_files": 735, "synergy_cap": 5, "inference": "pmi", "version": "phase-b-merge-v1", - "catalog_hash": "ae79af02508e2f9184aa74d63db5c9987fd65cfa87ce7adb50aec2f6ae8397c5" + "catalog_hash": "9f938ea43a438ed3d924bdb971bf4efa34b80fa3728877878729db50c9afa2fd" }, "description_fallback_summary": null } \ No newline at end of file