boardgames_tracker / pages /dungeon_draft.py
RandomCatLover's picture
Upload 4 files
7d74b4c verified
import streamlit as st
import copy
def _init_state():
if "dd_players" not in st.session_state:
st.session_state.dd_players = []
if "dd_game_started" not in st.session_state:
st.session_state.dd_game_started = False
if "dd_track_damage" not in st.session_state:
st.session_state.dd_track_damage = False
# dd_rounds[round_idx] = {player: {"money": int, "damage": int, "vp": int}}
# round 0 = starting balance
if "dd_rounds" not in st.session_state:
st.session_state.dd_rounds = {}
# dd_pending[player] = {"money": int, "damage": int, "vp": int}
if "dd_pending" not in st.session_state:
st.session_state.dd_pending = {}
if "dd_finished" not in st.session_state:
st.session_state.dd_finished = set()
if "dd_current_round" not in st.session_state:
st.session_state.dd_current_round = 1
# Gold income per player applied AFTER each round is committed
if "dd_gold_income" not in st.session_state:
st.session_state.dd_gold_income = {}
# Starting balance per player (set during setup)
if "dd_starting_balance" not in st.session_state:
st.session_state.dd_starting_balance = {}
def _reset_pending():
st.session_state.dd_pending = {
p: {"money": 0, "damage": 0, "vp": 0}
for p in st.session_state.dd_players
}
st.session_state.dd_finished = set()
def _setup_phase():
st.subheader("Game Setup")
st.session_state.dd_track_damage = st.checkbox(
"Track damage per round (optional)", value=st.session_state.dd_track_damage
)
st.markdown("---")
st.subheader("Players")
def _dd_add_player():
name = st.session_state.dd_new_player.strip()
if name and name not in st.session_state.dd_players:
st.session_state.dd_players.append(name)
st.session_state.dd_gold_income[name] = 5
st.session_state.dd_starting_balance[name] = 9
st.session_state.dd_new_player = ""
col1, col2 = st.columns([3, 1])
with col1:
st.text_input("Player name", key="dd_new_player")
with col2:
st.write("")
st.write("")
st.button("Add Player", key="dd_add_btn", on_click=_dd_add_player)
if st.session_state.dd_players:
for i, p in enumerate(st.session_state.dd_players):
col_name, col_del = st.columns([4, 1])
col_name.write(f"{i + 1}. {p}")
if col_del.button("Remove", key=f"dd_rm_{i}"):
st.session_state.dd_players.pop(i)
st.session_state.dd_gold_income.pop(p, None)
st.session_state.dd_starting_balance.pop(p, None)
st.rerun()
st.markdown("---")
st.subheader("Starting Balance & Gold Income")
for p in st.session_state.dd_players:
col_bal, col_inc = st.columns(2)
with col_bal:
st.session_state.dd_starting_balance[p] = st.number_input(
f"{p} - Starting Gold",
value=st.session_state.dd_starting_balance.get(p, 0),
step=1,
min_value=0,
key=f"dd_setup_bal_{p}",
)
with col_inc:
st.session_state.dd_gold_income[p] = st.number_input(
f"{p} - Gold Income per Round",
value=st.session_state.dd_gold_income.get(p, 0),
step=1,
min_value=0,
key=f"dd_setup_inc_{p}",
)
if len(st.session_state.dd_players) >= 2:
if st.button("Start Game", type="primary"):
st.session_state.dd_game_started = True
# Round 0 = starting balance
st.session_state.dd_rounds = {
0: {
p: {
"money": st.session_state.dd_starting_balance.get(p, 0),
"damage": 0,
"vp": 0,
}
for p in st.session_state.dd_players
}
}
st.session_state.dd_current_round = 1
_reset_pending()
st.rerun()
else:
st.info("Add at least 2 players to start.")
def _game_phase():
players = st.session_state.dd_players
rounds = st.session_state.dd_rounds
current_round = st.session_state.dd_current_round
track_damage = st.session_state.dd_track_damage
if st.button("Reset Game"):
st.session_state.dd_game_started = False
st.session_state.dd_players = []
st.session_state.dd_rounds = {}
st.session_state.dd_pending = {}
st.session_state.dd_finished = set()
st.session_state.dd_current_round = 1
st.session_state.dd_gold_income = {}
st.session_state.dd_starting_balance = {}
st.rerun()
# Gold income settings in sidebar (adjustable during game)
with st.sidebar:
st.markdown("---")
st.subheader("Gold Income per Round")
st.caption("Applied after each round is committed.")
for p in players:
st.session_state.dd_gold_income[p] = st.number_input(
f"{p}",
value=st.session_state.dd_gold_income.get(p, 0),
step=1,
min_value=0,
key=f"dd_income_{p}",
)
# Scoreboard (always visible since round 0 exists)
completed_rounds = sorted(rounds.keys())
st.subheader("Scoreboard")
_render_scoreboard(players, rounds, completed_rounds, track_damage)
# Current round
st.subheader(f"Round {current_round}")
st.caption(
"Add/reduce values for each player. Changes are staged until you finish the player's turn. "
"Use the Pirate Card section to steal from other players."
)
pending = st.session_state.dd_pending
finished = st.session_state.dd_finished
tabs = st.tabs(players)
for idx, player in enumerate(players):
with tabs[idx]:
is_finished = player in finished
if is_finished:
st.success(f"{player}'s turn is finished for this round.")
st.write(f"Money: **{pending[player]['money']:+d}**")
if track_damage:
st.write(f"Damage: **{pending[player]['damage']:+d}**")
st.write(f"Victory Points: **{pending[player]['vp']:+d}**")
continue
st.markdown(f"**{player}'s Turn**")
# Show current cumulative balance
cum = _get_cumulative(player, rounds, completed_rounds)
bal_parts = [f"Money: **{cum['money']}**", f"VP: **{cum['vp']}**"]
if track_damage:
bal_parts.append(f"Damage: **{cum['damage']}**")
st.caption("Current balance: " + " | ".join(bal_parts))
col_money, col_vp = st.columns(2)
with col_money:
money_change = st.number_input(
"Money +/-",
value=0,
step=1,
key=f"dd_money_{current_round}_{player}",
)
with col_vp:
vp_change = st.number_input(
"Victory Points +/-",
value=0,
step=1,
key=f"dd_vp_{current_round}_{player}",
)
damage_change = 0
if track_damage:
damage_change = st.number_input(
"Damage +/-",
value=0,
step=1,
key=f"dd_dmg_{current_round}_{player}",
)
if st.button("Stage Changes", key=f"dd_stage_{current_round}_{player}"):
pending[player]["money"] += money_change
pending[player]["vp"] += vp_change
if track_damage:
pending[player]["damage"] += damage_change
st.rerun()
if any(v != 0 for v in pending[player].values()):
st.markdown("**Staged changes:**")
st.write(f"Money: {pending[player]['money']:+d}")
if track_damage:
st.write(f"Damage: {pending[player]['damage']:+d}")
st.write(f"VP: {pending[player]['vp']:+d}")
# Pirate card
st.markdown("---")
show_pirate = st.checkbox(
"Pirate Card - Steal", key=f"dd_pirate_show_{current_round}_{player}"
)
other_players = [p for p in players if p != player]
if show_pirate and other_players:
steal_target = st.selectbox(
"Steal from",
other_players,
key=f"dd_pirate_target_{current_round}_{player}",
)
steal_col1, steal_col2 = st.columns(2)
with steal_col1:
steal_type = st.selectbox(
"Resource",
["money", "vp"] + (["damage"] if track_damage else []),
key=f"dd_pirate_type_{current_round}_{player}",
)
with steal_col2:
steal_amount = st.number_input(
"Amount to steal",
min_value=0,
value=0,
step=1,
key=f"dd_pirate_amt_{current_round}_{player}",
)
if st.button("Steal", key=f"dd_pirate_btn_{current_round}_{player}"):
if steal_amount > 0:
pending[player][steal_type] += steal_amount
pending[steal_target][steal_type] -= steal_amount
st.success(
f"{player} stole {steal_amount} {steal_type} from {steal_target}!"
)
st.rerun()
st.markdown("---")
if st.button(
"Finish Turn", type="primary", key=f"dd_finish_{current_round}_{player}"
):
st.session_state.dd_finished.add(player)
st.rerun()
# Commit round when all done
if len(finished) == len(players):
st.markdown("---")
st.info("All players have finished their turns.")
if st.button("Commit Round", type="primary", key=f"dd_commit_{current_round}"):
# Save the round's changes
round_data = copy.deepcopy(pending)
# Add gold income as a separate entry after the round
gold_income = st.session_state.dd_gold_income
for p in players:
round_data[p]["money"] += gold_income.get(p, 0)
st.session_state.dd_rounds[current_round] = round_data
st.session_state.dd_current_round += 1
_reset_pending()
st.rerun()
def _get_cumulative(player, rounds, completed_rounds):
cum = {"money": 0, "damage": 0, "vp": 0}
for r in completed_rounds:
for k in cum:
cum[k] += rounds[r][player].get(k, 0)
return cum
def _render_scoreboard(players, rounds, completed_rounds, track_damage):
metrics = ["Money", "VP"] + (["Damage"] if track_damage else [])
metric_keys = ["money", "vp"] + (["damage"] if track_damage else [])
header = ["Round"]
for p in players:
for m in metrics:
header.append(f"{p} - {m}")
cumsum = {p: {k: 0 for k in metric_keys} for p in players}
prev_cumsum = {p: {k: 0 for k in metric_keys} for p in players}
rows = []
for r in completed_rounds:
label = "Start" if r == 0 else str(r)
row = [f"**{label}**"]
for p in players:
for k in metric_keys:
prev_cumsum[p][k] = cumsum[p][k]
val = rounds[r][p].get(k, 0)
cumsum[p][k] += val
change = cumsum[p][k] - prev_cumsum[p][k]
if change > 0:
indicator = f' <span style="color:green">&#9650; +{change}</span>'
elif change < 0:
indicator = f' <span style="color:red">&#9660; {change}</span>'
else:
indicator = ""
row.append(f"**{cumsum[p][k]}**{indicator}")
rows.append(row)
md = "| " + " | ".join(header) + " |\n"
md += "| " + " | ".join(["---"] * len(header)) + " |\n"
for row in rows:
md += "| " + " | ".join(str(c) for c in row) + " |\n"
st.markdown(md, unsafe_allow_html=True)
# Page entry point
st.title("Dungeon Draft")
_init_state()
if not st.session_state.dd_game_started:
_setup_phase()
else:
_game_phase()