Architectural refactor to eliminate circular imports and establish clean module boundaries: - Move enums from app/core/models/enums.py to app/core/enums.py (foundational module with zero dependencies) - Update all imports across 30 files to use new enum location - Set up clean export structure: - app.core.enums: canonical source for all enums - app.core: convenience exports for full public API - app.core.models: exports models only (not enums) - Add module exports to app/core/__init__.py and app/core/effects/__init__.py - Remove circular import workarounds from game_state.py This enables app.core.models to export GameState without circular import issues, since enums no longer depend on the models package. All 826 tests passing.
1520 lines
53 KiB
Python
1520 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.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.TURN_LIMIT
|
|
|
|
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,
|
|
GameEndReason.TURN_LIMIT,
|
|
]:
|
|
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()
|