Spaces:
Sleeping
Sleeping
Upload 4 files
Browse files- app.py +10 -0
- pages/dungeon_draft.py +338 -0
- pages/home.py +30 -0
- 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">▲ +{change}</span>'
|
| 316 |
+
elif change < 0:
|
| 317 |
+
indicator = f' <span style="color:red">▼ {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">▲ +{change}</span>'
|
| 103 |
+
elif change < 0:
|
| 104 |
+
indicator = f' <span style="color:red">▼ {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()
|