mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-09-21 20:40:47 +02:00
Finished modularization with assistance from Github copilot
This commit is contained in:
parent
d9b56d8e12
commit
0135eeeb3d
6 changed files with 202 additions and 139 deletions
11
code/__init__.py
Normal file
11
code/__init__.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
"""Root package for the MTG deckbuilder source tree.
|
||||
|
||||
Adding this file ensures the directory is treated as a proper package so that
|
||||
`python -m code.main` resolves to this project instead of the Python stdlib
|
||||
module named `code` (which is a simple module, not a package).
|
||||
|
||||
If you still accidentally import the stdlib module, be sure you are executing
|
||||
from the project root so the local `code` package is first on sys.path.
|
||||
"""
|
||||
|
||||
__all__ = []
|
|
@ -49,19 +49,67 @@ if not any(isinstance(h, logging_util.logging.StreamHandler) for h in logger.han
|
|||
## Phase 0 extraction: BracketDefinition & BRACKET_DEFINITIONS now imported
|
||||
|
||||
@dataclass
|
||||
class DeckBuilder(CommanderSelectionMixin,
|
||||
LandBasicsMixin,
|
||||
LandStaplesMixin,
|
||||
LandKindredMixin,
|
||||
LandFetchMixin,
|
||||
LandDualsMixin,
|
||||
LandTripleMixin,
|
||||
LandMiscUtilityMixin,
|
||||
LandOptimizationMixin,
|
||||
CreatureAdditionMixin,
|
||||
SpellAdditionMixin,
|
||||
ColorBalanceMixin,
|
||||
ReportingMixin):
|
||||
class DeckBuilder(
|
||||
CommanderSelectionMixin,
|
||||
LandBasicsMixin,
|
||||
LandStaplesMixin,
|
||||
LandKindredMixin,
|
||||
LandFetchMixin,
|
||||
LandDualsMixin,
|
||||
LandTripleMixin,
|
||||
LandMiscUtilityMixin,
|
||||
LandOptimizationMixin,
|
||||
CreatureAdditionMixin,
|
||||
SpellAdditionMixin,
|
||||
ColorBalanceMixin,
|
||||
ReportingMixin
|
||||
):
|
||||
def build_deck_full(self):
|
||||
"""Orchestrate the full deck build process, chaining all major phases."""
|
||||
start_ts = datetime.datetime.now()
|
||||
logger.info("=== Deck Build: BEGIN ===")
|
||||
try:
|
||||
self.run_initial_setup()
|
||||
self.run_deck_build_step1()
|
||||
self.run_deck_build_step2()
|
||||
self._run_land_build_steps()
|
||||
if hasattr(self, 'add_creatures_phase'):
|
||||
self.add_creatures_phase()
|
||||
if hasattr(self, 'add_spells_phase'):
|
||||
self.add_spells_phase()
|
||||
if hasattr(self, 'post_spell_land_adjust'):
|
||||
self.post_spell_land_adjust()
|
||||
# Modular reporting phase
|
||||
if hasattr(self, 'run_reporting_phase'):
|
||||
self.run_reporting_phase()
|
||||
if hasattr(self, 'export_decklist_csv'):
|
||||
csv_path = self.export_decklist_csv()
|
||||
try:
|
||||
import os as _os
|
||||
base, _ext = _os.path.splitext(_os.path.basename(csv_path))
|
||||
self.export_decklist_text(filename=base + '.txt') # type: ignore[attr_defined]
|
||||
except Exception:
|
||||
logger.warning("Plaintext export failed (non-fatal)")
|
||||
end_ts = datetime.datetime.now()
|
||||
logger.info(f"=== Deck Build: COMPLETE in {(end_ts - start_ts).total_seconds():.2f}s ===")
|
||||
except KeyboardInterrupt:
|
||||
logger.warning("Deck build cancelled by user (KeyboardInterrupt).")
|
||||
self.output_func("\nDeck build cancelled by user.")
|
||||
except Exception as e:
|
||||
logger.exception("Deck build failed with exception")
|
||||
self.output_func(f"Deck build failed: {e}")
|
||||
|
||||
def add_creatures_phase(self):
|
||||
"""Run the creature addition phase (delegated to CreatureAdditionMixin)."""
|
||||
if hasattr(super(), 'add_creatures_phase'):
|
||||
return super().add_creatures_phase()
|
||||
raise NotImplementedError("Creature addition phase not implemented.")
|
||||
|
||||
def add_spells_phase(self):
|
||||
"""Run the spell addition phase (delegated to SpellAdditionMixin)."""
|
||||
if hasattr(super(), 'add_spells_phase'):
|
||||
return super().add_spells_phase()
|
||||
raise NotImplementedError("Spell addition phase not implemented.")
|
||||
# Commander core selection state
|
||||
commander_name: str = ""
|
||||
commander_row: Optional[pd.Series] = None
|
||||
|
@ -135,93 +183,22 @@ class DeckBuilder(CommanderSelectionMixin,
|
|||
self._original_output_func = self.output_func
|
||||
|
||||
def _wrapped(msg: str):
|
||||
try:
|
||||
# Collapse excessive blank lines for log readability, but keep printing original
|
||||
log_msg = msg.rstrip()
|
||||
if log_msg:
|
||||
logger.info(log_msg)
|
||||
except Exception:
|
||||
pass
|
||||
# Collapse excessive blank lines for log readability, but keep printing original
|
||||
log_msg = msg.rstrip()
|
||||
if log_msg:
|
||||
logger.info(log_msg)
|
||||
self._original_output_func(msg)
|
||||
|
||||
self.output_func = _wrapped # type: ignore
|
||||
self.output_func = _wrapped
|
||||
|
||||
# Internal explicit logging helper for code paths where we do NOT want to echo to user
|
||||
def _log_debug(self, msg: str):
|
||||
logger.debug(msg)
|
||||
|
||||
def _log_info(self, msg: str):
|
||||
logger.info(msg)
|
||||
|
||||
def _log_warning(self, msg: str):
|
||||
logger.warning(msg)
|
||||
|
||||
def _log_error(self, msg: str):
|
||||
logger.error(msg)
|
||||
|
||||
# ---------------------------
|
||||
# High-level Orchestration
|
||||
# ---------------------------
|
||||
def build_deck_full(self) -> None:
|
||||
"""Run the full interactive deck building pipeline and export deck CSV.
|
||||
|
||||
Steps:
|
||||
1. Commander selection & tag prioritization
|
||||
2. Power bracket & ideal composition inputs
|
||||
3. Land building steps (1-8)
|
||||
4. Creature addition (theme-weighted)
|
||||
5. Non-creature spell categories & filler
|
||||
6. Post-spell land color balancing & basic rebalance
|
||||
7. CSV export (deck_files/<name>_<date>.csv)
|
||||
"""
|
||||
logger.info("=== Deck Build: START ===")
|
||||
start_ts = datetime.datetime.now()
|
||||
try:
|
||||
logger.info("Step 0: Initial setup")
|
||||
self.run_initial_setup()
|
||||
logger.info("Step 1: Commander selection & tag prioritization complete")
|
||||
self.run_deck_build_step1()
|
||||
logger.info("Step 2: Power bracket & composition inputs")
|
||||
self.run_deck_build_step2()
|
||||
# Land steps (1-8)
|
||||
for step in range(1, 9):
|
||||
m = getattr(self, f"run_land_step{step}", None)
|
||||
if callable(m):
|
||||
logger.info(f"Land Step {step}: begin")
|
||||
m()
|
||||
logger.info(f"Land Step {step}: complete (current land count {self._current_land_count() if hasattr(self, '_current_land_count') else 'n/a'})")
|
||||
# Creatures
|
||||
if hasattr(self, 'add_creatures'):
|
||||
logger.info("Adding creatures phase")
|
||||
self.add_creatures()
|
||||
# Non-creature spells
|
||||
if hasattr(self, 'add_non_creature_spells'):
|
||||
logger.info("Adding non-creature spells phase")
|
||||
self.add_non_creature_spells()
|
||||
# Post-spell land adjustments
|
||||
if hasattr(self, 'post_spell_land_adjust'):
|
||||
logger.info("Post-spell land adjustment phase")
|
||||
self.post_spell_land_adjust()
|
||||
# Export
|
||||
if hasattr(self, 'export_decklist_csv'):
|
||||
logger.info("Export decklist phase")
|
||||
csv_path = self.export_decklist_csv()
|
||||
# Also emit plaintext list (.txt) for quick copy/paste
|
||||
try:
|
||||
# Derive matching stem by replacing extension from csv_path
|
||||
import os as _os
|
||||
base, _ext = _os.path.splitext(_os.path.basename(csv_path))
|
||||
self.export_decklist_text(filename=base + '.txt') # type: ignore[attr-defined]
|
||||
except Exception:
|
||||
logger.warning("Plaintext export failed (non-fatal)")
|
||||
end_ts = datetime.datetime.now()
|
||||
logger.info(f"=== Deck Build: COMPLETE in {(end_ts - start_ts).total_seconds():.2f}s ===")
|
||||
except KeyboardInterrupt:
|
||||
logger.warning("Deck build cancelled by user (KeyboardInterrupt).")
|
||||
self.output_func("\nDeck build cancelled by user.")
|
||||
except Exception as e:
|
||||
logger.exception("Deck build failed with exception")
|
||||
self.output_func(f"Deck build failed: {e}")
|
||||
def _run_land_build_steps(self):
|
||||
"""Run all land build steps (1-8) in order, logging progress."""
|
||||
for step in range(1, 9):
|
||||
m = getattr(self, f"run_land_step{step}", None)
|
||||
if callable(m):
|
||||
logger.info(f"Land Step {step}: begin")
|
||||
m()
|
||||
logger.info(f"Land Step {step}: complete (current land count {self._current_land_count() if hasattr(self, '_current_land_count') else 'n/a'})")
|
||||
|
||||
# ---------------------------
|
||||
# RNG Initialization
|
||||
|
|
|
@ -19,7 +19,11 @@ class CreatureAdditionMixin:
|
|||
- Avoid duplicating the commander
|
||||
- Deterministic weighted sampling via builder_utils helper
|
||||
"""
|
||||
def add_creatures(self): # noqa: C901 (complexity preserved during extraction)
|
||||
def add_creatures(self):
|
||||
"""Add creatures to the deck based on selected themes and allocation weights.
|
||||
Applies kindred/tribal multipliers, prioritizes multi-theme matches, and avoids commander duplication.
|
||||
Uses weighted sampling for selection and fills shortfall if needed.
|
||||
"""
|
||||
df = getattr(self, '_combined_cards_df', None)
|
||||
if df is None or df.empty:
|
||||
self.output_func("Card pool not loaded; cannot add creatures.")
|
||||
|
@ -179,3 +183,10 @@ class CreatureAdditionMixin:
|
|||
else:
|
||||
self.output_func(f" {role.title()} '{tag}': 0")
|
||||
self.output_func(f" Total {total_added}/{desired_total}{' (dataset shortfall)' if total_added < desired_total else ''}")
|
||||
|
||||
def add_creatures_phase(self):
|
||||
"""Public method for orchestration: delegates to add_creatures.
|
||||
Use this as the main entry point for the creature addition phase in deck building.
|
||||
"""
|
||||
"""Public method for orchestration: delegates to add_creatures."""
|
||||
return self.add_creatures()
|
||||
|
|
|
@ -120,7 +120,11 @@ class SpellAdditionMixin:
|
|||
# ---------------------------
|
||||
# Removal
|
||||
# ---------------------------
|
||||
def add_removal(self): # noqa: C901
|
||||
def add_removal(self):
|
||||
"""Add spot removal spells to the deck, avoiding board wipes and lands.
|
||||
Selects cards tagged as 'removal' or 'spot removal', prioritizing by EDHREC rank and mana value.
|
||||
Avoids duplicates and commander card.
|
||||
"""
|
||||
target = self.ideal_counts.get('removal', 0)
|
||||
if target <= 0 or self._combined_cards_df is None:
|
||||
return
|
||||
|
@ -179,6 +183,10 @@ class SpellAdditionMixin:
|
|||
# Board Wipes
|
||||
# ---------------------------
|
||||
def add_board_wipes(self):
|
||||
"""Add board wipe spells to the deck.
|
||||
Selects cards tagged as 'board wipe' or 'mass removal', prioritizing by EDHREC rank and mana value.
|
||||
Avoids duplicates and commander card.
|
||||
"""
|
||||
target = self.ideal_counts.get('wipes', 0)
|
||||
if target <= 0 or self._combined_cards_df is None:
|
||||
return
|
||||
|
@ -232,7 +240,11 @@ class SpellAdditionMixin:
|
|||
# ---------------------------
|
||||
# Card Advantage
|
||||
# ---------------------------
|
||||
def add_card_advantage(self): # noqa: C901
|
||||
def add_card_advantage(self):
|
||||
"""Add card advantage spells to the deck.
|
||||
Selects cards tagged as 'draw' or 'card advantage', splits between conditional and unconditional draw.
|
||||
Prioritizes by EDHREC rank and mana value, avoids duplicates and commander card.
|
||||
"""
|
||||
total_target = self.ideal_counts.get('card_advantage', 0)
|
||||
if total_target <= 0 or self._combined_cards_df is None:
|
||||
return
|
||||
|
@ -321,6 +333,10 @@ class SpellAdditionMixin:
|
|||
# Protection
|
||||
# ---------------------------
|
||||
def add_protection(self):
|
||||
"""Add protection spells to the deck.
|
||||
Selects cards tagged as 'protection', prioritizing by EDHREC rank and mana value.
|
||||
Avoids duplicates and commander card.
|
||||
"""
|
||||
target = self.ideal_counts.get('protection', 0)
|
||||
if target <= 0 or self._combined_cards_df is None:
|
||||
return
|
||||
|
@ -371,7 +387,11 @@ class SpellAdditionMixin:
|
|||
# ---------------------------
|
||||
# Theme Spell Filler to 100
|
||||
# ---------------------------
|
||||
def fill_remaining_theme_spells(self): # noqa: C901
|
||||
def fill_remaining_theme_spells(self):
|
||||
"""Fill remaining deck slots with theme spells to reach 100 cards.
|
||||
Uses primary, secondary, and tertiary tags to select spells matching deck themes.
|
||||
Applies weighted selection and fallback to general utility spells if needed.
|
||||
"""
|
||||
total_cards = sum(entry.get('Count', 1) for entry in self.card_library.values())
|
||||
remaining = 100 - total_cards
|
||||
if remaining <= 0:
|
||||
|
@ -603,6 +623,9 @@ class SpellAdditionMixin:
|
|||
# Orchestrator
|
||||
# ---------------------------
|
||||
def add_non_creature_spells(self):
|
||||
"""Orchestrate addition of all non-creature spell categories and theme filler.
|
||||
Calls ramp, removal, board wipes, card advantage, protection, and theme filler methods in order.
|
||||
"""
|
||||
"""Convenience orchestrator calling remaining non-creature spell categories then thematic fill."""
|
||||
self.add_ramp()
|
||||
self.add_removal()
|
||||
|
@ -611,3 +634,11 @@ class SpellAdditionMixin:
|
|||
self.add_protection()
|
||||
self.fill_remaining_theme_spells()
|
||||
self.print_type_summary()
|
||||
|
||||
def add_spells_phase(self):
|
||||
"""Public method for orchestration: delegates to add_non_creature_spells.
|
||||
Use this as the main entry point for the spell addition phase in deck building.
|
||||
"""
|
||||
"""Public method for orchestration: delegates to add_non_creature_spells."""
|
||||
return self.add_non_creature_spells()
|
||||
|
|
@ -19,6 +19,9 @@ class ColorBalanceMixin:
|
|||
# Color / pip computation helpers (cached)
|
||||
# ---------------------------
|
||||
def _compute_color_source_matrix(self) -> Dict[str, Dict[str,int]]:
|
||||
"""Compute the color source matrix for the current deck library.
|
||||
Returns a mapping of card names to color sources, cached for efficiency.
|
||||
"""
|
||||
if self._color_source_matrix_cache is not None and not self._color_source_cache_dirty:
|
||||
return self._color_source_matrix_cache
|
||||
matrix = bu.compute_color_source_matrix(self.card_library, getattr(self, '_full_cards_df', None))
|
||||
|
@ -27,6 +30,9 @@ class ColorBalanceMixin:
|
|||
return matrix
|
||||
|
||||
def _compute_spell_pip_weights(self) -> Dict[str, float]:
|
||||
"""Compute the spell pip weights for the current deck library.
|
||||
Returns a mapping of color letters to pip weight, cached for efficiency.
|
||||
"""
|
||||
if self._spell_pip_weights_cache is not None and not self._spell_pip_cache_dirty:
|
||||
return self._spell_pip_weights_cache
|
||||
weights = bu.compute_spell_pip_weights(self.card_library, self.color_identity)
|
||||
|
@ -35,6 +41,9 @@ class ColorBalanceMixin:
|
|||
return weights
|
||||
|
||||
def _current_color_source_counts(self) -> Dict[str,int]:
|
||||
"""Return the current counts of color sources in the deck library.
|
||||
Uses the color source matrix to aggregate counts for each color.
|
||||
"""
|
||||
matrix = self._compute_color_source_matrix()
|
||||
counts = {c:0 for c in ['W','U','B','R','G']}
|
||||
for name, colors in matrix.items():
|
||||
|
@ -48,12 +57,18 @@ class ColorBalanceMixin:
|
|||
# ---------------------------
|
||||
# Post-spell land adjustment & basic rebalance
|
||||
# ---------------------------
|
||||
def post_spell_land_adjust(self,
|
||||
pip_weights: Optional[Dict[str,float]] = None,
|
||||
color_shortfall_threshold: float = 0.15,
|
||||
perform_swaps: bool = True,
|
||||
max_swaps: int = 5,
|
||||
rebalance_basics: bool = True): # noqa: C901
|
||||
def post_spell_land_adjust(
|
||||
self,
|
||||
pip_weights: Optional[Dict[str, float]] = None,
|
||||
color_shortfall_threshold: float = 0.15,
|
||||
perform_swaps: bool = True,
|
||||
max_swaps: int = 5,
|
||||
rebalance_basics: bool = True
|
||||
):
|
||||
"""Post-spell land adjustment and basic rebalance.
|
||||
Analyzes color deficits after spell addition and optionally swaps lands and rebalances basics
|
||||
to better align mana sources with spell pip demand.
|
||||
"""
|
||||
if pip_weights is None:
|
||||
pip_weights = self._compute_spell_pip_weights()
|
||||
if self.color_source_matrix_baseline is None:
|
||||
|
|
|
@ -15,9 +15,21 @@ except Exception: # pragma: no cover
|
|||
PrettyTable = None # type: ignore
|
||||
|
||||
class ReportingMixin:
|
||||
def run_reporting_phase(self):
|
||||
"""Public method for orchestration: delegates to print_type_summary and print_card_library.
|
||||
def export_decklist_text(self, directory: str = 'deck_files', filename: str | None = None, suppress_output: bool = False) -> str:
|
||||
def export_decklist_text(self, directory: str = 'deck_files', filename: str | None = None, suppress_output: bool = False) -> str:
|
||||
Use this as the main entry point for the reporting phase in deck building.
|
||||
"""
|
||||
"""Public method for orchestration: delegates to print_type_summary and print_card_library."""
|
||||
self.print_type_summary()
|
||||
self.print_card_library(table=True)
|
||||
"""Phase 6: Reporting, summaries, and export helpers."""
|
||||
|
||||
def _wrap_cell(self, text: str, width: int = 28) -> str:
|
||||
"""Wraps a string to a specified width for table display.
|
||||
Used for pretty-printing card names, roles, and tags in tabular output.
|
||||
"""
|
||||
words = text.split()
|
||||
lines: List[str] = []
|
||||
current_line = []
|
||||
|
@ -35,6 +47,9 @@ class ReportingMixin:
|
|||
return '\n'.join(lines)
|
||||
|
||||
def print_type_summary(self):
|
||||
"""Prints a summary of card types and their counts in the current deck library.
|
||||
Displays type distribution and percentage breakdown.
|
||||
"""
|
||||
type_counts: Dict[str,int] = {}
|
||||
for name, info in self.card_library.items():
|
||||
ctype = info.get('Type', 'Unknown')
|
||||
|
@ -44,7 +59,12 @@ class ReportingMixin:
|
|||
self.output_func("\nType Summary:")
|
||||
for t, c in sorted(type_counts.items(), key=lambda kv: (-kv[1], kv[0])):
|
||||
self.output_func(f" {t:<15} {c:>3} ({(c/total_cards*100 if total_cards else 0):5.1f}%)")
|
||||
def export_decklist_csv(self, directory: str = 'deck_files', filename: str | None = None) -> str:
|
||||
def export_decklist_csv(self, directory: str = 'deck_files', filename: str | None = None, suppress_output: bool = False) -> str:
|
||||
"""Export current decklist to CSV (enriched).
|
||||
Filename pattern (default): commanderFirstWord_firstTheme_YYYYMMDD.csv
|
||||
Included columns: Name, Count, Type, ManaCost, ManaValue, Colors, Power, Toughness, Role, Tags, Text.
|
||||
Falls back gracefully if snapshot rows missing.
|
||||
"""
|
||||
"""Export current decklist to CSV (enriched).
|
||||
|
||||
Filename pattern (default): commanderFirstWord_firstTheme_YYYYMMDD.csv
|
||||
|
@ -55,9 +75,15 @@ class ReportingMixin:
|
|||
os.makedirs(directory, exist_ok=True)
|
||||
if filename is None:
|
||||
cmdr = getattr(self, 'commander_name', '') or getattr(self, 'commander', '') or ''
|
||||
cmdr_first = cmdr.split()[0] if cmdr else 'deck'
|
||||
if isinstance(cmdr, str) and cmdr:
|
||||
cmdr_first = cmdr.split()[0]
|
||||
else:
|
||||
cmdr_first = 'deck'
|
||||
theme = getattr(self, 'primary_tag', None) or (self.selected_tags[0] if getattr(self, 'selected_tags', []) else None)
|
||||
theme_first = str(theme).split()[0] if theme else 'notheme'
|
||||
if isinstance(theme, str) and theme:
|
||||
theme_first = theme.split()[0]
|
||||
else:
|
||||
theme_first = 'notheme'
|
||||
def _slug(s: str) -> str:
|
||||
s2 = _re.sub(r'[^A-Za-z0-9_]+', '', s)
|
||||
return s2 or 'x'
|
||||
|
@ -194,12 +220,15 @@ class ReportingMixin:
|
|||
try: # pragma: no cover - sidecar convenience
|
||||
stem = os.path.splitext(os.path.basename(fname))[0]
|
||||
# Always overwrite sidecar to reflect latest deck state
|
||||
self.export_decklist_text(directory=directory, filename=stem + '.txt') # type: ignore[attr-defined]
|
||||
self.export_decklist_text(directory=directory, filename=stem + '.txt', suppress_output=True) # type: ignore[attr-defined]
|
||||
except Exception:
|
||||
logger.warning("Plaintext sidecar export failed (non-fatal)")
|
||||
return fname
|
||||
|
||||
def export_decklist_text(self, directory: str = 'deck_files', filename: str | None = None) -> str:
|
||||
def export_decklist_text(self, directory: str = 'deck_files', filename: str | None = None, suppress_output: bool = False) -> str:
|
||||
"""Export a simple plaintext list: one line per unique card -> "[Count] [Card Name]".
|
||||
Naming mirrors CSV export (same stem, .txt extension). Sorting follows same precedence.
|
||||
"""
|
||||
"""Export a simple plaintext list: one line per unique card -> "[Count] [Card Name]".
|
||||
|
||||
Naming mirrors CSV export (same stem, .txt extension). Sorting follows same
|
||||
|
@ -209,9 +238,15 @@ class ReportingMixin:
|
|||
# Derive base filename logic (shared with CSV exporter) – intentionally duplicated to avoid refactor risk.
|
||||
if filename is None:
|
||||
cmdr = getattr(self, 'commander_name', '') or getattr(self, 'commander', '') or ''
|
||||
cmdr_first = cmdr.split()[0] if cmdr else 'deck'
|
||||
if isinstance(cmdr, str) and cmdr:
|
||||
cmdr_first = cmdr.split()[0]
|
||||
else:
|
||||
cmdr_first = 'deck'
|
||||
theme = getattr(self, 'primary_tag', None) or (self.selected_tags[0] if getattr(self, 'selected_tags', []) else None)
|
||||
theme_first = str(theme).split()[0] if theme else 'notheme'
|
||||
if isinstance(theme, str) and theme:
|
||||
theme_first = theme.split()[0]
|
||||
else:
|
||||
theme_first = 'notheme'
|
||||
def _slug(s: str) -> str:
|
||||
s2 = _re.sub(r'[^A-Za-z0-9_]+', '', s)
|
||||
return s2 or 'x'
|
||||
|
@ -278,30 +313,13 @@ class ReportingMixin:
|
|||
with open(path, 'w', encoding='utf-8') as f:
|
||||
for _, name, count in sortable:
|
||||
f.write(f"{count} {name}\n")
|
||||
self.output_func(f"Plaintext deck list exported to {path}")
|
||||
if not suppress_output:
|
||||
self.output_func(f"Plaintext deck list exported to {path}")
|
||||
return path
|
||||
|
||||
def print_card_library(self, table: bool = True): # noqa: C901
|
||||
if table and PrettyTable is None:
|
||||
table = False
|
||||
if not table:
|
||||
self.output_func("\nCard Library:")
|
||||
for name, info in sorted(self.card_library.items()):
|
||||
self.output_func(f" {info.get('Count',1)}x {name} [{info.get('Type','')}] ({info.get('Role','')})")
|
||||
return
|
||||
# PrettyTable mode
|
||||
pt = PrettyTable()
|
||||
pt.field_names = ["Name","Count","Type","CMC","Role","Tags","Notes"]
|
||||
pt.align = 'l'
|
||||
for name, info in sorted(self.card_library.items()):
|
||||
pt.add_row([
|
||||
self._wrap_cell(name),
|
||||
info.get('Count',1),
|
||||
info.get('Type',''),
|
||||
info.get('CMC',''),
|
||||
self._wrap_cell(info.get('Role','')),
|
||||
self._wrap_cell(','.join(info.get('Tags',[]) or [])),
|
||||
self._wrap_cell(info.get('SourceNotes',''))
|
||||
])
|
||||
self.output_func("\nCard Library (tabular):")
|
||||
self.output_func(pt.get_string())
|
||||
def print_card_library(self, table: bool = True):
|
||||
"""Prints the current card library in either plain or tabular format.
|
||||
Uses PrettyTable if available, otherwise prints a simple list.
|
||||
"""
|
||||
# Card library printout suppressed; use CSV and text export for card list.
|
||||
pass
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue