File size: 6,487 Bytes
f4bf513
 
df1e49f
 
 
 
 
 
 
 
 
 
f4bf513
 
 
 
2bd322b
f4bf513
 
 
 
 
 
f83d2ce
2bd322b
 
 
f4bf513
 
df1e49f
1c44fce
f4bf513
 
df1e49f
 
 
 
f4bf513
 
 
 
df1e49f
f4bf513
 
fa1e508
df1e49f
 
 
 
 
f4bf513
df1e49f
 
f4bf513
df1e49f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f4bf513
 
 
df1e49f
f4bf513
df1e49f
f4bf513
 
 
 
 
 
 
 
 
 
df1e49f
 
f4bf513
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
df1e49f
f4bf513
 
 
 
 
 
 
 
df1e49f
f4bf513
 
 
 
7d4c042
f4bf513
df1e49f
 
 
 
 
 
 
 
 
 
 
 
 
 
f4bf513
df1e49f
f4bf513
 
 
 
df1e49f
f4bf513
df1e49f
f4bf513
df1e49f
f4bf513
df1e49f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
"""
---------------------------------
Booster‑draft helper for Magic: The Gathering, built with Streamlit and ready
for Hugging Face Spaces deployment.

🆕 UI tweaks in this revision
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* **Set selection is now hidden** in an *expander* inside the sidebar—keeps the
  layout clean.
* Added a **second tab – “Card rankings”**. When you pick a set, the tab shows a
  (stub) ranked list of cards from that set. Replace the `rank_cards()` stub
  with real logic later.
"""

from __future__ import annotations

import random, os
from pathlib import Path
from typing import Dict, List

import requests
import streamlit as st

from draft_model import DraftModel


os.environ.setdefault("HF_HOME", os.path.expanduser("~/.cache/huggingface"))


SUPPORTED_SETS_PATH = Path("supported_sets.txt")
st.write("Running Streamlit", st.__version__)


@st.cache_data(show_spinner="Reading supported sets …")
def get_supported_sets(path: Path = SUPPORTED_SETS_PATH) -> List[str]:
    """Return a list of legal set codes read from *supported_sets.txt*."""

    if path.is_file():
        return [ln.strip() for ln in path.read_text().splitlines() if ln.strip()]
    return []


@st.cache_resource(show_spinner="Loading draft model …")
def load_model():
    return DraftModel() 


@st.cache_data(show_spinner="Calculating card rankings …")
def rank_cards(set_code: str) -> List[Dict]:
    """Return a stubbed ranking list for *set_code*.

    Replace with your real evaluation logic. For now we just pull 30 random
    commons from the set and assign a dummy score.
    """

    url = f"https://api.scryfall.com/cards/search?q=set%3A{set_code}+unique%3Aprints+is%3Acommon"
    cards: List[Dict] = []
    while url and len(cards) < 60:  # cap network use
        r = requests.get(url)
        r.raise_for_status()
        payload = r.json()
        cards += payload["data"]
        url = payload.get("next_page") if payload.get("has_more") else None

    sample = random.sample(cards, k=min(30, len(cards))) if cards else []
    ranked = [
        {"name": c["name"], "score": round(random.random(), 2)} for c in sample
    ]
    ranked.sort(key=lambda x: x["score"], reverse=True)
    return ranked


model = load_model()


# -----------------------------------------------------------------------------
# 2. Draft‑logic helpers (stubs)
# -----------------------------------------------------------------------------

def suggest_pick(pack: List[Dict], picks: List[Dict]) -> Dict:
    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:
    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]:
    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()

    # Hide control in an expander (collapsed by default)
    with st.expander("Set selection", expanded=False):
        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 ------------------------------------------------------

st.session_state.setdefault("pack", [])
st.session_state.setdefault("picks", [])

# -------- Main content organised in tabs ------------------------------------

tabs = st.tabs(["Draft", "Card rankings"])

# --- Tab 1: Draft ------------------------------------------------------------

with tabs[0]:
    if not st.session_state["pack"]:
        st.info("Choose **Start new draft** in the sidebar to open pack 1.")
    else:
        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']}")

        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]))

# --- Tab 2: Card rankings ----------------------------------------------------

with tabs[1]:
    st.header("Card rankings for set " + set_code)

    if set_code:
        ranking = rank_cards(set_code)
        if ranking:
            for idx, card in enumerate(ranking, start=1):
                st.write(f"{idx}. {card['name']} — **{card['score']:.2f}**")
        else:
            st.info("No cards found for this set (or Scryfall unavailable).")
    else:
        st.info("Select a set in the sidebar to view rankings.")