From 673a26f2715a6a92060114d98a57ab27223d068d Mon Sep 17 00:00:00 2001 From: Tim McCarthy Date: Fri, 4 Sep 2026 19:22:52 -0700 Subject: [PATCH] Align crew hand sizes and card refresh with the rules --- TODO.md | 2 +- frontend/src/lib/changelog.js | 3 ++- src/pirats/crud_base.py | 18 ++++--------- src/pirats/crud_upkeep.py | 11 +++----- tests/test_game.py | 48 ++++++++++++++++++++++++++++++----- 5 files changed, 53 insertions(+), 29 deletions(-) diff --git a/TODO.md b/TODO.md index 3c10f20..359df7f 100644 --- a/TODO.md +++ b/TODO.md @@ -20,7 +20,7 @@ ## Rules - [x] Check against rules on correct quantity of of obstacles and obstacle refresh rules -- [ ] Check against the rules on pi-rat hand size and conditions for drawing more cards +- [x] Check against the rules on pi-rat hand size and conditions for drawing more cards - [ ] Assisting other pi-rats should only be available if there are multiple obstacles in the current challenge - [ ] Checking off personal objectives (gat/name/death) should be handled by group vote rather than adjudication by the deep - [ ] Double check if PvP obstacles should allow card re-draw diff --git a/frontend/src/lib/changelog.js b/frontend/src/lib/changelog.js index e6b5810..b8c1eb5 100644 --- a/frontend/src/lib/changelog.js +++ b/frontend/src/lib/changelog.js @@ -6,10 +6,11 @@ // - Add a CHANGELOG entry only when a commit changes something players can // see. Skip refactors, tests, and tooling. Keep wording player-facing. -export const VERSION = 32; +export const VERSION = 33; // Newest first. Each entry: { version, date: 'YYYY-MM-DD', changes: [string, ...] }. export const CHANGELOG = [ + { version: 33, date: '2026-09-04', changes: ['Hand sizes compare ranks across the crew, including Pi-Rats whose players are the Deep.', 'Discarded cards return to the deck at scene setup. Only the previous Deep can refresh their hand during upkeep, once per scene.'] }, { version: 32, date: '2026-09-04', changes: ['Obstacles are automatically discarded with their columns once they reach one success per player. Unfinished obstacles carry over between scenes.'] }, { version: 31, date: '2026-09-04', changes: ['Change your rank-up vote until everyone has voted. The final vote now reveals the result and advances automatically, without a Ready button.', 'The vote result stays visible through upkeep and next-scene setup.'] }, { version: 30, date: '2026-09-04', changes: ['The crew roster shows voting progress with icons.', 'Your submitted vote now shows the crewmate you nominated.'] }, diff --git a/src/pirats/crud_base.py b/src/pirats/crud_base.py index 4f514af..5c813a6 100644 --- a/src/pirats/crud_base.py +++ b/src/pirats/crud_base.py @@ -92,13 +92,10 @@ def calculate_max_hand_size(player: Player, players_in_scene: List[Player], capt - Middle Rank Pi-Rat(s): max 3 cards - Lowest Rank Pi-Rat(s): max 2 cards If everyone shares a Rank, they are all 'highest' and get 4 cards. - Pi-Rats are ranked against the Pi-Rats in the scene; Deep players (whose hand size - only matters for the between-scenes redraw) are ranked against all players. + Compare the crew's Pi-Rats regardless of who currently plays the Deep. + Pending recruits do not yet have a Pi-Rat to compare. """ - if player.role == "deep": - pool = players_in_scene - else: - pool = [p for p in players_in_scene if p.role != "deep"] + pool = [p for p in players_in_scene if not p.needs_reroll] if not pool: pool = [player] @@ -208,19 +205,14 @@ def reshuffle_discard_pile(db: Session, game: Game): db.commit() def draw_cards_for_player(db: Session, game: Game, player: Player, count: int) -> List[str]: - """Draws count cards from the deck for a player, reshuffling if necessary.""" + """Draw from the remaining deck; discards return only at scene setup.""" deck = get_game_deck(game) hand = get_player_hand(player) drawn = [] for _ in range(count): if not deck: - # Reshuffle discard pile - reshuffle_discard_pile(db, game) - deck = get_game_deck(game) - if not deck: - # If still empty (all 54 cards are in hands or active), we can't draw - break + break card = deck.pop(0) hand.append(card) drawn.append(card) diff --git a/src/pirats/crud_upkeep.py b/src/pirats/crud_upkeep.py index 527e58a..e744f4a 100644 --- a/src/pirats/crud_upkeep.py +++ b/src/pirats/crud_upkeep.py @@ -197,17 +197,14 @@ def confirm_deep_refresh(db: Session, player_id: str, discard_cards: List[str]): if not game: return - # 1. Discard cards + if game.phase != "deep_upkeep" or player.previous_role != "deep" or player.is_ready: + return + + # Discards stay out of the deck until the next scene's shuffle. hand = get_player_hand(player) - deck = get_game_deck(game) - for card in discard_cards: if card in hand: hand.remove(card) - deck.append(card) - - random.shuffle(deck) - set_game_deck(game, deck) set_player_hand(player, hand) db.add(game) db.add(player) diff --git a/tests/test_game.py b/tests/test_game.py index 09e4420..2ccf2a9 100644 --- a/tests/test_game.py +++ b/tests/test_game.py @@ -512,6 +512,7 @@ def test_transition_to_deep_upkeep(session): def test_confirm_deep_refresh(session): game = crud.create_game(session) p1 = crud.add_player(session, game.id, "P1") + game.phase = "deep_upkeep" p1.previous_role = "deep" p1.role = "deep" p1.rank = 2 @@ -548,11 +549,11 @@ def test_confirm_deep_refresh(session): assert "7H" in hand assert len(hand) == 4 - # Discarded cards should be at the end of the deck + # Discarded cards stay out until scene setup deck = json.loads(game.deck_cards) - assert "2C" in deck - assert "3C" in deck - assert deck == ["8H", "2C", "3C"] + assert "2C" not in deck + assert "3C" not in deck + assert deck == ["8H"] # Since P1 was the only resting deep player and is now ready, phase should have advanced to scene_setup! assert game.phase == "scene_setup" @@ -1184,11 +1185,11 @@ def test_vote_skips_when_only_dead_options(session): def test_rank_up_draws_immediately(session): game, deep, (p2, p3) = make_scene_game(session, num_pirats=2) - # p2 is rank 1 (lowest -> max 2), p3 is rank 2 (highest -> max 4) + # p2 starts lowest. Reaching rank 3 ties the Deep for highest. hand_before = len(crud.get_player_hand(p2)) - crud.toggle_objective(session, game.id, p2.id, "personal_1", True) + crud.change_player_rank(session, game, p2, 2) session.refresh(p2) - assert p2.rank == 2 + assert p2.rank == 3 # p2 jumped from lowest (2 cards) to highest-tied (4 cards): draws 2 immediately assert len(crud.get_player_hand(p2)) == hand_before + 2 @@ -2692,3 +2693,36 @@ def test_setup_joker_only_increases_following_scene(session, monkeypatch): assert len(game.obstacles) == 2 assert game.extra_obstacles == 1 assert crud.get_game_deck(game) == ["4H"] + + +def test_hand_sizes_use_crew_ranks_and_ignore_role_changes(session): + game, deep, (low, middle) = make_scene_game(session, num_pirats=2) + assert [crud.calculate_max_hand_size(p, game.players) for p in (low, middle, deep)] == [2, 3, 4] + low.role, deep.role = "deep", "pirat" + assert [crud.calculate_max_hand_size(p, game.players) for p in (low, middle, deep)] == [2, 3, 4] + + +def test_draw_stops_at_empty_deck_without_recycling(session): + game, deep, (rat,) = make_scene_game(session) + rat.hand_cards = '[]' + game.deck_cards = '["AH"]' + session.commit() + assert crud.draw_cards_for_player(session, game, rat, 3) == ["AH"] + assert crud.get_player_hand(rat) == ["AH"] + assert crud.get_game_deck(game) == [] + + +def test_deep_refresh_does_not_recycle_discards_or_allow_pirats(session): + game, deep, (rat,) = make_scene_game(session) + game.phase = "deep_upkeep" + deep.hand_cards = '["KC"]' + rat.hand_cards = '["QC"]' + game.deck_cards = '["AH", "2H", "3H", "4H"]' + session.commit() + crud.confirm_deep_refresh(session, rat.id, ["QC"]) + assert crud.get_player_hand(rat) == ["QC"] + crud.confirm_deep_refresh(session, deep.id, ["KC"]) + assert crud.get_player_hand(deep) == ["AH", "2H", "3H", "4H"] + assert "KC" not in crud.get_game_deck(game) + crud.confirm_deep_refresh(session, deep.id, ["AH"]) + assert "AH" in crud.get_player_hand(deep)