Spaces:
Sleeping
Sleeping
| 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">▲ +{change}</span>' | |
| elif change < 0: | |
| indicator = f' <span style="color:red">▼ {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() | |