- Implement rules_validator.py with config-driven action validation for all 11 action types - Implement win_conditions.py with point/prize-based, knockout, deck-out, turn limit, and timeout checks - Add ForcedAction model to GameState for blocking actions (e.g., select new active after KO) - Add ActiveConfig with max_active setting for future double-battle support - Add TrainerConfig.stadium_same_name_replace option - Add DeckConfig.starting_hand_size option - Rename from_energy_deck to from_energy_zone for consistency - Fix unreachable code bug in GameState.get_opponent_id() - Add 16 coverage gap tests for edge cases (card registry corruption, forced actions, etc.) - 584 tests passing at 97% coverage Completes HIGH-005, HIGH-006, TEST-009, TEST-010 from PROJECT_PLAN.json
1519 lines
53 KiB
Python
1519 lines
53 KiB
Python
"""Tests for the win conditions checker module.
|
|
|
|
This module tests all win condition checking functionality:
|
|
- check_win_conditions (main entry point)
|
|
- check_prizes_taken (point/prize victory)
|
|
- check_no_pokemon_in_play (knockout victory)
|
|
- check_cannot_draw (deck out victory)
|
|
- check_turn_limit (turn limit victory/draw)
|
|
- check_resignation (resignation handling)
|
|
- check_timeout (timeout handling)
|
|
- apply_win_result (applying results to game state)
|
|
|
|
Each test includes a docstring explaining the "what" and "why" of the test.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from app.core.config import PrizeConfig, RulesConfig, WinConditionsConfig
|
|
from app.core.models.enums import GameEndReason, TurnPhase
|
|
from app.core.models.game_state import GameState, PlayerState
|
|
from app.core.win_conditions import (
|
|
WinResult,
|
|
apply_win_result,
|
|
check_cannot_draw,
|
|
check_no_pokemon_in_play,
|
|
check_prizes_taken,
|
|
check_resignation,
|
|
check_timeout,
|
|
check_turn_limit,
|
|
check_win_conditions,
|
|
)
|
|
|
|
# ============================================================================
|
|
# WinResult Model Tests
|
|
# ============================================================================
|
|
|
|
|
|
class TestWinResult:
|
|
"""Tests for the WinResult model."""
|
|
|
|
def test_win_result_creation(self):
|
|
"""Test that WinResult can be created with all required fields.
|
|
|
|
The WinResult model is the return type for all win condition checks.
|
|
It must contain winner_id, loser_id, end_reason, and reason.
|
|
"""
|
|
result = WinResult(
|
|
winner_id="player1",
|
|
loser_id="player2",
|
|
end_reason=GameEndReason.PRIZES_TAKEN,
|
|
reason="Player 1 scored 4 points",
|
|
)
|
|
|
|
assert result.winner_id == "player1"
|
|
assert result.loser_id == "player2"
|
|
assert result.end_reason == GameEndReason.PRIZES_TAKEN
|
|
assert result.reason == "Player 1 scored 4 points"
|
|
|
|
def test_win_result_serialization(self):
|
|
"""Test that WinResult serializes to JSON correctly.
|
|
|
|
WinResult may be sent to clients or logged, so JSON serialization
|
|
must work correctly.
|
|
"""
|
|
result = WinResult(
|
|
winner_id="player1",
|
|
loser_id="player2",
|
|
end_reason=GameEndReason.NO_POKEMON,
|
|
reason="Player 2 has no Pokemon in play",
|
|
)
|
|
|
|
json_data = result.model_dump()
|
|
assert json_data["winner_id"] == "player1"
|
|
assert json_data["loser_id"] == "player2"
|
|
assert json_data["end_reason"] == "no_pokemon"
|
|
assert "no Pokemon" in json_data["reason"]
|
|
|
|
|
|
# ============================================================================
|
|
# check_prizes_taken Tests
|
|
# ============================================================================
|
|
|
|
|
|
class TestCheckPrizesTaken:
|
|
"""Tests for the check_prizes_taken function."""
|
|
|
|
def test_player_wins_with_exact_points(self, extended_card_registry, card_instance_factory):
|
|
"""Test that a player wins when reaching exactly the required point count.
|
|
|
|
The default rules require 4 points to win. When a player's score
|
|
reaches exactly 4, they should win.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.score = 4 # Exactly the required amount
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(), # 4 points to win
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_prizes_taken(game)
|
|
|
|
assert result is not None
|
|
assert result.winner_id == "player1"
|
|
assert result.loser_id == "player2"
|
|
assert result.end_reason == GameEndReason.PRIZES_TAKEN
|
|
assert "4" in result.reason
|
|
|
|
def test_player_wins_with_more_than_required_points(
|
|
self, extended_card_registry, card_instance_factory
|
|
):
|
|
"""Test that a player wins when exceeding the required point count.
|
|
|
|
This can happen if a VMAX (3 points) knockout pushes score over the limit.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.score = 5 # More than 4 required
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_prizes_taken(game)
|
|
|
|
assert result is not None
|
|
assert result.winner_id == "player1"
|
|
|
|
def test_no_winner_with_insufficient_points(
|
|
self, extended_card_registry, card_instance_factory
|
|
):
|
|
"""Test that no winner is returned when neither player has enough points.
|
|
|
|
This is the normal game state when both players have scored but
|
|
neither has reached the winning threshold.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.score = 2
|
|
player2.score = 3
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(), # 4 points needed
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_prizes_taken(game)
|
|
|
|
assert result is None
|
|
|
|
def test_custom_point_threshold(self, extended_card_registry, card_instance_factory):
|
|
"""Test winning with a custom point threshold.
|
|
|
|
Free play mode allows configuring different win thresholds.
|
|
A player should win when reaching that custom threshold.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.score = 6 # Custom threshold
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
rules = RulesConfig(prizes=PrizeConfig(count=6))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=rules,
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_prizes_taken(game)
|
|
|
|
assert result is not None
|
|
assert result.winner_id == "player1"
|
|
|
|
def test_prize_card_mode_all_prizes_taken(self, extended_card_registry, card_instance_factory):
|
|
"""Test winning in prize card mode when all prizes are taken.
|
|
|
|
In classic Pokemon TCG mode (use_prize_cards=True), a player wins
|
|
when their prizes zone is empty (all prizes taken).
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
# Player1 has taken all prizes (empty prize zone)
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
# prizes zone is empty by default
|
|
|
|
# Player2 still has prizes
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.prizes.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
rules = RulesConfig(prizes=PrizeConfig(use_prize_cards=True, count=6))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=rules,
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_prizes_taken(game)
|
|
|
|
assert result is not None
|
|
assert result.winner_id == "player1"
|
|
assert result.end_reason == GameEndReason.PRIZES_TAKEN
|
|
assert "prize" in result.reason.lower()
|
|
|
|
def test_prize_card_mode_no_winner_with_remaining_prizes(
|
|
self, extended_card_registry, card_instance_factory
|
|
):
|
|
"""Test that no winner in prize card mode when prizes remain.
|
|
|
|
Neither player should win if both still have prize cards to take.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
# Both players have prizes remaining
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player1.prizes.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.prizes.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
rules = RulesConfig(prizes=PrizeConfig(use_prize_cards=True, count=6))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=rules,
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_prizes_taken(game)
|
|
|
|
assert result is None
|
|
|
|
def test_prize_card_mode_ignores_setup_phase(
|
|
self, extended_card_registry, card_instance_factory
|
|
):
|
|
"""Test that empty prizes during setup don't trigger win.
|
|
|
|
During setup, prizes haven't been dealt yet so the zone is empty.
|
|
This should not count as a win condition.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
# Empty prizes during setup
|
|
player1.hand.add(card_instance_factory("pikachu_base_001"))
|
|
player2.hand.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
rules = RulesConfig(prizes=PrizeConfig(use_prize_cards=True, count=6))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=rules,
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=0,
|
|
phase=TurnPhase.SETUP,
|
|
)
|
|
|
|
result = check_prizes_taken(game)
|
|
|
|
assert result is None
|
|
|
|
def test_player2_wins(self, extended_card_registry, card_instance_factory):
|
|
"""Test that player2 can win via points.
|
|
|
|
The win condition check should work for either player, not just
|
|
the current player.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.score = 2
|
|
player2.score = 4 # Player2 wins
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1", # It's player1's turn but player2 won
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_prizes_taken(game)
|
|
|
|
assert result is not None
|
|
assert result.winner_id == "player2"
|
|
assert result.loser_id == "player1"
|
|
|
|
|
|
# ============================================================================
|
|
# check_no_pokemon_in_play Tests
|
|
# ============================================================================
|
|
|
|
|
|
class TestCheckNoPokemonInPlay:
|
|
"""Tests for the check_no_pokemon_in_play function."""
|
|
|
|
def test_player_loses_with_no_pokemon(self, extended_card_registry, card_instance_factory):
|
|
"""Test that a player loses when they have no Pokemon in play.
|
|
|
|
If a player's active is knocked out and they have no bench Pokemon,
|
|
they lose the game.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
# Player1 has Pokemon
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
# Player2 has no Pokemon in play
|
|
# (active and bench are empty)
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_no_pokemon_in_play(game)
|
|
|
|
assert result is not None
|
|
assert result.winner_id == "player1" # Opponent wins
|
|
assert result.loser_id == "player2" # No pokemon player loses
|
|
assert result.end_reason == GameEndReason.NO_POKEMON
|
|
|
|
def test_player_survives_with_bench_pokemon(
|
|
self, extended_card_registry, card_instance_factory
|
|
):
|
|
"""Test that a player doesn't lose if they have bench Pokemon.
|
|
|
|
Even with no active Pokemon, having bench Pokemon means the game
|
|
continues (they must select a new active).
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
# Player2 has no active but has bench
|
|
player2.bench.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_no_pokemon_in_play(game)
|
|
|
|
assert result is None
|
|
|
|
def test_player_survives_with_active_pokemon(
|
|
self, extended_card_registry, card_instance_factory
|
|
):
|
|
"""Test that a player doesn't lose if they have an active Pokemon.
|
|
|
|
The normal case - player has an active Pokemon.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_no_pokemon_in_play(game)
|
|
|
|
assert result is None
|
|
|
|
def test_skipped_during_setup_phase(self, extended_card_registry, card_instance_factory):
|
|
"""Test that no-pokemon check is skipped during setup.
|
|
|
|
During setup phase, players are still placing their initial Pokemon.
|
|
Empty boards are expected and should not trigger a loss.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
# Both players have no Pokemon (setup phase)
|
|
player1.hand.add(card_instance_factory("pikachu_base_001"))
|
|
player2.hand.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=0,
|
|
phase=TurnPhase.SETUP,
|
|
)
|
|
|
|
result = check_no_pokemon_in_play(game)
|
|
|
|
assert result is None
|
|
|
|
def test_player1_loses_with_no_pokemon(self, extended_card_registry, card_instance_factory):
|
|
"""Test that player1 can lose via no Pokemon.
|
|
|
|
The check should work for any player, not just player2.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
# Player1 has no Pokemon
|
|
# Player2 has active
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_no_pokemon_in_play(game)
|
|
|
|
assert result is not None
|
|
assert result.winner_id == "player2"
|
|
assert result.loser_id == "player1"
|
|
|
|
|
|
# ============================================================================
|
|
# check_cannot_draw Tests
|
|
# ============================================================================
|
|
|
|
|
|
class TestCheckCannotDraw:
|
|
"""Tests for the check_cannot_draw function."""
|
|
|
|
def test_player_loses_with_empty_deck_at_draw_phase(
|
|
self, extended_card_registry, card_instance_factory
|
|
):
|
|
"""Test that a player loses if they can't draw at turn start.
|
|
|
|
In Pokemon TCG, a player loses if they must draw a card at the
|
|
start of their turn but their deck is empty.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
# Player1's deck is empty
|
|
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.deck.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.DRAW, # Draw phase!
|
|
)
|
|
|
|
result = check_cannot_draw(game)
|
|
|
|
assert result is not None
|
|
assert result.winner_id == "player2"
|
|
assert result.loser_id == "player1"
|
|
assert result.end_reason == GameEndReason.DECK_EMPTY
|
|
assert "cannot draw" in result.reason.lower() or "deck empty" in result.reason.lower()
|
|
|
|
def test_player_survives_with_cards_in_deck(
|
|
self, extended_card_registry, card_instance_factory
|
|
):
|
|
"""Test that a player doesn't lose if they have cards to draw.
|
|
|
|
Normal case - player has cards in deck.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player1.deck.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.DRAW,
|
|
)
|
|
|
|
result = check_cannot_draw(game)
|
|
|
|
assert result is None
|
|
|
|
def test_empty_deck_ignored_outside_draw_phase(
|
|
self, extended_card_registry, card_instance_factory
|
|
):
|
|
"""Test that empty deck doesn't trigger loss outside draw phase.
|
|
|
|
The deck-out loss only applies at the start of draw phase.
|
|
During main phase, having an empty deck is fine (cards might
|
|
be shuffled back in, etc.).
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
# Empty deck but not draw phase
|
|
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN, # Not draw phase
|
|
)
|
|
|
|
result = check_cannot_draw(game)
|
|
|
|
assert result is None
|
|
|
|
def test_opponent_deck_empty_not_checked(self, extended_card_registry, card_instance_factory):
|
|
"""Test that opponent's empty deck doesn't affect current player.
|
|
|
|
The deck-out check only applies to the current player who must draw.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player1.deck.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
# Player2's deck is empty but it's player1's turn
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.DRAW,
|
|
)
|
|
|
|
result = check_cannot_draw(game)
|
|
|
|
assert result is None
|
|
|
|
|
|
# ============================================================================
|
|
# check_turn_limit Tests
|
|
# ============================================================================
|
|
|
|
|
|
class TestCheckTurnLimit:
|
|
"""Tests for the check_turn_limit function."""
|
|
|
|
def test_higher_score_wins_at_turn_limit(self, extended_card_registry, card_instance_factory):
|
|
"""Test that the player with higher score wins at turn limit.
|
|
|
|
When the turn limit is reached, the player with more points wins.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.score = 3
|
|
player2.score = 2
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
rules = RulesConfig(
|
|
win_conditions=WinConditionsConfig(turn_limit_enabled=True, turn_limit=30)
|
|
)
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=rules,
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=31, # Exceeded limit
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_turn_limit(game)
|
|
|
|
assert result is not None
|
|
assert result.winner_id == "player1"
|
|
assert result.loser_id == "player2"
|
|
assert result.end_reason == GameEndReason.TIMEOUT
|
|
|
|
def test_player2_wins_with_higher_score(self, extended_card_registry, card_instance_factory):
|
|
"""Test that player2 wins if they have higher score at turn limit.
|
|
|
|
The win goes to whoever has more points, regardless of who is
|
|
the current player.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.score = 1
|
|
player2.score = 3
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
rules = RulesConfig(
|
|
win_conditions=WinConditionsConfig(turn_limit_enabled=True, turn_limit=30)
|
|
)
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=rules,
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=31,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_turn_limit(game)
|
|
|
|
assert result is not None
|
|
assert result.winner_id == "player2"
|
|
assert result.loser_id == "player1"
|
|
|
|
def test_draw_on_equal_scores(self, extended_card_registry, card_instance_factory):
|
|
"""Test that equal scores result in a draw at turn limit.
|
|
|
|
If both players have the same score when turn limit is reached,
|
|
the game ends in a draw.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.score = 2
|
|
player2.score = 2
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
rules = RulesConfig(
|
|
win_conditions=WinConditionsConfig(turn_limit_enabled=True, turn_limit=30)
|
|
)
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=rules,
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=31,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_turn_limit(game)
|
|
|
|
assert result is not None
|
|
assert result.winner_id == "" # Draw
|
|
assert result.loser_id == "" # Draw
|
|
assert result.end_reason == GameEndReason.DRAW
|
|
assert "draw" in result.reason.lower()
|
|
|
|
def test_no_check_when_turn_limit_disabled(self, extended_card_registry, card_instance_factory):
|
|
"""Test that turn limit is not checked when disabled.
|
|
|
|
Campaign mode or infinite games may disable the turn limit.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.score = 1
|
|
player2.score = 2
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
rules = RulesConfig(win_conditions=WinConditionsConfig(turn_limit_enabled=False))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=rules,
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=100, # Way past any reasonable limit
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_turn_limit(game)
|
|
|
|
assert result is None
|
|
|
|
def test_no_result_before_turn_limit(self, extended_card_registry, card_instance_factory):
|
|
"""Test that no result is returned before turn limit is reached.
|
|
|
|
The turn limit check should only trigger when the limit is exceeded.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.score = 1
|
|
player2.score = 2
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
rules = RulesConfig(
|
|
win_conditions=WinConditionsConfig(turn_limit_enabled=True, turn_limit=30)
|
|
)
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=rules,
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=30, # Exactly at limit, not exceeded
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_turn_limit(game)
|
|
|
|
assert result is None
|
|
|
|
def test_custom_turn_limit(self, extended_card_registry, card_instance_factory):
|
|
"""Test that custom turn limit is respected.
|
|
|
|
Different game modes may use different turn limits.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.score = 1
|
|
player2.score = 0
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
rules = RulesConfig(
|
|
win_conditions=WinConditionsConfig(turn_limit_enabled=True, turn_limit=10)
|
|
)
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=rules,
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=11, # Exceeded custom limit of 10
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_turn_limit(game)
|
|
|
|
assert result is not None
|
|
assert result.winner_id == "player1"
|
|
|
|
|
|
# ============================================================================
|
|
# check_resignation Tests
|
|
# ============================================================================
|
|
|
|
|
|
class TestCheckResignation:
|
|
"""Tests for the check_resignation function."""
|
|
|
|
def test_resignation_creates_correct_result(
|
|
self, extended_card_registry, card_instance_factory
|
|
):
|
|
"""Test that resignation creates the correct WinResult.
|
|
|
|
When a player resigns, their opponent wins immediately.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_resignation(game, "player1")
|
|
|
|
assert result.winner_id == "player2"
|
|
assert result.loser_id == "player1"
|
|
assert result.end_reason == GameEndReason.RESIGNATION
|
|
assert "resign" in result.reason.lower()
|
|
|
|
def test_player2_resignation(self, extended_card_registry, card_instance_factory):
|
|
"""Test that player2 can resign.
|
|
|
|
Either player should be able to resign.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_resignation(game, "player2")
|
|
|
|
assert result.winner_id == "player1"
|
|
assert result.loser_id == "player2"
|
|
|
|
def test_resignation_invalid_player_raises(self, extended_card_registry, card_instance_factory):
|
|
"""Test that resignation with invalid player ID raises error.
|
|
|
|
The resigning player must be a valid player in the game.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="not found"):
|
|
check_resignation(game, "invalid_player")
|
|
|
|
|
|
# ============================================================================
|
|
# check_timeout Tests
|
|
# ============================================================================
|
|
|
|
|
|
class TestCheckTimeout:
|
|
"""Tests for the check_timeout function."""
|
|
|
|
def test_timeout_creates_correct_result(self, extended_card_registry, card_instance_factory):
|
|
"""Test that timeout creates the correct WinResult.
|
|
|
|
When a player times out, their opponent wins.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_timeout(game, "player1")
|
|
|
|
assert result.winner_id == "player2"
|
|
assert result.loser_id == "player1"
|
|
assert result.end_reason == GameEndReason.TIMEOUT
|
|
assert "timed out" in result.reason.lower()
|
|
|
|
def test_player2_timeout(self, extended_card_registry, card_instance_factory):
|
|
"""Test that player2 can time out.
|
|
|
|
Either player should be able to time out.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_timeout(game, "player2")
|
|
|
|
assert result.winner_id == "player1"
|
|
assert result.loser_id == "player2"
|
|
|
|
def test_timeout_invalid_player_raises(self, extended_card_registry, card_instance_factory):
|
|
"""Test that timeout with invalid player ID raises error.
|
|
|
|
The timed out player must be a valid player in the game.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="not found"):
|
|
check_timeout(game, "invalid_player")
|
|
|
|
|
|
# ============================================================================
|
|
# check_win_conditions (Main Entry Point) Tests
|
|
# ============================================================================
|
|
|
|
|
|
class TestCheckWinConditions:
|
|
"""Tests for the check_win_conditions main entry point."""
|
|
|
|
def test_returns_none_when_no_win(self, extended_card_registry, card_instance_factory):
|
|
"""Test that None is returned when no win condition is met.
|
|
|
|
This is the normal case during gameplay.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.score = 1
|
|
player2.score = 2
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player1.deck.add(card_instance_factory("pikachu_base_001"))
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.deck.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_win_conditions(game)
|
|
|
|
assert result is None
|
|
|
|
def test_returns_none_when_game_already_over(
|
|
self, extended_card_registry, card_instance_factory
|
|
):
|
|
"""Test that None is returned when game is already over.
|
|
|
|
Once a game has ended, win conditions should not be re-checked.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.score = 4 # Would normally trigger win
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN,
|
|
winner_id="player1", # Game already over
|
|
end_reason=GameEndReason.PRIZES_TAKEN,
|
|
)
|
|
|
|
result = check_win_conditions(game)
|
|
|
|
assert result is None
|
|
|
|
def test_detects_prizes_win(self, extended_card_registry, card_instance_factory):
|
|
"""Test that prizes/points win is detected.
|
|
|
|
The main entry point should correctly delegate to check_prizes_taken.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.score = 4
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.deck.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_win_conditions(game)
|
|
|
|
assert result is not None
|
|
assert result.end_reason == GameEndReason.PRIZES_TAKEN
|
|
|
|
def test_detects_no_pokemon_win(self, extended_card_registry, card_instance_factory):
|
|
"""Test that no-Pokemon win is detected.
|
|
|
|
The main entry point should correctly delegate to check_no_pokemon_in_play.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player1.deck.add(card_instance_factory("pikachu_base_001"))
|
|
# Player2 has no Pokemon
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_win_conditions(game)
|
|
|
|
assert result is not None
|
|
assert result.end_reason == GameEndReason.NO_POKEMON
|
|
assert result.winner_id == "player1"
|
|
|
|
def test_detects_cannot_draw_win(self, extended_card_registry, card_instance_factory):
|
|
"""Test that cannot-draw win is detected.
|
|
|
|
The main entry point should correctly delegate to check_cannot_draw.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
# Player1's deck is empty
|
|
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.deck.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.DRAW, # Must be draw phase
|
|
)
|
|
|
|
result = check_win_conditions(game)
|
|
|
|
assert result is not None
|
|
assert result.end_reason == GameEndReason.DECK_EMPTY
|
|
assert result.winner_id == "player2"
|
|
|
|
def test_prizes_win_takes_priority_over_no_pokemon(
|
|
self, extended_card_registry, card_instance_factory
|
|
):
|
|
"""Test that prizes win is checked before no-Pokemon.
|
|
|
|
If a player scores enough points and the opponent has no Pokemon,
|
|
the points win should be detected first (though in practice both
|
|
would mean the same winner).
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.score = 4 # Winning score
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
# Player2 has no Pokemon (also a loss condition)
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_win_conditions(game)
|
|
|
|
assert result is not None
|
|
# Prizes should be checked first
|
|
assert result.end_reason == GameEndReason.PRIZES_TAKEN
|
|
|
|
def test_respects_disabled_conditions(self, extended_card_registry, card_instance_factory):
|
|
"""Test that disabled win conditions are not checked.
|
|
|
|
Free play mode may disable certain win conditions.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.score = 4 # Would normally win
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player1.deck.add(card_instance_factory("pikachu_base_001"))
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.deck.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
rules = RulesConfig(
|
|
win_conditions=WinConditionsConfig(
|
|
all_prizes_taken=False, # Disabled!
|
|
no_pokemon_in_play=True,
|
|
cannot_draw=True,
|
|
)
|
|
)
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=rules,
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_win_conditions(game)
|
|
|
|
# Prizes condition disabled, so no win
|
|
assert result is None
|
|
|
|
|
|
# ============================================================================
|
|
# apply_win_result Tests
|
|
# ============================================================================
|
|
|
|
|
|
class TestApplyWinResult:
|
|
"""Tests for the apply_win_result function."""
|
|
|
|
def test_applies_normal_win(self, extended_card_registry, card_instance_factory):
|
|
"""Test that a normal win result is applied correctly.
|
|
|
|
The game state should be updated with winner_id and end_reason.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = WinResult(
|
|
winner_id="player1",
|
|
loser_id="player2",
|
|
end_reason=GameEndReason.PRIZES_TAKEN,
|
|
reason="Player 1 wins",
|
|
)
|
|
|
|
apply_win_result(game, result)
|
|
|
|
assert game.winner_id == "player1"
|
|
assert game.end_reason == GameEndReason.PRIZES_TAKEN
|
|
assert game.is_game_over()
|
|
|
|
def test_applies_draw_result(self, extended_card_registry, card_instance_factory):
|
|
"""Test that a draw result is applied correctly.
|
|
|
|
For draws, winner_id should be None and end_reason should be DRAW.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = WinResult(
|
|
winner_id="",
|
|
loser_id="",
|
|
end_reason=GameEndReason.DRAW,
|
|
reason="Game ends in a draw",
|
|
)
|
|
|
|
apply_win_result(game, result)
|
|
|
|
assert game.winner_id is None
|
|
assert game.end_reason == GameEndReason.DRAW
|
|
|
|
def test_applies_different_end_reasons(self, extended_card_registry, card_instance_factory):
|
|
"""Test that different end reasons are applied correctly.
|
|
|
|
Each end reason type should be stored in the game state.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
for end_reason in [
|
|
GameEndReason.PRIZES_TAKEN,
|
|
GameEndReason.NO_POKEMON,
|
|
GameEndReason.DECK_EMPTY,
|
|
GameEndReason.RESIGNATION,
|
|
GameEndReason.TIMEOUT,
|
|
]:
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = WinResult(
|
|
winner_id="player1",
|
|
loser_id="player2",
|
|
end_reason=end_reason,
|
|
reason="Test",
|
|
)
|
|
|
|
apply_win_result(game, result)
|
|
|
|
assert game.end_reason == end_reason
|
|
|
|
|
|
# ============================================================================
|
|
# Integration / Edge Case Tests
|
|
# ============================================================================
|
|
|
|
|
|
class TestWinConditionsEdgeCases:
|
|
"""Edge case and integration tests for win conditions."""
|
|
|
|
def test_simultaneous_win_conditions(self, extended_card_registry, card_instance_factory):
|
|
"""Test behavior when multiple win conditions are met.
|
|
|
|
In Pokemon TCG, prizes/points is typically checked first. If player1
|
|
knocks out player2's last Pokemon and reaches 4 points, prizes win
|
|
should be reported (though both conditions are met).
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.score = 4 # Winning points
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
# Player2 has no Pokemon (also a loss)
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_win_conditions(game)
|
|
|
|
assert result is not None
|
|
# Prizes checked first
|
|
assert result.end_reason == GameEndReason.PRIZES_TAKEN
|
|
assert result.winner_id == "player1"
|
|
|
|
def test_zero_points_to_win(self, extended_card_registry, card_instance_factory):
|
|
"""Test edge case where 0 points are required to win.
|
|
|
|
This would mean everyone wins immediately, but the game should
|
|
handle it gracefully.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.score = 0
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
rules = RulesConfig(prizes=PrizeConfig(count=0))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=rules,
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=1,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_prizes_taken(game)
|
|
|
|
# Both players have >= 0 points, first one checked wins
|
|
assert result is not None
|
|
|
|
def test_all_conditions_disabled(self, extended_card_registry, card_instance_factory):
|
|
"""Test that game continues when all win conditions disabled.
|
|
|
|
While unusual, free play mode could disable all standard win
|
|
conditions (perhaps for practice/testing).
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.score = 100 # Would normally win many times over
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
# Empty deck
|
|
# Player2 has no Pokemon
|
|
|
|
rules = RulesConfig(
|
|
win_conditions=WinConditionsConfig(
|
|
all_prizes_taken=False,
|
|
no_pokemon_in_play=False,
|
|
cannot_draw=False,
|
|
)
|
|
)
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=rules,
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.DRAW,
|
|
)
|
|
|
|
result = check_win_conditions(game)
|
|
|
|
# All conditions disabled, no winner
|
|
assert result is None
|
|
|
|
def test_win_result_reason_contains_player_id(
|
|
self, extended_card_registry, card_instance_factory
|
|
):
|
|
"""Test that win result reasons include player IDs for clarity.
|
|
|
|
The reason string should indicate which player won/lost for logging.
|
|
"""
|
|
player1 = PlayerState(player_id="player1")
|
|
player2 = PlayerState(player_id="player2")
|
|
|
|
player1.score = 4
|
|
player1.active.add(card_instance_factory("pikachu_base_001"))
|
|
player2.active.add(card_instance_factory("pikachu_base_001"))
|
|
|
|
game = GameState(
|
|
game_id="test",
|
|
rules=RulesConfig(),
|
|
card_registry=extended_card_registry,
|
|
players={"player1": player1, "player2": player2},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=5,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
result = check_prizes_taken(game)
|
|
|
|
assert result is not None
|
|
assert "player1" in result.reason.lower()
|