Spaces:
Sleeping
Sleeping
| """ | |
| MTG Draft Assistant Streamlit App | |
| --------------------------------- | |
| A booster-draft helper written with Streamlit and deployable on Hugging Face Spaces. | |
| Changes in this revision | |
| ~~~~~~~~~~~~~~~~~~~~~~~~ | |
| * **supported_sets.txt** β one set code per line. | |
| * Sidebar now shows a **single-choice list** (radio buttons) sourced from that | |
| file instead of a free-text box, so users can only draft sets you actually | |
| support. | |
| * Fallback to the old text input if the text-file is missing or empty. | |
| Replace the three stub functions (`load_model`, `suggest_pick`, | |
| `generate_booster`) with your real model-/API-calls when you are ready. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import random | |
| from pathlib import Path | |
| from typing import Dict, List | |
| import requests | |
| import streamlit as st | |
| # ----------------------------------------------------------------------------- | |
| # 0. Constants & helpers | |
| # ----------------------------------------------------------------------------- | |
| SUPPORTED_SETS_PATH = Path("helper_files/supported_sets.txt") | |
| def get_supported_sets(path: Path = SUPPORTED_SETS_PATH) -> List[str]: | |
| """Return a list of legal set codes read from *supported_sets.txt*. | |
| The file should contain **one set tag per line**, e.g.:: | |
| WOE | |
| LCI | |
| MKM | |
| If the file is missing we fall back to an empty list so the UI | |
| degrades gracefully. | |
| """ | |
| if path.is_file(): | |
| return [ln.strip() for ln in path.read_text().splitlines() if ln.strip()] | |
| return [] | |
| # ----------------------------------------------------------------------------- | |
| # 1. Model loading (stub) | |
| # ----------------------------------------------------------------------------- | |
| def load_model(): | |
| """Load and return the trained drafting model. | |
| Adapt to your own pipeline; see previous revision for an example that pulls | |
| an artefact from *huggingface_hub*. Returning *None* leaves us in demo | |
| mode and will pick random cards. | |
| """ | |
| return None | |
| model = load_model() | |
| # ----------------------------------------------------------------------------- | |
| # 2. Draft-logic helpers (stubs) | |
| # ----------------------------------------------------------------------------- | |
| def suggest_pick(pack: List[Dict], picks: List[Dict]) -> Dict: | |
| """Return the card the model recommends from *pack*.""" | |
| if model is None: | |
| return random.choice(pack) | |
| return model.predict(pack=pack, picks=picks) # type: ignore[attr-defined] | |
| def fetch_card_image(card_name: str) -> str: | |
| """Fetch card art URL from Scryfall (normal size).""" | |
| r = requests.get( | |
| "https://api.scryfall.com/cards/named", params={"exact": card_name, "format": "json"} | |
| ) | |
| r.raise_for_status() | |
| data = r.json() | |
| if "image_uris" in data: | |
| return data["image_uris"]["normal"] | |
| return data["card_faces"][0]["image_uris"]["normal"] | |
| def generate_booster(set_code: str) -> List[Dict]: | |
| """Return a pseudo-random 15-card booster using Scryfall search.""" | |
| url = f"https://api.scryfall.com/cards/search?q=set%3A{set_code}+is%3Abooster+unique%3Aprints" | |
| cards: List[Dict] = [] | |
| while url: | |
| resp = requests.get(url) | |
| resp.raise_for_status() | |
| payload = resp.json() | |
| cards += payload["data"] | |
| url = payload.get("next_page") if payload.get("has_more") else None | |
| return random.sample(cards, 15) | |
| # ----------------------------------------------------------------------------- | |
| # 3. Streamlit UI | |
| # ----------------------------------------------------------------------------- | |
| st.set_page_config(page_title="MTG Draft Assistant", page_icon="π") | |
| st.title("π MTG Draft Assistant") | |
| # ---------------- Sidebar ----------------------------------------------------- | |
| with st.sidebar: | |
| st.header("Draft setup") | |
| supported_sets = get_supported_sets() | |
| if supported_sets: | |
| set_code = st.radio("Choose a set to draft", supported_sets, index=0) | |
| else: | |
| st.warning( | |
| "*supported_sets.txt* not found or empty. Using free-text input instead.", | |
| icon="β οΈ", | |
| ) | |
| set_code = st.text_input("Set code", value="WOE") | |
| if st.button("Start new draft", type="primary"): | |
| st.session_state["pack"] = generate_booster(set_code) | |
| st.session_state["picks"] = [] | |
| # ---------------- Session state guards --------------------------------------- | |
| st.session_state.setdefault("pack", []) | |
| st.session_state.setdefault("picks", []) | |
| if not st.session_state["pack"]: | |
| st.info("Choose **Start new draft** in the sidebar to open pack 1.") | |
| st.stop() | |
| pack: List[Dict] = st.session_state["pack"] | |
| picks: List[Dict] = st.session_state["picks"] | |
| st.subheader(f"Pack {len(picks) // 15 + 1} β Pick {len(picks) % 15 + 1}") | |
| suggested = suggest_pick(pack, picks) | |
| st.success(f"**Model suggests:** {suggested['name']}") | |
| # Display current pack as a 5-column grid of card images | |
| cols = st.columns(5) | |
| for idx, card in enumerate(pack): | |
| col = cols[idx % 5] | |
| col.image(fetch_card_image(card["name"]), use_column_width=True) | |
| if col.button(f"Pick {card['name']}", key=f"pick-{idx}"): | |
| picks.append(card) | |
| pack.remove(card) | |
| if not pack: # end of pack β open a fresh booster | |
| st.session_state["pack"] = generate_booster(set_code) | |
| st.experimental_rerun() | |
| with st.expander("Current picks", expanded=False): | |
| st.write("\n".join([c["name"] for c in picks])) | |