feat: add run_substitution stub + clean up adapter

This commit is contained in:
KoalaTrapLord authored and GitHub committed 2026-03-19 13:49:13 -04:00
1 parent b3b664872b
commit 6135feb644
1 file changed
+34 -56
+34 -56
View File
@@ -7,6 +7,7 @@ from __future__ import annotations
import logging
import sqlite3
from collections import Counter
from types import SimpleNamespace
from typing import Any, Callable, Dict, List, Optional
from .pipeline.build_deck import build_deck
@@ -30,7 +31,7 @@ class DeckBuilderAdapter:
self.db_conn_factory = db_conn_factory
self.model = model
# ── public API (matches DeckGeneratorV3.generate_deck signature) ──
# -- public API (matches DeckGeneratorV3.generate_deck signature) --
def generate_deck(
self,
commander_name: str,
@@ -47,36 +48,27 @@ class DeckBuilderAdapter:
Run the 7-step Ollama pipeline and return a dict that matches
the shape expected by /api/deck/v3/generate.
"""
# Map legacy params -> BuildRequest
request = BuildRequest(
commander_name=commander_name,
strategy_notes=strategy or None,
collection_only=use_collection,
budget_limit=budget_usd,
ratios=DeckRatios(), # use defaults
ratios=DeckRatios(),
)
result: BuildResult = build_deck(request)
deck = result.deck
commander = deck.commander
# Build color_identity list
color_identity = sorted(commander.color_identity)
# Convert cards to legacy dict format + check collection ownership
cards_with_status = self._check_ownership(
[self._card_to_dict(c) for c in deck.cards]
)
# Fill basic lands to 100 total
cards_with_status = self._fill_basic_lands(
cards_with_status, color_identity, target_total=100
)
# Compute stats
stats = self._compute_stats(cards_with_status)
# Build the raw_deck structure (what Ollama/Perplexity originally returned)
raw_deck = {
"commander": commander.name,
"strategy_summary": strategy or f"{commander.name} synergy deck",
@@ -102,7 +94,7 @@ class DeckBuilderAdapter:
},
"color_identity": color_identity,
"raw_deck": raw_deck,
"cards": [c for c in cards_with_status],
"cards": cards_with_status,
"stats": stats,
"strategy_summary": raw_deck["strategy_summary"],
"bracket": raw_deck["bracket"],
@@ -116,10 +108,27 @@ class DeckBuilderAdapter:
"build_time_seconds": result.build_time_seconds,
}
# ── helpers ───────────────────────────────────────────────────
# -- substitution stub --
def run_substitution(self, cards, commander: str = "", strategy: str = "", **kw):
"""
No-op substitution -- Ollama pipeline handles collection internally.
Returns a SimpleNamespace matching the shape DeckGeneratorV3 returns.
"""
owned = sum(
1 for c in cards
if getattr(c, "owned", False)
or (isinstance(c, dict) and c.get("owned"))
)
return SimpleNamespace(
cards=cards,
owned_count=owned,
substituted_count=0,
missing_count=len(cards) - owned,
)
# -- helpers --
@staticmethod
def _card_to_dict(card) -> Dict[str, Any]:
"""Convert a CardEntry to a flat dict."""
return {
"name": card.name,
"count": card.quantity,
@@ -135,7 +144,6 @@ class DeckBuilderAdapter:
@staticmethod
def _card_to_raw(card) -> Dict[str, Any]:
"""Convert a CardEntry to the raw_deck card format."""
return {
"name": card.name,
"count": card.quantity,
@@ -144,7 +152,6 @@ class DeckBuilderAdapter:
}
def _check_ownership(self, cards: List[Dict]) -> List[Dict]:
"""Check which cards are in the user's collection."""
if not self.db_conn_factory:
for c in cards:
c["owned"] = False
@@ -152,7 +159,6 @@ class DeckBuilderAdapter:
c["from_collection"] = False
c.setdefault("estimated_price_usd", 0)
return cards
try:
conn = self.db_conn_factory()
for card in cards:
@@ -160,87 +166,61 @@ class DeckBuilderAdapter:
if not name:
continue
row = conn.execute(
"SELECT name, scryfall_id, quantity, tcg_price, type_line, cmc "
"SELECT name, scryfall_id, quantity, tcg_price "
"FROM collection_entries WHERE name = ? COLLATE NOCASE LIMIT 1",
(name,),
).fetchone()
if row:
card["owned"] = True
card["owned_qty"] = row["quantity"] if isinstance(row, dict) else (row[2] if row else 0)
card["owned_qty"] = row[2] if row else 0
card["from_collection"] = True
card["scryfall_id"] = card.get("scryfall_id") or (row["scryfall_id"] if isinstance(row, dict) else row[1])
price = row["tcg_price"] if isinstance(row, dict) else row[3]
card["estimated_price_usd"] = float(price) if price else 0
card["scryfall_id"] = card.get("scryfall_id") or row[1]
card["estimated_price_usd"] = float(row[3]) if row[3] else 0
else:
card["owned"] = False
card["owned_qty"] = 0
card["from_collection"] = False
card.setdefault("estimated_price_usd", 0)
except Exception as e:
logger.warning(f"Collection check failed: {e}")
logger.warning("Collection check failed: %s", e)
for c in cards:
c.setdefault("owned", False)
c.setdefault("owned_qty", 0)
c.setdefault("from_collection", False)
c.setdefault("estimated_price_usd", 0)
return cards
@staticmethod
def _fill_basic_lands(
cards: List[Dict], color_identity: List[str], target_total: int = 100
) -> List[Dict]:
"""Fill basic lands to reach target card count."""
current = sum(c.get("count", 1) for c in cards)
needed = target_total - current
if needed <= 0:
return cards
ci_to_basic = {
"W": "Plains",
"U": "Island",
"B": "Swamp",
"R": "Mountain",
"G": "Forest",
}
basics = [ci_to_basic[c] for c in color_identity if c in ci_to_basic]
if not basics:
basics = ["Wastes"]
# Distribute evenly
ci_to_basic = {"W": "Plains", "U": "Island", "B": "Swamp", "R": "Mountain", "G": "Forest"}
basics = [ci_to_basic[c] for c in color_identity if c in ci_to_basic] or ["Wastes"]
for i in range(needed):
land_name = basics[i % len(basics)]
# Check if already in deck
existing = next((c for c in cards if c["name"] == land_name), None)
if existing:
existing["count"] = existing.get("count", 1) + 1
else:
cards.append({
"name": land_name,
"count": 1,
"category": "lands",
"mana_cost": "",
"cmc": 0,
"type_line": "Basic Land",
"color_identity": [],
"owned": False,
"owned_qty": 0,
"from_collection": False,
"estimated_price_usd": 0,
"name": land_name, "count": 1, "category": "lands",
"mana_cost": "", "cmc": 0, "type_line": "Basic Land",
"color_identity": [], "owned": False, "owned_qty": 0,
"from_collection": False, "estimated_price_usd": 0,
"source": "auto-fill",
})
return cards
@staticmethod
def _compute_stats(cards: List[Dict]) -> Dict[str, Any]:
"""Compute deck statistics."""
total = sum(c.get("count", 1) for c in cards)
categories = Counter()
cmc_total = 0.0
nonland_count = 0
for c in cards:
count = c.get("count", 1)
cat = c.get("category", "uncategorized")
@@ -248,9 +228,7 @@ class DeckBuilderAdapter:
if cat != "lands":
cmc_total += c.get("cmc", 0) * count
nonland_count += count
avg_cmc = round(cmc_total / nonland_count, 2) if nonland_count else 0
return {
"total_cards": total,
"category_breakdown": dict(categories),