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