mantimon-tcg/backend/tests/core/test_config.py
Cal Corum 32541af682 Add card/action models with stage/variant separation
- Add CardDefinition and CardInstance models for card templates and in-game state
- Add Attack, Ability, and WeaknessResistance models for Pokemon card components
- Add 11 action types as discriminated union (PlayPokemon, Evolve, Attack, etc.)
- Split PokemonStage (BASIC, STAGE_1, STAGE_2) from PokemonVariant (NORMAL, EX, GX, V, VMAX, VSTAR)
- Stage determines evolution mechanics, variant determines knockout points
- Update PrizeConfig to use variant for knockout point calculation
- VSTAR and VMAX both worth 3 points; EX, GX, V worth 2 points; NORMAL worth 1 point

Tests: 204 passing, all linting clean
2026-01-24 22:35:31 -06:00

550 lines
17 KiB
Python

"""Tests for the RulesConfig and sub-configuration models.
These tests verify that:
1. Default values match Mantimon TCG house rules from GAME_RULES.md
2. Configuration can be customized via constructor or JSON
3. Nested configs work correctly
4. Helper methods function as expected
"""
import json
from app.core.config import (
BenchConfig,
DeckConfig,
EnergyConfig,
EvolutionConfig,
FirstTurnConfig,
PrizeConfig,
RetreatConfig,
RulesConfig,
StatusConfig,
TrainerConfig,
WinConditionsConfig,
)
from app.core.models.enums import EnergyType, PokemonVariant
class TestDeckConfig:
"""Tests for DeckConfig."""
def test_default_values(self) -> None:
"""
Verify DeckConfig defaults match Mantimon TCG house rules.
Per GAME_RULES.md: 40-card main deck, separate 20-card energy deck.
"""
config = DeckConfig()
assert config.min_size == 40
assert config.max_size == 40
assert config.exact_size_required is True
assert config.max_copies_per_card == 4
assert config.max_copies_basic_energy is None # Unlimited
assert config.min_basic_pokemon == 1
assert config.energy_deck_enabled is True
assert config.energy_deck_size == 20
def test_custom_values(self) -> None:
"""
Verify DeckConfig can be customized.
This is important for free play mode where users can adjust rules.
"""
config = DeckConfig(
min_size=60,
max_size=60,
energy_deck_enabled=False,
)
assert config.min_size == 60
assert config.max_size == 60
assert config.energy_deck_enabled is False
# Other values should still be defaults
assert config.max_copies_per_card == 4
class TestBenchConfig:
"""Tests for BenchConfig."""
def test_default_values(self) -> None:
"""
Verify BenchConfig defaults to standard 5 Pokemon bench.
"""
config = BenchConfig()
assert config.max_size == 5
def test_custom_bench_size(self) -> None:
"""
Verify bench size can be customized for variant rules.
"""
config = BenchConfig(max_size=8)
assert config.max_size == 8
class TestEnergyConfig:
"""Tests for EnergyConfig."""
def test_default_values(self) -> None:
"""
Verify EnergyConfig defaults to Pokemon Pocket-style energy system.
"""
config = EnergyConfig()
assert config.attachments_per_turn == 1
assert config.auto_flip_from_deck is True
assert len(config.types_enabled) == 10
assert EnergyType.FIRE in config.types_enabled
assert EnergyType.COLORLESS in config.types_enabled
def test_all_energy_types_enabled_by_default(self) -> None:
"""
Verify all 10 energy types are enabled by default.
"""
config = EnergyConfig()
expected_types = set(EnergyType)
actual_types = set(config.types_enabled)
assert actual_types == expected_types
def test_restrict_energy_types(self) -> None:
"""
Verify energy types can be restricted for themed games.
"""
config = EnergyConfig(
types_enabled=[EnergyType.FIRE, EnergyType.WATER, EnergyType.COLORLESS]
)
assert len(config.types_enabled) == 3
assert EnergyType.FIRE in config.types_enabled
assert EnergyType.GRASS not in config.types_enabled
class TestPrizeConfig:
"""Tests for PrizeConfig."""
def test_default_values(self) -> None:
"""
Verify PrizeConfig defaults match Mantimon TCG house rules.
Per GAME_RULES.md: 4 points to win, points instead of prize cards.
Knockout points are based on variant (EX, V, etc.), not evolution stage.
"""
config = PrizeConfig()
assert config.count == 4
assert config.per_knockout_normal == 1
assert config.per_knockout_ex == 2
assert config.per_knockout_gx == 2
assert config.per_knockout_v == 2
assert config.per_knockout_vmax == 3
assert config.per_knockout_vstar == 3
assert config.use_prize_cards is False
def test_points_for_knockout_normal(self) -> None:
"""
Verify points_for_knockout returns correct value for normal Pokemon.
Normal Pokemon (non-variant) are worth 1 point regardless of evolution stage.
"""
config = PrizeConfig()
assert config.points_for_knockout(PokemonVariant.NORMAL) == 1
def test_points_for_knockout_ex(self) -> None:
"""
Verify points_for_knockout returns 2 for EX Pokemon.
EX Pokemon are worth 2 knockout points per GAME_RULES.md.
"""
config = PrizeConfig()
assert config.points_for_knockout(PokemonVariant.EX) == 2
def test_points_for_knockout_vmax(self) -> None:
"""
Verify points_for_knockout returns 3 for VMAX Pokemon.
VMAX are the highest-value Pokemon in the game.
"""
config = PrizeConfig()
assert config.points_for_knockout(PokemonVariant.VMAX) == 3
def test_points_for_knockout_all_variants(self) -> None:
"""
Verify points_for_knockout works for all Pokemon variants.
Knockout points are determined by variant, not evolution stage.
A Basic EX is worth the same as a Stage 2 EX.
"""
config = PrizeConfig()
assert config.points_for_knockout(PokemonVariant.NORMAL) == 1
assert config.points_for_knockout(PokemonVariant.EX) == 2
assert config.points_for_knockout(PokemonVariant.GX) == 2
assert config.points_for_knockout(PokemonVariant.V) == 2
assert config.points_for_knockout(PokemonVariant.VMAX) == 3
assert config.points_for_knockout(PokemonVariant.VSTAR) == 3
def test_custom_knockout_points(self) -> None:
"""
Verify knockout point values can be customized.
"""
config = PrizeConfig(per_knockout_normal=2, per_knockout_ex=3)
assert config.points_for_knockout(PokemonVariant.NORMAL) == 2
assert config.points_for_knockout(PokemonVariant.EX) == 3
class TestFirstTurnConfig:
"""Tests for FirstTurnConfig."""
def test_default_values(self) -> None:
"""
Verify FirstTurnConfig defaults match Mantimon TCG house rules.
Per GAME_RULES.md:
- First player CAN draw, attack, and play supporters on turn 1
- First player CANNOT attach energy or evolve on turn 1
"""
config = FirstTurnConfig()
assert config.can_draw is True
assert config.can_attack is True
assert config.can_play_supporter is True
assert config.can_attach_energy is False
assert config.can_evolve is False
def test_standard_pokemon_tcg_first_turn(self) -> None:
"""
Verify configuration for standard Pokemon TCG first turn rules.
In standard rules, first player cannot attack but can attach energy.
"""
config = FirstTurnConfig(
can_attack=False,
can_play_supporter=False,
can_attach_energy=True,
)
assert config.can_attack is False
assert config.can_play_supporter is False
assert config.can_attach_energy is True
class TestWinConditionsConfig:
"""Tests for WinConditionsConfig."""
def test_default_values(self) -> None:
"""
Verify WinConditionsConfig defaults enable all standard win conditions.
"""
config = WinConditionsConfig()
assert config.all_prizes_taken is True
assert config.no_pokemon_in_play is True
assert config.cannot_draw is True
assert config.turn_limit_enabled is True
assert config.turn_limit == 30
assert config.turn_timer_enabled is False
assert config.turn_timer_seconds == 90
assert config.game_timer_enabled is False
assert config.game_timer_minutes == 30
def test_turn_limit_config(self) -> None:
"""
Verify turn limit settings for AI/single-player matches.
Turn limits are useful when real-time timers don't make sense,
such as playing against AI or in puzzle mode.
"""
config = WinConditionsConfig(
turn_limit_enabled=True,
turn_limit=20,
)
assert config.turn_limit_enabled is True
assert config.turn_limit == 20
def test_multiplayer_timer_config(self) -> None:
"""
Verify timer settings for multiplayer mode.
"""
config = WinConditionsConfig(
turn_timer_enabled=True,
turn_timer_seconds=120,
game_timer_enabled=True,
game_timer_minutes=45,
)
assert config.turn_timer_enabled is True
assert config.turn_timer_seconds == 120
assert config.game_timer_enabled is True
assert config.game_timer_minutes == 45
class TestStatusConfig:
"""Tests for StatusConfig."""
def test_default_values(self) -> None:
"""
Verify StatusConfig defaults match GAME_RULES.md.
Per documentation:
- Poison: 10 damage between turns
- Burn: 20 damage between turns, flip to remove
- Confusion: 30 self-damage on tails
"""
config = StatusConfig()
assert config.poison_damage == 10
assert config.burn_damage == 20
assert config.burn_flip_to_remove is True
assert config.sleep_flip_to_wake is True
assert config.confusion_self_damage == 30
class TestTrainerConfig:
"""Tests for TrainerConfig."""
def test_default_values(self) -> None:
"""
Verify TrainerConfig defaults match standard rules.
Per GAME_RULES.md:
- Items: Unlimited per turn
- Supporters: One per turn
- Stadiums: One per turn
- Tools: One per Pokemon
"""
config = TrainerConfig()
assert config.supporters_per_turn == 1
assert config.stadiums_per_turn == 1
assert config.items_per_turn is None # Unlimited
assert config.tools_per_pokemon == 1
class TestEvolutionConfig:
"""Tests for EvolutionConfig."""
def test_default_values(self) -> None:
"""
Verify EvolutionConfig defaults to standard evolution rules.
By default, Pokemon cannot evolve:
- The same turn they were played
- The same turn they already evolved
- On the first turn of the game
"""
config = EvolutionConfig()
assert config.same_turn_as_played is False
assert config.same_turn_as_evolution is False
assert config.first_turn_of_game is False
class TestRetreatConfig:
"""Tests for RetreatConfig."""
def test_default_values(self) -> None:
"""
Verify RetreatConfig defaults to standard retreat rules.
"""
config = RetreatConfig()
assert config.retreats_per_turn == 1
assert config.free_retreat_cost is False
class TestRulesConfig:
"""Tests for the master RulesConfig."""
def test_default_instantiation(self) -> None:
"""
Verify RulesConfig can be instantiated with all defaults.
All nested configs should be created with their own defaults.
"""
rules = RulesConfig()
assert rules.deck.min_size == 40
assert rules.bench.max_size == 5
assert rules.energy.attachments_per_turn == 1
assert rules.prizes.count == 4
assert rules.first_turn.can_attack is True
assert rules.win_conditions.all_prizes_taken is True
assert rules.status.poison_damage == 10
assert rules.trainer.supporters_per_turn == 1
assert rules.evolution.same_turn_as_played is False
assert rules.retreat.retreats_per_turn == 1
def test_partial_override(self) -> None:
"""
Verify RulesConfig allows partial overrides of nested configs.
Only the specified values should change; others remain default.
"""
rules = RulesConfig(
deck=DeckConfig(min_size=60, max_size=60),
prizes=PrizeConfig(count=6),
)
# Overridden values
assert rules.deck.min_size == 60
assert rules.deck.max_size == 60
assert rules.prizes.count == 6
# Default values still in place
assert rules.deck.max_copies_per_card == 4
assert rules.bench.max_size == 5
assert rules.first_turn.can_attack is True
def test_standard_pokemon_tcg_preset(self) -> None:
"""
Verify standard_pokemon_tcg() returns configuration for official rules.
This preset should override Mantimon defaults to match standard Pokemon TCG.
"""
rules = RulesConfig.standard_pokemon_tcg()
# Standard rules: 60-card deck, no energy deck
assert rules.deck.min_size == 60
assert rules.deck.max_size == 60
assert rules.deck.energy_deck_enabled is False
# Standard rules: 6 prizes with prize cards
assert rules.prizes.count == 6
assert rules.prizes.use_prize_cards is True
# Standard rules: first player cannot attack or play supporter
assert rules.first_turn.can_attack is False
assert rules.first_turn.can_play_supporter is False
assert rules.first_turn.can_attach_energy is True
def test_json_round_trip(self) -> None:
"""
Verify RulesConfig serializes and deserializes correctly.
This is critical for:
- Saving game configurations to database
- Sending rules to clients
- Loading preset rule configurations
"""
original = RulesConfig(
deck=DeckConfig(min_size=50),
prizes=PrizeConfig(count=5, per_knockout_ex=3),
)
json_str = original.model_dump_json()
restored = RulesConfig.model_validate_json(json_str)
assert restored.deck.min_size == 50
assert restored.prizes.count == 5
assert restored.prizes.per_knockout_ex == 3
# Defaults should be preserved
assert restored.bench.max_size == 5
def test_json_output_format(self) -> None:
"""
Verify JSON output uses expected field names for compatibility.
Field names in JSON should match the Python attribute names.
"""
rules = RulesConfig()
data = rules.model_dump()
# Verify top-level keys
assert "deck" in data
assert "bench" in data
assert "energy" in data
assert "prizes" in data
assert "first_turn" in data
assert "win_conditions" in data
assert "status" in data
assert "trainer" in data
assert "evolution" in data
assert "retreat" in data
# Verify nested keys
assert "min_size" in data["deck"]
assert "max_size" in data["deck"]
assert "attachments_per_turn" in data["energy"]
def test_nested_config_independence(self) -> None:
"""
Verify nested configs don't share state between RulesConfig instances.
Each RulesConfig should have its own copies of nested configs.
"""
rules1 = RulesConfig()
rules2 = RulesConfig()
# Modify rules1's nested config
rules1.deck.min_size = 100
# rules2 should be unaffected
assert rules2.deck.min_size == 40
class TestRulesConfigFromJson:
"""Tests for loading RulesConfig from JSON strings."""
def test_load_minimal_json(self) -> None:
"""
Verify RulesConfig can be loaded from minimal JSON.
Missing fields should use defaults.
"""
json_str = '{"prizes": {"count": 8}}'
rules = RulesConfig.model_validate_json(json_str)
assert rules.prizes.count == 8
assert rules.deck.min_size == 40 # Default
def test_load_full_json(self) -> None:
"""
Verify RulesConfig can be loaded from comprehensive JSON.
This tests the format documented in GAME_RULES.md.
"""
config_dict = {
"deck": {
"min_size": 60,
"max_size": 60,
"max_copies_per_card": 4,
"min_basic_pokemon": 1,
"energy_deck_enabled": False,
},
"prizes": {
"count": 6,
"per_knockout_basic": 1,
"per_knockout_ex": 2,
"use_prize_cards": True,
},
"energy": {"attachments_per_turn": 1},
"first_turn": {
"can_draw": True,
"can_attack": False,
"can_play_supporter": False,
},
"win_conditions": {
"all_prizes_taken": True,
"no_pokemon_in_play": True,
"cannot_draw": True,
},
}
json_str = json.dumps(config_dict)
rules = RulesConfig.model_validate_json(json_str)
assert rules.deck.min_size == 60
assert rules.deck.energy_deck_enabled is False
assert rules.prizes.count == 6
assert rules.prizes.use_prize_cards is True
assert rules.first_turn.can_attack is False
def test_empty_json_uses_all_defaults(self) -> None:
"""
Verify empty JSON object creates config with all defaults.
"""
rules = RulesConfig.model_validate_json("{}")
assert rules.deck.min_size == 40
assert rules.prizes.count == 4
assert rules.first_turn.can_attack is True