Refactor turn action tracking from booleans to counters for RulesConfig support
Replace hardcoded boolean flags with integer counters to support configurable per-turn limits from RulesConfig. This enables custom game modes with different rules (e.g., 2 energy attachments per turn, unlimited items, etc.). PlayerState changes: - energy_attached_this_turn -> energy_attachments_this_turn (int) - supporter_played_this_turn -> supporters_played_this_turn (int) - stadium_played_this_turn -> stadiums_played_this_turn (int) - retreated_this_turn -> retreats_this_turn (int) - Added items_played_this_turn (int) - Added can_play_stadium() and can_play_item() methods - Renamed reset_turn_flags() to reset_turn_state() Ability/CardInstance changes: - Ability.once_per_turn (bool) -> uses_per_turn (int|None) - CardInstance.ability_used_this_turn -> ability_uses_this_turn (int) - Added CardInstance.can_use_ability(ability) method All methods now properly compare counters against RulesConfig or Ability limits. 270 tests passing.
This commit is contained in:
parent
725c8ccc5c
commit
325f1e8af5
@ -78,15 +78,15 @@ class Ability(BaseModel):
|
||||
effect_id: Reference to an effect handler.
|
||||
effect_params: Parameters passed to the effect handler.
|
||||
effect_description: Human-readable description of the ability.
|
||||
once_per_turn: If True, can only be used once per turn per Pokemon.
|
||||
activation_phase: When this ability can be activated (default: main phase).
|
||||
uses_per_turn: Maximum uses per turn. None means unlimited uses.
|
||||
Default is 1 (once per turn), which matches standard Pokemon TCG rules.
|
||||
"""
|
||||
|
||||
name: str
|
||||
effect_id: str
|
||||
effect_params: dict[str, Any] = Field(default_factory=dict)
|
||||
effect_description: str | None = None
|
||||
once_per_turn: bool = True
|
||||
uses_per_turn: int | None = 1 # None = unlimited, 1 = once per turn (default)
|
||||
|
||||
|
||||
class WeaknessResistance(BaseModel):
|
||||
@ -239,7 +239,8 @@ class CardInstance(BaseModel):
|
||||
attached_energy: List of CardInstance IDs for attached energy cards.
|
||||
attached_tools: List of CardInstance IDs for attached tool cards.
|
||||
status_conditions: Active status conditions on this Pokemon.
|
||||
ability_used_this_turn: Whether an ability was used this turn.
|
||||
ability_uses_this_turn: Number of times abilities have been used this turn.
|
||||
Compared against Ability.uses_per_turn to determine if more uses allowed.
|
||||
evolved_from_instance_id: The CardInstance this evolved from.
|
||||
turn_played: The turn number when this card was played/evolved.
|
||||
Used for evolution timing rules.
|
||||
@ -255,7 +256,7 @@ class CardInstance(BaseModel):
|
||||
attached_energy: list[str] = Field(default_factory=list)
|
||||
attached_tools: list[str] = Field(default_factory=list)
|
||||
status_conditions: list[StatusCondition] = Field(default_factory=list)
|
||||
ability_used_this_turn: bool = False
|
||||
ability_uses_this_turn: int = 0
|
||||
|
||||
# Evolution tracking
|
||||
evolved_from_instance_id: str | None = None
|
||||
@ -314,8 +315,24 @@ class CardInstance(BaseModel):
|
||||
self.status_conditions.clear()
|
||||
|
||||
def reset_turn_state(self) -> None:
|
||||
"""Reset per-turn state flags. Called at the start of each turn."""
|
||||
self.ability_used_this_turn = False
|
||||
"""Reset per-turn state counters. Called at the start of each turn."""
|
||||
self.ability_uses_this_turn = 0
|
||||
|
||||
def can_use_ability(self, ability: Ability) -> bool:
|
||||
"""Check if this Pokemon can use the given ability.
|
||||
|
||||
Compares ability_uses_this_turn against the ability's uses_per_turn limit.
|
||||
If uses_per_turn is None, the ability has no usage limit.
|
||||
|
||||
Args:
|
||||
ability: The Ability to check.
|
||||
|
||||
Returns:
|
||||
True if the ability can be used (hasn't hit its per-turn limit).
|
||||
"""
|
||||
if ability.uses_per_turn is None:
|
||||
return True # Unlimited uses
|
||||
return self.ability_uses_this_turn < ability.uses_per_turn
|
||||
|
||||
def can_evolve_this_turn(self, current_turn: int) -> bool:
|
||||
"""Check if this Pokemon can evolve this turn.
|
||||
|
||||
@ -159,7 +159,7 @@ class PlayerState(BaseModel):
|
||||
"""Complete state for a single player in a game.
|
||||
|
||||
Contains all zones (deck, hand, active, bench, etc.) and per-player
|
||||
state like score, turn flags, and action tracking.
|
||||
state like score, turn counters, and action tracking.
|
||||
|
||||
Attributes:
|
||||
player_id: Unique identifier for this player.
|
||||
@ -171,10 +171,11 @@ class PlayerState(BaseModel):
|
||||
prizes: Prize cards (hidden until taken, in point-based mode this is unused).
|
||||
energy_deck: Separate energy deck (Pokemon Pocket style).
|
||||
score: Points scored (knockouts).
|
||||
energy_attached_this_turn: Whether energy was attached this turn.
|
||||
supporter_played_this_turn: Whether a Supporter was played this turn.
|
||||
stadium_played_this_turn: Whether a Stadium was played this turn.
|
||||
retreated_this_turn: Whether the active Pokemon retreated this turn.
|
||||
energy_attachments_this_turn: Number of energy cards attached this turn.
|
||||
supporters_played_this_turn: Number of Supporter cards played this turn.
|
||||
stadiums_played_this_turn: Number of Stadium cards played this turn.
|
||||
items_played_this_turn: Number of Item cards played this turn.
|
||||
retreats_this_turn: Number of retreats performed this turn.
|
||||
gx_attack_used: Whether this player has used their GX attack (once per game).
|
||||
vstar_power_used: Whether this player has used their VSTAR power (once per game).
|
||||
"""
|
||||
@ -194,11 +195,13 @@ class PlayerState(BaseModel):
|
||||
# Score tracking (point-based system)
|
||||
score: int = 0
|
||||
|
||||
# Per-turn action flags (reset at turn start)
|
||||
energy_attached_this_turn: bool = False
|
||||
supporter_played_this_turn: bool = False
|
||||
stadium_played_this_turn: bool = False
|
||||
retreated_this_turn: bool = False
|
||||
# Per-turn action counters (reset at turn start)
|
||||
# These are integers to support configurable rules (e.g., 2 energy per turn)
|
||||
energy_attachments_this_turn: int = 0
|
||||
supporters_played_this_turn: int = 0
|
||||
stadiums_played_this_turn: int = 0
|
||||
items_played_this_turn: int = 0
|
||||
retreats_this_turn: int = 0
|
||||
|
||||
# Per-game flags
|
||||
gx_attack_used: bool = False
|
||||
@ -231,21 +234,77 @@ class PlayerState(BaseModel):
|
||||
return pokemon
|
||||
|
||||
def can_attach_energy(self, rules: RulesConfig) -> bool:
|
||||
"""Check if this player can attach energy based on rules and turn state."""
|
||||
# In standard rules, only one attachment per turn
|
||||
# This could be modified by card effects
|
||||
return not self.energy_attached_this_turn
|
||||
"""Check if this player can attach energy based on rules and turn state.
|
||||
|
||||
Compares the number of energy attachments made this turn against the
|
||||
rules.energy.attachments_per_turn limit.
|
||||
|
||||
Args:
|
||||
rules: The RulesConfig governing this game.
|
||||
|
||||
Returns:
|
||||
True if more energy can be attached this turn.
|
||||
"""
|
||||
return self.energy_attachments_this_turn < rules.energy.attachments_per_turn
|
||||
|
||||
def can_play_supporter(self, rules: RulesConfig) -> bool:
|
||||
"""Check if this player can play a Supporter card."""
|
||||
return not self.supporter_played_this_turn
|
||||
"""Check if this player can play a Supporter card.
|
||||
|
||||
Compares the number of Supporters played this turn against the
|
||||
rules.trainer.supporters_per_turn limit.
|
||||
|
||||
Args:
|
||||
rules: The RulesConfig governing this game.
|
||||
|
||||
Returns:
|
||||
True if more Supporters can be played this turn.
|
||||
"""
|
||||
return self.supporters_played_this_turn < rules.trainer.supporters_per_turn
|
||||
|
||||
def can_play_stadium(self, rules: RulesConfig) -> bool:
|
||||
"""Check if this player can play a Stadium card.
|
||||
|
||||
Compares the number of Stadiums played this turn against the
|
||||
rules.trainer.stadiums_per_turn limit.
|
||||
|
||||
Args:
|
||||
rules: The RulesConfig governing this game.
|
||||
|
||||
Returns:
|
||||
True if more Stadiums can be played this turn.
|
||||
"""
|
||||
return self.stadiums_played_this_turn < rules.trainer.stadiums_per_turn
|
||||
|
||||
def can_play_item(self, rules: RulesConfig) -> bool:
|
||||
"""Check if this player can play an Item card.
|
||||
|
||||
Compares the number of Items played this turn against the
|
||||
rules.trainer.items_per_turn limit. If items_per_turn is None,
|
||||
unlimited Items can be played.
|
||||
|
||||
Args:
|
||||
rules: The RulesConfig governing this game.
|
||||
|
||||
Returns:
|
||||
True if more Items can be played this turn.
|
||||
"""
|
||||
if rules.trainer.items_per_turn is None:
|
||||
return True # Unlimited items
|
||||
return self.items_played_this_turn < rules.trainer.items_per_turn
|
||||
|
||||
def can_retreat(self, rules: RulesConfig) -> bool:
|
||||
"""Check if this player can retreat based on rules and turn state."""
|
||||
if rules.retreat.retreats_per_turn == 0:
|
||||
return False
|
||||
# For now, we only support 1 retreat per turn
|
||||
return not self.retreated_this_turn
|
||||
"""Check if this player can retreat based on rules and turn state.
|
||||
|
||||
Compares the number of retreats performed this turn against the
|
||||
rules.retreat.retreats_per_turn limit.
|
||||
|
||||
Args:
|
||||
rules: The RulesConfig governing this game.
|
||||
|
||||
Returns:
|
||||
True if more retreats are allowed this turn.
|
||||
"""
|
||||
return self.retreats_this_turn < rules.retreat.retreats_per_turn
|
||||
|
||||
def bench_space_available(self, rules: RulesConfig) -> int:
|
||||
"""Return how many more Pokemon can be placed on the bench."""
|
||||
@ -255,12 +314,17 @@ class PlayerState(BaseModel):
|
||||
"""Check if there's room on the bench for another Pokemon."""
|
||||
return self.bench_space_available(rules) > 0
|
||||
|
||||
def reset_turn_flags(self) -> None:
|
||||
"""Reset all per-turn flags. Called at the start of each turn."""
|
||||
self.energy_attached_this_turn = False
|
||||
self.supporter_played_this_turn = False
|
||||
self.stadium_played_this_turn = False
|
||||
self.retreated_this_turn = False
|
||||
def reset_turn_state(self) -> None:
|
||||
"""Reset all per-turn counters. Called at the start of each turn.
|
||||
|
||||
Resets all action counters (energy attachments, trainer plays, retreats)
|
||||
to zero, and resets ability usage on all Pokemon in play.
|
||||
"""
|
||||
self.energy_attachments_this_turn = 0
|
||||
self.supporters_played_this_turn = 0
|
||||
self.stadiums_played_this_turn = 0
|
||||
self.items_played_this_turn = 0
|
||||
self.retreats_this_turn = 0
|
||||
|
||||
# Also reset ability usage on all Pokemon in play
|
||||
for pokemon in self.get_all_pokemon_in_play():
|
||||
@ -398,8 +462,8 @@ class GameState(BaseModel):
|
||||
# Fallback for games without explicit turn order
|
||||
self.turn_number += 1
|
||||
|
||||
# Reset the new player's turn flags
|
||||
self.get_current_player().reset_turn_flags()
|
||||
# Reset the new player's turn state
|
||||
self.get_current_player().reset_turn_state()
|
||||
|
||||
# Start at draw phase
|
||||
self.phase = TurnPhase.DRAW
|
||||
|
||||
@ -107,7 +107,9 @@ class TestAbility:
|
||||
|
||||
def test_basic_ability(self) -> None:
|
||||
"""
|
||||
Verify an ability can be created.
|
||||
Verify an ability can be created with default uses_per_turn.
|
||||
|
||||
Default is 1 use per turn, matching standard Pokemon TCG rules.
|
||||
"""
|
||||
ability = Ability(
|
||||
name="Rain Dish",
|
||||
@ -117,19 +119,36 @@ class TestAbility:
|
||||
|
||||
assert ability.name == "Rain Dish"
|
||||
assert ability.effect_id == "heal_between_turns"
|
||||
assert ability.once_per_turn is True # Default
|
||||
assert ability.uses_per_turn == 1 # Default: once per turn
|
||||
|
||||
def test_ability_multiple_use_per_turn(self) -> None:
|
||||
def test_ability_unlimited_uses(self) -> None:
|
||||
"""
|
||||
Verify abilities can be marked as usable multiple times per turn.
|
||||
Verify abilities can be configured for unlimited uses per turn.
|
||||
|
||||
Setting uses_per_turn to None allows the ability to be used
|
||||
any number of times per turn.
|
||||
"""
|
||||
ability = Ability(
|
||||
name="Energy Transfer",
|
||||
effect_id="move_energy",
|
||||
once_per_turn=False,
|
||||
uses_per_turn=None,
|
||||
)
|
||||
|
||||
assert ability.once_per_turn is False
|
||||
assert ability.uses_per_turn is None
|
||||
|
||||
def test_ability_multiple_uses_per_turn(self) -> None:
|
||||
"""
|
||||
Verify abilities can have custom uses_per_turn limit.
|
||||
|
||||
Some abilities may be usable 2 or more times per turn.
|
||||
"""
|
||||
ability = Ability(
|
||||
name="Double Draw",
|
||||
effect_id="draw_cards",
|
||||
uses_per_turn=2,
|
||||
)
|
||||
|
||||
assert ability.uses_per_turn == 2
|
||||
|
||||
|
||||
class TestWeaknessResistance:
|
||||
@ -979,14 +998,65 @@ class TestCardInstanceTurnState:
|
||||
|
||||
def test_reset_turn_state(self) -> None:
|
||||
"""
|
||||
Verify reset_turn_state clears ability usage flag.
|
||||
Verify reset_turn_state resets ability usage counter to 0.
|
||||
|
||||
This is called at the start of each turn to allow abilities
|
||||
to be used again.
|
||||
"""
|
||||
instance = CardInstance(instance_id="uuid", definition_id="pikachu")
|
||||
instance.ability_used_this_turn = True
|
||||
instance.ability_uses_this_turn = 3
|
||||
|
||||
instance.reset_turn_state()
|
||||
|
||||
assert instance.ability_used_this_turn is False
|
||||
assert instance.ability_uses_this_turn == 0
|
||||
|
||||
def test_can_use_ability_once_per_turn(self) -> None:
|
||||
"""
|
||||
Verify can_use_ability respects uses_per_turn = 1 (default).
|
||||
|
||||
Standard Pokemon TCG abilities can be used once per turn.
|
||||
"""
|
||||
instance = CardInstance(instance_id="uuid", definition_id="pikachu")
|
||||
ability = Ability(name="Static", effect_id="static_paralysis") # Default: uses_per_turn=1
|
||||
|
||||
assert instance.can_use_ability(ability)
|
||||
|
||||
instance.ability_uses_this_turn = 1
|
||||
assert not instance.can_use_ability(ability)
|
||||
|
||||
def test_can_use_ability_multiple_per_turn(self) -> None:
|
||||
"""
|
||||
Verify can_use_ability respects custom uses_per_turn limit.
|
||||
|
||||
Some abilities can be used multiple times per turn.
|
||||
"""
|
||||
instance = CardInstance(instance_id="uuid", definition_id="pikachu")
|
||||
ability = Ability(name="Double Draw", effect_id="draw_cards", uses_per_turn=2)
|
||||
|
||||
assert instance.can_use_ability(ability)
|
||||
|
||||
instance.ability_uses_this_turn = 1
|
||||
assert instance.can_use_ability(ability)
|
||||
|
||||
instance.ability_uses_this_turn = 2
|
||||
assert not instance.can_use_ability(ability)
|
||||
|
||||
def test_can_use_ability_unlimited(self) -> None:
|
||||
"""
|
||||
Verify can_use_ability allows unlimited uses when uses_per_turn is None.
|
||||
|
||||
Some abilities have no per-turn usage limit.
|
||||
"""
|
||||
instance = CardInstance(instance_id="uuid", definition_id="pikachu")
|
||||
ability = Ability(name="Energy Transfer", effect_id="move_energy", uses_per_turn=None)
|
||||
|
||||
assert instance.can_use_ability(ability)
|
||||
|
||||
instance.ability_uses_this_turn = 10
|
||||
assert instance.can_use_ability(ability)
|
||||
|
||||
instance.ability_uses_this_turn = 100
|
||||
assert instance.can_use_ability(ability)
|
||||
|
||||
|
||||
class TestCardInstanceJsonRoundTrip:
|
||||
|
||||
@ -365,7 +365,10 @@ class TestPlayerStateCreation:
|
||||
|
||||
def test_player_state_defaults(self) -> None:
|
||||
"""
|
||||
Verify PlayerState initializes with empty zones and default flags.
|
||||
Verify PlayerState initializes with empty zones and default counters.
|
||||
|
||||
All per-turn counters should start at 0, and per-game flags should
|
||||
start as False.
|
||||
"""
|
||||
player = PlayerState(player_id="player1")
|
||||
|
||||
@ -377,9 +380,15 @@ class TestPlayerStateCreation:
|
||||
assert player.discard.is_empty()
|
||||
assert player.prizes.is_empty()
|
||||
assert player.score == 0
|
||||
assert not player.energy_attached_this_turn
|
||||
assert not player.supporter_played_this_turn
|
||||
# Per-turn counters start at 0
|
||||
assert player.energy_attachments_this_turn == 0
|
||||
assert player.supporters_played_this_turn == 0
|
||||
assert player.stadiums_played_this_turn == 0
|
||||
assert player.items_played_this_turn == 0
|
||||
assert player.retreats_this_turn == 0
|
||||
# Per-game flags start as False
|
||||
assert not player.gx_attack_used
|
||||
assert not player.vstar_power_used
|
||||
|
||||
def test_player_state_zone_types(self) -> None:
|
||||
"""
|
||||
@ -475,40 +484,171 @@ class TestPlayerStatePokemonTracking:
|
||||
class TestPlayerStateTurnActions:
|
||||
"""Tests for PlayerState turn action tracking."""
|
||||
|
||||
def test_can_attach_energy(self) -> None:
|
||||
def test_can_attach_energy_default_rules(self) -> None:
|
||||
"""
|
||||
Verify can_attach_energy() respects turn flag.
|
||||
Verify can_attach_energy() respects turn counter with default rules.
|
||||
|
||||
Default rules allow 1 energy attachment per turn. Counter starts at 0,
|
||||
so first attachment is allowed, second is not.
|
||||
"""
|
||||
player = PlayerState(player_id="player1")
|
||||
rules = RulesConfig()
|
||||
rules = RulesConfig() # Default: attachments_per_turn = 1
|
||||
|
||||
assert player.can_attach_energy(rules)
|
||||
|
||||
player.energy_attached_this_turn = True
|
||||
player.energy_attachments_this_turn = 1
|
||||
assert not player.can_attach_energy(rules)
|
||||
|
||||
def test_can_play_supporter(self) -> None:
|
||||
def test_can_attach_energy_multiple_per_turn(self) -> None:
|
||||
"""
|
||||
Verify can_play_supporter() respects turn flag.
|
||||
Verify can_attach_energy() respects custom attachments_per_turn rule.
|
||||
|
||||
Some game modes allow multiple energy attachments per turn. This tests
|
||||
that the counter-based approach works with configurable limits.
|
||||
"""
|
||||
player = PlayerState(player_id="player1")
|
||||
rules = RulesConfig()
|
||||
from app.core.config import EnergyConfig
|
||||
|
||||
rules = RulesConfig(energy=EnergyConfig(attachments_per_turn=3))
|
||||
|
||||
assert player.can_attach_energy(rules)
|
||||
|
||||
player.energy_attachments_this_turn = 1
|
||||
assert player.can_attach_energy(rules)
|
||||
|
||||
player.energy_attachments_this_turn = 2
|
||||
assert player.can_attach_energy(rules)
|
||||
|
||||
player.energy_attachments_this_turn = 3
|
||||
assert not player.can_attach_energy(rules)
|
||||
|
||||
def test_can_play_supporter_default_rules(self) -> None:
|
||||
"""
|
||||
Verify can_play_supporter() respects turn counter with default rules.
|
||||
|
||||
Default rules allow 1 Supporter per turn.
|
||||
"""
|
||||
player = PlayerState(player_id="player1")
|
||||
rules = RulesConfig() # Default: supporters_per_turn = 1
|
||||
|
||||
assert player.can_play_supporter(rules)
|
||||
|
||||
player.supporter_played_this_turn = True
|
||||
player.supporters_played_this_turn = 1
|
||||
assert not player.can_play_supporter(rules)
|
||||
|
||||
def test_can_retreat(self) -> None:
|
||||
def test_can_play_supporter_multiple_per_turn(self) -> None:
|
||||
"""
|
||||
Verify can_retreat() respects turn flag and rules.
|
||||
Verify can_play_supporter() respects custom supporters_per_turn rule.
|
||||
|
||||
Tests configurable Supporter limits for custom game modes.
|
||||
"""
|
||||
player = PlayerState(player_id="player1")
|
||||
rules = RulesConfig()
|
||||
from app.core.config import TrainerConfig
|
||||
|
||||
rules = RulesConfig(trainer=TrainerConfig(supporters_per_turn=2))
|
||||
|
||||
assert player.can_play_supporter(rules)
|
||||
|
||||
player.supporters_played_this_turn = 1
|
||||
assert player.can_play_supporter(rules)
|
||||
|
||||
player.supporters_played_this_turn = 2
|
||||
assert not player.can_play_supporter(rules)
|
||||
|
||||
def test_can_play_stadium(self) -> None:
|
||||
"""
|
||||
Verify can_play_stadium() respects turn counter and rules.
|
||||
|
||||
Default rules allow 1 Stadium per turn.
|
||||
"""
|
||||
player = PlayerState(player_id="player1")
|
||||
rules = RulesConfig() # Default: stadiums_per_turn = 1
|
||||
|
||||
assert player.can_play_stadium(rules)
|
||||
|
||||
player.stadiums_played_this_turn = 1
|
||||
assert not player.can_play_stadium(rules)
|
||||
|
||||
def test_can_play_item_unlimited(self) -> None:
|
||||
"""
|
||||
Verify can_play_item() allows unlimited Items when items_per_turn is None.
|
||||
|
||||
In standard Pokemon TCG rules, Items have no per-turn limit.
|
||||
"""
|
||||
player = PlayerState(player_id="player1")
|
||||
rules = RulesConfig() # Default: items_per_turn = None (unlimited)
|
||||
|
||||
assert player.can_play_item(rules)
|
||||
|
||||
player.items_played_this_turn = 10
|
||||
assert player.can_play_item(rules)
|
||||
|
||||
player.items_played_this_turn = 100
|
||||
assert player.can_play_item(rules)
|
||||
|
||||
def test_can_play_item_limited(self) -> None:
|
||||
"""
|
||||
Verify can_play_item() respects limit when items_per_turn is set.
|
||||
|
||||
Some custom game modes may limit Items per turn for balance.
|
||||
"""
|
||||
player = PlayerState(player_id="player1")
|
||||
from app.core.config import TrainerConfig
|
||||
|
||||
rules = RulesConfig(trainer=TrainerConfig(items_per_turn=2))
|
||||
|
||||
assert player.can_play_item(rules)
|
||||
|
||||
player.items_played_this_turn = 1
|
||||
assert player.can_play_item(rules)
|
||||
|
||||
player.items_played_this_turn = 2
|
||||
assert not player.can_play_item(rules)
|
||||
|
||||
def test_can_retreat_default_rules(self) -> None:
|
||||
"""
|
||||
Verify can_retreat() respects turn counter with default rules.
|
||||
|
||||
Default rules allow 1 retreat per turn.
|
||||
"""
|
||||
player = PlayerState(player_id="player1")
|
||||
rules = RulesConfig() # Default: retreats_per_turn = 1
|
||||
|
||||
assert player.can_retreat(rules)
|
||||
|
||||
player.retreated_this_turn = True
|
||||
player.retreats_this_turn = 1
|
||||
assert not player.can_retreat(rules)
|
||||
|
||||
def test_can_retreat_disabled(self) -> None:
|
||||
"""
|
||||
Verify can_retreat() returns False when retreats_per_turn is 0.
|
||||
|
||||
Some game modes may disable retreating entirely.
|
||||
"""
|
||||
player = PlayerState(player_id="player1")
|
||||
from app.core.config import RetreatConfig
|
||||
|
||||
rules = RulesConfig(retreat=RetreatConfig(retreats_per_turn=0))
|
||||
|
||||
assert not player.can_retreat(rules)
|
||||
|
||||
def test_can_retreat_multiple_per_turn(self) -> None:
|
||||
"""
|
||||
Verify can_retreat() respects custom retreats_per_turn rule.
|
||||
|
||||
Some game modes may allow multiple retreats per turn.
|
||||
"""
|
||||
player = PlayerState(player_id="player1")
|
||||
from app.core.config import RetreatConfig
|
||||
|
||||
rules = RulesConfig(retreat=RetreatConfig(retreats_per_turn=2))
|
||||
|
||||
assert player.can_retreat(rules)
|
||||
|
||||
player.retreats_this_turn = 1
|
||||
assert player.can_retreat(rules)
|
||||
|
||||
player.retreats_this_turn = 2
|
||||
assert not player.can_retreat(rules)
|
||||
|
||||
def test_bench_space_available(self) -> None:
|
||||
@ -540,28 +680,33 @@ class TestPlayerStateTurnActions:
|
||||
|
||||
assert not player.can_bench_pokemon(rules)
|
||||
|
||||
def test_reset_turn_flags(self) -> None:
|
||||
def test_reset_turn_state(self) -> None:
|
||||
"""
|
||||
Verify reset_turn_flags() clears all per-turn flags.
|
||||
Verify reset_turn_state() resets all per-turn counters to 0.
|
||||
|
||||
This is called at the start of each turn to allow the player to
|
||||
take all their per-turn actions again.
|
||||
"""
|
||||
player = PlayerState(player_id="player1")
|
||||
player.energy_attached_this_turn = True
|
||||
player.supporter_played_this_turn = True
|
||||
player.stadium_played_this_turn = True
|
||||
player.retreated_this_turn = True
|
||||
player.energy_attachments_this_turn = 3
|
||||
player.supporters_played_this_turn = 2
|
||||
player.stadiums_played_this_turn = 1
|
||||
player.items_played_this_turn = 5
|
||||
player.retreats_this_turn = 2
|
||||
|
||||
# Add a pokemon with ability used
|
||||
# Add a pokemon with ability uses this turn
|
||||
pokemon = make_card_instance("pokemon-1")
|
||||
pokemon.ability_used_this_turn = True
|
||||
pokemon.ability_uses_this_turn = 3
|
||||
player.active.add(pokemon)
|
||||
|
||||
player.reset_turn_flags()
|
||||
player.reset_turn_state()
|
||||
|
||||
assert not player.energy_attached_this_turn
|
||||
assert not player.supporter_played_this_turn
|
||||
assert not player.stadium_played_this_turn
|
||||
assert not player.retreated_this_turn
|
||||
assert not pokemon.ability_used_this_turn
|
||||
assert player.energy_attachments_this_turn == 0
|
||||
assert player.supporters_played_this_turn == 0
|
||||
assert player.stadiums_played_this_turn == 0
|
||||
assert player.items_played_this_turn == 0
|
||||
assert player.retreats_this_turn == 0
|
||||
assert pokemon.ability_uses_this_turn == 0
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@ -710,6 +855,12 @@ class TestGameStateTurnManagement:
|
||||
def test_advance_turn(self) -> None:
|
||||
"""
|
||||
Verify advance_turn() switches players and updates state.
|
||||
|
||||
Tests that advancing the turn:
|
||||
1. Switches to the next player in turn order
|
||||
2. Marks first turn as completed after turn 1
|
||||
3. Sets phase to DRAW
|
||||
4. Resets the new player's turn counters
|
||||
"""
|
||||
game = GameState(
|
||||
game_id="game-1",
|
||||
@ -723,15 +874,17 @@ class TestGameStateTurnManagement:
|
||||
phase=TurnPhase.END,
|
||||
)
|
||||
|
||||
# Set some turn flags on player 2 to verify reset
|
||||
game.players["player2"].energy_attached_this_turn = True
|
||||
# Set some turn counters on player 2 to verify reset
|
||||
game.players["player2"].energy_attachments_this_turn = 1
|
||||
game.players["player2"].supporters_played_this_turn = 1
|
||||
|
||||
game.advance_turn()
|
||||
|
||||
assert game.current_player_id == "player2"
|
||||
assert game.first_turn_completed is True
|
||||
assert game.phase == TurnPhase.DRAW
|
||||
assert not game.players["player2"].energy_attached_this_turn
|
||||
assert game.players["player2"].energy_attachments_this_turn == 0
|
||||
assert game.players["player2"].supporters_played_this_turn == 0
|
||||
|
||||
def test_advance_turn_wraps_around(self) -> None:
|
||||
"""
|
||||
@ -886,11 +1039,14 @@ class TestGameStateJsonRoundTrip:
|
||||
def test_player_state_round_trip(self) -> None:
|
||||
"""
|
||||
Verify PlayerState serializes and deserializes correctly.
|
||||
|
||||
Tests that all fields including turn counters survive JSON round-trip.
|
||||
"""
|
||||
player = PlayerState(player_id="player1")
|
||||
player.hand.add(make_card_instance("hand-1"))
|
||||
player.score = 3
|
||||
player.energy_attached_this_turn = True
|
||||
player.energy_attachments_this_turn = 2
|
||||
player.supporters_played_this_turn = 1
|
||||
|
||||
json_str = player.model_dump_json()
|
||||
restored = PlayerState.model_validate_json(json_str)
|
||||
@ -898,7 +1054,8 @@ class TestGameStateJsonRoundTrip:
|
||||
assert restored.player_id == "player1"
|
||||
assert len(restored.hand) == 1
|
||||
assert restored.score == 3
|
||||
assert restored.energy_attached_this_turn is True
|
||||
assert restored.energy_attachments_this_turn == 2
|
||||
assert restored.supporters_played_this_turn == 1
|
||||
|
||||
def test_game_state_round_trip(self) -> None:
|
||||
"""
|
||||
|
||||
Loading…
Reference in New Issue
Block a user