Coverage for tests / test_core / test_game.py: 100.0%
370 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-05 20:45 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-05 20:45 +0000
1"""Tests for beaverbunch.core.game — the main game engine."""
3import pytest
5from beaverbunch.core.actions import (
6 DiscardDrawnAction,
7 PeekOwnCardAction,
8 ReplaceCardAction,
9 SnapAction,
10 SwapCardAction,
11)
12from beaverbunch.core.card import Card, FaceCard, Suit
13from beaverbunch.core.game import Game, GamePhase, GameState
14from beaverbunch.core.game_settings import GameSettings
15from beaverbunch.core.hand import CardSlot, Hand
16from beaverbunch.core.player import Player
17from beaverbunch.core.rules import RuleViolation
20# ---------------------------------------------------------------------------
21# Helpers
22# ---------------------------------------------------------------------------
24def _card(value: int, suit: Suit = Suit.HEARTS) -> Card:
25 """Shorthand for creating a card (value 0 → Joker)."""
26 if value == 0:
27 return Card(suit=None, value=0)
28 return Card(suit=suit, value=value)
31def _setup_game(
32 num_players: int = 2,
33 hand_size: int = 4,
34 deck_cards: list[Card] | None = None,
35) -> Game:
36 """
37 Create a Game with players added and a deterministic deck, ready to start().
39 NOTE: start() shuffles the deck, so for tests needing deterministic card order,
40 use _setup_game_manual() instead.
41 """
42 if deck_cards is None:
43 deck_cards = [_card(v % 13 + 1) for v in range(54)]
45 settings = GameSettings(
46 initial_hand_size=hand_size,
47 initial_cards_known=2,
48 deck_factory=lambda: list(deck_cards),
49 )
50 state = GameState()
51 for i in range(num_players):
52 state.add_player(Player(name=f"P{i}", hand=Hand()))
53 return Game(state=state, settings=settings)
56def _setup_game_manual(
57 player_hands: dict[str, list[Card]],
58 deck_remaining: list[Card],
59 discard_pile: list[Card],
60) -> Game:
61 """
62 Create a Game with pre-set hands, deck, and discard pile.
63 Skips start() entirely — sets phase to WAITING_FOR_DRAW directly.
64 All players have already 'peeked'.
65 """
66 state = GameState()
67 for name, cards in player_hands.items():
68 hand = Hand(slots=[CardSlot(card=c) for c in cards])
69 # Mark first 2 as known (simulating initial peek)
70 for i in range(min(2, len(hand.slots))):
71 hand.slots[i].known = True
72 state.add_player(Player(name=name, hand=hand))
74 from beaverbunch.core.deck import Deck
75 state.deck = Deck(cards=list(deck_remaining))
76 state.discard_pile = list(discard_pile)
77 state.phase = GamePhase.WAITING_FOR_DRAW
78 state.initial_peeks_done = {name for name in player_hands}
80 return Game(state=state)
83def _peek_all(game: Game) -> None:
84 for p in game.state.players:
85 game.peek_initial(p.name, [0, 1])
88# ===================================================================
89# Setup & Initial Peek
90# ===================================================================
92class TestGameSetup:
93 def test_start_deals_correct_hand_size(self):
94 game = _setup_game(num_players=3, hand_size=4)
95 game.start()
96 for p in game.state.players:
97 assert len(p.hand) == 4
99 def test_start_places_one_discard(self):
100 game = _setup_game()
101 game.start()
102 assert len(game.state.discard_pile) == 1
104 def test_start_reduces_deck(self):
105 """Deck should have 54 - (2*4) - 1 = 45 cards left."""
106 game = _setup_game(num_players=2, hand_size=4)
107 game.start()
108 assert len(game.state.deck) == 54 - 2 * 4 - 1
110 def test_start_phase_is_starting(self):
111 game = _setup_game()
112 game.start()
113 assert game.state.phase == GamePhase.STARTING
115 def test_start_fails_without_enough_players(self):
116 settings = GameSettings(min_players=2)
117 state = GameState()
118 state.add_player(Player(name="P0", hand=Hand()))
119 game = Game(state=state, settings=settings)
120 with pytest.raises(RuleViolation, match="Not enough players"):
121 game.start()
123 def test_start_fails_with_duplicate_names(self):
124 state = GameState()
125 state.add_player(Player(name="Alice", hand=Hand()))
126 state.add_player(Player(name="Alice", hand=Hand()))
127 game = Game(state=state)
128 with pytest.raises(RuleViolation, match="unique"):
129 game.start()
131 def test_empty_player_name_raises(self):
132 with pytest.raises(ValueError, match="empty"):
133 Player(name="", hand=Hand())
135 def test_start_fails_when_not_in_starting_phase(self):
136 game = _setup_game()
137 game.start()
138 _peek_all(game)
139 with pytest.raises(RuleViolation):
140 game.start()
143class TestGameStateHelpers:
144 def test_is_game_over_false_initially(self):
145 state = GameState()
146 assert state.is_game_over() is False
148 def test_is_game_over_true_when_finished(self):
149 state = GameState()
150 state.set_phase(GamePhase.FINISHED)
151 assert state.is_game_over() is True
153 def test_current_player_returns_active_player(self):
154 game = _setup_game_manual(
155 player_hands={"P0": [_card(3)] * 4, "P1": [_card(7)] * 4},
156 deck_remaining=[_card(2)],
157 discard_pile=[_card(5)],
158 )
159 assert game.state.current_player.name == "P0"
160 game.state.current_player_index = 1
161 assert game.state.current_player.name == "P1"
164class TestInitialPeek:
165 def test_peek_marks_cards_known(self):
166 game = _setup_game()
167 game.start()
168 p = game.state.players[0]
169 game.peek_initial(p.name, [0, 1])
170 assert p.hand.slots[0].known is True
171 assert p.hand.slots[1].known is True
172 assert p.hand.slots[2].known is False
173 assert p.hand.slots[3].known is False
175 def test_peek_returns_correct_cards(self):
176 game = _setup_game()
177 game.start()
178 p = game.state.players[0]
179 result = game.peek_initial(p.name, [0, 1])
180 assert result == [p.hand[0], p.hand[1]]
182 def test_phase_transitions_after_all_peek(self):
183 game = _setup_game(num_players=2)
184 game.start()
185 game.peek_initial("P0", [0, 1])
186 assert game.state.phase == GamePhase.STARTING # not all peeked yet
187 game.peek_initial("P1", [0, 1])
188 assert game.state.phase == GamePhase.WAITING_FOR_DRAW
190 def test_peek_wrong_count_raises(self):
191 game = _setup_game()
192 game.start()
193 with pytest.raises(RuleViolation, match="Must peek exactly"):
194 game.peek_initial("P0", [0])
196 def test_peek_duplicate_indices_raises(self):
197 game = _setup_game()
198 game.start()
199 with pytest.raises(RuleViolation, match="Duplicate"):
200 game.peek_initial("P0", [1, 1])
202 def test_peek_twice_raises(self):
203 game = _setup_game()
204 game.start()
205 game.peek_initial("P0", [0, 1])
206 with pytest.raises(RuleViolation, match="already peeked"):
207 game.peek_initial("P0", [2, 3])
210# ===================================================================
211# Drawing (using manual setup for deterministic tests)
212# ===================================================================
214class TestDrawCard:
215 def test_draw_from_deck(self):
216 game = _setup_game_manual(
217 player_hands={"P0": [_card(3)] * 4, "P1": [_card(3)] * 4},
218 deck_remaining=[_card(7), _card(8)],
219 discard_pile=[_card(5)],
220 )
221 deck_size_before = len(game.state.deck)
222 card = game.draw_card("P0")
223 assert isinstance(card, Card)
224 assert card.value == 8 # top of deck (last element)
225 assert len(game.state.deck) == deck_size_before - 1
226 assert game.state.drawn_card == card
227 assert game.state.phase == GamePhase.WAITING_FOR_ACTION
229 def test_draw_wrong_player_raises(self):
230 game = _setup_game_manual(
231 player_hands={"P0": [_card(3)] * 4, "P1": [_card(3)] * 4},
232 deck_remaining=[_card(7)],
233 discard_pile=[_card(5)],
234 )
235 with pytest.raises(RuleViolation, match="turn"):
236 game.draw_card("P1")
238 def test_draw_from_discard(self):
239 game = _setup_game_manual(
240 player_hands={"P0": [_card(3)] * 4, "P1": [_card(3)] * 4},
241 deck_remaining=[_card(7)],
242 discard_pile=[_card(5)],
243 )
244 card = game.draw_from_discard("P0")
245 assert card.value == 5
246 assert game.state.drawn_card == card
247 assert len(game.state.discard_pile) == 0
249 def test_draw_in_wrong_phase_raises(self):
250 game = _setup_game_manual(
251 player_hands={"P0": [_card(3)] * 4, "P1": [_card(3)] * 4},
252 deck_remaining=[_card(7), _card(8)],
253 discard_pile=[_card(5)],
254 )
255 game.draw_card("P0") # now WAITING_FOR_ACTION
256 with pytest.raises(RuleViolation, match="WAITING_FOR_ACTION"):
257 game.draw_card("P0")
260# ===================================================================
261# Keep / Discard drawn card
262# ===================================================================
264class TestKeepDrawnCard:
265 def test_keep_replaces_hand_card(self):
266 game = _setup_game_manual(
267 player_hands={"P0": [_card(3)] * 4, "P1": [_card(3)] * 4},
268 deck_remaining=[_card(7)],
269 discard_pile=[_card(5)],
270 )
271 p = game.state.players[0]
272 old_hand_card = p.hand[0]
273 drawn = game.draw_card("P0")
274 game.keep_drawn_card(ReplaceCardAction(player_id="P0", hand_index=0))
275 assert p.hand[0] == drawn
276 assert game.state.discard_pile[-1] == old_hand_card
277 assert game.state.drawn_card is None
279 def test_keep_advances_turn(self):
280 game = _setup_game_manual(
281 player_hands={"P0": [_card(3)] * 4, "P1": [_card(3)] * 4},
282 deck_remaining=[_card(7)],
283 discard_pile=[_card(5)],
284 )
285 game.draw_card("P0")
286 game.keep_drawn_card(ReplaceCardAction(player_id="P0", hand_index=0))
287 assert game.state.current_player_index == 1
288 assert game.state.phase == GamePhase.WAITING_FOR_DRAW
290 def test_keep_resets_known_flag(self):
291 game = _setup_game_manual(
292 player_hands={"P0": [_card(3)] * 4, "P1": [_card(3)] * 4},
293 deck_remaining=[_card(7)],
294 discard_pile=[_card(5)],
295 )
296 p = game.state.players[0]
297 assert p.hand.slots[0].known is True # set by _setup_game_manual
298 game.draw_card("P0")
299 game.keep_drawn_card(ReplaceCardAction(player_id="P0", hand_index=0))
300 assert p.hand.slots[0].known is False # replaced → unknown
303class TestDiscardDrawnCard:
304 def test_discard_no_bonus(self):
305 game = _setup_game_manual(
306 player_hands={"P0": [_card(3)] * 4, "P1": [_card(3)] * 4},
307 deck_remaining=[_card(5)], # 5 has no bonus
308 discard_pile=[_card(2)],
309 )
310 game.draw_card("P0")
311 result = game.discard_drawn_card(DiscardDrawnAction(player_id="P0"))
312 assert result is None
313 assert game.state.drawn_card is None
314 assert game.state.phase == GamePhase.WAITING_FOR_DRAW
315 assert game.state.current_player_index == 1
317 def test_discard_jack_triggers_bonus(self):
318 game = _setup_game_manual(
319 player_hands={"P0": [_card(3)] * 4, "P1": [_card(3)] * 4},
320 deck_remaining=[_card(11)], # Jack
321 discard_pile=[_card(2)],
322 )
323 game.draw_card("P0")
324 result = game.discard_drawn_card(DiscardDrawnAction(player_id="P0"))
325 assert result == FaceCard.JACK
326 assert game.state.phase == GamePhase.WAITING_FOR_BONUS
327 assert game.state.pending_bonus == FaceCard.JACK
329 def test_discard_queen_triggers_bonus(self):
330 game = _setup_game_manual(
331 player_hands={"P0": [_card(3)] * 4, "P1": [_card(3)] * 4},
332 deck_remaining=[_card(12)], # Queen
333 discard_pile=[_card(2)],
334 )
335 game.draw_card("P0")
336 result = game.discard_drawn_card(DiscardDrawnAction(player_id="P0"))
337 assert result == FaceCard.QUEEN
338 assert game.state.pending_bonus == FaceCard.QUEEN
340 def test_discard_king_triggers_bonus(self):
341 game = _setup_game_manual(
342 player_hands={"P0": [_card(3)] * 4, "P1": [_card(3)] * 4},
343 deck_remaining=[_card(13)], # King
344 discard_pile=[_card(2)],
345 )
346 game.draw_card("P0")
347 result = game.discard_drawn_card(DiscardDrawnAction(player_id="P0"))
348 assert result == FaceCard.KING
349 assert game.state.pending_bonus == FaceCard.KING
352# ===================================================================
353# Bonus Actions
354# ===================================================================
356class TestBonusActions:
357 def _game_with_bonus(self, face_value: int) -> Game:
358 """Return a game in WAITING_FOR_BONUS with the given face card as pending."""
359 game = _setup_game_manual(
360 player_hands={"P0": [_card(3)] * 4, "P1": [_card(3)] * 4},
361 deck_remaining=[_card(face_value)],
362 discard_pile=[_card(2)],
363 )
364 game.draw_card("P0")
365 game.discard_drawn_card(DiscardDrawnAction(player_id="P0"))
366 return game
368 # -- Jack (Peek) --
370 def test_jack_peek(self):
371 game = self._game_with_bonus(11)
372 p = game.state.players[0]
373 assert p.hand.slots[2].known is False
374 card = game.execute_peek(PeekOwnCardAction(player_id="P0", hand_index=2))
375 assert p.hand.slots[2].known is True
376 assert card == p.hand[2]
377 assert game.state.pending_bonus is None
378 assert game.state.current_player_index == 1
380 # -- Queen (Swap) --
382 def test_queen_swap(self):
383 game = self._game_with_bonus(12)
384 p0 = game.state.players[0]
385 p1 = game.state.players[1]
386 p0_card = p0.hand[0]
387 p1_card = p1.hand[0]
388 game.execute_swap(
389 SwapCardAction(
390 player_id="P0",
391 own_index=0,
392 target_player_id="P1",
393 target_index=0,
394 ),
395 )
396 assert p0.hand[0] == p1_card
397 assert p1.hand[0] == p0_card
398 assert p0.hand.slots[0].known is False
399 assert p1.hand.slots[0].known is False
400 assert game.state.pending_bonus is None
402 def test_queen_swap_self_raises(self):
403 game = self._game_with_bonus(12)
404 with pytest.raises(RuleViolation, match="yourself"):
405 game.execute_swap(
406 SwapCardAction(
407 player_id="P0",
408 own_index=0,
409 target_player_id="P0",
410 target_index=1,
411 ),
412 )
414 # -- King (draw again) --
416 def test_king_bonus_returns_to_draw(self):
417 game = self._game_with_bonus(13)
418 assert game.state.phase == GamePhase.WAITING_FOR_BONUS
419 game.execute_king_bonus()
420 # Same player, back to draw phase
421 assert game.state.current_player_index == 0
422 assert game.state.phase == GamePhase.WAITING_FOR_DRAW
424 def test_king_bonus_wrong_pending_raises(self):
425 game = self._game_with_bonus(11) # Jack, not King
426 with pytest.raises(RuleViolation, match="King"):
427 game.execute_king_bonus()
429 # -- Skip bonus --
431 def test_skip_bonus(self):
432 game = self._game_with_bonus(11)
433 game.skip_bonus("P0")
434 assert game.state.pending_bonus is None
435 assert game.state.current_player_index == 1
438# ===================================================================
439# Snap (exclusively in WAITING_FOR_DRAW)
440# ===================================================================
442class TestSnap:
443 def test_correct_snap_removes_card(self):
444 # P0 has a 5 in hand, discard top is 5 → match
445 game = _setup_game_manual(
446 player_hands={
447 "P0": [_card(5), _card(3), _card(3), _card(3)],
448 "P1": [_card(7)] * 4,
449 },
450 deck_remaining=[_card(2)] * 5,
451 discard_pile=[_card(5)],
452 )
453 p0 = game.state.players[0]
454 hand_size_before = len(p0.hand)
455 result = game.snap(SnapAction(player_id="P0", hand_index=0))
456 assert result is True
457 assert len(p0.hand) == hand_size_before - 1
459 def test_incorrect_snap_adds_penalty(self):
460 # P0 has a 5 in hand, discard top is 7 → mismatch
461 game = _setup_game_manual(
462 player_hands={
463 "P0": [_card(5), _card(3), _card(3), _card(3)],
464 "P1": [_card(7)] * 4,
465 },
466 deck_remaining=[_card(2)] * 5,
467 discard_pile=[_card(7)],
468 )
469 p0 = game.state.players[0]
470 hand_size_before = len(p0.hand)
471 result = game.snap(SnapAction(player_id="P0", hand_index=0))
472 assert result is False
473 assert len(p0.hand) == hand_size_before + 1
475 def test_any_player_can_snap(self):
476 # P1 (not current player) can also snap
477 game = _setup_game_manual(
478 player_hands={
479 "P0": [_card(7)] * 4,
480 "P1": [_card(5), _card(3), _card(3), _card(3)],
481 },
482 deck_remaining=[_card(2)] * 5,
483 discard_pile=[_card(5)],
484 )
485 p1 = game.state.players[1]
486 assert p1.hand[0].value == 5
487 assert game.state.top_discard.value == 5
488 result = game.snap(SnapAction(player_id="P1", hand_index=0))
489 assert result is True
491 def test_snap_in_wrong_phase_raises(self):
492 game = _setup_game_manual(
493 player_hands={"P0": [_card(3)] * 4, "P1": [_card(3)] * 4},
494 deck_remaining=[_card(7)],
495 discard_pile=[_card(5)],
496 )
497 game.draw_card("P0") # now WAITING_FOR_ACTION
498 with pytest.raises(RuleViolation, match="WAITING_FOR_ACTION"):
499 game.snap(SnapAction(player_id="P0", hand_index=0))
501 def test_snap_stays_in_same_phase(self):
502 game = _setup_game_manual(
503 player_hands={
504 "P0": [_card(5), _card(3), _card(3), _card(3)],
505 "P1": [_card(7)] * 4,
506 },
507 deck_remaining=[_card(2)] * 5,
508 discard_pile=[_card(5)],
509 )
510 game.snap(SnapAction(player_id="P0", hand_index=0))
511 assert game.state.phase == GamePhase.WAITING_FOR_DRAW
512 assert game.state.current_player_index == 0 # turn did NOT advance
515# ===================================================================
516# Last Round & Game End
517# ===================================================================
519class TestLastRoundAndEnd:
520 def test_joker_discard_triggers_last_round(self):
521 # P0 has a joker in hand[0], draws something, replaces → joker goes to discard
522 joker = _card(0)
523 game = _setup_game_manual(
524 player_hands={
525 "P0": [joker, _card(3), _card(3), _card(3)],
526 "P1": [_card(3)] * 4,
527 },
528 deck_remaining=[_card(7)] * 5,
529 discard_pile=[_card(2)],
530 )
531 game.draw_card("P0")
532 game.keep_drawn_card(ReplaceCardAction(player_id="P0", hand_index=0))
534 assert game.state.top_discard.is_joker
535 assert game.state.last_round_starter == 0
536 assert game.state.current_player_index == 1
537 assert game.state.phase == GamePhase.LAST_ROUND
539 def test_game_finishes_after_last_round_completes(self):
540 joker = _card(0)
541 game = _setup_game_manual(
542 player_hands={
543 "P0": [joker, _card(3), _card(3), _card(3)],
544 "P1": [_card(3)] * 4,
545 },
546 deck_remaining=[_card(7)] * 5,
547 discard_pile=[_card(2)],
548 )
549 # P0 discards joker → last round
550 game.draw_card("P0")
551 game.keep_drawn_card(ReplaceCardAction(player_id="P0", hand_index=0))
552 assert game.state.phase == GamePhase.LAST_ROUND
554 # P1 takes their last turn
555 game.draw_card("P1")
556 game.keep_drawn_card(ReplaceCardAction(player_id="P1", hand_index=0))
558 # Back to P0 → FINISHED
559 assert game.state.current_player_index == 0
560 assert game.state.phase == GamePhase.FINISHED
562 def test_last_round_three_players(self):
563 joker = _card(0)
564 game = _setup_game_manual(
565 player_hands={
566 "P0": [joker, _card(3), _card(3), _card(3)],
567 "P1": [_card(3)] * 4,
568 "P2": [_card(3)] * 4,
569 },
570 deck_remaining=[_card(7)] * 10,
571 discard_pile=[_card(2)],
572 )
573 # P0 discards joker → last round
574 game.draw_card("P0")
575 game.keep_drawn_card(ReplaceCardAction(player_id="P0", hand_index=0))
576 assert game.state.phase == GamePhase.LAST_ROUND
578 # P1 plays
579 game.draw_card("P1")
580 game.keep_drawn_card(ReplaceCardAction(player_id="P1", hand_index=0))
581 assert game.state.phase == GamePhase.LAST_ROUND
583 # P2 plays → back to P0 → FINISHED
584 game.draw_card("P2")
585 game.keep_drawn_card(ReplaceCardAction(player_id="P2", hand_index=0))
586 assert game.state.phase == GamePhase.FINISHED
589# ===================================================================
590# Scoring
591# ===================================================================
593class TestScoring:
594 def test_get_scores(self):
595 game = _setup_game_manual(
596 player_hands={"P0": [_card(1)] * 4, "P1": [_card(9)] * 4},
597 deck_remaining=[],
598 discard_pile=[],
599 )
600 scores = game.get_scores()
601 assert len(scores) == 2
602 assert all(isinstance(s, tuple) and len(s) == 2 for s in scores)
604 def test_get_winner_lowest_points(self):
605 game = _setup_game_manual(
606 player_hands={"P0": [_card(1)] * 4, "P1": [_card(9)] * 4},
607 deck_remaining=[],
608 discard_pile=[],
609 )
610 winners = game.get_winner()
611 assert len(winners) == 1
612 assert winners[0].name == "P0"
614 def test_get_winner_tie_fewest_cards(self):
615 # P0: 4 × 3 = 12 pts, P1: 3 × 4 = 12 pts → P1 wins (fewer cards)
616 game = _setup_game_manual(
617 player_hands={
618 "P0": [_card(3)] * 4,
619 "P1": [_card(4)] * 3,
620 },
621 deck_remaining=[],
622 discard_pile=[],
623 )
624 winners = game.get_winner()
625 assert len(winners) == 1
626 assert winners[0].name == "P1"
629# ===================================================================
630# Deck refill
631# ===================================================================
633class TestDeckRefill:
634 def test_refill_when_deck_empty(self):
635 # Deck has 1 card, discard has 3 cards
636 game = _setup_game_manual(
637 player_hands={"P0": [_card(3)] * 4, "P1": [_card(3)] * 4},
638 deck_remaining=[_card(5)],
639 discard_pile=[_card(2), _card(4), _card(6)],
640 )
641 # P0 draws the last card from deck → deck now empty
642 game.draw_card("P0")
643 assert len(game.state.deck) == 0
645 # Discard it → adds to discard pile
646 game.discard_drawn_card(DiscardDrawnAction(player_id="P0"))
648 # Now P1 draws. _refill_deck_if_needed should reshuffle discard into deck.
649 card = game.draw_card("P1")
650 assert isinstance(card, Card)
651 # Deck was refilled from discard (minus top card)
652 assert game.state.drawn_card is not None
655# ===================================================================
656# Full game flow integration test
657# ===================================================================
659class TestFullGameFlow:
660 def test_complete_two_player_game(self):
661 """Run through a full minimal game: start → peek → draw → keep/discard → end."""
662 joker = _card(0)
663 game = _setup_game_manual(
664 player_hands={
665 "P0": [joker, _card(3), _card(3), _card(3)],
666 "P1": [_card(3)] * 4,
667 },
668 deck_remaining=[_card(5)] * 5,
669 discard_pile=[_card(2)],
670 )
671 assert game.state.phase == GamePhase.WAITING_FOR_DRAW
673 # P0 draws, replaces joker (index 0) → joker on discard → last round
674 drawn = game.draw_card("P0")
675 assert game.state.phase == GamePhase.WAITING_FOR_ACTION
676 game.keep_drawn_card(ReplaceCardAction(player_id="P0", hand_index=0))
677 assert game.state.top_discard.is_joker
678 assert game.state.phase == GamePhase.LAST_ROUND
680 # P1's last turn
681 game.draw_card("P1")
682 game.keep_drawn_card(ReplaceCardAction(player_id="P1", hand_index=0))
683 assert game.state.phase == GamePhase.FINISHED
685 # Scoring
686 scores = game.get_scores()
687 assert len(scores) == 2
688 winners = game.get_winner()
689 assert len(winners) >= 1
691 def test_king_chain_draw(self):
692 """King bonus allows the same player to draw again."""
693 game = _setup_game_manual(
694 player_hands={"P0": [_card(3)] * 4, "P1": [_card(3)] * 4},
695 deck_remaining=[_card(5), _card(13)], # top=King, then 5
696 discard_pile=[_card(2)],
697 )
698 # P0 draws King, discards it → bonus
699 game.draw_card("P0")
700 result = game.discard_drawn_card(DiscardDrawnAction(player_id="P0"))
701 assert result == FaceCard.KING
702 assert game.state.phase == GamePhase.WAITING_FOR_BONUS
704 # Execute King bonus → same player draws again
705 game.execute_king_bonus()
706 assert game.state.current_player_index == 0
707 assert game.state.phase == GamePhase.WAITING_FOR_DRAW
709 # P0 draws again (the 5), keeps it
710 card = game.draw_card("P0")
711 assert card.value == 5
712 game.keep_drawn_card(ReplaceCardAction(player_id="P0", hand_index=0))
713 assert game.state.current_player_index == 1 # now P1's turn