Files
pirats/tests/test_game.py
2026-06-11 14:39:41 -07:00

592 lines
18 KiB
Python

import pytest
import json
from sqlmodel import SQLModel, create_engine, Session
from pirats import cards
from pirats import crud
from pirats.models import Game, Player, Obstacle
# In-memory database for testing
@pytest.fixture(name="session")
def session_fixture():
engine = create_engine("sqlite://", connect_args={"check_same_thread": False})
SQLModel.metadata.create_all(engine)
with Session(engine) as session:
yield session
def test_card_parsing():
c_10d = cards.parse_card("10D")
assert c_10d["value"] == "10"
assert c_10d["suit"] == "D"
assert c_10d["color"] == "red"
assert c_10d["numeric_value"] == 10
assert not c_10d["is_joker"]
c_ac = cards.parse_card("AC")
assert c_ac["value"] == "A"
assert c_ac["suit"] == "C"
assert c_ac["color"] == "black"
assert c_ac["numeric_value"] == 1
c_kh = cards.parse_card("KH")
assert c_kh["value"] == "K"
assert c_kh["numeric_value"] == 13
c_joker = cards.parse_card("Joker1")
assert c_joker["is_joker"]
assert c_joker["color"] == "black"
def test_obstacle_info():
obs = cards.get_obstacle_info("7C")
assert obs["title"] == "Lustful Bean Whale"
assert "bean" in obs["description"].lower()
assert obs["initial_value"] == 7
assert obs["color"] == "black"
def test_game_creation(session):
game = crud.create_game(session)
assert game.id is not None
assert game.phase == "lobby"
assert len(crud.get_game_deck(game)) == 54
def test_character_creation_and_swap(session):
game = crud.create_game(session)
p1 = crud.add_player(session, game.id, "Carlos", is_creator=True)
p2 = crud.add_player(session, game.id, "Barry")
p3 = crud.add_player(session, game.id, "Jaspar")
session.refresh(game)
assert len(game.players) == 3
# Fill basic details
p1.avatar_look = "Bandana"
p2.avatar_look = "Eyepatch"
p3.avatar_look = "Pegleg"
session.add_all([p1, p2, p3])
session.commit()
# Submit techniques
crud.submit_techniques(session, p1.id, "Carlos Strike", "Hide", "Roll")
crud.submit_techniques(session, p2.id, "Barry Leap", "Bite", "Scream")
crud.submit_techniques(session, p3.id, "Jaspar Blast", "Shoot", "Run")
session.refresh(game)
# The swap should have automatically triggered because all 3 submitted
assert game.phase == "swap_techniques"
session.refresh(p1)
session.refresh(p2)
session.refresh(p3)
techs_p1 = json.loads(p1.swapped_techniques)
assert len(techs_p1) == 3
# Check that Carlos didn't get any of his own techniques
assert "Carlos Strike" not in techs_p1
# Now assign to face cards
crud.assign_face_card_techniques(session, p1.id, techs_p1[0], techs_p1[1], techs_p1[2])
crud.assign_face_card_techniques(session, p2.id, "T2", "T3", "T4")
crud.assign_face_card_techniques(session, p3.id, "T5", "T6", "T7")
session.refresh(game)
# All are ready, should transition to scene_setup and assign starting ranks!
assert game.phase == "scene_setup"
session.refresh(p1)
session.refresh(p2)
session.refresh(p3)
ranks = [p1.rank, p2.rank, p3.rank]
assert 3 in ranks
assert 1 in ranks
assert 2 in ranks
roles = [p1.role, p2.role, p3.role]
assert "deep" in roles
assert "pirat" in roles
def test_scene_start_and_challenges(session):
game = crud.create_game(session)
p1 = crud.add_player(session, game.id, "P1", is_creator=True)
p2 = crud.add_player(session, game.id, "P2")
# Bypass char creation for quick scene test
p1.rank = 3
p1.role = "deep"
p1.previous_role = "deep"
p2.rank = 1
p2.role = "pirat"
p2.previous_role = "pirat"
session.add_all([p1, p2])
session.commit()
# Confirm setup (1 Deep, 1 Pi-Rat)
success, msg = crud.confirm_scene_setup(session, game.id)
assert success
session.refresh(game)
assert game.phase == "scene"
assert len(game.obstacles) == 2 # Deeps (1) + 1 = 2 obstacles
obs_list = game.obstacles
obs = obs_list[0]
# Give p2 a known hand for testing
p2.hand_cards = json.dumps(["10D", "2H", "JS", "Joker1"])
session.add(p2)
session.commit()
# Play card: 10D (value 10) vs Obstacle. Let's make sure obstacle value is less than 10.
# Set obstacle value to 5
obs.current_value = 5
session.add(obs)
session.commit()
# Play 10D (red) on obstacle. Let's check original obstacle color.
# If original obstacle was red, we expect a card draw.
orig_is_red = cards.parse_card(obs.original_card)["color"] == "red"
deck_before = len(crud.get_game_deck(game))
ok, msg, res = crud.play_card_on_obstacle(session, p2.id, obs.id, "10D")
assert ok
assert res["success"]
assert "Success" in res["details"]
session.refresh(p2)
session.refresh(obs)
# Obstacle current value should update to 10
assert obs.current_value == 10
# Check if hand size matches card draw rule
# 10D was played (-1 card). If colors matched, drew 1 back (+1 card).
hand = crud.get_player_hand(p2)
if orig_is_red:
assert len(hand) == 4
assert res["drew_card"] is not None
else:
assert len(hand) == 3
assert res["drew_card"] is None
def test_secret_technique_auto_success(session):
game = crud.create_game(session)
p1 = crud.add_player(session, game.id, "P1")
p1.role = "pirat"
p1.rank = 2
p1.tech_jack = "Pocket Sand"
p1.hand_cards = json.dumps(["JS"])
session.add(p1)
obs = Obstacle(
game_id=game.id,
original_card="10C",
suit="C",
title="Knights",
current_value=10
)
session.add(obs)
session.commit()
# Play Jack. J is a face card -> auto success!
ok, msg, res = crud.play_card_on_obstacle(session, p1.id, obs.id, "JS")
assert ok
assert res["success"]
assert res["is_technique"]
assert res["tech_name"] == "Pocket Sand"
def test_joker_play(session):
game = crud.create_game(session)
# Remove Joker1 and 10C from deck to prevent flakiness
deck = json.loads(game.deck_cards)
if "Joker1" in deck:
deck.remove("Joker1")
if "10C" in deck:
deck.remove("10C")
game.deck_cards = json.dumps(deck)
session.add(game)
p1 = crud.add_player(session, game.id, "P1")
p1.role = "pirat"
p1.hand_cards = json.dumps(["Joker1"])
session.add(p1)
obs = Obstacle(
game_id=game.id,
original_card="10C",
suit="C",
title="Knights",
current_value=10
)
session.add(obs)
session.commit()
# Play Joker
ok, msg, res = crud.play_card_on_obstacle(session, p1.id, obs.id, "Joker1")
assert ok
assert res["is_joker"]
# Verify obstacle was deleted and replaced
session.refresh(game)
assert len(game.obstacles) == 1
assert game.obstacles[0].original_card != "10C"
def test_obstacle_success_count(session):
game = crud.create_game(session)
obs = Obstacle(
game_id=game.id,
original_card="10C",
suit="C",
title="Knights",
current_value=10,
played_cards=json.dumps([
{"card": "9H", "player_id": "p1", "player_name": "P1", "success": True},
{"card": "8S", "player_id": "p2", "player_name": "P2", "success": False},
{"card": "AH", "player_id": "p1", "player_name": "P1", "success": True}
])
)
session.add(obs)
session.commit()
assert obs.success_count == 2
def test_non_deep_player_treatment(session):
game = crud.create_game(session)
p1 = crud.add_player(session, game.id, "Captain Barnaby")
p2 = crud.add_player(session, game.id, "Crewmate Pip")
# p1 is deep, p2 is None (non-deep)
p1.role = "deep"
p2.role = None
session.add_all([p1, p2])
session.commit()
# Check captain status: p2 should be captain because p2 is the only non-deep player
assert crud.is_player_captain(p2, game.players)
assert not crud.is_player_captain(p1, game.players)
# Check hand size: p2 should participate in hand size calculations and get Captain privileges (+1)
assert crud.calculate_max_hand_size(p2, game.players) == 4
def test_set_role_endpoint():
from fastapi.testclient import TestClient
from sqlalchemy.pool import StaticPool
from sqlmodel import SQLModel, create_engine, Session
from pirats.main import app
from pirats import crud
# Setup in-memory DB with StaticPool to share connection across threads
engine = create_engine(
"sqlite://",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
SQLModel.metadata.create_all(engine)
session = Session(engine)
# Create game and player
game = crud.create_game(session)
player = crud.add_player(session, game.id, "TestPirate")
game.phase = "scene_setup"
session.add(game)
session.commit()
def get_session_override():
yield session
from pirats.database import get_session
app.dependency_overrides[get_session] = get_session_override
client = TestClient(app)
try:
response = client.post(f"/api/game/{game.id}/player/{player.id}/set-role", data={"role": "pirat"})
assert response.status_code == 200
assert response.json()["status"] == "ok"
# The database should be updated
session.refresh(player)
assert player.role == "pirat"
finally:
app.dependency_overrides.clear()
def test_game_creation_with_crew_name(session):
game = crud.create_game(session, crew_name="The Black Gats")
assert game.id is not None
assert game.crew_name == "The Black Gats"
assert game.phase == "lobby"
def test_create_game_endpoint():
from fastapi.testclient import TestClient
from sqlalchemy.pool import StaticPool
from sqlmodel import SQLModel, create_engine, Session, select
from pirats.main import app
from pirats import crud
# Setup in-memory DB
engine = create_engine(
"sqlite://",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
SQLModel.metadata.create_all(engine)
session = Session(engine)
def get_session_override():
yield session
from pirats.database import get_session
app.dependency_overrides[get_session] = get_session_override
client = TestClient(app, base_url="http://testserver")
try:
response = client.post("/api/game", data={"crew_name": "The Math Mutineers"})
assert response.status_code == 200
# Verify the database has the game with the crew name
from pirats.models import Game
games = session.exec(select(Game)).all()
assert len(games) == 1
assert games[0].crew_name == "The Math Mutineers"
finally:
app.dependency_overrides.clear()
def test_transition_to_deep_upkeep(session):
game = crud.create_game(session)
p1 = crud.add_player(session, game.id, "P1")
p2 = crud.add_player(session, game.id, "P2")
p1.previous_role = "deep"
p2.previous_role = "pirat"
session.add_all([p1, p2])
session.commit()
crud.transition_to_deep_upkeep(session, game.id)
session.refresh(game)
assert game.phase == "deep_upkeep"
def test_confirm_deep_refresh(session):
game = crud.create_game(session)
p1 = crud.add_player(session, game.id, "P1")
p1.previous_role = "deep"
p1.role = "deep"
p1.rank = 2
# Hand has 3 cards: ['2C', '3C', '4C']
p1.hand_cards = json.dumps(["2C", "3C", "4C"])
# Rest of deck has some known cards
game.deck_cards = json.dumps(["5H", "6H", "7H", "8H"])
session.add_all([game, p1])
session.commit()
# Stub shuffle to keep card order deterministic
import random
orig_shuffle = random.shuffle
random.shuffle = lambda x: None
try:
# Discard 2C and 3C
crud.confirm_deep_refresh(session, p1.id, ["2C", "3C"])
finally:
random.shuffle = orig_shuffle
session.refresh(p1)
session.refresh(game)
# Hand should not contain 2C and 3C, and should have drawn 5H and 6H
hand = json.loads(p1.hand_cards)
assert "2C" not in hand
assert "3C" not in hand
assert "4C" in hand
assert "5H" in hand
assert "6H" in hand
assert len(hand) == 3
# Discarded cards should be at the end of the deck
deck = json.loads(game.deck_cards)
assert "2C" in deck
assert "3C" in deck
assert deck == ["7H", "8H", "2C", "3C"]
# Since P1 was the only resting deep player and is now ready, phase should have advanced to scene_setup!
assert game.phase == "scene_setup"
def test_submit_vote_endpoint():
from fastapi.testclient import TestClient
from sqlalchemy.pool import StaticPool
from sqlmodel import SQLModel, create_engine, Session
from pirats.main import app
from pirats import crud
# Setup in-memory DB
engine = create_engine(
"sqlite://",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
SQLModel.metadata.create_all(engine)
session = Session(engine)
# Create game and players
game = crud.create_game(session)
p1 = crud.add_player(session, game.id, "P1")
p2 = crud.add_player(session, game.id, "P2")
game.phase = "between_scenes"
session.add(game)
session.commit()
def get_session_override():
yield session
from pirats.database import get_session
app.dependency_overrides[get_session] = get_session_override
client = TestClient(app)
try:
response = client.post(f"/api/game/{game.id}/player/{p1.id}/submit-vote", data={"nominated_id": p2.id})
assert response.status_code == 200
assert response.json()["status"] == "ok"
finally:
app.dependency_overrides.clear()
def test_single_eligible_deep_player(session):
game = crud.create_game(session)
p1 = crud.add_player(session, game.id, "P1")
p2 = crud.add_player(session, game.id, "P2")
p3 = crud.add_player(session, game.id, "P3")
# Scene number > 1
game.current_scene_number = 2
# 2 players were Deep in the last scene, 1 was Pi-Rat
p1.previous_role = "deep"
p2.previous_role = "deep"
p3.previous_role = "pirat"
# Set their current roles to something invalid first
# P3 is the only one eligible to be Deep, but plays Pi-Rat. This should fail!
p1.role = "pirat"
p2.role = "pirat"
p3.role = "pirat"
session.add_all([game, p1, p2, p3])
session.commit()
success, msg = crud.confirm_scene_setup(session, game.id)
assert not success
assert "is the only player eligible to play the Deep" in msg
# Now set P3 to play Deep. This should succeed!
p3.role = "deep"
session.add(p3)
session.commit()
success, msg = crud.confirm_scene_setup(session, game.id)
assert success
def test_player_death_toggle(session):
game = crud.create_game(session)
player = crud.add_player(session, game.id, "DeadRat")
assert not player.is_dead
assert not player.completed_personal_3
# Toggle objective 3
crud.toggle_objective(session, game.id, player.id, "personal_3", True)
session.refresh(player)
assert player.is_dead
assert player.completed_personal_3
# Toggle off
crud.toggle_objective(session, game.id, player.id, "personal_3", False)
session.refresh(player)
assert not player.is_dead
assert not player.completed_personal_3
def test_become_ghost_flow(session):
game = crud.create_game(session)
player = crud.add_player(session, game.id, "DeadRat")
player.is_dead = True
session.add(player)
session.commit()
from fastapi.testclient import TestClient
from pirats.main import app
from pirats.database import get_session
def get_session_override():
yield session
app.dependency_overrides[get_session] = get_session_override
client = TestClient(app)
try:
response = client.post(f"/api/game/{game.id}/player/{player.id}/become-ghost")
assert response.status_code == 200
session.refresh(player)
assert player.is_ghost
assert not player.is_dead
finally:
app.dependency_overrides.clear()
def test_roll_new_character(session):
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
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])
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"
)
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"
# Verify rank randomized between 1-3
assert p1.rank in [1, 2, 3]
# 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