From af8970f3a8cc5fb74f8fa797d5fe1ae8a8139bf9 Mon Sep 17 00:00:00 2001 From: KoalaTrapLord <265133236+KoalaTrapLord@users.noreply.github.com> Date: Thu, 2 Apr 2026 22:59:01 -0400 Subject: [PATCH] fix: _load_deck_cards_by_name falls back to .dck file when deck not in DB Precon decks (Elven_Empire, Blood_Rites, etc.) are stored as .dck files on disk but not inserted into the SQLite decks/deck_cards tables. The DeepSeek batch engine calls _load_deck_cards_by_name() which previously returned [] for any deck not in the DB, causing 'No decks could be loaded.' errors. Adds _find_dck_file() and _load_deck_cards_from_dck() helpers. _find_dck_file searches CFG.precon_dir, CFG.forge_decks_dir, and FORGE_DIR/res/decks/commander with exact + fuzzy matching. _load_deck_cards_by_name now tries DB first, then .dck fallback. --- services/deck_service.py | 151 ++++++++++++++++++++++++++++++++------- 1 file changed, 125 insertions(+), 26 deletions(-) diff --git a/services/deck_service.py b/services/deck_service.py index 727b8a0..851a4e4 100644 --- a/services/deck_service.py +++ b/services/deck_service.py @@ -205,35 +205,134 @@ def _save_profile_to_dck(profile: dict) -> Path: return out_path +def _find_dck_file(deck_name: str) -> Optional[Path]: + """Search known deck directories for a .dck file matching deck_name. + + Resolution order: + 1. precon_dir (precon-decks/) + 2. forge_decks_dir (Forge user decks) + 3. FORGE_DIR/res/decks/commander (Forge bundled commander decks) + + Within each directory tries: + a. Exact stem match (Elven_Empire.dck) + b. Case-insensitive fuzzy match stripping underscores/spaces + """ + search_dirs = [] + if CFG.precon_dir and os.path.isdir(CFG.precon_dir): + search_dirs.append(Path(CFG.precon_dir)) + if CFG.forge_decks_dir and os.path.isdir(CFG.forge_decks_dir): + search_dirs.append(Path(CFG.forge_decks_dir)) + if CFG.forge_dir: + forge_cmdr = Path(CFG.forge_dir) / "res" / "decks" / "commander" + if forge_cmdr.is_dir(): + search_dirs.append(forge_cmdr) + + target_norm = deck_name.lower().replace("_", "").replace(" ", "") + + for d in search_dirs: + exact = d / f"{deck_name}.dck" + if exact.exists(): + return exact + try: + for f in d.iterdir(): + if f.suffix.lower() == ".dck": + if f.stem.lower().replace("_", "").replace(" ", "") == target_norm: + log_collect.info(f"[DeckService] Fuzzy .dck match: '{deck_name}' -> '{f}'") + return f + except OSError: + pass + + return None + + +def _load_deck_cards_from_dck(dck_path: Path) -> list: + """Parse a .dck file and return card dicts compatible with the DeepSeek engine. + + Oracle data is left blank -- the engine enriches cards via enrich_card() + after loading, so minimal stubs are sufficient. + """ + parsed = parse_dck_file(str(dck_path)) + cards = [] + for card_name, qty in parsed.get("commanders", {}).items(): + cards.append({ + "card_name": card_name, + "name": card_name, + "quantity": qty, + "is_commander": 1, + "type_line": "", + "oracle_text": "", + "cmc": 0, + "color_identity": "[]", + "mana_cost": "", + "power": "", + "toughness": "", + "keywords": "[]", + }) + for card_name, qty in parsed.get("mainboard", {}).items(): + cards.append({ + "card_name": card_name, + "name": card_name, + "quantity": qty, + "is_commander": 0, + "type_line": "", + "oracle_text": "", + "cmc": 0, + "color_identity": "[]", + "mana_cost": "", + "power": "", + "toughness": "", + "keywords": "[]", + }) + return cards + + def _load_deck_cards_by_name(deck_name: str) -> list: - """Load cards from a deck by name, returning list of card dicts.""" + """Load cards from a deck by name, returning list of card dicts. + + Resolution order: + 1. SQLite DB (decks built/imported via the Deck Builder UI) + 2. .dck file fallback -- searches precon_dir, forge_decks_dir, and + FORGE_DIR/res/decks/commander so that precon decks work without + needing to be manually imported into the DB first. + """ + # -- 1. DB lookup -- conn = _get_db_conn() deck_row = conn.execute("SELECT id FROM decks WHERE name = ?", (deck_name,)).fetchone() - if not deck_row: - return [] - deck_id = deck_row[0] - rows = conn.execute(""" - SELECT dc.card_name, dc.quantity, dc.is_commander, dc.role_tag, dc.scryfall_id, - COALESCE(ce.type_line, cr.type_line, '') as type_line, - COALESCE(ce.oracle_text, cr.oracle_text, '') as oracle_text, - COALESCE(ce.cmc, cr.cmc, 0) as cmc, - COALESCE(ce.color_identity, cr.color_identity, '[]') as color_identity, - COALESCE(ce.mana_cost, '', '') as mana_cost, - COALESCE(ce.power, '', '') as power, - COALESCE(ce.toughness, '', '') as toughness, - COALESCE(ce.keywords, cr.keywords, '[]') as keywords - FROM deck_cards dc - LEFT JOIN ( - SELECT scryfall_id, type_line, oracle_text, cmc, color_identity, mana_cost, power, toughness, keywords - FROM collection_entries GROUP BY scryfall_id - ) ce ON ce.scryfall_id = dc.scryfall_id - LEFT JOIN ( - SELECT name, type_line, oracle_text, cmc, color_identity, keywords - FROM card_records GROUP BY name - ) cr ON cr.name = dc.card_name - WHERE dc.deck_id = ? - """, (deck_id,)).fetchall() - return [_row_to_dict(r) for r in rows] + if deck_row: + deck_id = deck_row[0] + rows = conn.execute(""" + SELECT dc.card_name, dc.quantity, dc.is_commander, dc.role_tag, dc.scryfall_id, + COALESCE(ce.type_line, cr.type_line, '') as type_line, + COALESCE(ce.oracle_text, cr.oracle_text, '') as oracle_text, + COALESCE(ce.cmc, cr.cmc, 0) as cmc, + COALESCE(ce.color_identity, cr.color_identity, '[]') as color_identity, + COALESCE(ce.mana_cost, '', '') as mana_cost, + COALESCE(ce.power, '', '') as power, + COALESCE(ce.toughness, '', '') as toughness, + COALESCE(ce.keywords, cr.keywords, '[]') as keywords + FROM deck_cards dc + LEFT JOIN ( + SELECT scryfall_id, type_line, oracle_text, cmc, color_identity, mana_cost, power, toughness, keywords + FROM collection_entries GROUP BY scryfall_id + ) ce ON ce.scryfall_id = dc.scryfall_id + LEFT JOIN ( + SELECT name, type_line, oracle_text, cmc, color_identity, keywords + FROM card_records GROUP BY name + ) cr ON cr.name = dc.card_name + WHERE dc.deck_id = ? + """, (deck_id,)).fetchall() + return [_row_to_dict(r) for r in rows] + + # -- 2. .dck file fallback -- + dck_path = _find_dck_file(deck_name) + if dck_path: + log_collect.info( + f"[DeckService] '{deck_name}' not in DB -- loading from .dck file: {dck_path}" + ) + return _load_deck_cards_from_dck(dck_path) + + log_collect.warning(f"[DeckService] Deck '{deck_name}' not found in DB or any .dck search path.") + return [] log_deckgen = logging.getLogger("commander_ai_lab.deckgen")