diff --git a/TODO.md b/TODO.md
index 43cf658..6515312 100644
--- a/TODO.md
+++ b/TODO.md
@@ -10,7 +10,7 @@
- [x] Visual feedback confirming hand refresh in Deep Upkeep
-- [ ] On the screen after pirat rank up voting, it should display the winner
+- [x] On the screen after pirat rank up voting, it should display the winner
- [ ] When Deep issues a challenge, stakes should be two fields, success and failure
diff --git a/frontend/src/components/UpkeepPhase.svelte b/frontend/src/components/UpkeepPhase.svelte
index 513c7d7..3bef644 100644
--- a/frontend/src/components/UpkeepPhase.svelte
+++ b/frontend/src/components/UpkeepPhase.svelte
@@ -13,6 +13,10 @@
// Helpers
$: hand = state.player.hand_cards ? JSON.parse(state.player.hand_cards) : [];
+ // Winner of the rank-up vote, surfaced on the post-voting (deep_upkeep) screen.
+ $: rankUpWinner = state.game.last_rank_up_player_id
+ ? state.players.find(p => p.id === state.game.last_rank_up_player_id)
+ : null;
// A nominee must be another living, in-play Pi-Rat: not the voter, not a
// previous-scene Deep, and not dead / awaiting-recruit (they'd reset to
@@ -202,7 +206,11 @@
{#if state.game.phase === 'deep_upkeep'}
-
✔️ Voting complete! Results tallied.
+ {#if rankUpWinner}
+
🏆 {displayName(rankUpWinner)} won the vote and ranked up to Rank {rankUpWinner.rank}!
+ {:else}
+
✔️ Voting complete! No one ranked up this time.
+ {/if}
{:else if hasVoted}
diff --git a/src/pirats/crud_upkeep.py b/src/pirats/crud_upkeep.py
index b67b01b..1a87b70 100644
--- a/src/pirats/crud_upkeep.py
+++ b/src/pirats/crud_upkeep.py
@@ -83,24 +83,30 @@ def process_between_scenes_votes(db: Session, game: Game):
Then sets players who were Deep to discard and redraw.
"""
votes = game.votes
+ # Reset the previous scene's winner before tallying this one.
+ game.last_rank_up_player_id = None
+ db.add(game)
if not votes:
+ db.commit()
return
-
+
# Count votes
counts = {}
for v in votes:
counts[v.nominated_player_id] = counts.get(v.nominated_player_id, 0) + 1
-
+
if counts:
max_votes = max(counts.values())
winners = [pid for pid, val in counts.items() if val == max_votes]
-
+
# If there is a clear winner (or random in case of tie)
if winners:
winner_id = random.choice(winners)
winner = get_player(db, winner_id)
if winner:
change_player_rank(db, game, winner, 1)
+ game.last_rank_up_player_id = winner.id
+ db.add(game)
add_game_event(db, game.id, f"{winner.name} was voted to rank up! They are now Rank {winner.rank}.", kind="rank")
# Clear all votes
@@ -112,6 +118,7 @@ def begin_next_scene_setup(db: Session, game: Game):
"""Advances to the next scene's setup: bump the scene counter and clear roles."""
game.current_scene_number += 1
game.phase = "scene_setup"
+ game.last_rank_up_player_id = None # Stale once we leave the post-voting screen
for p in game.players:
p.role = None
p.is_ready = False
diff --git a/src/pirats/migrations/versions/3bbf6812c261_add_last_rank_up_player_id_to_game.py b/src/pirats/migrations/versions/3bbf6812c261_add_last_rank_up_player_id_to_game.py
new file mode 100644
index 0000000..6fc34a5
--- /dev/null
+++ b/src/pirats/migrations/versions/3bbf6812c261_add_last_rank_up_player_id_to_game.py
@@ -0,0 +1,32 @@
+"""add last_rank_up_player_id to game
+
+Revision ID: 3bbf6812c261
+Revises: 19ee9ae5ca0e
+Create Date: 2026-06-14 09:21:11.037751
+
+"""
+from alembic import op
+import sqlalchemy as sa
+import sqlmodel
+
+
+revision = '3bbf6812c261'
+down_revision = '19ee9ae5ca0e'
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('game', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('last_rank_up_player_id', sqlmodel.sql.sqltypes.AutoString(), nullable=True))
+
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('game', schema=None) as batch_op:
+ batch_op.drop_column('last_rank_up_player_id')
+
+ # ### end Alembic commands ###
diff --git a/src/pirats/models.py b/src/pirats/models.py
index 71a05fb..8f818dd 100644
--- a/src/pirats/models.py
+++ b/src/pirats/models.py
@@ -19,6 +19,7 @@ class Game(SQLModel, table=True):
completed_crew_3: bool = Field(default=False) # Commit Piracy
captain_player_id: Optional[str] = Field(default=None) # Persistent captaincy; set once the crew controls a ship
teaching_deep_player_id: Optional[str] = Field(default=None) # Teaching mode: an admin who seeds themselves as the forced Rank-3 Deep for the first scene (set in the lobby, honored when starting ranks are rolled)
+ last_rank_up_player_id: Optional[str] = Field(default=None) # Winner of the most recent rank-up vote, shown on the post-voting upkeep screen; cleared when the next scene begins
players: List["Player"] = Relationship(back_populates="game", cascade_delete=True)
obstacles: List["Obstacle"] = Relationship(back_populates="game", cascade_delete=True)