diff --git a/code/deck_builder/builder_constants.py b/code/deck_builder/builder_constants.py index e1aa217..9761812 100644 --- a/code/deck_builder/builder_constants.py +++ b/code/deck_builder/builder_constants.py @@ -1,4 +1,9 @@ -from typing import Dict, List, Optional, Final, Tuple, Pattern, Union, Callable +from typing import Dict, List, Final, Tuple, Union, Callable +from settings import CARD_DATA_COLUMNS as CSV_REQUIRED_COLUMNS # unified + +__all__ = [ + 'CSV_REQUIRED_COLUMNS' +] import ast # Commander selection configuration @@ -390,12 +395,7 @@ CSV_VALIDATION_RULES: Final[Dict[str, Dict[str, Union[str, int, float]]]] = { 'toughness': {'type': ('str', 'int', 'float', 'object'), 'pattern': r'^[\d*+-]+$'} } -# Required columns for CSV validation -CSV_REQUIRED_COLUMNS: Final[List[str]] = [ - 'name', 'faceName', 'edhrecRank', 'colorIdentity', 'colors', - 'manaCost', 'manaValue', 'type', 'creatureTypes', 'text', - 'power', 'toughness', 'keywords', 'themeTags', 'layout', 'side' -] +# (CSV_REQUIRED_COLUMNS imported from settings to avoid duplication) # DataFrame processing configuration BATCH_SIZE: Final[int] = 1000 # Number of records to process at once diff --git a/code/file_setup/setup_constants.py b/code/file_setup/setup_constants.py index 31582bc..962f710 100644 --- a/code/file_setup/setup_constants.py +++ b/code/file_setup/setup_constants.py @@ -1,41 +1,40 @@ -from typing import Dict, List, Optional, Final, Tuple, Pattern, Union, Callable +from typing import Dict, List +from settings import ( + SETUP_COLORS, + COLOR_ABRV, + CARD_DATA_COLUMNS as COLUMN_ORDER, # backward compatible alias + CARD_DATA_COLUMNS as TAGGED_COLUMN_ORDER, +) -BANNED_CARDS: List[str] = [# in commander - 'Ancestral Recall', 'Balance', 'Biorhythm', 'Black Lotus', - 'Braids, Cabal Minion', 'Chaos Orb', 'Coalition Victory', - 'Channel', 'Dockside Extortionist', 'Emrakul, the Aeons Torn', - 'Erayo, Soratami Ascendant', 'Falling Star', 'Fastbond', - 'Flash', 'Gifts Ungiven', 'Golos, Tireless Pilgrim', - 'Griselbrand', 'Hullbreacher', 'Iona, Shield of Emeria', - 'Karakas', 'Jeweled Lotus', 'Leovold, Emissary of Trest', - 'Library of Alexandria', 'Limited Resources', 'Lutri, the Spellchaser', - 'Mana Crypt', 'Mox Emerald', 'Mox Jet', 'Mox Pearl', 'Mox Ruby', - 'Mox Sapphire', 'Nadu, Winged Wisdom', 'Panoptic Mirror', - 'Paradox Engine', 'Primeval Titan', 'Prophet of Kruphix', - 'Recurring Nightmare', 'Rofellos, Llanowar Emissary', 'Shahrazad', - 'Sundering Titan', 'Sway of the Stars', 'Sylvan Primordial', - 'Time Vault', 'Time Walk', 'Tinker', 'Tolarian Academy', - 'Trade Secrets', 'Upheaval', 'Yawgmoth\'s Bargain', - - # In constructed - 'Invoke Prejudice', 'Cleanse', 'Stone-Throwing Devils', 'Pradesh Gypsies', - 'Jihad', 'Imprison', 'Crusade' - ] +__all__ = [ + 'SETUP_COLORS', 'COLOR_ABRV', 'COLUMN_ORDER', 'TAGGED_COLUMN_ORDER', + 'BANNED_CARDS', 'MTGJSON_API_URL', 'LEGENDARY_OPTIONS', 'NON_LEGAL_SETS', + 'CARD_TYPES_TO_EXCLUDE', 'CSV_PROCESSING_COLUMNS', 'SORT_CONFIG', + 'FILTER_CONFIG' +] -SETUP_COLORS: List[str] = ['colorless', 'white', 'blue', 'black', 'green', 'red', - 'azorius', 'orzhov', 'selesnya', 'boros', 'dimir', - 'simic', 'izzet', 'golgari', 'rakdos', 'gruul', - 'bant', 'esper', 'grixis', 'jund', 'naya', - 'abzan', 'jeskai', 'mardu', 'sultai', 'temur', - 'dune', 'glint', 'ink', 'witch', 'yore', 'wubrg'] - -COLOR_ABRV: List[str] = ['Colorless', 'W', 'U', 'B', 'G', 'R', - 'U, W', 'B, W', 'G, W', 'R, W', 'B, U', - 'G, U', 'R, U', 'B, G', 'B, R', 'G, R', - 'G, U, W', 'B, U, W', 'B, R, U', 'B, G, R', 'G, R, W', - 'B, G, W', 'R, U, W', 'B, R, W', 'B, G, U', 'G, R, U', - 'B, G, R, W', 'B, G, R, U', 'G, R, U, W', 'B, G, U, W', - 'B, R, U, W', 'B, G, R, U, W'] +# Banned cards consolidated here (remains specific to setup concerns) +BANNED_CARDS: List[str] = [ + # Commander banned list + 'Ancestral Recall', 'Balance', 'Biorhythm', 'Black Lotus', + 'Braids, Cabal Minion', 'Chaos Orb', 'Coalition Victory', + 'Channel', 'Dockside Extortionist', 'Emrakul, the Aeons Torn', + 'Erayo, Soratami Ascendant', 'Falling Star', 'Fastbond', + 'Flash', 'Gifts Ungiven', 'Golos, Tireless Pilgrim', + 'Griselbrand', 'Hullbreacher', 'Iona, Shield of Emeria', + 'Karakas', 'Jeweled Lotus', 'Leovold, Emissary of Trest', + 'Library of Alexandria', 'Limited Resources', 'Lutri, the Spellchaser', + 'Mana Crypt', 'Mox Emerald', 'Mox Jet', 'Mox Pearl', 'Mox Ruby', + 'Mox Sapphire', 'Nadu, Winged Wisdom', 'Panoptic Mirror', + 'Paradox Engine', 'Primeval Titan', 'Prophet of Kruphix', + 'Recurring Nightmare', 'Rofellos, Llanowar Emissary', 'Shahrazad', + 'Sundering Titan', 'Sway of the Stars', 'Sylvan Primordial', + 'Time Vault', 'Time Walk', 'Tinker', 'Tolarian Academy', + 'Trade Secrets', 'Upheaval', "Yawgmoth's Bargain", + # Problematic / culturally sensitive or banned in other formats + 'Invoke Prejudice', 'Cleanse', 'Stone-Throwing Devils', 'Pradesh Gypsies', + 'Jihad', 'Imprison', 'Crusade' +] # Constants for setup and CSV processing MTGJSON_API_URL: str = 'https://mtgjson.com/api/v5/csv/cards.csv' @@ -105,14 +104,4 @@ FILTER_CONFIG: Dict[str, Dict[str, List[str]]] = { } } -COLUMN_ORDER: List[str] = [ - 'name', 'faceName', 'edhrecRank', 'colorIdentity', 'colors', - 'manaCost', 'manaValue', 'type', 'creatureTypes', 'text', - 'power', 'toughness', 'keywords', 'themeTags', 'layout', 'side' -] - -TAGGED_COLUMN_ORDER: List[str] = [ - 'name', 'faceName', 'edhrecRank', 'colorIdentity', 'colors', - 'manaCost', 'manaValue', 'type', 'creatureTypes', 'text', - 'power', 'toughness', 'keywords', 'themeTags', 'layout', 'side' -] \ No newline at end of file +# COLUMN_ORDER and TAGGED_COLUMN_ORDER now sourced from settings via CARD_DATA_COLUMNS \ No newline at end of file diff --git a/code/logging_util.py b/code/logging_util.py index 695fecb..92c97ad 100644 --- a/code/logging_util.py +++ b/code/logging_util.py @@ -1,6 +1,6 @@ from __future__ import annotations -from settings import os +import os import logging # Create logs directory if it doesn't exist @@ -26,4 +26,13 @@ file_handler.setFormatter(NoDunderFormatter(LOG_FORMAT)) # Stream handler stream_handler = logging.StreamHandler() -stream_handler.setFormatter(NoDunderFormatter(LOG_FORMAT)) \ No newline at end of file +stream_handler.setFormatter(NoDunderFormatter(LOG_FORMAT)) + +# Root logger assembly helper (idempotent) +def get_logger(name: str = 'deck_builder') -> logging.Logger: + logger = logging.getLogger(name) + if not logger.handlers: + logger.setLevel(LOG_LEVEL) + logger.addHandler(file_handler) + logger.addHandler(stream_handler) + return logger \ No newline at end of file diff --git a/code/settings.py b/code/settings.py index 6b63e09..c61365b 100644 --- a/code/settings.py +++ b/code/settings.py @@ -1,27 +1,94 @@ from __future__ import annotations # Standard library imports -import os -from sys import exit -from typing import Dict, List, Optional, Final, Tuple, Pattern, Union, Callable +from typing import Dict, List, Optional -# Third-party imports +# ---------------------------------------------------------------------------------- +# COLOR CONSTANTS +# ---------------------------------------------------------------------------------- +# NOTE: +# Existing code in setup uses an ordered list (green before red) to align indices +# with the parallel COLOR_ABRV list. The previously defined COLORS list had a +# different ordering (red before green) which made it unsuitable for index based +# mapping. To avoid subtle bugs we expose two explicit constants: +# SETUP_COLORS -> ordering required for setup / abbreviation mapping +# COLORS -> legacy superset including 'commander' (kept for compatibility) -COLORS = ['colorless', 'white', 'blue', 'black', 'red', 'green', - 'azorius', 'orzhov', 'selesnya', 'boros', 'dimir', - 'simic', 'izzet', 'golgari', 'rakdos', 'gruul', - 'bant', 'esper', 'grixis', 'jund', 'naya', - 'abzan', 'jeskai', 'mardu', 'sultai', 'temur', - 'dune', 'glint', 'ink', 'witch', 'yore', 'wubrg', - 'commander'] +SETUP_COLORS: List[str] = [ + 'colorless', 'white', 'blue', 'black', 'green', 'red', + 'azorius', 'orzhov', 'selesnya', 'boros', 'dimir', + 'simic', 'izzet', 'golgari', 'rakdos', 'gruul', + 'bant', 'esper', 'grixis', 'jund', 'naya', + 'abzan', 'jeskai', 'mardu', 'sultai', 'temur', + 'dune', 'glint', 'ink', 'witch', 'yore', 'wubrg' +] -COLOR_ABRV: List[str] = ['Colorless', 'W', 'U', 'B', 'G', 'R', - 'U, W', 'B, W', 'G, W', 'R, W', 'B, U', - 'G, U', 'R, U', 'B, G', 'B, R', 'G, R', - 'G, U, W', 'B, U, W', 'B, R, U', 'B, G, R', 'G, R, W', - 'B, G, W', 'R, U, W', 'B, R, W', 'B, G, U', 'G, R, U', - 'B, G, R, W', 'B, G, R, U', 'G, R, U, W', 'B, G, U, W', - 'B, R, U, W', 'B, G, R, U, W'] +# Legacy constant (includes 'commander', preserves previous external usage) +COLORS: List[str] = [ + *SETUP_COLORS, + 'commander' +] + +COLOR_ABRV: List[str] = [ + 'Colorless', 'W', 'U', 'B', 'G', 'R', + 'U, W', 'B, W', 'G, W', 'R, W', 'B, U', + 'G, U', 'R, U', 'B, G', 'B, R', 'G, R', + 'G, U, W', 'B, U, W', 'B, R, U', 'B, G, R', 'G, R, W', + 'B, G, W', 'R, U, W', 'B, R, W', 'B, G, U', 'G, R, U', + 'B, G, R, W', 'B, G, R, U', 'G, R, U, W', 'B, G, U, W', + 'B, R, U, W', 'B, G, R, U, W' +] + +# Convenience mapping from long color name to primary abbreviation +PRIMARY_COLOR_ABBR_MAP: Dict[str, str] = { + 'colorless': 'Colorless', 'white': 'W', 'blue': 'U', 'black': 'B', 'green': 'G', 'red': 'R' +} + +# ---------------------------------------------------------------------------------- +# CARD / DATAFRAME COLUMN CONSTANTS +# ---------------------------------------------------------------------------------- +# Unified column definition used across setup, tagging, and deck building modules. +# This consolidates previously duplicated lists: COLUMN_ORDER, TAGGED_COLUMN_ORDER, +# REQUIRED_COLUMNS (tag_constants), and CSV_REQUIRED_COLUMNS (builder_constants). +CARD_DATA_COLUMNS: List[str] = [ + 'name', 'faceName', 'edhrecRank', 'colorIdentity', 'colors', + 'manaCost', 'manaValue', 'type', 'creatureTypes', 'text', + 'power', 'toughness', 'keywords', 'themeTags', 'layout', 'side' +] + +# Alias for semantic clarity in different contexts +REQUIRED_CARD_COLUMNS = CARD_DATA_COLUMNS # Validation +CARD_COLUMN_ORDER = CARD_DATA_COLUMNS # Output / ordering + +# ---------------------------------------------------------------------------------- +# MENU / UI CONSTANTS +# ---------------------------------------------------------------------------------- +MAIN_MENU_ITEMS: List[str] = ['Build A Deck', 'Setup CSV Files', 'Tag CSV Files', 'Quit'] +SETUP_MENU_ITEMS: List[str] = ['Initial Setup', 'Regenerate CSV', 'Main Menu'] + +CSV_DIRECTORY: str = 'csv_files' + +# ---------------------------------------------------------------------------------- +# DATAFRAME NA HANDLING +# ---------------------------------------------------------------------------------- +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 +} + +# ---------------------------------------------------------------------------------- +# SPECIAL CARD EXCEPTIONS +# ---------------------------------------------------------------------------------- +MULTIPLE_COPY_CARDS = [ + 'Dragon\'s Approach', 'Hare Apparent', 'Nazgûl', 'Persistent Petitioners', + 'Rat Colony', 'Relentless Rats', 'Seven Dwarves', 'Shadowborn Apostle', + 'Slime Against Humanity', 'Templar Knight' +] + +# Backwards compatibility exports (older modules may still import these names) +COLUMN_ORDER = CARD_COLUMN_ORDER +TAGGED_COLUMN_ORDER = CARD_COLUMN_ORDER +REQUIRED_COLUMNS = REQUIRED_CARD_COLUMNS MAIN_MENU_ITEMS: List[str] = ['Build A Deck', 'Setup CSV Files', 'Tag CSV Files', 'Quit'] diff --git a/code/tagging/tag_constants.py b/code/tagging/tag_constants.py index 232e040..5cec06b 100644 --- a/code/tagging/tag_constants.py +++ b/code/tagging/tag_constants.py @@ -1,4 +1,16 @@ -from typing import Dict, List, Final +from typing import Dict, List, Final, Iterable +from dataclasses import dataclass +from settings import REQUIRED_CARD_COLUMNS as REQUIRED_COLUMNS # unified column list + +__all__ = [ + 'TRIGGERS', 'NUM_TO_SEARCH', 'TAG_GROUPS', 'PATTERN_GROUPS', 'PHRASE_GROUPS', + 'CREATE_ACTION_PATTERN', 'COUNTER_TYPES', 'CREATURE_TYPES', 'NON_CREATURE_TYPES', + 'OUTLAW_TYPES', 'ENCHANTMENT_TOKENS', 'ARTIFACT_TOKENS', 'REQUIRED_COLUMNS', + 'TYPE_TAG_MAPPING', 'DRAW_RELATED_TAGS', 'DRAW_EXCLUSION_PATTERNS', + 'EQUIPMENT_EXCLUSIONS', 'EQUIPMENT_SPECIFIC_CARDS', 'EQUIPMENT_RELATED_TAGS', + 'EQUIPMENT_TEXT_PATTERNS', 'AURA_SPECIFIC_CARDS', 'VOLTRON_COMMANDER_CARDS', + 'VOLTRON_PATTERNS' +] TRIGGERS: List[str] = ['when', 'whenever', 'at'] @@ -56,41 +68,75 @@ PHRASE_GROUPS: Dict[str, List[str]] = { CREATE_ACTION_PATTERN: Final[str] = r"create|put" # Creature/Counter types -COUNTER_TYPES: List[str] = [r'\+0/\+1', r'\+0/\+2', r'\+1/\+0', r'\+1/\+2', r'\+2/\+0', r'\+2/\+2', - '-0/-1', '-0/-2', '-1/-0', '-1/-2', '-2/-0', '-2/-2', - 'Acorn', 'Aegis', 'Age', 'Aim', 'Arrow', 'Arrowhead','Awakening', - 'Bait', 'Blaze', 'Blessing', 'Blight',' Blood', 'Bloddline', - 'Bloodstain', 'Book', 'Bounty', 'Brain', 'Bribery', 'Brick', - 'Burden', 'Cage', 'Carrion', 'Charge', 'Coin', 'Collection', - 'Component', 'Contested', 'Corruption', 'CRANK!', 'Credit', - 'Croak', 'Corpse', 'Crystal', 'Cube', 'Currency', 'Death', - 'Defense', 'Delay', 'Depletion', 'Descent', 'Despair', 'Devotion', - 'Divinity', 'Doom', 'Dream', 'Duty', 'Echo', 'Egg', 'Elixir', - 'Ember', 'Energy', 'Enlightened', 'Eon', 'Eruption', 'Everything', - 'Experience', 'Eyeball', 'Eyestalk', 'Fade', 'Fate', 'Feather', - 'Feeding', 'Fellowship', 'Fetch', 'Filibuster', 'Finality', 'Flame', - 'Flood', 'Foreshadow', 'Fungus', 'Fury', 'Fuse', 'Gem', 'Ghostform', - 'Glpyh', 'Gold', 'Growth', 'Hack', 'Harmony', 'Hatching', 'Hatchling', - 'Healing', 'Hit', 'Hope',' Hone', 'Hoofprint', 'Hour', 'Hourglass', - 'Hunger', 'Ice', 'Imposter', 'Incarnation', 'Incubation', 'Infection', - 'Influence', 'Ingenuity', 'Intel', 'Intervention', 'Invitation', - 'Isolation', 'Javelin', 'Judgment', 'Keyword', 'Ki', 'Kick', - 'Knickknack', 'Knowledge', 'Landmark', 'Level', 'Loot', 'Lore', - 'Loyalty', 'Luck', 'Magnet', 'Manabond', 'Manifestation', 'Mannequin', - 'Mask', 'Matrix', 'Memory', 'Midway', 'Mine', 'Mining', 'Mire', - 'Music', 'Muster', 'Necrodermis', 'Nest', 'Net', 'Night', 'Oil', - 'Omen', 'Ore', 'Page', 'Pain', 'Palliation', 'Paralyzing', 'Pause', - 'Petal', 'Petrification', 'Phyresis', 'Phylatery', 'Pin', 'Plague', - 'Plot', 'Point', 'Poison', 'Polyp', 'Possession', 'Pressure', 'Prey', - 'Pupa', 'Quest', 'Rad', 'Rejection', 'Reprieve', 'Rev', 'Revival', - 'Ribbon', 'Ritual', 'Rope', 'Rust', 'Scream', 'Scroll', 'Shell', - 'Shield', 'Silver', 'Shred', 'Sleep', 'Sleight', 'Slime', 'Slumber', - 'Soot', 'Soul', 'Spark', 'Spite', 'Spore', 'Stash', 'Storage', - 'Story', 'Strife', 'Study', 'Stun', 'Supply', 'Suspect', 'Takeover', - 'Task', 'Ticket', 'Tide', 'Time', 'Tower', 'Training', 'Trap', - 'Treasure', 'Unity', 'Unlock', 'Valor', 'Velocity', 'Verse', - 'Vitality', 'Void', 'Volatile', 'Vortex', 'Vow', 'Voyage', 'Wage', - 'Winch', 'Wind', 'Wish'] +"""Counter type vocabularies.""" + +# Power/Toughness modifier counters (regex fragments already escaped where needed) +PT_COUNTER_TYPES: List[str] = [ + r'\+0/\+1', r'\+0/\+2', r'\+1/\+0', r'\+1/\+2', r'\+2/\+0', r'\+2/\+2', + '-0/-1', '-0/-2', '-1/-0', '-1/-2', '-2/-0', '-2/-2' +] + +# Named counters (alphabetical within rough thematic blocks) +NAMED_COUNTER_TYPES: List[str] = [ + 'Acorn', 'Aegis', 'Age', 'Aim', 'Arrow', 'Arrowhead', 'Awakening', + 'Bait', 'Blaze', 'Blessing', 'Blight', 'Blood', 'Bloodline', 'Bloodstain', 'Book', + 'Bounty', 'Brain', 'Bribery', 'Brick', 'Burden', 'Cage', 'Carrion', 'Charge', 'Coin', + 'Collection', 'Component', 'Contested', 'Corruption', 'CRANK!', 'Credit', 'Croak', + 'Corpse', 'Crystal', 'Cube', 'Currency', 'Death', 'Defense', 'Delay', 'Depletion', + 'Descent', 'Despair', 'Devotion', 'Divinity', 'Doom', 'Dream', 'Duty', 'Echo', 'Egg', + 'Elixir', 'Ember', 'Energy', 'Enlightened', 'Eon', 'Eruption', 'Everything', + 'Experience', 'Eyeball', 'Eyestalk', 'Fade', 'Fate', 'Feather', 'Feeding', + 'Fellowship', 'Fetch', 'Filibuster', 'Finality', 'Flame', 'Flood', 'Foreshadow', + 'Fungus', 'Fury', 'Fuse', 'Gem', 'Ghostform', 'Glyph', 'Gold', 'Growth', 'Hack', + 'Harmony', 'Hatching', 'Hatchling', 'Healing', 'Hit', 'Hope', 'Hone', 'Hoofprint', + 'Hour', 'Hourglass', 'Hunger', 'Ice', 'Imposter', 'Incarnation', 'Incubation', + 'Infection', 'Influence', 'Ingenuity', 'Intel', 'Intervention', 'Invitation', + 'Isolation', 'Javelin', 'Judgment', 'Keyword', 'Ki', 'Kick', 'Knickknack', + 'Knowledge', 'Landmark', 'Level', 'Loot', 'Lore', 'Loyalty', 'Luck', 'Magnet', + 'Manabond', 'Manifestation', 'Mannequin', 'Mask', 'Matrix', 'Memory', 'Midway', + 'Mine', 'Mining', 'Mire', 'Music', 'Muster', 'Necrodermis', 'Nest', 'Net', 'Night', + 'Oil', 'Omen', 'Ore', 'Page', 'Pain', 'Palliation', 'Paralyzing', 'Pause', 'Petal', + 'Petrification', 'Phyresis', 'Phylactery', 'Pin', 'Plague', 'Plot', 'Point', 'Poison', + 'Polyp', 'Possession', 'Pressure', 'Prey', 'Pupa', 'Quest', 'Rad', 'Rejection', + 'Reprieve', 'Rev', 'Revival', 'Ribbon', 'Ritual', 'Rope', 'Rust', 'Scream', 'Scroll', + 'Shell', 'Shield', 'Silver', 'Shred', 'Sleep', 'Sleight', 'Slime', 'Slumber', 'Soot', + 'Soul', 'Spark', 'Spite', 'Spore', 'Stash', 'Storage', 'Story', 'Strife', 'Study', + 'Stun', 'Supply', 'Suspect', 'Takeover', 'Task', 'Ticket', 'Tide', 'Time', 'Tower', + 'Training', 'Trap', 'Treasure', 'Unity', 'Unlock', 'Valor', 'Velocity', 'Verse', + 'Vitality', 'Void', 'Volatile', 'Vortex', 'Vow', 'Voyage', 'Wage', 'Winch', 'Wind', + 'Wish' +] + +# Dataclass describing a counter pattern and display label +@dataclass(frozen=True) +class CounterSpec: + pattern: str # Regex fragment (without trailing " counter") + label: str # Human-readable label (used in tag text) + group: str # 'pt' or 'named' (for future filtering) + + def search_pattern(self) -> str: + """Full regex used for searching (matches singular/plural).""" + return rf"{self.pattern} counter[s]?" + +# Helper to derive label from pattern (unescape common sequences) +def _derive_label(p: str) -> str: + return p.replace('\\+','+') + +def _build_counter_specs(pt_list: Iterable[str], named_list: Iterable[str]) -> List[CounterSpec]: + specs: List[CounterSpec] = [] + specs.extend(CounterSpec(pattern=p, label=_derive_label(p), group='pt') for p in pt_list) + specs.extend(CounterSpec(pattern=p, label=p, group='named') for p in named_list) + return specs + +ALL_COUNTER_SPECS: List[CounterSpec] = _build_counter_specs(PT_COUNTER_TYPES, NAMED_COUNTER_TYPES) + +# Backward-compatible flat list (legacy usage) +COUNTER_TYPES: List[str] = [s.pattern for s in ALL_COUNTER_SPECS] + +# Basic duplication guard (fails fast during import if misconfigured) +if len(COUNTER_TYPES) != len(set(COUNTER_TYPES)): + duplicate = sorted({p for p in COUNTER_TYPES if COUNTER_TYPES.count(p) > 1}) + raise ValueError(f"Duplicate counter patterns detected: {duplicate}") CREATURE_TYPES: List[str] = ['Advisor', 'Aetherborn', 'Alien', 'Ally', 'Angel', 'Antelope', 'Ape', 'Archer', 'Archon', 'Armadillo', 'Army', 'Artificer', 'Assassin', 'Assembly-Worker', 'Astartes', 'Atog', 'Aurochs', 'Automaton', @@ -145,12 +191,7 @@ ENCHANTMENT_TOKENS: List[str] = ['Cursed Role', 'Monster Role', 'Royal Role', 'S ARTIFACT_TOKENS: List[str] = ['Blood', 'Clue', 'Food', 'Gold', 'Incubator', 'Junk','Map','Powerstone', 'Treasure'] -# Constants for DataFrame validation and processing -REQUIRED_COLUMNS: List[str] = [ - 'name', 'faceName', 'edhrecRank', 'colorIdentity', 'colors', - 'manaCost', 'manaValue', 'type', 'creatureTypes', 'text', - 'power', 'toughness', 'keywords', 'themeTags', 'layout', 'side' -] +# (REQUIRED_COLUMNS imported from settings to avoid duplication) # Mapping of card types to their corresponding theme tags TYPE_TAG_MAPPING: Dict[str, List[str]] = { diff --git a/code/tagging/tagger.py b/code/tagging/tagger.py index 6e200ee..595bc9f 100644 --- a/code/tagging/tagger.py +++ b/code/tagging/tagger.py @@ -3044,18 +3044,15 @@ def tag_for_special_counters(df: pd.DataFrame, color: str) -> None: start_time = pd.Timestamp.now() try: - # Process each counter type + # Process each counter type (supports singular/plural) counter_counts = {} - for counter_type in tag_constants.COUNTER_TYPES: - # Create pattern for this counter type - pattern = f'{counter_type} counter' + for spec in tag_constants.ALL_COUNTER_SPECS: + pattern = spec.search_pattern() mask = tag_utils.create_text_mask(df, pattern) - if mask.any(): - # Apply tags via rules engine - tags = [f'{counter_type} Counters', 'Counters Matter'] - tag_utils.apply_rules(df, [ { 'mask': mask, 'tags': tags } ]) - counter_counts[counter_type] = mask.sum() + tags = [f'{spec.label} Counters', 'Counters Matter'] + tag_utils.apply_rules(df, [ {'mask': mask, 'tags': tags} ]) + counter_counts[spec.label] = int(mask.sum()) # Log results duration = (pd.Timestamp.now() - start_time).total_seconds()