RandomCatLover commited on
Commit
7d74b4c
·
verified ·
1 Parent(s): 694ff80

Upload 4 files

Browse files
Files changed (4) hide show
  1. app.py +10 -0
  2. pages/dungeon_draft.py +338 -0
  3. pages/home.py +30 -0
  4. pages/skull_king.py +134 -0
app.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+
3
+ st.set_page_config(page_title="Board Game Tracker", layout="wide")
4
+
5
+ home_page = st.Page("pages/home.py", title="Home", url_path="home", default=True)
6
+ skull_king_page = st.Page("pages/skull_king.py", title="Skull King", url_path="skull_king")
7
+ dungeon_draft_page = st.Page("pages/dungeon_draft.py", title="Dungeon Draft", url_path="dungeon_draft")
8
+
9
+ pg = st.navigation([home_page, skull_king_page, dungeon_draft_page])
10
+ pg.run()
pages/dungeon_draft.py ADDED
@@ -0,0 +1,338 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import copy
3
+
4
+
5
+ def _init_state():
6
+ if "dd_players" not in st.session_state:
7
+ st.session_state.dd_players = []
8
+ if "dd_game_started" not in st.session_state:
9
+ st.session_state.dd_game_started = False
10
+ if "dd_track_damage" not in st.session_state:
11
+ st.session_state.dd_track_damage = False
12
+ # dd_rounds[round_idx] = {player: {"money": int, "damage": int, "vp": int}}
13
+ # round 0 = starting balance
14
+ if "dd_rounds" not in st.session_state:
15
+ st.session_state.dd_rounds = {}
16
+ # dd_pending[player] = {"money": int, "damage": int, "vp": int}
17
+ if "dd_pending" not in st.session_state:
18
+ st.session_state.dd_pending = {}
19
+ if "dd_finished" not in st.session_state:
20
+ st.session_state.dd_finished = set()
21
+ if "dd_current_round" not in st.session_state:
22
+ st.session_state.dd_current_round = 1
23
+ # Gold income per player applied AFTER each round is committed
24
+ if "dd_gold_income" not in st.session_state:
25
+ st.session_state.dd_gold_income = {}
26
+ # Starting balance per player (set during setup)
27
+ if "dd_starting_balance" not in st.session_state:
28
+ st.session_state.dd_starting_balance = {}
29
+
30
+
31
+ def _reset_pending():
32
+ st.session_state.dd_pending = {
33
+ p: {"money": 0, "damage": 0, "vp": 0}
34
+ for p in st.session_state.dd_players
35
+ }
36
+ st.session_state.dd_finished = set()
37
+
38
+
39
+ def _setup_phase():
40
+ st.subheader("Game Setup")
41
+
42
+ st.session_state.dd_track_damage = st.checkbox(
43
+ "Track damage per round (optional)", value=st.session_state.dd_track_damage
44
+ )
45
+
46
+ st.markdown("---")
47
+ st.subheader("Players")
48
+
49
+ def _dd_add_player():
50
+ name = st.session_state.dd_new_player.strip()
51
+ if name and name not in st.session_state.dd_players:
52
+ st.session_state.dd_players.append(name)
53
+ st.session_state.dd_gold_income[name] = 5
54
+ st.session_state.dd_starting_balance[name] = 9
55
+ st.session_state.dd_new_player = ""
56
+
57
+ col1, col2 = st.columns([3, 1])
58
+ with col1:
59
+ st.text_input("Player name", key="dd_new_player")
60
+ with col2:
61
+ st.write("")
62
+ st.write("")
63
+ st.button("Add Player", key="dd_add_btn", on_click=_dd_add_player)
64
+
65
+ if st.session_state.dd_players:
66
+ for i, p in enumerate(st.session_state.dd_players):
67
+ col_name, col_del = st.columns([4, 1])
68
+ col_name.write(f"{i + 1}. {p}")
69
+ if col_del.button("Remove", key=f"dd_rm_{i}"):
70
+ st.session_state.dd_players.pop(i)
71
+ st.session_state.dd_gold_income.pop(p, None)
72
+ st.session_state.dd_starting_balance.pop(p, None)
73
+ st.rerun()
74
+
75
+ st.markdown("---")
76
+ st.subheader("Starting Balance & Gold Income")
77
+ for p in st.session_state.dd_players:
78
+ col_bal, col_inc = st.columns(2)
79
+ with col_bal:
80
+ st.session_state.dd_starting_balance[p] = st.number_input(
81
+ f"{p} - Starting Gold",
82
+ value=st.session_state.dd_starting_balance.get(p, 0),
83
+ step=1,
84
+ min_value=0,
85
+ key=f"dd_setup_bal_{p}",
86
+ )
87
+ with col_inc:
88
+ st.session_state.dd_gold_income[p] = st.number_input(
89
+ f"{p} - Gold Income per Round",
90
+ value=st.session_state.dd_gold_income.get(p, 0),
91
+ step=1,
92
+ min_value=0,
93
+ key=f"dd_setup_inc_{p}",
94
+ )
95
+
96
+ if len(st.session_state.dd_players) >= 2:
97
+ if st.button("Start Game", type="primary"):
98
+ st.session_state.dd_game_started = True
99
+ # Round 0 = starting balance
100
+ st.session_state.dd_rounds = {
101
+ 0: {
102
+ p: {
103
+ "money": st.session_state.dd_starting_balance.get(p, 0),
104
+ "damage": 0,
105
+ "vp": 0,
106
+ }
107
+ for p in st.session_state.dd_players
108
+ }
109
+ }
110
+ st.session_state.dd_current_round = 1
111
+ _reset_pending()
112
+ st.rerun()
113
+ else:
114
+ st.info("Add at least 2 players to start.")
115
+
116
+
117
+ def _game_phase():
118
+ players = st.session_state.dd_players
119
+ rounds = st.session_state.dd_rounds
120
+ current_round = st.session_state.dd_current_round
121
+ track_damage = st.session_state.dd_track_damage
122
+
123
+ if st.button("Reset Game"):
124
+ st.session_state.dd_game_started = False
125
+ st.session_state.dd_players = []
126
+ st.session_state.dd_rounds = {}
127
+ st.session_state.dd_pending = {}
128
+ st.session_state.dd_finished = set()
129
+ st.session_state.dd_current_round = 1
130
+ st.session_state.dd_gold_income = {}
131
+ st.session_state.dd_starting_balance = {}
132
+ st.rerun()
133
+
134
+ # Gold income settings in sidebar (adjustable during game)
135
+ with st.sidebar:
136
+ st.markdown("---")
137
+ st.subheader("Gold Income per Round")
138
+ st.caption("Applied after each round is committed.")
139
+ for p in players:
140
+ st.session_state.dd_gold_income[p] = st.number_input(
141
+ f"{p}",
142
+ value=st.session_state.dd_gold_income.get(p, 0),
143
+ step=1,
144
+ min_value=0,
145
+ key=f"dd_income_{p}",
146
+ )
147
+
148
+ # Scoreboard (always visible since round 0 exists)
149
+ completed_rounds = sorted(rounds.keys())
150
+ st.subheader("Scoreboard")
151
+ _render_scoreboard(players, rounds, completed_rounds, track_damage)
152
+
153
+ # Current round
154
+ st.subheader(f"Round {current_round}")
155
+ st.caption(
156
+ "Add/reduce values for each player. Changes are staged until you finish the player's turn. "
157
+ "Use the Pirate Card section to steal from other players."
158
+ )
159
+
160
+ pending = st.session_state.dd_pending
161
+ finished = st.session_state.dd_finished
162
+
163
+ tabs = st.tabs(players)
164
+ for idx, player in enumerate(players):
165
+ with tabs[idx]:
166
+ is_finished = player in finished
167
+
168
+ if is_finished:
169
+ st.success(f"{player}'s turn is finished for this round.")
170
+ st.write(f"Money: **{pending[player]['money']:+d}**")
171
+ if track_damage:
172
+ st.write(f"Damage: **{pending[player]['damage']:+d}**")
173
+ st.write(f"Victory Points: **{pending[player]['vp']:+d}**")
174
+ continue
175
+
176
+ st.markdown(f"**{player}'s Turn**")
177
+
178
+ # Show current cumulative balance
179
+ cum = _get_cumulative(player, rounds, completed_rounds)
180
+ bal_parts = [f"Money: **{cum['money']}**", f"VP: **{cum['vp']}**"]
181
+ if track_damage:
182
+ bal_parts.append(f"Damage: **{cum['damage']}**")
183
+ st.caption("Current balance: " + " | ".join(bal_parts))
184
+
185
+ col_money, col_vp = st.columns(2)
186
+ with col_money:
187
+ money_change = st.number_input(
188
+ "Money +/-",
189
+ value=0,
190
+ step=1,
191
+ key=f"dd_money_{current_round}_{player}",
192
+ )
193
+ with col_vp:
194
+ vp_change = st.number_input(
195
+ "Victory Points +/-",
196
+ value=0,
197
+ step=1,
198
+ key=f"dd_vp_{current_round}_{player}",
199
+ )
200
+
201
+ damage_change = 0
202
+ if track_damage:
203
+ damage_change = st.number_input(
204
+ "Damage +/-",
205
+ value=0,
206
+ step=1,
207
+ key=f"dd_dmg_{current_round}_{player}",
208
+ )
209
+
210
+ if st.button("Stage Changes", key=f"dd_stage_{current_round}_{player}"):
211
+ pending[player]["money"] += money_change
212
+ pending[player]["vp"] += vp_change
213
+ if track_damage:
214
+ pending[player]["damage"] += damage_change
215
+ st.rerun()
216
+
217
+ if any(v != 0 for v in pending[player].values()):
218
+ st.markdown("**Staged changes:**")
219
+ st.write(f"Money: {pending[player]['money']:+d}")
220
+ if track_damage:
221
+ st.write(f"Damage: {pending[player]['damage']:+d}")
222
+ st.write(f"VP: {pending[player]['vp']:+d}")
223
+
224
+ # Pirate card
225
+ st.markdown("---")
226
+ show_pirate = st.checkbox(
227
+ "Pirate Card - Steal", key=f"dd_pirate_show_{current_round}_{player}"
228
+ )
229
+ other_players = [p for p in players if p != player]
230
+ if show_pirate and other_players:
231
+ steal_target = st.selectbox(
232
+ "Steal from",
233
+ other_players,
234
+ key=f"dd_pirate_target_{current_round}_{player}",
235
+ )
236
+ steal_col1, steal_col2 = st.columns(2)
237
+ with steal_col1:
238
+ steal_type = st.selectbox(
239
+ "Resource",
240
+ ["money", "vp"] + (["damage"] if track_damage else []),
241
+ key=f"dd_pirate_type_{current_round}_{player}",
242
+ )
243
+ with steal_col2:
244
+ steal_amount = st.number_input(
245
+ "Amount to steal",
246
+ min_value=0,
247
+ value=0,
248
+ step=1,
249
+ key=f"dd_pirate_amt_{current_round}_{player}",
250
+ )
251
+ if st.button("Steal", key=f"dd_pirate_btn_{current_round}_{player}"):
252
+ if steal_amount > 0:
253
+ pending[player][steal_type] += steal_amount
254
+ pending[steal_target][steal_type] -= steal_amount
255
+ st.success(
256
+ f"{player} stole {steal_amount} {steal_type} from {steal_target}!"
257
+ )
258
+ st.rerun()
259
+
260
+ st.markdown("---")
261
+ if st.button(
262
+ "Finish Turn", type="primary", key=f"dd_finish_{current_round}_{player}"
263
+ ):
264
+ st.session_state.dd_finished.add(player)
265
+ st.rerun()
266
+
267
+ # Commit round when all done
268
+ if len(finished) == len(players):
269
+ st.markdown("---")
270
+ st.info("All players have finished their turns.")
271
+ if st.button("Commit Round", type="primary", key=f"dd_commit_{current_round}"):
272
+ # Save the round's changes
273
+ round_data = copy.deepcopy(pending)
274
+ # Add gold income as a separate entry after the round
275
+ gold_income = st.session_state.dd_gold_income
276
+ for p in players:
277
+ round_data[p]["money"] += gold_income.get(p, 0)
278
+ st.session_state.dd_rounds[current_round] = round_data
279
+ st.session_state.dd_current_round += 1
280
+ _reset_pending()
281
+ st.rerun()
282
+
283
+
284
+ def _get_cumulative(player, rounds, completed_rounds):
285
+ cum = {"money": 0, "damage": 0, "vp": 0}
286
+ for r in completed_rounds:
287
+ for k in cum:
288
+ cum[k] += rounds[r][player].get(k, 0)
289
+ return cum
290
+
291
+
292
+ def _render_scoreboard(players, rounds, completed_rounds, track_damage):
293
+ metrics = ["Money", "VP"] + (["Damage"] if track_damage else [])
294
+ metric_keys = ["money", "vp"] + (["damage"] if track_damage else [])
295
+
296
+ header = ["Round"]
297
+ for p in players:
298
+ for m in metrics:
299
+ header.append(f"{p} - {m}")
300
+
301
+ cumsum = {p: {k: 0 for k in metric_keys} for p in players}
302
+ prev_cumsum = {p: {k: 0 for k in metric_keys} for p in players}
303
+
304
+ rows = []
305
+ for r in completed_rounds:
306
+ label = "Start" if r == 0 else str(r)
307
+ row = [f"**{label}**"]
308
+ for p in players:
309
+ for k in metric_keys:
310
+ prev_cumsum[p][k] = cumsum[p][k]
311
+ val = rounds[r][p].get(k, 0)
312
+ cumsum[p][k] += val
313
+ change = cumsum[p][k] - prev_cumsum[p][k]
314
+ if change > 0:
315
+ indicator = f' <span style="color:green">&#9650; +{change}</span>'
316
+ elif change < 0:
317
+ indicator = f' <span style="color:red">&#9660; {change}</span>'
318
+ else:
319
+ indicator = ""
320
+ row.append(f"**{cumsum[p][k]}**{indicator}")
321
+ rows.append(row)
322
+
323
+ md = "| " + " | ".join(header) + " |\n"
324
+ md += "| " + " | ".join(["---"] * len(header)) + " |\n"
325
+ for row in rows:
326
+ md += "| " + " | ".join(str(c) for c in row) + " |\n"
327
+
328
+ st.markdown(md, unsafe_allow_html=True)
329
+
330
+
331
+ # Page entry point
332
+ st.title("Dungeon Draft")
333
+ _init_state()
334
+
335
+ if not st.session_state.dd_game_started:
336
+ _setup_phase()
337
+ else:
338
+ _game_phase()
pages/home.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+
3
+ st.title("Board Game Tracker")
4
+
5
+ st.markdown(
6
+ """
7
+ ### Free & Open Source
8
+
9
+ This application is **completely free** and **open source**. No accounts, no subscriptions,
10
+ no data collection. Your game data lives in your browser session only.
11
+
12
+ ---
13
+
14
+ ### Supported Games
15
+
16
+ | Game | Description |
17
+ |------|-------------|
18
+ | **Skull King** | A trick-taking pirate card game for 2-6 players. Track bids and scores across up to 10 rounds. |
19
+ | **Dungeon Draft** | A dungeon-crawling card drafting game. Track money, damage, and victory points with full transaction support. |
20
+
21
+ ---
22
+
23
+ ### How It Works
24
+
25
+ - Select a game from the **left sidebar** to start tracking.
26
+ - All data persists within your current browser session.
27
+ - Add players, record rounds, and view cumulative scores in real time.
28
+ - Starting a new browser session gives you a fresh slate.
29
+ """
30
+ )
pages/skull_king.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+
3
+
4
+ def _init_state():
5
+ if "sk_players" not in st.session_state:
6
+ st.session_state.sk_players = []
7
+ if "sk_game_started" not in st.session_state:
8
+ st.session_state.sk_game_started = False
9
+ if "sk_scores" not in st.session_state:
10
+ st.session_state.sk_scores = {}
11
+
12
+
13
+ def _setup_phase():
14
+ st.subheader("Player Setup")
15
+
16
+ def _sk_add_player():
17
+ name = st.session_state.sk_new_player.strip()
18
+ if name and name not in st.session_state.sk_players:
19
+ st.session_state.sk_players.append(name)
20
+ st.session_state.sk_new_player = ""
21
+
22
+ col1, col2 = st.columns([3, 1])
23
+ with col1:
24
+ st.text_input("Player name", key="sk_new_player")
25
+ with col2:
26
+ st.write("")
27
+ st.write("")
28
+ st.button("Add Player", key="sk_add_btn", on_click=_sk_add_player)
29
+
30
+ if st.session_state.sk_players:
31
+ st.markdown("**Players:**")
32
+ for i, p in enumerate(st.session_state.sk_players):
33
+ col_name, col_del = st.columns([4, 1])
34
+ col_name.write(f"{i + 1}. {p}")
35
+ if col_del.button("Remove", key=f"sk_rm_{i}"):
36
+ st.session_state.sk_players.pop(i)
37
+ st.rerun()
38
+
39
+ if len(st.session_state.sk_players) >= 2:
40
+ if st.button("Start Game", type="primary"):
41
+ st.session_state.sk_game_started = True
42
+ st.session_state.sk_scores = {}
43
+ st.rerun()
44
+ else:
45
+ st.info("Add at least 2 players to start.")
46
+
47
+
48
+ def _game_phase():
49
+ players = st.session_state.sk_players
50
+ scores = st.session_state.sk_scores
51
+ completed_rounds = sorted(scores.keys())
52
+ next_round = (completed_rounds[-1] + 1) if completed_rounds else 1
53
+
54
+ if st.button("Reset Game"):
55
+ st.session_state.sk_game_started = False
56
+ st.session_state.sk_players = []
57
+ st.session_state.sk_scores = {}
58
+ st.rerun()
59
+
60
+ if completed_rounds:
61
+ st.subheader("Scoreboard")
62
+ _render_scoreboard(players, scores, completed_rounds)
63
+
64
+ if next_round <= 10:
65
+ st.subheader(f"Round {next_round}")
66
+ st.caption(f"Each player is dealt {next_round} card{'s' if next_round > 1 else ''}.")
67
+
68
+ round_scores = {}
69
+ cols = st.columns(len(players))
70
+ for i, player in enumerate(players):
71
+ with cols[i]:
72
+ round_scores[player] = st.number_input(
73
+ player,
74
+ value=0,
75
+ step=1,
76
+ key=f"sk_input_r{next_round}_{player}",
77
+ )
78
+
79
+ if st.button("Save Round", type="primary", key=f"sk_save_r{next_round}"):
80
+ st.session_state.sk_scores[next_round] = round_scores
81
+ st.rerun()
82
+ else:
83
+ st.success("Game complete! All 10 rounds played.")
84
+ _render_final_standings(players, scores, completed_rounds)
85
+
86
+
87
+ def _render_scoreboard(players, scores, completed_rounds):
88
+ header_cols = ["Round"] + players
89
+ rows_html = []
90
+
91
+ cumsum = {p: 0 for p in players}
92
+ prev_cumsum = {p: 0 for p in players}
93
+
94
+ for r in completed_rounds:
95
+ row = [f"**{r}**"]
96
+ for p in players:
97
+ pts = scores[r].get(p, 0)
98
+ prev_cumsum[p] = cumsum[p]
99
+ cumsum[p] += pts
100
+ change = cumsum[p] - prev_cumsum[p]
101
+ if change > 0:
102
+ indicator = f' <span style="color:green">&#9650; +{change}</span>'
103
+ elif change < 0:
104
+ indicator = f' <span style="color:red">&#9660; {change}</span>'
105
+ else:
106
+ indicator = ""
107
+ row.append(f"**{cumsum[p]}**{indicator}")
108
+ rows_html.append(row)
109
+
110
+ md = "| " + " | ".join(header_cols) + " |\n"
111
+ md += "| " + " | ".join(["---"] * len(header_cols)) + " |\n"
112
+ for row in rows_html:
113
+ md += "| " + " | ".join(str(c) for c in row) + " |\n"
114
+
115
+ st.markdown(md, unsafe_allow_html=True)
116
+
117
+
118
+ def _render_final_standings(players, scores, completed_rounds):
119
+ st.subheader("Final Standings")
120
+ totals = {p: sum(scores[r].get(p, 0) for r in completed_rounds) for p in players}
121
+ ranked = sorted(totals.items(), key=lambda x: x[1], reverse=True)
122
+ for i, (player, total) in enumerate(ranked):
123
+ medal = ["1st", "2nd", "3rd"][i] if i < 3 else f"{i+1}th"
124
+ st.write(f"**{medal}** - {player}: **{total}** points")
125
+
126
+
127
+ # Page entry point
128
+ st.title("Skull King")
129
+ _init_state()
130
+
131
+ if not st.session_state.sk_game_started:
132
+ _setup_phase()
133
+ else:
134
+ _game_phase()