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' ▲ +{change}' elif change < 0: indicator = f' ▼ {change}' 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()