"""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 ( ActiveConfig, 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 assert config.starting_hand_size == 7 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 def test_custom_starting_hand_size(self) -> None: """ Verify starting_hand_size can be customized. Some game variants may use different starting hand sizes. """ config = DeckConfig(starting_hand_size=5) assert config.starting_hand_size == 5 def test_starting_hand_size_standard_tcg(self) -> None: """ Verify starting_hand_size defaults to 7 (standard Pokemon TCG). This is the standard starting hand size across all Pokemon TCG eras. """ config = DeckConfig() assert config.starting_hand_size == 7 class TestActiveConfig: """Tests for ActiveConfig.""" def test_default_values(self) -> None: """ Verify ActiveConfig defaults to standard single-battle (1 active). Standard Pokemon TCG has exactly one active Pokemon per player. """ config = ActiveConfig() assert config.max_active == 1 def test_double_battle_config(self) -> None: """ Verify ActiveConfig can be configured for double battles. Double battle variants allow 2 active Pokemon per player. """ config = ActiveConfig(max_active=2) assert config.max_active == 2 def test_triple_battle_config(self) -> None: """ Verify ActiveConfig supports exotic battle formats. While unusual, the config should support any number of active Pokemon. """ config = ActiveConfig(max_active=3) assert config.max_active == 3 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 - Same-name stadium replacement: Blocked (standard rules) """ 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 assert config.stadium_same_name_replace is False def test_stadium_same_name_replace_enabled(self) -> None: """ Verify stadium_same_name_replace can be enabled for house rules. Some variants may allow replacing a stadium with the same stadium (e.g., to refresh its effects or reset counters). """ config = TrainerConfig(stadium_same_name_replace=True) assert config.stadium_same_name_replace is True def test_stadium_same_name_replace_disabled_by_default(self) -> None: """ Verify stadium_same_name_replace is disabled by default. In standard Pokemon TCG rules, you cannot play a stadium if a stadium with the same name is already in play. """ config = TrainerConfig() assert config.stadium_same_name_replace is False 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.active.max_active == 1 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.trainer.stadium_same_name_replace is False 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 "active" 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 "max_active" in data["active"] assert "attachments_per_turn" in data["energy"] assert "stadium_same_name_replace" in data["trainer"] 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