From d9b56d8e127f7eea44ed618ba6a25e2ecfcf3b5b Mon Sep 17 00:00:00 2001 From: mwisnowski Date: Wed, 20 Aug 2025 11:17:51 -0700 Subject: [PATCH] Add text file export along with csv, for easy import into moxfield or other deck building sites --- code/deck_builder/builder.py | 10 ++- code/deck_builder/phases/phase6_reporting.py | 89 ++++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/code/deck_builder/builder.py b/code/deck_builder/builder.py index c3d65da..a3f30c8 100644 --- a/code/deck_builder/builder.py +++ b/code/deck_builder/builder.py @@ -205,7 +205,15 @@ class DeckBuilder(CommanderSelectionMixin, # Export if hasattr(self, 'export_decklist_csv'): logger.info("Export decklist phase") - self.export_decklist_csv() + 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: diff --git a/code/deck_builder/phases/phase6_reporting.py b/code/deck_builder/phases/phase6_reporting.py index 783b0a9..54199c8 100644 --- a/code/deck_builder/phases/phase6_reporting.py +++ b/code/deck_builder/phases/phase6_reporting.py @@ -190,8 +190,97 @@ class ReportingMixin: 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') # 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: + """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 '' + cmdr_first = cmdr.split()[0] if cmdr else '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' + 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") + 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