Align crew hand sizes and card refresh with the rules
This commit is contained in:
@@ -20,7 +20,7 @@
|
|||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
- [x] Check against rules on correct quantity of of obstacles and obstacle refresh 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
|
- [ ] 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
|
- [ ] 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
|
- [ ] Double check if PvP obstacles should allow card re-draw
|
||||||
|
|||||||
@@ -6,10 +6,11 @@
|
|||||||
// - Add a CHANGELOG entry only when a commit changes something players can
|
// - Add a CHANGELOG entry only when a commit changes something players can
|
||||||
// see. Skip refactors, tests, and tooling. Keep wording player-facing.
|
// 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, ...] }.
|
// Newest first. Each entry: { version, date: 'YYYY-MM-DD', changes: [string, ...] }.
|
||||||
export const CHANGELOG = [
|
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: 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: 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.'] },
|
{ version: 30, date: '2026-09-04', changes: ['The crew roster shows voting progress with icons.', 'Your submitted vote now shows the crewmate you nominated.'] },
|
||||||
|
|||||||
+5
-13
@@ -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
|
- Middle Rank Pi-Rat(s): max 3 cards
|
||||||
- Lowest Rank Pi-Rat(s): max 2 cards
|
- Lowest Rank Pi-Rat(s): max 2 cards
|
||||||
If everyone shares a Rank, they are all 'highest' and get 4 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
|
Compare the crew's Pi-Rats regardless of who currently plays the Deep.
|
||||||
only matters for the between-scenes redraw) are ranked against all players.
|
Pending recruits do not yet have a Pi-Rat to compare.
|
||||||
"""
|
"""
|
||||||
if player.role == "deep":
|
pool = [p for p in players_in_scene if not p.needs_reroll]
|
||||||
pool = players_in_scene
|
|
||||||
else:
|
|
||||||
pool = [p for p in players_in_scene if p.role != "deep"]
|
|
||||||
if not pool:
|
if not pool:
|
||||||
pool = [player]
|
pool = [player]
|
||||||
|
|
||||||
@@ -208,19 +205,14 @@ def reshuffle_discard_pile(db: Session, game: Game):
|
|||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
def draw_cards_for_player(db: Session, game: Game, player: Player, count: int) -> List[str]:
|
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)
|
deck = get_game_deck(game)
|
||||||
hand = get_player_hand(player)
|
hand = get_player_hand(player)
|
||||||
drawn = []
|
drawn = []
|
||||||
|
|
||||||
for _ in range(count):
|
for _ in range(count):
|
||||||
if not deck:
|
if not deck:
|
||||||
# Reshuffle discard pile
|
break
|
||||||
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
|
|
||||||
card = deck.pop(0)
|
card = deck.pop(0)
|
||||||
hand.append(card)
|
hand.append(card)
|
||||||
drawn.append(card)
|
drawn.append(card)
|
||||||
|
|||||||
@@ -197,17 +197,14 @@ def confirm_deep_refresh(db: Session, player_id: str, discard_cards: List[str]):
|
|||||||
if not game:
|
if not game:
|
||||||
return
|
return
|
||||||
|
|
||||||
# 1. Discard cards
|
if game.phase != "deep_upkeep" or player.previous_role != "deep" or player.is_ready:
|
||||||
hand = get_player_hand(player)
|
return
|
||||||
deck = get_game_deck(game)
|
|
||||||
|
|
||||||
|
# Discards stay out of the deck until the next scene's shuffle.
|
||||||
|
hand = get_player_hand(player)
|
||||||
for card in discard_cards:
|
for card in discard_cards:
|
||||||
if card in hand:
|
if card in hand:
|
||||||
hand.remove(card)
|
hand.remove(card)
|
||||||
deck.append(card)
|
|
||||||
|
|
||||||
random.shuffle(deck)
|
|
||||||
set_game_deck(game, deck)
|
|
||||||
set_player_hand(player, hand)
|
set_player_hand(player, hand)
|
||||||
db.add(game)
|
db.add(game)
|
||||||
db.add(player)
|
db.add(player)
|
||||||
|
|||||||
+41
-7
@@ -512,6 +512,7 @@ def test_transition_to_deep_upkeep(session):
|
|||||||
def test_confirm_deep_refresh(session):
|
def test_confirm_deep_refresh(session):
|
||||||
game = crud.create_game(session)
|
game = crud.create_game(session)
|
||||||
p1 = crud.add_player(session, game.id, "P1")
|
p1 = crud.add_player(session, game.id, "P1")
|
||||||
|
game.phase = "deep_upkeep"
|
||||||
p1.previous_role = "deep"
|
p1.previous_role = "deep"
|
||||||
p1.role = "deep"
|
p1.role = "deep"
|
||||||
p1.rank = 2
|
p1.rank = 2
|
||||||
@@ -548,11 +549,11 @@ def test_confirm_deep_refresh(session):
|
|||||||
assert "7H" in hand
|
assert "7H" in hand
|
||||||
assert len(hand) == 4
|
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)
|
deck = json.loads(game.deck_cards)
|
||||||
assert "2C" in deck
|
assert "2C" not in deck
|
||||||
assert "3C" in deck
|
assert "3C" not in deck
|
||||||
assert deck == ["8H", "2C", "3C"]
|
assert deck == ["8H"]
|
||||||
|
|
||||||
# Since P1 was the only resting deep player and is now ready, phase should have advanced to scene_setup!
|
# Since P1 was the only resting deep player and is now ready, phase should have advanced to scene_setup!
|
||||||
assert game.phase == "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):
|
def test_rank_up_draws_immediately(session):
|
||||||
game, deep, (p2, p3) = make_scene_game(session, num_pirats=2)
|
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))
|
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)
|
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
|
# p2 jumped from lowest (2 cards) to highest-tied (4 cards): draws 2 immediately
|
||||||
assert len(crud.get_player_hand(p2)) == hand_before + 2
|
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 len(game.obstacles) == 2
|
||||||
assert game.extra_obstacles == 1
|
assert game.extra_obstacles == 1
|
||||||
assert crud.get_game_deck(game) == ["4H"]
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user