File size: 7,082 Bytes
9f14abf
 
86ff9a7
 
9f14abf
86ff9a7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f14abf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
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)