Dead Pi-Rat reroll rework

This commit is contained in:
2026-06-12 11:32:15 -07:00
parent df7a0cc83f
commit f61201144f
18 changed files with 908 additions and 460 deletions

View File

@@ -616,68 +616,102 @@ def test_become_ghost_flow(session):
finally:
app.dependency_overrides.clear()
def test_roll_new_character(session):
def test_recruit_creation_full_flow(session):
"""A dead Pi-Rat re-rolls: queued in between_scenes, created cooperatively
in the recruit_creation phase, then the game advances to scene_setup."""
game = crud.create_game(session)
p1 = crud.add_player(session, game.id, "P1")
p2 = crud.add_player(session, game.id, "P2") # helper player for delegation target
p2 = crud.add_player(session, game.id, "P2")
p3 = crud.add_player(session, game.id, "P3")
game.phase = "between_scenes"
game.current_scene_number = 2
p1.is_dead = True
p1.rank = 3
p1.hand_cards = json.dumps(["2C", "3D", "4H"])
p1.completed_personal_1 = True
p1.completed_personal_2 = True
p1.completed_personal_3 = True
session.add_all([p1, p2])
p1.name = "Dread Pirate Roberts"
session.add_all([game, p1, p2, p3])
session.commit()
crud.roll_new_character(
session,
game.id,
p1.id,
avatar_look="Shiny eyes",
avatar_smell="Spiced Rum",
first_words="No fear!",
good_at_math="Yes"
)
# 1. The dead player opts to re-roll (a living player can't)
ok, msg = crud.choose_reroll(session, p2.id)
assert not ok
ok, msg = crud.choose_reroll(session, p1.id)
assert ok, msg
session.refresh(p1)
assert p1.needs_reroll
assert p1.is_dead # still dead until the recruit is created
# 2. Upkeep ends (no resting Deep) -> detours into recruit_creation
crud.transition_to_deep_upkeep(session, game.id)
session.refresh(game)
session.refresh(p1)
assert game.phase == "recruit_creation"
assert game.current_scene_number == 2 # not incremented yet
# Activation reset the sheet and returned the old hand to the deck
assert p1.rank == 1
assert not p1.is_dead
assert not p1.completed_personal_1
assert p1.avatar_look == ""
assert p1.tech_jack == ""
assert json.loads(p1.hand_cards) == []
deck = json.loads(game.deck_cards)
assert all(c in deck for c in ["2C", "3D", "4H"])
# Crewmates were assigned to answer Like/Hate and write 3 techniques
assert p1.other_like_from_player_id in (p2.id, p3.id)
assert p1.other_hate_from_player_id in (p2.id, p3.id)
assert p1.other_like_from_player_id != p1.other_hate_from_player_id
slots = json.loads(p1.incoming_techniques)
assert len(slots) == 3
assert all(s["from_id"] in (p2.id, p3.id) for s in slots)
assert all(s["text"] == "" for s in slots)
# 3. Can't finalize before the crew has done its part
ok, msg = crud.finalize_recruit(session, p1.id, "a", "b", "c")
assert not ok
# 4. The recruit fills in their basic records
p1.avatar_look = "Shiny eyes"
p1.avatar_smell = "Spiced Rum"
p1.first_words = "No fear!"
p1.good_at_math = "Yes"
p1.name = crud.recruit_name(p1)
session.add(p1)
session.commit()
assert p1.name == "Recruit Spiced Rum"
# 5. Crewmates answer Like/Hate and write the techniques
crud.submit_delegated_answer(session, p1.id, "like", "Great hat", p1.other_like_from_player_id)
crud.submit_delegated_answer(session, p1.id, "hate", "Bad jokes", p1.other_hate_from_player_id)
techs = ["Plank Pirouette", "Bilge Bomb", "Rat King Roar"]
for i, s in enumerate(slots):
# The wrong crewmate can't fill someone else's slot
wrong = p3.id if s["from_id"] == p2.id else p2.id
ok, msg = crud.contribute_recruit_technique(session, wrong, p1.id, i, techs[i])
assert not ok
ok, msg = crud.contribute_recruit_technique(session, s["from_id"], p1.id, i, techs[i])
assert ok, msg
# 6. The recruit assigns J/Q/K and joins the crew
ok, msg = crud.finalize_recruit(session, p1.id, techs[1], techs[0], techs[2])
assert ok, msg
session.refresh(p1)
session.refresh(game)
# Verify properties
assert not p1.is_dead
assert not p1.is_ghost
assert not p1.completed_personal_1
assert not p1.completed_personal_2
assert not p1.completed_personal_3
assert p1.avatar_look == "Shiny eyes"
assert p1.avatar_smell == "Spiced Rum"
assert p1.first_words == "No fear!"
assert p1.good_at_math == "Yes"
# New recruits always start at Rank 1
assert p1.rank == 1
# Verify name is "Recruit Spiced Rum"
assert p1.name == "Recruit Spiced Rum"
# Verify techniques auto-assigned
assert p1.tech_jack != ""
assert p1.tech_queen != ""
assert p1.tech_king != ""
# Verify delegated targets exist (p2 is the only other player)
assert p1.other_like_from_player_id == p2.id
assert p1.other_hate_from_player_id == p2.id
# Verify hand was refreshed up to new max hand size
hand = json.loads(p1.hand_cards)
assert len(hand) > 0
# Old cards returned to deck
deck = json.loads(game.deck_cards)
assert "2C" in deck or "2C" in hand
assert "3D" in deck or "3D" in hand
assert "4H" in deck or "4H" in hand
assert not p1.needs_reroll
assert p1.tech_jack == techs[1]
assert p1.tech_queen == techs[0]
assert p1.tech_king == techs[2]
assert json.loads(p1.swapped_techniques) == techs
assert len(json.loads(p1.hand_cards)) > 0 # starting hand dealt
# Last recruit done -> next scene's setup begins
assert game.phase == "scene_setup"
assert game.current_scene_number == 3
def test_gat_tax_accept(session):
game, deep, (p2, p3) = make_scene_game(session, num_pirats=2)
@@ -1049,55 +1083,77 @@ def test_submit_techniques_rejects_collisions(session):
session.refresh(game)
assert game.phase == "swap_techniques"
def test_roll_new_character_smell_name_normalization(session):
def test_recruit_smell_name_normalization(session):
game = crud.create_game(session)
p1 = crud.add_player(session, game.id, "P1")
crud.add_player(session, game.id, "P2")
p1.is_dead = True
session.add(p1)
session.commit()
p1.avatar_smell = "suspiciously smelling of ink and sour lemons"
assert crud.recruit_name(p1) == "Recruit Ink and sour lemons"
crud.roll_new_character(
session,
game.id,
p1.id,
avatar_look="Shiny eyes",
avatar_smell="suspiciously smelling of ink and sour lemons",
first_words="No fear!",
good_at_math="Yes"
)
session.refresh(p1)
assert p1.name == "Recruit Ink and sour lemons"
def test_roll_new_character_avoids_technique_collisions(session):
def test_contribute_technique_rejects_collisions(session):
game = crud.create_game(session)
p1 = crud.add_player(session, game.id, "P1")
p2 = crud.add_player(session, game.id, "P2")
# Give the surviving player techniques straight from the suggestion pool
p2.tech_jack = cards.TECHNIQUE_SUGGESTIONS[0]
p2.tech_queen = cards.TECHNIQUE_SUGGESTIONS[1]
p2.tech_king = cards.TECHNIQUE_SUGGESTIONS[2]
p2.tech_jack = "Parabolic Boarding Bounce"
p1.is_dead = True
p1.needs_reroll = True
session.add_all([p1, p2])
session.commit()
# Shrink the pool so a collision would be guaranteed without the exclusion logic
import pirats.crud_character # noqa: F401 (module uses cards.TECHNIQUE_SUGGESTIONS)
original = cards.TECHNIQUE_SUGGESTIONS
cards.TECHNIQUE_SUGGESTIONS = original[:6]
try:
crud.roll_new_character(
session, game.id, p1.id,
avatar_look="Shiny eyes", avatar_smell="Spiced Rum",
first_words="No fear!", good_at_math="Yes"
)
finally:
cards.TECHNIQUE_SUGGESTIONS = original
crud.start_recruit_creation(session, game)
session.refresh(p1)
recruit_techs = {p1.tech_jack, p1.tech_queen, p1.tech_king}
assert recruit_techs == set(original[3:6])
slots = json.loads(p1.incoming_techniques)
assert all(s["from_id"] == p2.id for s in slots)
# A name already on another sheet is rejected (case-insensitively)
ok, msg = crud.contribute_recruit_technique(session, p2.id, p1.id, 0, "parabolic boarding bounce")
assert not ok
ok, msg = crud.contribute_recruit_technique(session, p2.id, p1.id, 0, "Bilge Bomb")
assert ok, msg
# A name already written into another of the recruit's slots is rejected too
ok, msg = crud.contribute_recruit_technique(session, p2.id, p1.id, 1, "Bilge Bomb")
assert not ok
def test_solo_recruit_gets_pool_techniques(session):
"""With no crewmates to ask, the recruit's techniques come from the pool."""
game = crud.create_game(session)
p1 = crud.add_player(session, game.id, "P1")
p1.is_dead = True
p1.needs_reroll = True
session.add(p1)
session.commit()
crud.start_recruit_creation(session, game)
session.refresh(p1)
slots = json.loads(p1.incoming_techniques)
assert len(slots) == 3
assert all(s["from_id"] is None for s in slots)
assert all(s["text"] in cards.TECHNIQUE_SUGGESTIONS for s in slots)
assert p1.other_like_from_player_id is None
def test_late_joiner_queued_for_recruit_creation(session):
game, deep, (p2,) = make_scene_game(session)
assert game.phase == "scene"
newbie = crud.add_player(session, game.id, "Latecomer")
assert newbie.needs_reroll
assert newbie.role is None
assert json.loads(newbie.hand_cards) == []
# Scene ends, everyone votes/readies, resting Deep refreshes...
crud.end_scene_and_transition(session, game.id)
crud.transition_to_deep_upkeep(session, game.id)
session.refresh(game)
assert game.phase == "deep_upkeep"
crud.confirm_deep_refresh(session, deep.id, [])
session.refresh(game)
# ...and instead of scene_setup we land in recruit_creation
assert game.phase == "recruit_creation"
session.refresh(newbie)
slots = json.loads(newbie.incoming_techniques)
assert len(slots) == 3
assert all(s["from_id"] in (deep.id, p2.id) for s in slots)
def test_reshuffle_keeps_open_pvp_temp_card_out_of_deck(session):
game, deep, (p2, p3) = make_scene_game(session, num_pirats=2)
@@ -1123,17 +1179,6 @@ def test_reshuffle_keeps_open_pvp_temp_card_out_of_deck(session):
crud.reshuffle_discard_pile(session, game)
assert "7C" in crud.get_game_deck(game)
def test_late_joiner_is_dealt_a_hand(session):
game, deep, (p2,) = make_scene_game(session)
assert game.phase == "scene"
newbie = crud.add_player(session, game.id, "Stowaway")
assert newbie.role == "pirat"
session.refresh(game)
expected = crud.calculate_max_hand_size(newbie, game.players, game.captain_player_id)
assert expected > 0
assert len(crud.get_player_hand(newbie)) == expected
def test_technique_suggestions_endpoint():
from fastapi.testclient import TestClient
from pirats.main import app