Spaces:
Sleeping
Sleeping
Commit ·
2377378
1
Parent(s): f7b1a7b
new tracker
Browse files- app.py +2 -1
- pages/home.py +1 -0
- pages/points_tracker.py +204 -0
app.py
CHANGED
|
@@ -5,6 +5,7 @@ st.set_page_config(page_title="Board Game Tracker", layout="wide")
|
|
| 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()
|
|
|
|
| 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 |
+
points_tracker_page = st.Page("pages/points_tracker.py", title="Points Tracker", url_path="points_tracker")
|
| 9 |
|
| 10 |
+
pg = st.navigation([home_page, skull_king_page, dungeon_draft_page, points_tracker_page])
|
| 11 |
pg.run()
|
pages/home.py
CHANGED
|
@@ -17,6 +17,7 @@ no data collection. Your game data lives in your browser session only.
|
|
| 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 |
|
|
|
|
| 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 |
+
| **Points Tracker** | Universal tracker for any points-based game. Set a winning score, enter round-by-round points, and view a cumulative progression chart. |
|
| 21 |
|
| 22 |
---
|
| 23 |
|
pages/points_tracker.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import altair as alt
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def _init_state():
|
| 7 |
+
if "pt_players" not in st.session_state:
|
| 8 |
+
st.session_state.pt_players = []
|
| 9 |
+
if "pt_game_started" not in st.session_state:
|
| 10 |
+
st.session_state.pt_game_started = False
|
| 11 |
+
if "pt_scores" not in st.session_state:
|
| 12 |
+
st.session_state.pt_scores = []
|
| 13 |
+
if "pt_winning_score" not in st.session_state:
|
| 14 |
+
st.session_state.pt_winning_score = 100
|
| 15 |
+
if "pt_game_name" not in st.session_state:
|
| 16 |
+
st.session_state.pt_game_name = ""
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _setup_phase():
|
| 20 |
+
st.subheader("Game Setup")
|
| 21 |
+
|
| 22 |
+
st.session_state.pt_game_name = st.text_input(
|
| 23 |
+
"Game name (optional)",
|
| 24 |
+
value=st.session_state.pt_game_name,
|
| 25 |
+
placeholder="e.g. Catan, Ticket to Ride…",
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
st.session_state.pt_winning_score = st.number_input(
|
| 29 |
+
"Winning score (points to win)",
|
| 30 |
+
min_value=1,
|
| 31 |
+
value=st.session_state.pt_winning_score,
|
| 32 |
+
step=10,
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
st.subheader("Players")
|
| 36 |
+
|
| 37 |
+
def _add_player():
|
| 38 |
+
name = st.session_state.pt_new_player.strip()
|
| 39 |
+
if name and name not in st.session_state.pt_players:
|
| 40 |
+
st.session_state.pt_players.append(name)
|
| 41 |
+
st.session_state.pt_new_player = ""
|
| 42 |
+
|
| 43 |
+
col1, col2 = st.columns([3, 1])
|
| 44 |
+
with col1:
|
| 45 |
+
st.text_input("Player name", key="pt_new_player")
|
| 46 |
+
with col2:
|
| 47 |
+
st.write("")
|
| 48 |
+
st.write("")
|
| 49 |
+
st.button("Add Player", key="pt_add_btn", on_click=_add_player)
|
| 50 |
+
|
| 51 |
+
if st.session_state.pt_players:
|
| 52 |
+
st.markdown("**Players:**")
|
| 53 |
+
for i, p in enumerate(st.session_state.pt_players):
|
| 54 |
+
col_name, col_del = st.columns([4, 1])
|
| 55 |
+
col_name.write(f"{i + 1}. {p}")
|
| 56 |
+
if col_del.button("Remove", key=f"pt_rm_{i}"):
|
| 57 |
+
st.session_state.pt_players.pop(i)
|
| 58 |
+
st.rerun()
|
| 59 |
+
|
| 60 |
+
if len(st.session_state.pt_players) >= 2:
|
| 61 |
+
if st.button("Start Game", type="primary"):
|
| 62 |
+
st.session_state.pt_game_started = True
|
| 63 |
+
st.session_state.pt_scores = []
|
| 64 |
+
st.rerun()
|
| 65 |
+
else:
|
| 66 |
+
st.info("Add at least 2 players to start.")
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _compute_cumulative(players, scores):
|
| 70 |
+
cumsum = {p: 0 for p in players}
|
| 71 |
+
history = []
|
| 72 |
+
for round_data in scores:
|
| 73 |
+
for p in players:
|
| 74 |
+
cumsum[p] += round_data.get(p, 0)
|
| 75 |
+
history.append(dict(cumsum))
|
| 76 |
+
return cumsum, history
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _render_scoreboard(players, scores, winning_score):
|
| 80 |
+
cumsum = {p: 0 for p in players}
|
| 81 |
+
|
| 82 |
+
header_cols = ["Round"] + players
|
| 83 |
+
md = "| " + " | ".join(header_cols) + " |\n"
|
| 84 |
+
md += "| " + " | ".join(["---"] * len(header_cols)) + " |\n"
|
| 85 |
+
|
| 86 |
+
for i, round_data in enumerate(scores):
|
| 87 |
+
row = [f"**{i + 1}**"]
|
| 88 |
+
for p in players:
|
| 89 |
+
pts = round_data.get(p, 0)
|
| 90 |
+
prev = cumsum[p]
|
| 91 |
+
cumsum[p] += pts
|
| 92 |
+
change = cumsum[p] - prev
|
| 93 |
+
if change > 0:
|
| 94 |
+
indicator = f' <span style="color:green">▲ +{change}</span>'
|
| 95 |
+
elif change < 0:
|
| 96 |
+
indicator = f' <span style="color:red">▼ {change}</span>'
|
| 97 |
+
else:
|
| 98 |
+
indicator = ""
|
| 99 |
+
winner_flag = " ★" if cumsum[p] >= winning_score else ""
|
| 100 |
+
row.append(f"**{cumsum[p]}**{indicator}{winner_flag}")
|
| 101 |
+
md += "| " + " | ".join(row) + " |\n"
|
| 102 |
+
|
| 103 |
+
st.markdown(md, unsafe_allow_html=True)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def _render_chart(players, scores):
|
| 107 |
+
if not scores:
|
| 108 |
+
return
|
| 109 |
+
|
| 110 |
+
cumsum = {p: 0 for p in players}
|
| 111 |
+
rows = [{"Round": 0, **{p: 0 for p in players}}]
|
| 112 |
+
for i, round_data in enumerate(scores):
|
| 113 |
+
for p in players:
|
| 114 |
+
cumsum[p] += round_data.get(p, 0)
|
| 115 |
+
rows.append({"Round": i + 1, **dict(cumsum)})
|
| 116 |
+
|
| 117 |
+
df = pd.DataFrame(rows)
|
| 118 |
+
df_long = df.melt(id_vars="Round", var_name="Player", value_name="Points")
|
| 119 |
+
|
| 120 |
+
chart = (
|
| 121 |
+
alt.Chart(df_long)
|
| 122 |
+
.mark_line(point=True)
|
| 123 |
+
.encode(
|
| 124 |
+
x=alt.X("Round:Q", axis=alt.Axis(tickMinStep=1)),
|
| 125 |
+
y=alt.Y("Points:Q"),
|
| 126 |
+
color=alt.Color("Player:N"),
|
| 127 |
+
tooltip=["Round", "Player", "Points"],
|
| 128 |
+
)
|
| 129 |
+
.properties(height=300)
|
| 130 |
+
.interactive()
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
st.altair_chart(chart, use_container_width=True)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def _game_phase():
|
| 137 |
+
players = st.session_state.pt_players
|
| 138 |
+
scores = st.session_state.pt_scores
|
| 139 |
+
winning_score = st.session_state.pt_winning_score
|
| 140 |
+
game_name = st.session_state.pt_game_name
|
| 141 |
+
|
| 142 |
+
if game_name:
|
| 143 |
+
st.subheader(f"Tracking: {game_name}")
|
| 144 |
+
|
| 145 |
+
col_reset, col_undo = st.columns([1, 1])
|
| 146 |
+
with col_reset:
|
| 147 |
+
if st.button("Reset Game"):
|
| 148 |
+
st.session_state.pt_game_started = False
|
| 149 |
+
st.session_state.pt_players = []
|
| 150 |
+
st.session_state.pt_scores = []
|
| 151 |
+
st.rerun()
|
| 152 |
+
with col_undo:
|
| 153 |
+
if scores and st.button("Undo Last Round"):
|
| 154 |
+
st.session_state.pt_scores.pop()
|
| 155 |
+
st.rerun()
|
| 156 |
+
|
| 157 |
+
cumsum, _ = _compute_cumulative(players, scores)
|
| 158 |
+
winners = [p for p in players if cumsum[p] >= winning_score]
|
| 159 |
+
|
| 160 |
+
if scores:
|
| 161 |
+
st.subheader("Scoreboard")
|
| 162 |
+
_render_scoreboard(players, scores, winning_score)
|
| 163 |
+
|
| 164 |
+
with st.expander("Points progression chart", expanded=False):
|
| 165 |
+
_render_chart(players, scores)
|
| 166 |
+
|
| 167 |
+
if winners:
|
| 168 |
+
st.success(f"Game over! Winner{'s' if len(winners) > 1 else ''}: **{', '.join(winners)}** reached {winning_score} points!")
|
| 169 |
+
st.subheader("Final Standings")
|
| 170 |
+
ranked = sorted(cumsum.items(), key=lambda x: x[1], reverse=True)
|
| 171 |
+
for i, (player, total) in enumerate(ranked):
|
| 172 |
+
medal = ["1st", "2nd", "3rd"][i] if i < 3 else f"{i + 1}th"
|
| 173 |
+
st.write(f"**{medal}** - {player}: **{total}** points")
|
| 174 |
+
return
|
| 175 |
+
|
| 176 |
+
next_round = len(scores) + 1
|
| 177 |
+
st.subheader(f"Round {next_round}")
|
| 178 |
+
st.caption(f"Target: {winning_score} points to win.")
|
| 179 |
+
|
| 180 |
+
round_scores = {}
|
| 181 |
+
cols = st.columns(len(players))
|
| 182 |
+
for i, player in enumerate(players):
|
| 183 |
+
with cols[i]:
|
| 184 |
+
current = cumsum[player]
|
| 185 |
+
round_scores[player] = st.number_input(
|
| 186 |
+
f"{player} ({current} pts)",
|
| 187 |
+
value=0,
|
| 188 |
+
step=1,
|
| 189 |
+
key=f"pt_input_r{next_round}_{player}",
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
if st.button("Save Round", type="primary", key=f"pt_save_r{next_round}"):
|
| 193 |
+
st.session_state.pt_scores.append(round_scores)
|
| 194 |
+
st.rerun()
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
# Page entry point
|
| 198 |
+
st.title("Points Tracker")
|
| 199 |
+
_init_state()
|
| 200 |
+
|
| 201 |
+
if not st.session_state.pt_game_started:
|
| 202 |
+
_setup_phase()
|
| 203 |
+
else:
|
| 204 |
+
_game_phase()
|