import asyncio import pytest from app import Card, GameError, Player, Room, create_deck_for_tests, rooms, run_auto_draw def make_room(player_count=3): players = [ Player(id=f"p{i}", name=f"P{i}", token=f"t{i}", connected=True) for i in range(player_count) ] return Room(code="TEST", host_player_id="p0", total_players=player_count, players=players) def test_deck_has_108_cards(): deck = create_deck_for_tests() assert len(deck) == 108 assert sum(1 for c in deck if c.color == "wild" and c.value == "wild") == 4 assert sum(1 for c in deck if c.color == "wild" and c.value == "wild_draw4") == 4 def test_start_game_with_15_players_is_legal(): room = make_room(15) event = room.start_game("p0") assert event["type"] == "game_started" assert room.phase == "playing" assert all(len(p.hand) == 7 for p in room.players) assert len(room.discard_pile) == 1 assert room.discard_pile[-1].color != "wild" assert len(room.deck) == 2 def test_reshuffle_keeps_top_discard(): room = make_room() top = Card("top", "red", "5") old = Card("old", "blue", "7") room.discard_pile = [old, top] room.deck = [] room.reshuffle_deck() assert room.discard_pile == [top] assert room.deck == [old] def test_normal_play_rules_and_draw_stacking(): room = make_room() room.phase = "playing" room.current_player_id = "p0" room.current_color = "red" room.discard_pile = [Card("top", "red", "3")] assert room.is_valid_play(Card("same_color", "red", "9")) assert room.is_valid_play(Card("same_value", "blue", "3")) assert room.is_valid_play(Card("wild", "wild", "wild")) assert not room.is_valid_play(Card("bad", "green", "8")) room.pending_draw = 2 room.discard_pile = [Card("draw_top", "red", "draw2")] assert room.is_valid_play(Card("stack2", "blue", "draw2")) assert room.is_valid_play(Card("stack4", "wild", "wild_draw4")) assert not room.is_valid_play(Card("number", "red", "3")) def test_strict_jump_in_requires_color_and_value(): room = make_room() room.phase = "playing" room.discard_pile = [Card("top", "red", "7")] assert room.is_strict_jump_in(Card("same", "red", "7")) assert not room.is_strict_jump_in(Card("value_only", "blue", "7")) assert not room.is_strict_jump_in(Card("color_only", "red", "8")) room.discard_pile = [Card("wild_top", "wild", "wild_draw4")] assert room.is_strict_jump_in(Card("wild_same", "wild", "wild_draw4")) assert not room.is_strict_jump_in(Card("wild_other", "wild", "wild")) def test_jump_in_interrupts_and_continues_from_jumper(): room = make_room(4) room.phase = "playing" room.current_player_id = "p0" room.current_color = "red" room.discard_pile = [Card("top", "red", "5")] room.players[2].hand = [Card("jump", "red", "5")] room.play_card("p2", "jump", expected_top_card_id="top", expected_version=0) assert room.current_player_id == "p3" assert room.discard_pile[-1].id == "jump" def test_jump_in_with_reverse_uses_jumper_as_turn_source(): room = make_room(4) room.phase = "playing" room.current_player_id = "p0" room.direction = 1 room.current_color = "green" room.discard_pile = [Card("top", "green", "reverse")] room.players[2].hand = [Card("jump", "green", "reverse")] room.play_card("p2", "jump", expected_top_card_id="top", expected_version=0) assert room.direction == -1 assert room.current_player_id == "p1" def test_second_simultaneous_jump_in_revalidates_top_card(): room = make_room(4) room.phase = "playing" room.current_player_id = "p0" room.current_color = "red" room.discard_pile = [Card("top", "red", "5")] room.players[2].hand = [Card("jump1", "red", "5")] room.players[3].hand = [Card("jump2", "red", "5")] room.play_card("p2", "jump1", expected_top_card_id="top", expected_version=0) with pytest.raises(GameError): room.version += 1 room.play_card("p3", "jump2", expected_top_card_id="top", expected_version=0) def test_bot_jump_in_revalidation_shape(): room = make_room(4) room.players[2].is_bot = True room.phase = "playing" room.current_player_id = "p0" room.current_color = "red" room.discard_pile = [Card("top", "red", "5")] bot_card = Card("bot_jump", "red", "5") room.players[2].hand = [bot_card] assert any(room.is_strict_jump_in(card) for card in room.players[2].hand) room.discard_pile.append(Card("changed", "blue", "9")) assert not any(room.is_strict_jump_in(card) for card in room.players[2].hand) def test_auto_draw_no_playable_human_draws_and_advances(): room = make_room() room.code = "AUTO1" room.phase = "playing" room.current_player_id = "p0" room.current_color = "red" room.discard_pile = [Card("top", "red", "5")] room.players[0].hand = [Card("bad", "green", "8")] room.players[1].hand = [Card("p1_playable", "red", "1")] room.deck = [Card("drawn", "blue", "9")] rooms[room.code] = room try: asyncio.run(run_auto_draw(room.code, "p0", room.version, "top", "red", 0, delay=0)) assert [card.id for card in room.players[0].hand] == ["bad", "drawn"] assert room.current_player_id == "p1" assert room.version == 1 finally: rooms.pop(room.code, None) def test_auto_draw_takes_pending_penalty_and_clears_it(): room = make_room() room.code = "AUTO2" room.phase = "playing" room.current_player_id = "p0" room.current_color = "red" room.pending_draw = 2 room.discard_pile = [Card("top", "red", "draw2")] room.players[0].hand = [Card("bad", "green", "8")] room.players[1].hand = [Card("p1_playable", "red", "1")] room.deck = [Card("drawn1", "blue", "9"), Card("drawn2", "yellow", "1")] rooms[room.code] = room try: asyncio.run(run_auto_draw(room.code, "p0", room.version, "top", "red", 2, delay=0)) assert len(room.players[0].hand) == 3 assert room.pending_draw == 0 assert room.current_player_id == "p1" finally: rooms.pop(room.code, None) def test_auto_draw_revalidates_before_drawing(): room = make_room() room.code = "AUTO3" room.phase = "playing" room.current_player_id = "p0" room.current_color = "red" room.discard_pile = [Card("top", "red", "5")] room.players[0].hand = [Card("playable", "red", "8")] room.deck = [Card("drawn", "blue", "9")] rooms[room.code] = room try: asyncio.run(run_auto_draw(room.code, "p0", room.version, "top", "red", 0, delay=0)) assert [card.id for card in room.players[0].hand] == ["playable"] assert room.current_player_id == "p0" room.players[0].hand = [Card("bad", "green", "8")] room.version += 1 asyncio.run(run_auto_draw(room.code, "p0", 0, "top", "red", 0, delay=0)) assert [card.id for card in room.players[0].hand] == ["bad"] assert room.current_player_id == "p0" finally: rooms.pop(room.code, None)