Timo commited on
Commit
df1e49f
·
1 Parent(s): 7d4c042
Files changed (1) hide show
  1. src/streamlit_app.py +115 -73
src/streamlit_app.py CHANGED
@@ -1,23 +1,20 @@
1
  """
2
  MTG Draft Assistant Streamlit App
3
  ---------------------------------
4
- A booster-draft helper written with Streamlit and deployable on Hugging Face Spaces.
5
-
6
- Changes in this revision
7
- ~~~~~~~~~~~~~~~~~~~~~~~~
8
- * **supported_sets.txt** – one set code per line.
9
- * Sidebar now shows a **single-choice list** (radio buttons) sourced from that
10
- file instead of a free-text box, so users can only draft sets you actually
11
- support.
12
- * Fallback to the old text input if the text-file is missing or empty.
13
-
14
- Replace the three stub functions (`load_model`, `suggest_pick`,
15
- `generate_booster`) with your real model-/API-calls when you are ready.
16
  """
17
 
18
  from __future__ import annotations
19
 
20
- import os
21
  import random
22
  from pathlib import Path
23
  from typing import Dict, List
@@ -26,59 +23,83 @@ import requests
26
  import streamlit as st
27
 
28
  # -----------------------------------------------------------------------------
29
- # 0. Constants & helpers
30
  # -----------------------------------------------------------------------------
 
 
31
 
32
- SUPPORTED_SETS_PATH = Path("src/helper_files/supported_sets.txt")
 
 
33
 
34
- @st.cache_data(show_spinner="Reading supported sets …")
35
- def get_supported_sets(path: Path = SUPPORTED_SETS_PATH) -> List[str]:
36
- """Return a list of legal set codes read from *supported_sets.txt*.
37
 
38
- The file should contain **one set tag per line**, e.g.::
39
 
40
- WOE
41
- LCI
42
- MKM
43
 
44
- If the file is missing we fall back to an empty list so the UI
45
- degrades gracefully.
46
- """
 
47
  if path.is_file():
48
  return [ln.strip() for ln in path.read_text().splitlines() if ln.strip()]
49
  return []
50
 
 
51
  # -----------------------------------------------------------------------------
52
- # 1. Model loading (stub)
53
  # -----------------------------------------------------------------------------
54
 
55
  @st.cache_resource(show_spinner="Loading draft model …")
56
  def load_model():
57
- """Load and return the trained drafting model.
 
 
 
 
 
 
58
 
59
- Adapt to your own pipeline; see previous revision for an example that pulls
60
- an artefact from *huggingface_hub*. Returning *None* leaves us in demo
61
- mode and will pick random cards.
62
  """
63
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
  model = load_model()
66
 
 
67
  # -----------------------------------------------------------------------------
68
- # 2. Draft-logic helpers (stubs)
69
  # -----------------------------------------------------------------------------
70
 
71
  def suggest_pick(pack: List[Dict], picks: List[Dict]) -> Dict:
72
- """Return the card the model recommends from *pack*."""
73
  if model is None:
74
  return random.choice(pack)
75
  return model.predict(pack=pack, picks=picks) # type: ignore[attr-defined]
76
 
77
 
78
  def fetch_card_image(card_name: str) -> str:
79
- """Fetch card art URL from Scryfall (normal size)."""
80
  r = requests.get(
81
- "https://api.scryfall.com/cards/named", params={"exact": card_name, "format": "json"}
 
82
  )
83
  r.raise_for_status()
84
  data = r.json()
@@ -88,7 +109,6 @@ def fetch_card_image(card_name: str) -> str:
88
 
89
 
90
  def generate_booster(set_code: str) -> List[Dict]:
91
- """Return a pseudo-random 15-card booster using Scryfall search."""
92
  url = f"https://api.scryfall.com/cards/search?q=set%3A{set_code}+is%3Abooster+unique%3Aprints"
93
  cards: List[Dict] = []
94
  while url:
@@ -99,6 +119,7 @@ def generate_booster(set_code: str) -> List[Dict]:
99
  url = payload.get("next_page") if payload.get("has_more") else None
100
  return random.sample(cards, 15)
101
 
 
102
  # -----------------------------------------------------------------------------
103
  # 3. Streamlit UI
104
  # -----------------------------------------------------------------------------
@@ -107,54 +128,75 @@ st.set_page_config(page_title="MTG Draft Assistant", page_icon="🃏")
107
 
108
  st.title("🃏 MTG Draft Assistant")
109
 
110
- # ---------------- Sidebar -----------------------------------------------------
111
 
112
  with st.sidebar:
113
  st.header("Draft setup")
114
 
115
  supported_sets = get_supported_sets()
116
 
117
- if supported_sets:
118
- set_code = st.radio("Choose a set to draft", supported_sets, index=0)
119
- else:
120
- st.warning(
121
- "*supported_sets.txt* not found or empty. Using free-text input instead.",
122
- icon="⚠️",
123
- )
124
- set_code = st.text_input("Set code", value="WOE")
125
-
126
- if st.button("Start new draft", type="primary"):
127
- st.session_state["pack"] = generate_booster(set_code)
128
- st.session_state["picks"] = []
 
 
129
 
130
- # ---------------- Session state guards ---------------------------------------
131
 
132
  st.session_state.setdefault("pack", [])
133
  st.session_state.setdefault("picks", [])
134
 
135
- if not st.session_state["pack"]:
136
- st.info("Choose **Start new draft** in the sidebar to open pack 1.")
137
- st.stop()
138
 
139
- pack: List[Dict] = st.session_state["pack"]
140
- picks: List[Dict] = st.session_state["picks"]
141
 
142
- st.subheader(f"Pack {len(picks) // 15 + 1} Pick {len(picks) % 15 + 1}")
143
- suggested = suggest_pick(pack, picks)
144
 
145
- st.success(f"**Model suggests:** {suggested['name']}")
146
-
147
- # Display current pack as a 5-column grid of card images
148
- cols = st.columns(5)
149
- for idx, card in enumerate(pack):
150
- col = cols[idx % 5]
151
- col.image(fetch_card_image(card["name"]), use_column_width=True)
152
- if col.button(f"Pick {card['name']}", key=f"pick-{idx}"):
153
- picks.append(card)
154
- pack.remove(card)
155
- if not pack: # end of pack ⇒ open a fresh booster
156
- st.session_state["pack"] = generate_booster(set_code)
157
- st.experimental_rerun()
158
-
159
- with st.expander("Current picks", expanded=False):
160
- st.write("\n".join([c["name"] for c in picks]))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
  MTG Draft Assistant Streamlit App
3
  ---------------------------------
4
+ Booster‑draft helper for Magic: The Gathering, built with Streamlit and ready
5
+ for Hugging Face Spaces deployment.
6
+
7
+ 🆕 UI tweaks in this revision
8
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
9
+ * **Set selection is now hidden** in an *expander* inside the sidebar—keeps the
10
+ layout clean.
11
+ * Added a **second tab – “Card rankings”**. When you pick a set, the tab shows a
12
+ (stub) ranked list of cards from that set. Replace the `rank_cards()` stub
13
+ with real logic later.
 
 
14
  """
15
 
16
  from __future__ import annotations
17
 
 
18
  import random
19
  from pathlib import Path
20
  from typing import Dict, List
 
23
  import streamlit as st
24
 
25
  # -----------------------------------------------------------------------------
26
+ # Disable telemetry write‑outs (avoids PermissionError: /.streamlit on Spaces)
27
  # -----------------------------------------------------------------------------
28
+ try:
29
+ from streamlit.runtime.metrics_util import disable_gather_usage_stats
30
 
31
+ disable_gather_usage_stats()
32
+ except Exception:
33
+ st.set_option("browser.gatherUsageStats", False)
34
 
35
+ # -----------------------------------------------------------------------------
36
+ # 0. Constants & helpers
37
+ # -----------------------------------------------------------------------------
38
 
39
+ SUPPORTED_SETS_PATH = Path("supported_sets.txt")
40
 
 
 
 
41
 
42
+ @st.cache_data(show_spinner="Reading supported sets …")
43
+ def get_supported_sets(path: Path = SUPPORTED_SETS_PATH) -> List[str]:
44
+ """Return a list of legal set codes read from *supported_sets.txt*."""
45
+
46
  if path.is_file():
47
  return [ln.strip() for ln in path.read_text().splitlines() if ln.strip()]
48
  return []
49
 
50
+
51
  # -----------------------------------------------------------------------------
52
+ # 1. Model & ranking stubs
53
  # -----------------------------------------------------------------------------
54
 
55
  @st.cache_resource(show_spinner="Loading draft model …")
56
  def load_model():
57
+ # 🔄 Replace with your own pipeline or `hf_hub_download` call
58
+ return None
59
+
60
+
61
+ @st.cache_data(show_spinner="Calculating card rankings …")
62
+ def rank_cards(set_code: str) -> List[Dict]:
63
+ """Return a stubbed ranking list for *set_code*.
64
 
65
+ Replace with your real evaluation logic. For now we just pull 30 random
66
+ commons from the set and assign a dummy score.
 
67
  """
68
+
69
+ url = f"https://api.scryfall.com/cards/search?q=set%3A{set_code}+unique%3Aprints+is%3Acommon"
70
+ cards: List[Dict] = []
71
+ while url and len(cards) < 60: # cap network use
72
+ r = requests.get(url)
73
+ r.raise_for_status()
74
+ payload = r.json()
75
+ cards += payload["data"]
76
+ url = payload.get("next_page") if payload.get("has_more") else None
77
+
78
+ sample = random.sample(cards, k=min(30, len(cards))) if cards else []
79
+ ranked = [
80
+ {"name": c["name"], "score": round(random.random(), 2)} for c in sample
81
+ ]
82
+ ranked.sort(key=lambda x: x["score"], reverse=True)
83
+ return ranked
84
+
85
 
86
  model = load_model()
87
 
88
+
89
  # -----------------------------------------------------------------------------
90
+ # 2. Draftlogic helpers (stubs)
91
  # -----------------------------------------------------------------------------
92
 
93
  def suggest_pick(pack: List[Dict], picks: List[Dict]) -> Dict:
 
94
  if model is None:
95
  return random.choice(pack)
96
  return model.predict(pack=pack, picks=picks) # type: ignore[attr-defined]
97
 
98
 
99
  def fetch_card_image(card_name: str) -> str:
 
100
  r = requests.get(
101
+ "https://api.scryfall.com/cards/named",
102
+ params={"exact": card_name, "format": "json"},
103
  )
104
  r.raise_for_status()
105
  data = r.json()
 
109
 
110
 
111
  def generate_booster(set_code: str) -> List[Dict]:
 
112
  url = f"https://api.scryfall.com/cards/search?q=set%3A{set_code}+is%3Abooster+unique%3Aprints"
113
  cards: List[Dict] = []
114
  while url:
 
119
  url = payload.get("next_page") if payload.get("has_more") else None
120
  return random.sample(cards, 15)
121
 
122
+
123
  # -----------------------------------------------------------------------------
124
  # 3. Streamlit UI
125
  # -----------------------------------------------------------------------------
 
128
 
129
  st.title("🃏 MTG Draft Assistant")
130
 
131
+ # -------- Sidebar ------------------------------------------------------------
132
 
133
  with st.sidebar:
134
  st.header("Draft setup")
135
 
136
  supported_sets = get_supported_sets()
137
 
138
+ # Hide control in an expander (collapsed by default)
139
+ with st.expander("Set selection", expanded=False):
140
+ if supported_sets:
141
+ set_code = st.radio("Choose a set to draft", supported_sets, index=0)
142
+ else:
143
+ st.warning(
144
+ "*supported_sets.txt* not found or empty. Using free‑text input instead.",
145
+ icon="⚠️",
146
+ )
147
+ set_code = st.text_input("Set code", value="WOE")
148
+
149
+ if st.button("Start new draft", type="primary"):
150
+ st.session_state["pack"] = generate_booster(set_code)
151
+ st.session_state["picks"] = []
152
 
153
+ # -------- Session state ------------------------------------------------------
154
 
155
  st.session_state.setdefault("pack", [])
156
  st.session_state.setdefault("picks", [])
157
 
158
+ # -------- Main content organised in tabs ------------------------------------
 
 
159
 
160
+ tabs = st.tabs(["Draft", "Card rankings"])
 
161
 
162
+ # --- Tab 1: Draft ------------------------------------------------------------
 
163
 
164
+ with tabs[0]:
165
+ if not st.session_state["pack"]:
166
+ st.info("Choose **Start new draft** in the sidebar to open pack 1.")
167
+ else:
168
+ pack: List[Dict] = st.session_state["pack"]
169
+ picks: List[Dict] = st.session_state["picks"]
170
+
171
+ st.subheader(f"Pack {len(picks) // 15 + 1} — Pick {len(picks) % 15 + 1}")
172
+ suggested = suggest_pick(pack, picks)
173
+ st.success(f"**Model suggests:** {suggested['name']}")
174
+
175
+ cols = st.columns(5)
176
+ for idx, card in enumerate(pack):
177
+ col = cols[idx % 5]
178
+ col.image(fetch_card_image(card["name"]), use_column_width=True)
179
+ if col.button(f"Pick {card['name']}", key=f"pick-{idx}"):
180
+ picks.append(card)
181
+ pack.remove(card)
182
+ if not pack: # end of pack ⇒ open a fresh booster
183
+ st.session_state["pack"] = generate_booster(set_code)
184
+ st.experimental_rerun()
185
+
186
+ with st.expander("Current picks", expanded=False):
187
+ st.write("\n".join([c["name"] for c in picks]))
188
+
189
+ # --- Tab 2: Card rankings ----------------------------------------------------
190
+
191
+ with tabs[1]:
192
+ st.header("Card rankings for set " + set_code)
193
+
194
+ if set_code:
195
+ ranking = rank_cards(set_code)
196
+ if ranking:
197
+ for idx, card in enumerate(ranking, start=1):
198
+ st.write(f"{idx}. {card['name']} — **{card['score']:.2f}**")
199
+ else:
200
+ st.info("No cards found for this set (or Scryfall unavailable).")
201
+ else:
202
+ st.info("Select a set in the sidebar to view rankings.")