Add stat modifiers and attack cost overrides to CardInstance
Support card effects that modify Pokemon stats: - hp_modifier: Additive HP change (e.g., +20 from Giant Cape tool) - retreat_cost_modifier: Additive retreat cost change (e.g., -99 for Float Stone) - damage_modifier: Additive damage change for attacks - attack_cost_overrides: dict[int, list[EnergyType]] for per-attack cost changes Added helper methods: - effective_hp(base_hp) - returns max(1, base_hp + hp_modifier) - effective_retreat_cost(base_cost) - returns max(0, base_cost + modifier) - effective_attack_cost(attack_index, base_cost) - returns override or base Updated is_knocked_out() and remaining_hp() to use effective HP. Also updated PROJECT_PLAN.json: - Marked HIGH-003, TEST-006, HIGH-004 as completed (Week 2 complete) - Updated test counts and notes to reflect stat modifier coverage 289 tests passing.
This commit is contained in:
parent
325f1e8af5
commit
092f493cc8
@ -8,7 +8,7 @@
|
||||
"description": "Core game engine scaffolding for a highly configurable Pokemon TCG-inspired card game. The engine must support campaign mode with fixed rules and free play mode with user-configurable rules.",
|
||||
"totalEstimatedHours": 48,
|
||||
"totalTasks": 32,
|
||||
"completedTasks": 10
|
||||
"completedTasks": 14
|
||||
},
|
||||
"categories": {
|
||||
"critical": "Foundation components that block all other work",
|
||||
@ -170,7 +170,7 @@
|
||||
],
|
||||
"suggestedFix": "CardDefinition: immutable template with id, name, card_type, stage, variant, hp, attacks, abilities, weakness, resistance, retreat_cost. CardInstance: mutable state with instance_id, definition_id, damage, attached_energy, status_conditions, turn_played.",
|
||||
"estimatedHours": 2,
|
||||
"notes": "CardDefinition has separate stage (evolution) and variant (knockout points) fields. Includes helper methods: is_basic_pokemon(), is_evolution(), requires_evolution_from_variant(), knockout_points(). CardInstance has status condition management with override rules.",
|
||||
"notes": "CardDefinition has separate stage (evolution) and variant (knockout points) fields. Includes helper methods: is_basic_pokemon(), is_evolution(), requires_evolution_from_variant(), knockout_points(). CardInstance has status condition management with override rules. Added stat modifiers: hp_modifier, retreat_cost_modifier, damage_modifier, attack_cost_overrides for effect support. Ability.uses_per_turn (int|None) replaces once_per_turn boolean for configurable limits.",
|
||||
"completedDate": "2026-01-25"
|
||||
},
|
||||
{
|
||||
@ -187,7 +187,7 @@
|
||||
],
|
||||
"suggestedFix": "Test: Pokemon card creation with attacks, Trainer card creation, Energy card creation, CardInstance damage tracking, energy attachment, status conditions",
|
||||
"estimatedHours": 1.5,
|
||||
"notes": "54 tests covering all card types, stage/variant combinations (Basic EX, Stage 2 GX, etc.), status condition override rules, evolution timing, energy attachment/detachment",
|
||||
"notes": "77 tests covering all card types, stage/variant combinations (Basic EX, Stage 2 GX, etc.), status condition override rules, evolution timing, energy attachment/detachment, stat modifiers (HP, retreat cost, damage), attack cost overrides, ability usage limits (once/multiple/unlimited per turn).",
|
||||
"completedDate": "2026-01-25"
|
||||
},
|
||||
{
|
||||
@ -230,15 +230,16 @@
|
||||
"description": "Define the complete game state model hierarchy: Zone (card collection with operations), PlayerState (all player zones and turn state), GameState (full game including card_registry, rules, players, turn tracking)",
|
||||
"category": "high",
|
||||
"priority": 12,
|
||||
"completed": false,
|
||||
"tested": false,
|
||||
"completed": true,
|
||||
"tested": true,
|
||||
"dependencies": ["CRIT-003", "HIGH-001"],
|
||||
"files": [
|
||||
{"path": "app/core/models/game_state.py", "issue": "File does not exist"}
|
||||
{"path": "app/core/models/game_state.py", "status": "created"}
|
||||
],
|
||||
"suggestedFix": "Zone: list of CardInstance with add/remove/shuffle/draw methods. PlayerState: deck, hand, active, bench, prizes, discard zones plus turn state flags. GameState: game_id, rules, card_registry, players dict, turn tracking, winner.",
|
||||
"estimatedHours": 3,
|
||||
"notes": "GameState.card_registry holds all CardDefinitions used in this game. Zone operations need RandomProvider for shuffle."
|
||||
"notes": "GameState.card_registry holds all CardDefinitions used in this game. Zone operations need RandomProvider for shuffle. PlayerState uses integer counters (not booleans) for per-turn actions to support configurable RulesConfig limits.",
|
||||
"completedDate": "2026-01-25"
|
||||
},
|
||||
{
|
||||
"id": "TEST-006",
|
||||
@ -246,15 +247,16 @@
|
||||
"description": "Test Zone operations, PlayerState initialization and turn resets, GameState properties and player access",
|
||||
"category": "high",
|
||||
"priority": 13,
|
||||
"completed": false,
|
||||
"tested": false,
|
||||
"completed": true,
|
||||
"tested": true,
|
||||
"dependencies": ["HIGH-003"],
|
||||
"files": [
|
||||
{"path": "tests/core/test_models/test_game_state.py", "issue": "File does not exist"}
|
||||
{"path": "tests/core/test_models/test_game_state.py", "status": "created"}
|
||||
],
|
||||
"suggestedFix": "Test: Zone add/remove/shuffle/draw, PlayerState turn state resets, GameState current_player property, GameState is_first_turn logic",
|
||||
"estimatedHours": 2,
|
||||
"notes": "Use SeededRandom for deterministic shuffle tests"
|
||||
"notes": "62 tests covering Zone operations, PlayerState counters with configurable rules (energy, supporters, items, stadiums, retreats), and GameState turn management. Uses SeededRandom for deterministic shuffle tests.",
|
||||
"completedDate": "2026-01-25"
|
||||
},
|
||||
{
|
||||
"id": "HIGH-004",
|
||||
@ -262,15 +264,16 @@
|
||||
"description": "Create shared pytest fixtures for sample cards, game states, and seeded RNG instances",
|
||||
"category": "high",
|
||||
"priority": 14,
|
||||
"completed": false,
|
||||
"tested": false,
|
||||
"completed": true,
|
||||
"tested": true,
|
||||
"dependencies": ["HIGH-001", "HIGH-003", "CRIT-004"],
|
||||
"files": [
|
||||
{"path": "tests/core/conftest.py", "issue": "File does not exist"}
|
||||
{"path": "tests/core/conftest.py", "status": "created"}
|
||||
],
|
||||
"suggestedFix": "Create fixtures: sample_pokemon_card, sample_trainer_card, sample_energy_card, sample_deck, empty_game_state, mid_game_state, seeded_rng",
|
||||
"estimatedHours": 1.5,
|
||||
"notes": "Fixtures should be composable and reusable across all test modules"
|
||||
"notes": "Fixtures include sample cards (Pikachu, Raichu, Charmander), factory functions for CardInstance/GameState creation, and SeededRandom fixtures for deterministic tests.",
|
||||
"completedDate": "2026-01-25"
|
||||
},
|
||||
{
|
||||
"id": "MED-001",
|
||||
@ -608,8 +611,9 @@
|
||||
"tasks": ["HIGH-001", "TEST-004", "HIGH-002", "TEST-005", "HIGH-003", "TEST-006", "HIGH-004"],
|
||||
"estimatedHours": 12,
|
||||
"goals": ["All data models complete", "Test fixtures established"],
|
||||
"status": "IN_PROGRESS",
|
||||
"progress": "Card and Action models complete (4/7 tasks). GameState, fixtures remaining."
|
||||
"status": "COMPLETED",
|
||||
"completedDate": "2026-01-25",
|
||||
"progress": "All 7 tasks complete. CardInstance includes stat modifiers (hp_modifier, retreat_cost_modifier, damage_modifier, attack_cost_overrides) for effect support. PlayerState uses integer counters for configurable per-turn action limits. 289 tests passing."
|
||||
},
|
||||
"week3": {
|
||||
"theme": "Effects System",
|
||||
|
||||
@ -232,6 +232,11 @@ class CardInstance(BaseModel):
|
||||
|
||||
The definition_id links back to the CardDefinition in the game's card registry.
|
||||
|
||||
Stat Modifiers:
|
||||
Card effects can modify base stats using the modifier fields. These are
|
||||
additive: effective_stat = base_stat + modifier. For example, a Tool card
|
||||
that grants +20 HP would set hp_modifier=20.
|
||||
|
||||
Attributes:
|
||||
instance_id: Unique identifier for this specific instance (UUID).
|
||||
definition_id: Reference to the CardDefinition.id.
|
||||
@ -241,6 +246,18 @@ class CardInstance(BaseModel):
|
||||
status_conditions: Active status conditions on this Pokemon.
|
||||
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.
|
||||
hp_modifier: Additive modifier to the Pokemon's base HP. Can be positive
|
||||
(e.g., from Tool cards) or negative (e.g., from effects).
|
||||
retreat_cost_modifier: Additive modifier to retreat cost. Negative values
|
||||
reduce the cost (e.g., Float Stone sets this to -99 to effectively
|
||||
make retreat free). Effective retreat cost is clamped to minimum 0.
|
||||
damage_modifier: Additive modifier to damage dealt by this Pokemon's attacks.
|
||||
Positive values increase damage, negative reduce it.
|
||||
attack_cost_overrides: Override the energy cost for specific attacks. Maps
|
||||
attack index (0-based) to a new list of EnergyType requirements. If an
|
||||
attack index is not in this dict, the base cost from CardDefinition is used.
|
||||
Example: {0: [EnergyType.COLORLESS, EnergyType.COLORLESS]} makes the first
|
||||
attack cost 2 colorless instead of its normal cost.
|
||||
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.
|
||||
@ -258,32 +275,83 @@ class CardInstance(BaseModel):
|
||||
status_conditions: list[StatusCondition] = Field(default_factory=list)
|
||||
ability_uses_this_turn: int = 0
|
||||
|
||||
# Stat modifiers (applied by card effects, tools, abilities, etc.)
|
||||
hp_modifier: int = 0
|
||||
retreat_cost_modifier: int = 0
|
||||
damage_modifier: int = 0
|
||||
attack_cost_overrides: dict[int, list[EnergyType]] = Field(default_factory=dict)
|
||||
|
||||
# Evolution tracking
|
||||
evolved_from_instance_id: str | None = None
|
||||
turn_played: int | None = None
|
||||
turn_evolved: int | None = None
|
||||
|
||||
def is_knocked_out(self, hp: int) -> bool:
|
||||
def effective_hp(self, base_hp: int) -> int:
|
||||
"""Calculate effective max HP including modifiers.
|
||||
|
||||
Args:
|
||||
base_hp: The Pokemon's base HP (from CardDefinition.hp).
|
||||
|
||||
Returns:
|
||||
Effective max HP (minimum 1 to prevent instant KO from negative modifiers).
|
||||
"""
|
||||
return max(1, base_hp + self.hp_modifier)
|
||||
|
||||
def effective_retreat_cost(self, base_cost: int) -> int:
|
||||
"""Calculate effective retreat cost including modifiers.
|
||||
|
||||
Args:
|
||||
base_cost: The Pokemon's base retreat cost (from CardDefinition.retreat_cost).
|
||||
|
||||
Returns:
|
||||
Effective retreat cost (minimum 0, can't be negative).
|
||||
"""
|
||||
return max(0, base_cost + self.retreat_cost_modifier)
|
||||
|
||||
def effective_attack_cost(
|
||||
self, attack_index: int, base_cost: list[EnergyType]
|
||||
) -> list[EnergyType]:
|
||||
"""Get the effective energy cost for an attack, including any overrides.
|
||||
|
||||
Args:
|
||||
attack_index: The index of the attack in CardDefinition.attacks (0-based).
|
||||
base_cost: The attack's base energy cost (from Attack.cost).
|
||||
|
||||
Returns:
|
||||
The effective energy cost - either the override if set, or the base cost.
|
||||
|
||||
Example:
|
||||
# An opponent's effect makes your first attack cost 2 more colorless
|
||||
instance.attack_cost_overrides[0] = base_cost + [EnergyType.COLORLESS] * 2
|
||||
|
||||
# A control effect completely changes the cost
|
||||
instance.attack_cost_overrides[1] = [EnergyType.PSYCHIC, EnergyType.PSYCHIC]
|
||||
"""
|
||||
if attack_index in self.attack_cost_overrides:
|
||||
return self.attack_cost_overrides[attack_index]
|
||||
return base_cost
|
||||
|
||||
def is_knocked_out(self, base_hp: int) -> bool:
|
||||
"""Check if this Pokemon is knocked out.
|
||||
|
||||
Args:
|
||||
hp: The Pokemon's max HP (from CardDefinition).
|
||||
base_hp: The Pokemon's base HP (from CardDefinition.hp).
|
||||
|
||||
Returns:
|
||||
True if damage >= hp.
|
||||
True if damage >= effective HP.
|
||||
"""
|
||||
return self.damage >= hp
|
||||
return self.damage >= self.effective_hp(base_hp)
|
||||
|
||||
def remaining_hp(self, hp: int) -> int:
|
||||
def remaining_hp(self, base_hp: int) -> int:
|
||||
"""Calculate remaining HP.
|
||||
|
||||
Args:
|
||||
hp: The Pokemon's max HP (from CardDefinition).
|
||||
base_hp: The Pokemon's base HP (from CardDefinition.hp).
|
||||
|
||||
Returns:
|
||||
Remaining HP (minimum 0).
|
||||
"""
|
||||
return max(0, hp - self.damage)
|
||||
return max(0, self.effective_hp(base_hp) - self.damage)
|
||||
|
||||
def has_status(self, status: StatusCondition) -> bool:
|
||||
"""Check if this Pokemon has a specific status condition."""
|
||||
|
||||
@ -804,6 +804,268 @@ class TestCardInstance:
|
||||
assert instance.remaining_hp(hp) == 0 # Minimum 0
|
||||
|
||||
|
||||
class TestCardInstanceStatModifiers:
|
||||
"""Tests for stat modifier functionality on CardInstance."""
|
||||
|
||||
def test_effective_hp_no_modifier(self) -> None:
|
||||
"""
|
||||
Verify effective_hp returns base HP when no modifier is set.
|
||||
"""
|
||||
instance = CardInstance(instance_id="uuid", definition_id="pikachu")
|
||||
base_hp = 60
|
||||
|
||||
assert instance.effective_hp(base_hp) == 60
|
||||
|
||||
def test_effective_hp_positive_modifier(self) -> None:
|
||||
"""
|
||||
Verify effective_hp adds positive modifiers (e.g., from Tool cards).
|
||||
|
||||
Example: A Tool card like "Giant Cape" grants +20 HP.
|
||||
"""
|
||||
instance = CardInstance(instance_id="uuid", definition_id="pikachu")
|
||||
instance.hp_modifier = 20
|
||||
base_hp = 60
|
||||
|
||||
assert instance.effective_hp(base_hp) == 80
|
||||
|
||||
def test_effective_hp_negative_modifier(self) -> None:
|
||||
"""
|
||||
Verify effective_hp subtracts negative modifiers.
|
||||
|
||||
Some effects may reduce a Pokemon's max HP.
|
||||
"""
|
||||
instance = CardInstance(instance_id="uuid", definition_id="pikachu")
|
||||
instance.hp_modifier = -20
|
||||
base_hp = 60
|
||||
|
||||
assert instance.effective_hp(base_hp) == 40
|
||||
|
||||
def test_effective_hp_minimum_is_one(self) -> None:
|
||||
"""
|
||||
Verify effective_hp never goes below 1.
|
||||
|
||||
This prevents instant KO from extreme negative modifiers.
|
||||
"""
|
||||
instance = CardInstance(instance_id="uuid", definition_id="pikachu")
|
||||
instance.hp_modifier = -100 # More than base HP
|
||||
base_hp = 60
|
||||
|
||||
assert instance.effective_hp(base_hp) == 1
|
||||
|
||||
def test_is_knocked_out_with_hp_modifier(self) -> None:
|
||||
"""
|
||||
Verify is_knocked_out uses effective HP including modifiers.
|
||||
|
||||
A Pokemon with +20 HP modifier should survive 60 damage
|
||||
when base HP is 60.
|
||||
"""
|
||||
instance = CardInstance(instance_id="uuid", definition_id="pikachu")
|
||||
instance.hp_modifier = 20
|
||||
base_hp = 60
|
||||
|
||||
instance.damage = 60
|
||||
assert instance.is_knocked_out(base_hp) is False # Effective HP is 80
|
||||
|
||||
instance.damage = 80
|
||||
assert instance.is_knocked_out(base_hp) is True
|
||||
|
||||
def test_remaining_hp_with_hp_modifier(self) -> None:
|
||||
"""
|
||||
Verify remaining_hp uses effective HP including modifiers.
|
||||
"""
|
||||
instance = CardInstance(instance_id="uuid", definition_id="pikachu")
|
||||
instance.hp_modifier = 20
|
||||
base_hp = 60
|
||||
|
||||
assert instance.remaining_hp(base_hp) == 80 # Effective HP
|
||||
|
||||
instance.damage = 30
|
||||
assert instance.remaining_hp(base_hp) == 50
|
||||
|
||||
def test_effective_retreat_cost_no_modifier(self) -> None:
|
||||
"""
|
||||
Verify effective_retreat_cost returns base cost when no modifier is set.
|
||||
"""
|
||||
instance = CardInstance(instance_id="uuid", definition_id="pikachu")
|
||||
base_cost = 2
|
||||
|
||||
assert instance.effective_retreat_cost(base_cost) == 2
|
||||
|
||||
def test_effective_retreat_cost_negative_modifier(self) -> None:
|
||||
"""
|
||||
Verify effective_retreat_cost with negative modifier (reduces cost).
|
||||
|
||||
Example: An ability or Tool that reduces retreat cost by 1.
|
||||
"""
|
||||
instance = CardInstance(instance_id="uuid", definition_id="pikachu")
|
||||
instance.retreat_cost_modifier = -1
|
||||
base_cost = 2
|
||||
|
||||
assert instance.effective_retreat_cost(base_cost) == 1
|
||||
|
||||
def test_effective_retreat_cost_free_retreat(self) -> None:
|
||||
"""
|
||||
Verify retreat can be made free with large negative modifier.
|
||||
|
||||
Example: Float Stone sets retreat_cost_modifier to a large negative
|
||||
value to effectively make retreat free regardless of base cost.
|
||||
"""
|
||||
instance = CardInstance(instance_id="uuid", definition_id="snorlax")
|
||||
instance.retreat_cost_modifier = -99 # Effectively free
|
||||
base_cost = 4
|
||||
|
||||
assert instance.effective_retreat_cost(base_cost) == 0
|
||||
|
||||
def test_effective_retreat_cost_minimum_is_zero(self) -> None:
|
||||
"""
|
||||
Verify effective_retreat_cost never goes below 0.
|
||||
"""
|
||||
instance = CardInstance(instance_id="uuid", definition_id="pikachu")
|
||||
instance.retreat_cost_modifier = -10
|
||||
base_cost = 2
|
||||
|
||||
assert instance.effective_retreat_cost(base_cost) == 0
|
||||
|
||||
def test_effective_retreat_cost_positive_modifier(self) -> None:
|
||||
"""
|
||||
Verify effective_retreat_cost with positive modifier (increases cost).
|
||||
|
||||
Some effects may increase retreat cost.
|
||||
"""
|
||||
instance = CardInstance(instance_id="uuid", definition_id="pikachu")
|
||||
instance.retreat_cost_modifier = 2
|
||||
base_cost = 1
|
||||
|
||||
assert instance.effective_retreat_cost(base_cost) == 3
|
||||
|
||||
def test_damage_modifier_default(self) -> None:
|
||||
"""
|
||||
Verify damage_modifier defaults to 0.
|
||||
|
||||
The damage_modifier field exists for attack damage calculations
|
||||
in the game engine.
|
||||
"""
|
||||
instance = CardInstance(instance_id="uuid", definition_id="pikachu")
|
||||
|
||||
assert instance.damage_modifier == 0
|
||||
|
||||
def test_stat_modifiers_in_json_round_trip(self) -> None:
|
||||
"""
|
||||
Verify stat modifiers survive JSON serialization.
|
||||
"""
|
||||
instance = CardInstance(
|
||||
instance_id="uuid",
|
||||
definition_id="pikachu",
|
||||
hp_modifier=20,
|
||||
retreat_cost_modifier=-1,
|
||||
damage_modifier=10,
|
||||
)
|
||||
|
||||
json_str = instance.model_dump_json()
|
||||
restored = CardInstance.model_validate_json(json_str)
|
||||
|
||||
assert restored.hp_modifier == 20
|
||||
assert restored.retreat_cost_modifier == -1
|
||||
assert restored.damage_modifier == 10
|
||||
|
||||
def test_effective_attack_cost_no_override(self) -> None:
|
||||
"""
|
||||
Verify effective_attack_cost returns base cost when no override is set.
|
||||
"""
|
||||
instance = CardInstance(instance_id="uuid", definition_id="pikachu")
|
||||
base_cost = [EnergyType.LIGHTNING, EnergyType.COLORLESS]
|
||||
|
||||
result = instance.effective_attack_cost(0, base_cost)
|
||||
|
||||
assert result == base_cost
|
||||
|
||||
def test_effective_attack_cost_with_override(self) -> None:
|
||||
"""
|
||||
Verify effective_attack_cost returns override when set.
|
||||
|
||||
Example: A control card effect completely changes the attack cost.
|
||||
"""
|
||||
instance = CardInstance(instance_id="uuid", definition_id="pikachu")
|
||||
base_cost = [EnergyType.LIGHTNING]
|
||||
override_cost = [EnergyType.PSYCHIC, EnergyType.PSYCHIC, EnergyType.COLORLESS]
|
||||
|
||||
instance.attack_cost_overrides[0] = override_cost
|
||||
|
||||
result = instance.effective_attack_cost(0, base_cost)
|
||||
|
||||
assert result == override_cost
|
||||
assert result != base_cost
|
||||
|
||||
def test_effective_attack_cost_different_attacks(self) -> None:
|
||||
"""
|
||||
Verify attack cost overrides are per-attack (by index).
|
||||
|
||||
One attack can be overridden while another uses base cost.
|
||||
"""
|
||||
instance = CardInstance(instance_id="uuid", definition_id="pikachu")
|
||||
attack_0_base = [EnergyType.LIGHTNING]
|
||||
attack_1_base = [EnergyType.LIGHTNING, EnergyType.LIGHTNING]
|
||||
|
||||
# Only override attack 1
|
||||
instance.attack_cost_overrides[1] = [EnergyType.COLORLESS]
|
||||
|
||||
# Attack 0 uses base cost
|
||||
assert instance.effective_attack_cost(0, attack_0_base) == attack_0_base
|
||||
# Attack 1 uses override
|
||||
assert instance.effective_attack_cost(1, attack_1_base) == [EnergyType.COLORLESS]
|
||||
|
||||
def test_effective_attack_cost_add_colorless(self) -> None:
|
||||
"""
|
||||
Verify adding colorless energy to an attack cost.
|
||||
|
||||
Common control effect: "Your opponent's attacks cost 2 more colorless."
|
||||
"""
|
||||
instance = CardInstance(instance_id="uuid", definition_id="pikachu")
|
||||
base_cost = [EnergyType.LIGHTNING]
|
||||
|
||||
# Effect adds 2 colorless to the cost
|
||||
new_cost = base_cost + [EnergyType.COLORLESS, EnergyType.COLORLESS]
|
||||
instance.attack_cost_overrides[0] = new_cost
|
||||
|
||||
result = instance.effective_attack_cost(0, base_cost)
|
||||
|
||||
assert result == [EnergyType.LIGHTNING, EnergyType.COLORLESS, EnergyType.COLORLESS]
|
||||
|
||||
def test_effective_attack_cost_make_free(self) -> None:
|
||||
"""
|
||||
Verify making an attack cost nothing.
|
||||
|
||||
Some effects allow attacks for free.
|
||||
"""
|
||||
instance = CardInstance(instance_id="uuid", definition_id="pikachu")
|
||||
base_cost = [EnergyType.LIGHTNING, EnergyType.LIGHTNING, EnergyType.COLORLESS]
|
||||
|
||||
instance.attack_cost_overrides[0] = [] # Free!
|
||||
|
||||
result = instance.effective_attack_cost(0, base_cost)
|
||||
|
||||
assert result == []
|
||||
|
||||
def test_attack_cost_overrides_in_json_round_trip(self) -> None:
|
||||
"""
|
||||
Verify attack cost overrides survive JSON serialization.
|
||||
"""
|
||||
instance = CardInstance(
|
||||
instance_id="uuid",
|
||||
definition_id="pikachu",
|
||||
attack_cost_overrides={
|
||||
0: [EnergyType.COLORLESS, EnergyType.COLORLESS],
|
||||
1: [EnergyType.PSYCHIC],
|
||||
},
|
||||
)
|
||||
|
||||
json_str = instance.model_dump_json()
|
||||
restored = CardInstance.model_validate_json(json_str)
|
||||
|
||||
assert restored.attack_cost_overrides[0] == [EnergyType.COLORLESS, EnergyType.COLORLESS]
|
||||
assert restored.attack_cost_overrides[1] == [EnergyType.PSYCHIC]
|
||||
|
||||
|
||||
class TestCardInstanceStatusConditions:
|
||||
"""Tests for status condition handling on CardInstance."""
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user