mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-16 23:50:12 +01:00
Merge pull request #42 from mwisnowski/feature/colorless-commander-improvements
Add colorless commander filtering and display fixes
This commit is contained in:
commit
ab1aac1ee7
9 changed files with 211 additions and 18 deletions
14
CHANGELOG.md
14
CHANGELOG.md
|
|
@ -9,16 +9,18 @@ This format follows Keep a Changelog principles and aims for Semantic Versioning
|
|||
|
||||
## [Unreleased]
|
||||
### Summary
|
||||
_No unreleased changes yet._
|
||||
Improved colorless commander support with automatic card filtering and display fixes.
|
||||
|
||||
### Added
|
||||
_No unreleased changes yet._
|
||||
|
||||
### Changed
|
||||
_No unreleased changes yet._
|
||||
- **Colorless Commander Filtering**: 25 cards that don't work in colorless decks are now automatically excluded
|
||||
- Filters out cards like Arcane Signet, Commander's Sphere, and medallions that reference "commander's color identity" or colored spells
|
||||
- Only applies to colorless identity commanders (Karn, Kozilek, Liberator, etc.)
|
||||
|
||||
### Fixed
|
||||
_No unreleased changes yet._
|
||||
- **Colorless Commander Display**: Fixed three bugs affecting colorless commander decks
|
||||
- Color identity now displays correctly (grey "C" button with "Colorless" label)
|
||||
- Wastes now correctly added as basic lands in colorless decks
|
||||
- Colored basics (Plains, Island, etc.) no longer incorrectly added to colorless decks
|
||||
|
||||
## [2.8.0] - 2025-10-15
|
||||
### Summary
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
# MTG Python Deckbuilder ${VERSION}
|
||||
|
||||
### Summary
|
||||
_No unreleased changes yet._
|
||||
Improved colorless commander support with automatic card filtering and display fixes.
|
||||
|
||||
### Added
|
||||
_No unreleased changes yet._
|
||||
|
||||
### Changed
|
||||
_No unreleased changes yet._
|
||||
- **Colorless Commander Filtering**: 25 cards that don't work in colorless decks are now automatically excluded
|
||||
- Filters out cards like Arcane Signet, Commander's Sphere, and medallions that reference "commander's color identity" or colored spells
|
||||
- Only applies to colorless identity commanders (Karn, Kozilek, Liberator, etc.)
|
||||
|
||||
### Fixed
|
||||
_No unreleased changes yet._
|
||||
- **Colorless Commander Display**: Fixed three bugs affecting colorless commander decks
|
||||
- Color identity now displays correctly (grey "C" button with "Colorless" label)
|
||||
- Wastes now correctly added as basic lands in colorless decks
|
||||
- Colored basics (Plains, Island, etc.) no longer incorrectly added to colorless decks
|
||||
|
|
|
|||
|
|
@ -1063,8 +1063,11 @@ class DeckBuilder(
|
|||
if isinstance(raw_ci, list):
|
||||
colors_list = [str(c).strip().upper() for c in raw_ci]
|
||||
elif isinstance(raw_ci, str) and raw_ci.strip():
|
||||
# Handle the literal string "Colorless" specially (from commander_cards.csv)
|
||||
if raw_ci.strip().lower() == 'colorless':
|
||||
colors_list = []
|
||||
# Could be formatted like "['B','G']" or 'BG'; attempt simple parsing
|
||||
if ',' in raw_ci:
|
||||
elif ',' in raw_ci:
|
||||
colors_list = [c.strip().strip("'[] ").upper() for c in raw_ci.split(',') if c.strip().strip("'[] ")]
|
||||
else:
|
||||
colors_list = [c.upper() for c in raw_ci if c.isalpha()]
|
||||
|
|
@ -1136,10 +1139,18 @@ class DeckBuilder(
|
|||
required = getattr(bc, 'CSV_REQUIRED_COLUMNS', [])
|
||||
from path_util import csv_dir as _csv_dir
|
||||
base = _csv_dir()
|
||||
|
||||
# Define converters for list columns (same as tagger.py)
|
||||
converters = {
|
||||
'themeTags': pd.eval,
|
||||
'creatureTypes': pd.eval,
|
||||
'metadataTags': pd.eval # M2: Parse metadataTags column
|
||||
}
|
||||
|
||||
for stem in self.files_to_load:
|
||||
path = f"{base}/{stem}_cards.csv"
|
||||
try:
|
||||
df = pd.read_csv(path)
|
||||
df = pd.read_csv(path, converters=converters)
|
||||
if required:
|
||||
missing = [c for c in required if c not in df.columns]
|
||||
if missing:
|
||||
|
|
@ -1175,6 +1186,54 @@ class DeckBuilder(
|
|||
self.output_func(f"Owned-only mode: failed to filter combined pool: {_e}")
|
||||
# Soft prefer-owned does not filter the pool; biasing is applied later at selection time
|
||||
|
||||
# M2: Filter out cards useless in colorless identity decks
|
||||
if self.color_identity_key == 'COLORLESS':
|
||||
logger.info(f"M2 COLORLESS FILTER: Activated for color_identity_key='{self.color_identity_key}'")
|
||||
try:
|
||||
if 'metadataTags' in combined.columns and 'name' in combined.columns:
|
||||
# Find cards with "Useless in Colorless" metadata tag
|
||||
def has_useless_tag(metadata_tags):
|
||||
# Handle various types: NaN, empty list, list with values
|
||||
if metadata_tags is None:
|
||||
return False
|
||||
# Check for pandas NaN or numpy NaN
|
||||
try:
|
||||
import numpy as np
|
||||
if isinstance(metadata_tags, float) and np.isnan(metadata_tags):
|
||||
return False
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
# Handle empty list or numpy array
|
||||
if isinstance(metadata_tags, (list, np.ndarray)):
|
||||
if len(metadata_tags) == 0:
|
||||
return False
|
||||
return 'Useless in Colorless' in metadata_tags
|
||||
return False
|
||||
|
||||
useless_mask = combined['metadataTags'].apply(has_useless_tag)
|
||||
useless_count = useless_mask.sum()
|
||||
|
||||
if useless_count > 0:
|
||||
useless_names = combined.loc[useless_mask, 'name'].tolist()
|
||||
combined = combined[~useless_mask].copy()
|
||||
self.output_func(f"Colorless commander: filtered out {useless_count} cards useless in colorless identity")
|
||||
logger.info(f"M2 COLORLESS FILTER: Filtered out {useless_count} cards")
|
||||
# Log first few cards for transparency
|
||||
for name in useless_names[:3]:
|
||||
self.output_func(f" - Filtered: {name}")
|
||||
logger.info(f"M2 COLORLESS FILTER: Removed '{name}'")
|
||||
if useless_count > 3:
|
||||
self.output_func(f" - ... and {useless_count - 3} more")
|
||||
else:
|
||||
logger.warning(f"M2 COLORLESS FILTER: No cards found with 'Useless in Colorless' tag!")
|
||||
else:
|
||||
logger.warning(f"M2 COLORLESS FILTER: Missing required columns (metadataTags or name)")
|
||||
except Exception as e:
|
||||
self.output_func(f"Warning: Failed to apply colorless filter: {e}")
|
||||
logger.error(f"M2 COLORLESS FILTER: Exception: {e}", exc_info=True)
|
||||
else:
|
||||
logger.info(f"M2 COLORLESS FILTER: Not activated - color_identity_key='{self.color_identity_key}' (not 'Colorless')")
|
||||
|
||||
# Apply exclude card filtering (M0.5: Phase 1 - Exclude Only)
|
||||
if hasattr(self, 'exclude_cards') and self.exclude_cards:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -286,7 +286,7 @@ COLORED_MANA_SYMBOLS: Final[List[str]] = ['{w}','{u}','{b}','{r}','{g}']
|
|||
|
||||
|
||||
# Basic Lands
|
||||
BASIC_LANDS = ['Plains', 'Island', 'Swamp', 'Mountain', 'Forest']
|
||||
BASIC_LANDS = ['Plains', 'Island', 'Swamp', 'Mountain', 'Forest', 'Wastes']
|
||||
|
||||
# Basic land mappings
|
||||
COLOR_TO_BASIC_LAND: Final[Dict[str, str]] = {
|
||||
|
|
|
|||
|
|
@ -159,7 +159,8 @@ class ColorBalanceMixin:
|
|||
self.output_func(" (No viable swaps executed.)")
|
||||
|
||||
# Always consider basic-land rebalance when requested
|
||||
if rebalance_basics:
|
||||
# M5: Skip rebalance for colorless commanders (they should have only Wastes)
|
||||
if rebalance_basics and self.color_identity: # Only rebalance if commander has colors
|
||||
try:
|
||||
basic_map = getattr(bc, 'COLOR_TO_BASIC_LAND', {})
|
||||
basics_present = {nm: entry for nm, entry in self.card_library.items() if nm in basic_map.values()}
|
||||
|
|
|
|||
119
code/tagging/colorless_filter_applier.py
Normal file
119
code/tagging/colorless_filter_applier.py
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
"""Apply 'Useless in Colorless' metadata tags to cards that don't work in colorless identity decks.
|
||||
|
||||
This module identifies and tags cards using regex patterns to match oracle text:
|
||||
1. Cards referencing "your commander's color identity"
|
||||
2. Cards that reduce costs of colored spells
|
||||
3. Cards that trigger on casting colored spells
|
||||
|
||||
Examples include:
|
||||
- Arcane Signet, Command Tower (commander color identity)
|
||||
- Pearl/Sapphire/Jet/Ruby/Emerald Medallion (colored cost reduction)
|
||||
- Oketra's/Kefnet's/Bontu's/Hazoret's/Rhonas's Monument (colored creature cost reduction)
|
||||
- Shrine of Loyal Legions, etc. (colored spell triggers)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
import pandas as pd
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Regex patterns for cards that don't work in colorless identity decks
|
||||
COLORLESS_FILTER_PATTERNS = [
|
||||
# Cards referencing "your commander's color identity"
|
||||
# BUT exclude Commander's Plate (protection from colors NOT in identity = amazing in colorless!)
|
||||
# and Study Hall (still draws/scrys in colorless)
|
||||
r"commander'?s?\s+color\s+identity",
|
||||
|
||||
# Colored cost reduction - medallions and monuments
|
||||
# Matches: "white spells you cast cost", "blue creature spells you cast cost", etc.
|
||||
r"(white|blue|black|red|green)\s+(creature\s+)?spells?\s+you\s+cast\s+cost.*less",
|
||||
|
||||
# Colored spell triggers - shrines and similar
|
||||
# Matches: "whenever you cast a white spell", etc.
|
||||
r"whenever\s+you\s+cast\s+a\s+(white|blue|black|red|green)\s+spell",
|
||||
]
|
||||
|
||||
# Cards that should NOT be filtered despite matching patterns
|
||||
# These cards actually work great in colorless decks
|
||||
COLORLESS_FILTER_EXCEPTIONS = [
|
||||
"Commander's Plate", # Protection from colors NOT in identity = protection from all colors in colorless!
|
||||
"Study Hall", # Still provides colorless mana and scrys when casting commander
|
||||
]
|
||||
|
||||
USELESS_IN_COLORLESS_TAG = "Useless in Colorless"
|
||||
|
||||
|
||||
def apply_colorless_filter_tags(df: pd.DataFrame) -> None:
|
||||
"""Apply 'Useless in Colorless' metadata tag to cards that don't work in colorless decks.
|
||||
|
||||
Uses regex patterns to identify cards in oracle text that:
|
||||
- Reference "your commander's color identity"
|
||||
- Reduce costs of colored spells
|
||||
- Trigger on casting colored spells
|
||||
|
||||
Modifies the DataFrame in-place by adding tags to the 'themeTags' column.
|
||||
These tags will later be moved to 'metadataTags' during the partition phase.
|
||||
|
||||
Args:
|
||||
df: DataFrame with 'name', 'text', and 'themeTags' columns
|
||||
|
||||
Returns:
|
||||
None (modifies DataFrame in-place)
|
||||
"""
|
||||
if 'name' not in df.columns:
|
||||
logger.warning("No 'name' column found, skipping colorless filter tagging")
|
||||
return
|
||||
|
||||
if 'text' not in df.columns:
|
||||
logger.warning("No 'text' column found, skipping colorless filter tagging")
|
||||
return
|
||||
|
||||
if 'themeTags' not in df.columns:
|
||||
logger.warning("No 'themeTags' column found, skipping colorless filter tagging")
|
||||
return
|
||||
|
||||
# Combine all patterns with OR
|
||||
combined_pattern = "|".join(f"({pattern})" for pattern in COLORLESS_FILTER_PATTERNS)
|
||||
|
||||
# Find cards matching any pattern
|
||||
df['text'] = df['text'].fillna('')
|
||||
matches_pattern = df['text'].str.contains(
|
||||
combined_pattern,
|
||||
case=False,
|
||||
regex=True,
|
||||
na=False
|
||||
)
|
||||
|
||||
# Exclude cards that work well in colorless despite matching patterns
|
||||
is_exception = df['name'].isin(COLORLESS_FILTER_EXCEPTIONS)
|
||||
matches_pattern = matches_pattern & ~is_exception
|
||||
|
||||
tagged_count = 0
|
||||
|
||||
for idx in df[matches_pattern].index:
|
||||
card_name = df.at[idx, 'name']
|
||||
tags = df.at[idx, 'themeTags']
|
||||
|
||||
# Ensure themeTags is a list
|
||||
if not isinstance(tags, list):
|
||||
tags = []
|
||||
|
||||
# Add tag if not already present
|
||||
if USELESS_IN_COLORLESS_TAG not in tags:
|
||||
tags.append(USELESS_IN_COLORLESS_TAG)
|
||||
df.at[idx, 'themeTags'] = tags
|
||||
tagged_count += 1
|
||||
logger.debug(f"Tagged '{card_name}' with '{USELESS_IN_COLORLESS_TAG}'")
|
||||
|
||||
if tagged_count > 0:
|
||||
logger.info(f"Applied '{USELESS_IN_COLORLESS_TAG}' tag to {tagged_count} cards")
|
||||
else:
|
||||
logger.info(f"No '{USELESS_IN_COLORLESS_TAG}' tags applied (no matches or already tagged)")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"apply_colorless_filter_tags",
|
||||
"COLORLESS_FILTER_PATTERNS",
|
||||
"COLORLESS_FILTER_EXCEPTIONS",
|
||||
"USELESS_IN_COLORLESS_TAG",
|
||||
]
|
||||
|
|
@ -1072,6 +1072,9 @@ METADATA_TAG_ALLOWLIST: set[str] = {
|
|||
# Cost reduction diagnostics (from Applied: namespace)
|
||||
'Applied: Cost Reduction',
|
||||
|
||||
# Colorless commander filtering (M1)
|
||||
'Useless in Colorless',
|
||||
|
||||
# Kindred-specific protection metadata (from M2)
|
||||
# Format: "{CreatureType}s Gain Protection"
|
||||
# These are auto-generated for kindred-specific protection grants
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from . import regex_patterns as rgx
|
|||
from . import tag_constants
|
||||
from . import tag_utils
|
||||
from .bracket_policy_applier import apply_bracket_policy_tags
|
||||
from .colorless_filter_applier import apply_colorless_filter_tags
|
||||
from .multi_face_merger import merge_multi_face_rows
|
||||
import logging_util
|
||||
from file_setup import setup
|
||||
|
|
@ -493,6 +494,9 @@ def tag_by_color(df: pd.DataFrame, color: str) -> None:
|
|||
|
||||
# Apply bracket policy tags (from config/card_lists/*.json)
|
||||
apply_bracket_policy_tags(df)
|
||||
|
||||
# Apply colorless filter tags (M1: Useless in Colorless)
|
||||
apply_colorless_filter_tags(df)
|
||||
print('\n====================\n')
|
||||
|
||||
# Merge multi-face entries before final ordering (feature-flagged)
|
||||
|
|
|
|||
|
|
@ -321,8 +321,11 @@ def commander_hover_context(
|
|||
commander_color_label = str(combined_info.get("color_label") or "").strip()
|
||||
if not commander_color_label and commander_color_identity:
|
||||
commander_color_label = " / ".join(commander_color_identity)
|
||||
if has_combined and not commander_color_label:
|
||||
commander_color_label = "Colorless (C)"
|
||||
# M5: Set colorless label for ANY commander with empty color identity (not just partner/combined)
|
||||
if not commander_color_label and (has_combined or commander_name):
|
||||
# Empty color_identity list means colorless
|
||||
if not commander_color_identity:
|
||||
commander_color_label = "Colorless (C)"
|
||||
|
||||
commander_color_code = str(combined_info.get("color_code") or "").strip() if has_combined else ""
|
||||
commander_partner_mode = str(combined_info.get("partner_mode") or "").strip() if has_combined else ""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue