Need to fix up the smart deck build, and fix tokens not being able to add

This commit is contained in:
2025-06-03 20:29:56 -04:00
parent 93f4826148
commit de02aeb5e0
3 changed files with 301 additions and 121 deletions

15
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "MTGC Debug",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/main.py",
"console": "integratedTerminal"
}
]
}

Binary file not shown.

407
main.py
View File

@ -1,7 +1,7 @@
# main.py
import os
import io
import subprocess, shlex
import subprocess
import shlex
import requests
import winsound
import webbrowser
@ -16,6 +16,19 @@ from collection_manager import load_collection, save_collection
from battle_simulator import simulate_match, record_manual_result, load_match_history
from models import Deck, Card
# ──────────────────────────────────────────────────────────────────────────────
# Play a custom WAV if available; otherwise default beep on error only.
# ──────────────────────────────────────────────────────────────────────────────
def play_sound(sound_name: str):
path = os.path.join("assets", "sounds", f"{sound_name}.wav")
if os.path.isfile(path):
winsound.PlaySound(path, winsound.SND_FILENAME | winsound.SND_ASYNC)
else:
# Only make a sound on error; button clicks are now silent.
pass
# ──────────────────────────────────────────────────────────────────────────────
# VERSIONING (build number from Git commits, fallback __version__)
# ──────────────────────────────────────────────────────────────────────────────
@ -80,7 +93,7 @@ def check_for_updates(local_version: str, repo: str) -> None:
# ──────────────────────────────────────────────────────────────────────────────
# MTGDeckBuilder GUI
# MAIN APPLICATION
# ──────────────────────────────────────────────────────────────────────────────
class MTGDeckBuilder(tk.Tk):
def __init__(self):
@ -88,19 +101,19 @@ class MTGDeckBuilder(tk.Tk):
self.title("MTG Deck Builder")
self.geometry("1200x750")
# Ensure data folders/files
# Ensure data folders/files exist
init_cache_db()
_ = load_collection()
_ = load_match_history()
# Theme tracking
# Track theme: "dark" or "light"
self.theme = tk.StringVar(value="dark")
# Current deck
# Currently loaded Deck
self.current_deck: Deck | None = None
# Caches
self.card_cache: dict[str, Card] = {} # name → Card
self.card_cache: dict[str, Card] = {}
self.search_images: dict[str, ImageTk.PhotoImage] = {}
self.coll_images: dict[str, dict[str, ImageTk.PhotoImage]] = {
tab: {} for tab in ["All", "Black", "White", "Red", "Green", "Blue", "Unmarked", "Tokens"]
@ -123,7 +136,7 @@ class MTGDeckBuilder(tk.Tk):
self.after(1000, lambda: check_for_updates(local_ver, GITHUB_REPO))
# -----------------------------------------------------------------------------
# Preload W/U/B/R/G icons
# Preload W/U/B/R/G color icons
# -----------------------------------------------------------------------------
def _load_color_icons(self):
icon_folder = os.path.join("assets", "icons")
@ -136,17 +149,17 @@ class MTGDeckBuilder(tk.Tk):
self.color_icon_images[symbol] = None
# -----------------------------------------------------------------------------
# Ensure sounds folder
# Ensure sound folder
# -----------------------------------------------------------------------------
def _load_sounds(self):
sound_folder = os.path.join("assets", "sounds")
os.makedirs(sound_folder, exist_ok=True)
# -----------------------------------------------------------------------------
# Build all widgets (including Combobox for autocomplete)
# Create all widgets (search is now a plain Entry again)
# -----------------------------------------------------------------------------
def _build_widgets(self):
# Deck controls (top)
# --- Top row: Deck controls + theme toggle ---
self.deck_frame = ttk.LabelFrame(self, text="Deck Controls", padding=8)
self.new_deck_btn = ttk.Button(self.deck_frame, text="New Deck", command=self._on_new_deck)
self.load_deck_btn = ttk.Button(self.deck_frame, text="Load Deck", command=self._on_load_deck)
@ -164,7 +177,7 @@ class MTGDeckBuilder(tk.Tk):
command=self.apply_theme
)
# Collection panel with tabs (left)
# --- Collection panel with tabs (left) ---
self.coll_frame = ttk.LabelFrame(self, text="Your Collection", padding=8)
self.coll_notebook = ttk.Notebook(self.coll_frame)
self.coll_tabs = {}
@ -187,26 +200,35 @@ class MTGDeckBuilder(tk.Tk):
self.coll_qty_spin = ttk.Spinbox(self.coll_frame, from_=1, to=1000, width=6)
self.coll_set_qty_btn = ttk.Button(self.coll_frame, text="Set Quantity", command=self._on_set_coll_qty)
# Right side: Search panel + Deck panel + Preview
# --- Right side: Search panel + Deck panel + Preview ---
self.right_frame = ttk.Frame(self)
# Search / Add Cards
# Search / Add Cards (plain Entry)
self.search_frame = ttk.LabelFrame(self.right_frame, text="Search / Add Cards", padding=8)
self.search_entry = ttk.Combobox(self.search_frame, width=30)
self.search_entry.set("")
self.search_entry.bind("<<ComboboxSelected>>", lambda e: self._on_autocomplete_select())
self.search_entry.bind("<KeyRelease>", lambda e: self._update_autocomplete())
self.search_entry.bind("<FocusIn>", lambda e: self.search_entry.select_range(0, tk.END))
self.preview_container = ttk.Frame(self.search_frame)
self.preview_frame = ttk.Frame(self.preview_container, borderwidth=1, relief="solid")
self.preview_inner = ttk.Frame(self.preview_frame, padding=1)
self.preview_frame.configure(width=200, height=200)
self.preview_frame.grid_propagate(False)
self.card_image_label = ttk.Label(self.preview_inner)
self.color_icons_frame = ttk.Frame(self.preview_frame)
self.search_entry = ttk.Entry(self.search_frame, width=30)
self.search_entry.bind("<Return>", lambda e: self._on_perform_search())
self.search_btn = ttk.Button(self.search_frame, text="Search", command=self._on_perform_search)
self.results_tree = ttk.Treeview(self.search_frame, height=12, columns=("info",), show="tree")
self.results_scroll = ttk.Scrollbar(self.search_frame, orient="vertical", command=self.results_tree.yview)
self.results_tree.configure(yscrollcommand=self.results_scroll.set)
self.results_tree.bind("<<TreeviewSelect>>", self._on_result_select)
self.add_qty_label = ttk.Label(self.search_frame, text="Qty:")
self.add_qty_spin = ttk.Spinbox(self.search_frame, from_=1, to=20, width=5)
self.qty_add_frame = ttk.Frame(self.preview_container)
self.add_qty_label = ttk.Label(self.qty_add_frame, text="Qty:")
self.add_qty_spin = ttk.Spinbox(self.qty_add_frame, from_=1, to=20, width=5)
self.add_qty_spin.set("1")
self.add_to_coll_btn = ttk.Button(self.search_frame, text="Add to Collection", command=self._on_add_to_collection)
self.add_to_deck_btn = ttk.Button(self.search_frame, text="Add to Deck", command=self._on_add_to_deck)
self.add_to_coll_btn = ttk.Button(
self.qty_add_frame, text="Add to Collection", command=self._on_add_to_collection
)
self.add_to_deck_btn = ttk.Button(
self.qty_add_frame, text="Add to Deck", command=self._on_add_to_deck
)
# Deck panel with tabs
self.deck_view_frame = ttk.LabelFrame(self.right_frame, text="Deck Contents", padding=8)
@ -231,17 +253,12 @@ class MTGDeckBuilder(tk.Tk):
self.deck_qty_spin = ttk.Spinbox(self.deck_view_frame, from_=1, to=1000, width=6)
self.deck_set_qty_btn = ttk.Button(self.deck_view_frame, text="Set Quantity", command=self._on_set_deck_qty)
# Card preview (bottom)
self.preview_frame = ttk.LabelFrame(self, text="Card Preview", padding=8)
self.card_image_label = ttk.Label(self.preview_frame)
self.color_icons_frame = ttk.Frame(self.preview_frame)
# -----------------------------------------------------------------------------
# Layout everything with pack() and grid()
# Arrange everything with pack() and grid()
# -----------------------------------------------------------------------------
def _layout_widgets(self):
# Deck controls
self.deck_frame.pack(fill="x", padx=10, pady=(10,5))
# ─── Deck controls (top) ───────────────────────────────────────────
self.deck_frame.pack(fill="x", padx=10, pady=(10, 5))
self.new_deck_btn.grid(row=0, column=0, padx=4, pady=4)
self.load_deck_btn.grid(row=0, column=1, padx=4, pady=4)
self.save_deck_btn.grid(row=0, column=2, padx=4, pady=4)
@ -251,60 +268,94 @@ class MTGDeckBuilder(tk.Tk):
self.theme_toggle.grid(row=0, column=6, padx=20, pady=4)
self.deck_name_label.grid(row=0, column=7, padx=10, pady=4, sticky="w")
# Collection panel
# ─── Collection panel (left) ────────────────────────────────────────
self.coll_frame.pack(fill="y", side="left", padx=(10,5), pady=5)
self.coll_frame.configure(width=280)
self.coll_notebook.pack(fill="both", expand=True, padx=4, pady=4)
# 1) “Remove from Collection” at top
self.remove_from_coll_btn.pack(fill="x", padx=4, pady=(4,4))
# 2) Tabs for All/Black/White/etc.
self.coll_notebook.pack(fill="both", expand=True, padx=4, pady=4)
# 3) Quantity + Set Quantity under the Collection tree
qty_frame_c = ttk.Frame(self.coll_frame)
qty_frame_c.pack(fill="x", padx=4, pady=(0,10))
self.coll_qty_label.pack(in_=qty_frame_c, side="left")
self.coll_qty_spin.pack(in_=qty_frame_c, side="left", padx=(4,10))
self.coll_set_qty_btn.pack(in_=qty_frame_c, side="left")
self.coll_qty_spin.pack(in_=qty_frame_c, side="left", padx=(4,10))
self.coll_set_qty_btn.pack(in_=qty_frame_c, side="left")
# Right side: search + deck
# ─── Right side: Search panel + Deck panel ──────────────────────────
self.right_frame.pack(fill="both", expand=True, side="left", padx=(5,10), pady=5)
self.right_frame.columnconfigure(0, weight=1)
self.right_frame.columnconfigure(1, weight=1)
self.right_frame.rowconfigure(0, weight=1)
# Search panel
self.search_frame.grid(row=0, column=0, sticky="nsew", padx=(0,5))
# --- Search panel (right_frame at row=0, col=0) ---
self.search_frame.grid(row=0, column=0, sticky="nsew", padx=(0,5), pady=(0,0))
# Let the search results expand vertically and horizontally
self.search_frame.columnconfigure(0, weight=1)
self.search_frame.rowconfigure(1, weight=1)
# Row 0: Search entry + Search button
self.search_entry.grid(row=0, column=0, padx=4, pady=4, sticky="w")
self.search_btn.grid(row=0, column=1, padx=4, pady=4, sticky="w")
self.search_btn .grid(row=0, column=1, padx=4, pady=4, sticky="w")
self.results_tree.grid(row=1, column=0, columnspan=2, padx=(4,0), pady=4, sticky="nsew")
self.results_scroll.grid(row=1, column=2, sticky="ns", pady=4)
# Row 1: search results (col=0), scrollbar (col=1), preview_container (col=2)
self.results_tree .grid(row=1, column=0, padx=(4,0), pady=4, sticky="nsew")
self.results_scroll.grid(row=1, column=1, sticky="ns", pady=4)
self.preview_container.grid(row=1, column=2, padx=(10,4), pady=4, sticky="n")
self.add_qty_label.grid(row=2, column=0, padx=4, pady=(4,10), sticky="w")
self.add_qty_spin.grid(row=2, column=1, padx=4, pady=(4,10), sticky="w")
self.add_to_coll_btn.grid(row=2, column=2, padx=4, pady=(4,10), sticky="w")
self.add_to_deck_btn.grid(row=2, column=3, padx=4, pady=(4,10), sticky="w")
# Keep results_tree filling space; preview_container stays its natural size
self.search_frame.rowconfigure(1, weight=1)
self.search_frame.columnconfigure(2, weight=0)
# Deck panel
self.deck_view_frame.grid(row=0, column=1, sticky="nsew", padx=(5,0))
# ─── Inside preview_container ────────────────────────────────────
# Row 0: preview_frame (autosizes to card image + 1px border)
# Row 1: qty_add_frame (holds the spinbox and two “Add” buttons side by side)
self.preview_container.columnconfigure(0, weight=0)
self.preview_container.rowconfigure(0, weight=0)
self.preview_container.rowconfigure(1, weight=0)
# 1) Place the framed preview (borderwidth=1, relief="solid")
self.preview_frame.grid(row=0, column=0, padx=0, pady=0, sticky="n")
# Inside the frame, pack preview_inner (which holds a 1px padding)
self.preview_inner.pack(fill="both", expand=True)
# And pack the image label inside that
self.card_image_label.pack(expand=True)
# 2) Immediately under that, place qty_add_frame
self.qty_add_frame.grid(row=1, column=0, pady=(4,4))
# Inside qty_add_frame, arrange: “Qty:” label, spinbox, Add to Collection, Add to Deck
self.add_qty_label.grid(row=0, column=0, padx=(0,4))
self.add_qty_spin .grid(row=0, column=1, padx=(0,10))
self.add_to_coll_btn.grid(row=0, column=2, padx=(0,4))
self.add_to_deck_btn .grid(row=0, column=3, padx=(0,4))
# Row 2 of search_frame: we no longer need a quantity row, because we moved
# the spinbox into qty_add_frame. If you want an extra blank row, you can comment this out.
# --- Deck panel (right_frame at row=0, col=1) ---
self.deck_view_frame.grid(row=0, column=1, sticky="nsew", padx=(5,0), pady=(0,0))
self.deck_view_frame.columnconfigure(0, weight=1)
self.deck_view_frame.rowconfigure(0, weight=1)
self.deck_notebook.pack(fill="both", expand=True, padx=4, pady=4)
# “Remove Selected” at top of deck_view_frame
self.remove_card_btn.pack(fill="x", padx=4, pady=(4,4))
# Then the decks Notebook (tabs) below, which expands
self.deck_notebook.pack(fill="both", expand=True, padx=4, pady=4)
# Qty + Set Quantity under the Deck notebook
qty_frame_d = ttk.Frame(self.deck_view_frame)
qty_frame_d.pack(fill="x", padx=4, pady=(0,10))
self.deck_qty_label.pack(in_=qty_frame_d, side="left")
self.deck_qty_spin.pack(in_=qty_frame_d, side="left", padx=(4,10))
self.deck_qty_label.pack( in_=qty_frame_d, side="left")
self.deck_qty_spin.pack( in_=qty_frame_d, side="left", padx=(4,10))
self.deck_set_qty_btn.pack(in_=qty_frame_d, side="left")
# Preview panel
self.preview_frame.pack(fill="x", padx=10, pady=(0,10))
self.preview_frame.columnconfigure(0, weight=1)
self.preview_frame.rowconfigure(0, weight=1)
self.card_image_label.grid(row=0, column=0, padx=4, pady=4)
self.color_icons_frame.grid(row=1, column=0, padx=4, pady=(4,8))
# Populate collection + deck
# ─── Finally, refresh both lists ───────────────────────────────────
self._refresh_collection()
self._refresh_deck()
@ -339,7 +390,6 @@ class MTGDeckBuilder(tk.Tk):
style.map("Treeview", background=[("selected", select_bg)])
style.configure("TEntry", fieldbackground=entry_bg, foreground=entry_fg)
style.configure("TSpinbox", fieldbackground=entry_bg, foreground=entry_fg)
style.configure("TCombobox", fieldbackground=entry_bg, foreground=entry_fg)
self.configure(background=bg)
for frame in [self.deck_frame, self.coll_frame, self.search_frame,
@ -347,43 +397,9 @@ class MTGDeckBuilder(tk.Tk):
frame.configure(style="TLabelframe")
# -----------------------------------------------------------------------------
# Autocomplete: update Combobox values as user types
# -----------------------------------------------------------------------------
def _update_autocomplete(self):
text = self.search_entry.get().strip()
if len(text) < 2:
return
# Fetch up to 10 matching card names
try:
results = search_cards(text + "*",) # wildcard to get broader matches
except Exception:
return
names = [c.name for c in results[:10]]
# Update the Combobox dropdown
self.search_entry["values"] = names
# If there are suggestions, open the dropdown
if names:
self.search_entry.event_generate("<Down>")
# -----------------------------------------------------------------------------
# When the user selects from autocomplete dropdown
# -----------------------------------------------------------------------------
def _on_autocomplete_select(self):
selected = self.search_entry.get().strip()
if not selected:
return
# Fetch full Card and preview it
card = self.card_cache.get(selected) or get_card_by_name(selected)
if not card:
return
self.card_cache[card.name] = card
self._show_preview(card)
# -----------------------------------------------------------------------------
# Perform a normal “Search” (when user clicks Search)
# Perform “Search” (when user clicks Search or hits Enter)
# -----------------------------------------------------------------------------
def _on_perform_search(self):
play_sound("click")
query = self.search_entry.get().strip()
if not query:
return
@ -438,12 +454,15 @@ class MTGDeckBuilder(tk.Tk):
self._show_preview(card)
# -----------------------------------------------------------------------------
# Show full image + color icons in preview
# Show full image + color pips in preview
# -----------------------------------------------------------------------------
def _show_preview(self, card: Card):
# Clear out any old contents in color icons and image:
for w in self.color_icons_frame.winfo_children():
w.destroy()
self.card_image_label.config(image="", text="")
# Display color pips at the top inside preview_inner:
x = 0
for symbol in card.colors:
icon = self.color_icon_images.get(symbol)
@ -459,9 +478,15 @@ class MTGDeckBuilder(tk.Tk):
resp.raise_for_status()
img_data = resp.content
image = Image.open(io.BytesIO(img_data))
image.thumbnail((250,350), Image.LANCZOS)
# Optional: resize the image so its not gigantic.
# For example, if you want max width=180, max height=260, do:
max_w, max_h = 180, 260
image.thumbnail((max_w, max_h), Image.LANCZOS)
photo = ImageTk.PhotoImage(image)
self.preview_photo = photo
self.preview_photo = photo # keep a reference
# Put the image in the Label:
self.card_image_label.config(image=photo, text="")
except Exception:
self.card_image_label.config(text="Could not load image", image="")
@ -470,11 +495,16 @@ class MTGDeckBuilder(tk.Tk):
self.card_image_label.config(text="No image available", image="")
self.preview_photo = None
# Now repack / grid so that preview_frame wraps to its contents:
# (If it was hidden or empty before, we need to ensure layout is updated.)
self.preview_inner.update_idletasks()
self.preview_frame.update_idletasks()
self.preview_container.update_idletasks()
# -----------------------------------------------------------------------------
# Add to Collection (silent, cache thumbnails automatically)
# Add to Collection (silent)—auto-caches thumbnails on refresh
# -----------------------------------------------------------------------------
def _on_add_to_collection(self):
play_sound("click")
coll = load_collection()
sel = self.results_tree.selection()
if not sel:
@ -495,7 +525,7 @@ class MTGDeckBuilder(tk.Tk):
self._refresh_collection()
# Clear the search box so user can type another name
self.search_entry.set("")
self.search_entry.delete(0, tk.END)
self.search_entry.focus_set()
self.results_tree.delete(*self.results_tree.get_children())
self._clear_preview()
@ -504,7 +534,6 @@ class MTGDeckBuilder(tk.Tk):
# Add to Deck (silent)
# -----------------------------------------------------------------------------
def _on_add_to_deck(self):
play_sound("click")
if not self.current_deck:
return
sel = self.results_tree.selection()
@ -534,7 +563,6 @@ class MTGDeckBuilder(tk.Tk):
# Remove selected from collection
# -----------------------------------------------------------------------------
def _on_remove_from_collection(self):
play_sound("click")
current_tab = self.coll_notebook.tab(self.coll_notebook.select(), "text")
tree = self.coll_trees[current_tab]
sel = tree.selection()
@ -572,7 +600,6 @@ class MTGDeckBuilder(tk.Tk):
# Set quantity in collection (inline)
# -----------------------------------------------------------------------------
def _on_set_coll_qty(self):
play_sound("click")
current_tab = self.coll_notebook.tab(self.coll_notebook.select(), "text")
tree = self.coll_trees[current_tab]
sel = tree.selection()
@ -623,7 +650,7 @@ class MTGDeckBuilder(tk.Tk):
for tab_name, tree in self.coll_trees.items():
tree.delete(*tree.get_children())
# Do NOT clear self.coll_images[tab_name]; reuse cached thumbnails
# Keep self.coll_images[tab_name] intact—reuse cached thumbnails
fnt_spec = ttk.Style().lookup("Treeview", "font")
if fnt_spec:
fnt = tkfont.Font(font=fnt_spec)
@ -662,10 +689,9 @@ class MTGDeckBuilder(tk.Tk):
tree.column("#0", width=max_width)
# -----------------------------------------------------------------------------
# New Deck
# New Deck” callback
# -----------------------------------------------------------------------------
def _on_new_deck(self):
play_sound("click")
name = simpledialog.askstring("New Deck", "Enter deck name:", parent=self)
if not name:
return
@ -675,10 +701,9 @@ class MTGDeckBuilder(tk.Tk):
self._clear_preview()
# -----------------------------------------------------------------------------
# Load Deck
# Load Deck” callback
# -----------------------------------------------------------------------------
def _on_load_deck(self):
play_sound("click")
choices = list_saved_decks()
if not choices:
return
@ -697,10 +722,9 @@ class MTGDeckBuilder(tk.Tk):
self._clear_preview()
# -----------------------------------------------------------------------------
# Save Deck
# Save Deck” callback
# -----------------------------------------------------------------------------
def _on_save_deck(self):
play_sound("click")
if not self.current_deck:
return
dm_save_deck(self.current_deck)
@ -736,10 +760,9 @@ class MTGDeckBuilder(tk.Tk):
self._show_preview(card)
# -----------------------------------------------------------------------------
# Set quantity in deck (inline)
# Set Quantity in Deck callback
# -----------------------------------------------------------------------------
def _on_set_deck_qty(self):
play_sound("click")
if not self.current_deck:
return
current_tab = self.deck_notebook.tab(self.deck_notebook.select(), "text")
@ -769,10 +792,9 @@ class MTGDeckBuilder(tk.Tk):
self._refresh_deck()
# -----------------------------------------------------------------------------
# Remove selected from deck
# Remove Selected from deck callback
# -----------------------------------------------------------------------------
def _on_remove_selected(self):
play_sound("click")
if not self.current_deck:
return
current_tab = self.deck_notebook.tab(self.deck_notebook.select(), "text")
@ -797,7 +819,7 @@ class MTGDeckBuilder(tk.Tk):
self._clear_preview()
# -----------------------------------------------------------------------------
# Refresh the deck tabs + autofit
# Refresh deck tabs + autofit columns
# -----------------------------------------------------------------------------
def _refresh_deck(self):
if not self.current_deck:
@ -845,7 +867,7 @@ class MTGDeckBuilder(tk.Tk):
resp = requests.get(card.thumbnail_url, timeout=5)
resp.raise_for_status()
pil = Image.open(io.BytesIO(resp.content))
pil.thumbnail((24,36), Image.LANCZOS)
pil.thumbnail((24, 36), Image.LANCZOS)
img_obj = ImageTk.PhotoImage(pil)
self.deck_images[tab_name][card_name] = img_obj
except Exception:
@ -879,10 +901,152 @@ class MTGDeckBuilder(tk.Tk):
self.preview_photo = None
# -----------------------------------------------------------------------------
# Simulate Battle
# “Smart Build Deck” callback
# -----------------------------------------------------------------------------
def _on_smart_build(self):
color_input = simpledialog.askstring(
"Smart Build: Colors",
"Enter 13 colors (e.g. R G) separated by spaces:",
parent=self
)
if not color_input:
return
colors = [c.strip().upper() for c in color_input.split() if c.strip().upper() in {"W","U","B","R","G"}]
if not 1 <= len(colors) <= 3:
play_sound("error")
messagebox.showerror("Invalid Colors", "You must pick 13 of W, U, B, R, G.")
return
history = load_match_history()
archetypes = ["Aggro", "Midrange", "Control"]
best_arch = None
best_rate = -1.0
combo = "/".join(colors)
for arch in archetypes:
total = wins = 0
for record in history:
dn = record.get("deck", "")
if dn.startswith(arch) and combo in dn:
res = record.get("result", "")
if res in ("W","L"):
total += 1
if res == "W":
wins += 1
if total > 0:
rate = wins / total
if rate > best_rate:
best_rate = rate
best_arch = arch
if best_arch:
confirm = messagebox.askokcancel(
"Choose Archetype",
f"Based on history, {best_arch} {combo} has win rate {best_rate:.0%}.\nUse it?"
)
if confirm:
archetype = best_arch.lower()
else:
archetype = None
else:
archetype = None
if not archetype:
arch_input = simpledialog.askstring(
"Smart Build: Archetype",
"Enter archetype (Aggro, Control, Midrange):",
parent=self
)
if not arch_input:
return
arch_input = arch_input.strip().lower()
if arch_input not in {"aggro", "control", "midrange"}:
play_sound("error")
messagebox.showerror("Invalid Archetype", "Must be 'Aggro', 'Control', or 'Midrange'.")
return
archetype = arch_input
deck_name = f"{archetype.capitalize()} {combo} Auto"
deck = Deck(name=deck_name)
land_count = 24
if archetype == "aggro":
creature_target = 24; noncreature_target = 12
elif archetype == "midrange":
creature_target = 18; noncreature_target = 18
else:
creature_target = 12; noncreature_target = 24
per_color = land_count // len(colors)
extra = land_count % len(colors)
basic_map = {"W":"Plains","U":"Island","B":"Swamp","R":"Mountain","G":"Forest"}
for idx, col in enumerate(colors):
qty = per_color + (1 if idx < extra else 0)
deck.add_card(basic_map[col], qty)
creature_query = f"c:{''.join(colors)} type:creature"
if archetype == "aggro":
creature_query += " cmc<=3"
elif archetype == "midrange":
creature_query += " cmc<=4"
else:
creature_query += " cmc<=5"
creatures = search_cards(creature_query)
creatures = [c for c in creatures if set(c.colors).issubset(set(colors))]
used = set()
added = 0
for c in creatures:
if added >= creature_target:
break
if c.name not in used:
deck.add_card(c.name, 1)
used.add(c.name)
added += 1
noncre_query = f"c:{''.join(colors)} (type:instant or type:sorcery)"
if archetype == "aggro":
noncre_query += " cmc<=3"
elif archetype == "midrange":
noncre_query += " cmc<=4"
else:
noncre_query += " cmc>=3"
noncre = search_cards(noncre_query)
noncre = [c for c in noncre if set(c.colors).issubset(set(colors))]
added_non = 0
for c in noncre:
if added_non >= noncreature_target:
break
if c.name not in used:
deck.add_card(c.name, 1)
used.add(c.name)
added_non += 1
total_cards = sum(deck.cards.values())
if total_cards < 60:
fill_needed = 60 - total_cards
filler = search_cards("type:creature cmc<=3")
for c in filler:
if c.name not in used:
deck.add_card(c.name, 1)
used.add(c.name)
fill_needed -= 1
if fill_needed == 0:
break
self.current_deck = deck
self.deck_name_label.config(text=f"Deck: {deck.name} ({deck.total_cards()} cards)")
self._refresh_deck()
self._clear_preview()
messagebox.showinfo(
"Smart Build Complete",
f"Created deck '{deck.name}' with {deck.total_cards()} cards."
)
# -----------------------------------------------------------------------------
# “Simulate Battle” callback
# -----------------------------------------------------------------------------
def _on_simulate_battle(self):
play_sound("click")
choices = list_saved_decks()
if len(choices) < 2:
return
@ -919,10 +1083,9 @@ class MTGDeckBuilder(tk.Tk):
)
# -----------------------------------------------------------------------------
# Record Result
# Record Result” callback
# -----------------------------------------------------------------------------
def _on_record_result(self):
play_sound("click")
choices = list_saved_decks()
if not choices:
return
@ -949,6 +1112,8 @@ class MTGDeckBuilder(tk.Tk):
parent=self
)
if not result or result.upper() not in {"W","L","T"}:
play_sound("error")
messagebox.showerror("Invalid Result", "Result must be W, L, or T.")
return
record_manual_result(deck_name, opponent, result.upper())
@ -959,7 +1124,7 @@ class MTGDeckBuilder(tk.Tk):
# ──────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
missing_icons = [s for s in ["W","U","B","R","G"]
if not os.path.isfile(os.path.join("assets","icons",f"{s}.png"))]
if not os.path.isfile(os.path.join("assets","icons", f"{s}.png"))]
if missing_icons:
print(f"Warning: Missing color icon(s) for {missing_icons} in assets/icons/. Cards will still load.")