mtg_python_deckbuilder/code/deck_builder/phases/phase6_reporting.py

325 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
from typing import Dict, List
import csv
import os
import datetime as _dt
import re as _re
import logging_util
logger = logging_util.logging.getLogger(__name__)
try:
from prettytable import PrettyTable # type: ignore
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 = []
current_len = 0
for w in words:
if current_len + len(w) + (1 if current_line else 0) > width:
lines.append(' '.join(current_line))
current_line = [w]
current_len = len(w)
else:
current_line.append(w)
current_len += len(w) + (1 if len(current_line) > 1 else 0)
if current_line:
lines.append(' '.join(current_line))
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')
cnt = info.get('Count',1)
type_counts[ctype] = type_counts.get(ctype,0) + cnt
total_cards = sum(type_counts.values())
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, 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
Included columns (enriched when possible):
Name, Count, Type, ManaCost, ManaValue, Colors, Power, Toughness, Role, Tags, Text
Falls back gracefully if snapshot rows missing.
"""
os.makedirs(directory, exist_ok=True)
if filename is None:
cmdr = getattr(self, 'commander_name', '') or getattr(self, 'commander', '') or ''
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)
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'
cmdr_slug = _slug(cmdr_first)
theme_slug = _slug(theme_first)
date_part = _dt.date.today().strftime('%Y%m%d')
filename = f"{cmdr_slug}_{theme_slug}_{date_part}.csv"
fname = os.path.join(directory, filename)
full_df = getattr(self, '_full_cards_df', None)
combined_df = getattr(self, '_combined_cards_df', None)
snapshot = full_df if full_df is not None else combined_df
row_lookup: Dict[str, any] = {}
if snapshot is not None and not snapshot.empty and 'name' in snapshot.columns:
for _, r in snapshot.iterrows():
nm = str(r.get('name'))
if nm not in row_lookup:
row_lookup[nm] = r
headers = [
"Name","Count","Type","ManaCost","ManaValue","Colors","Power","Toughness",
"Role","SubRole","AddedBy","TriggerTag","Synergy","Tags","Text"
]
# Precedence list for sorting
precedence_order = [
'Commander', 'Battle', 'Planeswalker', 'Creature', 'Instant', 'Sorcery', 'Artifact', 'Enchantment', 'Land'
]
precedence_index = {k: i for i, k in enumerate(precedence_order)}
commander_name = getattr(self, 'commander_name', '') or getattr(self, 'commander', '') or ''
def classify(primary_type_line: str, card_name: str) -> str:
if commander_name and card_name == commander_name:
return 'Commander'
tl = (primary_type_line or '').lower()
if 'battle' in tl:
return 'Battle'
if 'planeswalker' in tl:
return 'Planeswalker'
if 'creature' in tl:
return 'Creature'
if 'instant' in tl:
return 'Instant'
if 'sorcery' in tl:
return 'Sorcery'
if 'artifact' in tl:
return 'Artifact'
if 'enchantment' in tl:
return 'Enchantment'
if 'land' in tl:
return 'Land'
return 'ZZZ'
rows: List[tuple] = [] # (sort_key, row_data)
for name, info in self.card_library.items():
base_type = info.get('Card Type') or info.get('Type','')
base_mc = info.get('Mana Cost','')
base_mv = info.get('Mana Value', info.get('CMC',''))
role = info.get('Role','') or ''
tags = info.get('Tags',[]) or []
tags_join = '; '.join(tags)
text_field = ''
colors = ''
power = ''
toughness = ''
row = row_lookup.get(name)
if row is not None:
row_type = row.get('type', row.get('type_line', ''))
if row_type:
base_type = row_type
mc = row.get('manaCost', '')
if mc:
base_mc = mc
mv = row.get('manaValue', row.get('cmc', ''))
if mv not in (None, ''):
base_mv = mv
colors_raw = row.get('colorIdentity', row.get('colors', []))
if isinstance(colors_raw, list):
colors = ''.join(colors_raw)
elif colors_raw not in (None, ''):
colors = str(colors_raw)
power = row.get('power', '') or ''
toughness = row.get('toughness', '') or ''
text_field = row.get('text', row.get('oracleText', '')) or ''
# Normalize and coerce text
if isinstance(text_field, str):
cleaned = text_field
else:
try:
import math as _math
if isinstance(text_field, float) and (_math.isnan(text_field)):
cleaned = ''
else:
cleaned = str(text_field) if text_field is not None else ''
except Exception:
cleaned = str(text_field) if text_field is not None else ''
cleaned = cleaned.replace('\n', ' ').replace('\r', ' ')
while ' ' in cleaned:
cleaned = cleaned.replace(' ', ' ')
text_field = cleaned
cat = classify(base_type, name)
prec = precedence_index.get(cat, 999)
# Alphabetical within category (no mana value sorting)
rows.append(((prec, name.lower()), [
name,
info.get('Count',1),
base_type,
base_mc,
base_mv,
colors,
power,
toughness,
info.get('Role') or role,
info.get('SubRole') or '',
info.get('AddedBy') or '',
info.get('TriggerTag') or '',
info.get('Synergy') if info.get('Synergy') is not None else '',
tags_join,
text_field[:800] if isinstance(text_field, str) else str(text_field)[:800]
]))
# Now sort (category precedence, then alphabetical name)
rows.sort(key=lambda x: x[0])
with open(fname, 'w', newline='', encoding='utf-8') as f:
w = csv.writer(f)
w.writerow(headers)
for _, data_row in rows:
w.writerow(data_row)
self.output_func(f"Deck exported to {fname}")
# Auto-generate matching plaintext list (best-effort; ignore failures)
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', 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, 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
category precedence then alphabetical within category for consistency.
"""
os.makedirs(directory, exist_ok=True)
# 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 ''
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)
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'
cmdr_slug = _slug(cmdr_first)
theme_slug = _slug(theme_first)
date_part = _dt.date.today().strftime('%Y%m%d')
filename = f"{cmdr_slug}_{theme_slug}_{date_part}.txt"
if not filename.lower().endswith('.txt'):
filename = filename + '.txt'
path = os.path.join(directory, filename)
# Sorting reproduction
precedence_order = [
'Commander', 'Battle', 'Planeswalker', 'Creature', 'Instant', 'Sorcery', 'Artifact', 'Enchantment', 'Land'
]
precedence_index = {k: i for i, k in enumerate(precedence_order)}
commander_name = getattr(self, 'commander_name', '') or getattr(self, 'commander', '') or ''
def classify(primary_type_line: str, card_name: str) -> str:
if commander_name and card_name == commander_name:
return 'Commander'
tl = (primary_type_line or '').lower()
if 'battle' in tl:
return 'Battle'
if 'planeswalker' in tl:
return 'Planeswalker'
if 'creature' in tl:
return 'Creature'
if 'instant' in tl:
return 'Instant'
if 'sorcery' in tl:
return 'Sorcery'
if 'artifact' in tl:
return 'Artifact'
if 'enchantment' in tl:
return 'Enchantment'
if 'land' in tl:
return 'Land'
return 'ZZZ'
# We may want enriched type lines from snapshot; build quick lookup
full_df = getattr(self, '_full_cards_df', None)
combined_df = getattr(self, '_combined_cards_df', None)
snapshot = full_df if full_df is not None else combined_df
row_lookup: Dict[str, any] = {}
if snapshot is not None and not snapshot.empty and 'name' in snapshot.columns:
for _, r in snapshot.iterrows():
nm = str(r.get('name'))
if nm not in row_lookup:
row_lookup[nm] = r
sortable: List[tuple] = []
for name, info in self.card_library.items():
base_type = info.get('Card Type') or info.get('Type','')
row = row_lookup.get(name)
if row is not None:
row_type = row.get('type', row.get('type_line', ''))
if row_type:
base_type = row_type
cat = classify(base_type, name)
prec = precedence_index.get(cat, 999)
sortable.append(((prec, name.lower()), name, info.get('Count',1)))
sortable.sort(key=lambda x: x[0])
with open(path, 'w', encoding='utf-8') as f:
for _, name, count in sortable:
f.write(f"{count} {name}\n")
if not suppress_output:
self.output_func(f"Plaintext deck list exported to {path}")
return path
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