mantimon-tcg/backend/app/core/config.py
Cal Corum 3e82280efb Add game engine foundation: enums, config, and RNG modules
- Create core module structure with models and effects subdirectories
- Add enums module with CardType, EnergyType, TurnPhase, StatusCondition, etc.
- Add RulesConfig with Mantimon TCG defaults (40-card deck, 4 points to win)
- Add RandomProvider protocol with SeededRandom (testing) and SecureRandom (production)
- Include comprehensive tests for all modules (97 tests passing)

Defaults reflect GAME_RULES.md: Pokemon Pocket-style energy deck,
first turn can attack but not attach energy, 30-turn limit enabled.
2026-01-24 22:14:45 -06:00

306 lines
11 KiB
Python

"""Game rules configuration for Mantimon TCG.
This module defines the master configuration system for all game rules. The engine
is highly configurable to support both campaign mode (with fixed rules) and free
play mode (with user-adjustable rules).
Default values are based on the Mantimon TCG house rules documented in GAME_RULES.md,
which use a Pokemon Pocket-inspired energy system with a 40-card main deck and
separate 20-card energy deck.
Usage:
# Use default rules
rules = RulesConfig()
# Customize specific rules
rules = RulesConfig(
deck=DeckConfig(min_size=60, max_size=60),
prizes=PrizeConfig(count=6),
)
# Load from JSON
rules = RulesConfig.model_validate_json(json_string)
"""
from pydantic import BaseModel, Field
from app.core.models.enums import EnergyType, PokemonStage
class DeckConfig(BaseModel):
"""Configuration for deck building rules.
Attributes:
min_size: Minimum number of cards in the main deck.
max_size: Maximum number of cards in the main deck.
exact_size_required: If True, deck must be exactly min_size cards.
max_copies_per_card: Maximum copies of any single card (by name).
max_copies_basic_energy: Max copies of basic energy in energy deck.
None means unlimited.
min_basic_pokemon: Minimum number of Basic Pokemon required.
energy_deck_enabled: If True, use separate energy deck (Pokemon Pocket style).
energy_deck_size: Size of the separate energy deck.
"""
min_size: int = 40
max_size: int = 40
exact_size_required: bool = True
max_copies_per_card: int = 4
max_copies_basic_energy: int | None = None
min_basic_pokemon: int = 1
energy_deck_enabled: bool = True
energy_deck_size: int = 20
class BenchConfig(BaseModel):
"""Configuration for bench rules.
Attributes:
max_size: Maximum number of Pokemon on the bench.
"""
max_size: int = 5
class EnergyConfig(BaseModel):
"""Configuration for energy attachment rules.
Attributes:
attachments_per_turn: Number of energy cards that can be attached per turn.
types_enabled: List of energy types available in this game.
auto_flip_from_deck: If True, flip top card of energy deck at turn start
(Pokemon Pocket style).
"""
attachments_per_turn: int = 1
types_enabled: list[EnergyType] = Field(default_factory=lambda: list(EnergyType))
auto_flip_from_deck: bool = True
class PrizeConfig(BaseModel):
"""Configuration for prize/scoring rules.
In core Mantimon TCG rules, "prizes" are replaced with "points" - players
score points instead of taking prize cards. This simplifies the game while
maintaining the knockout scoring mechanic.
Attributes:
count: Number of points needed to win (or prize cards if using classic rules).
per_knockout_basic: Points scored for knocking out a basic Pokemon.
per_knockout_stage_1: Points scored for knocking out a Stage 1 Pokemon.
per_knockout_stage_2: Points scored for knocking out a Stage 2 Pokemon.
per_knockout_ex: Points scored for knocking out an EX Pokemon.
per_knockout_v: Points scored for knocking out a V Pokemon.
per_knockout_vmax: Points scored for knocking out a VMAX Pokemon.
per_knockout_gx: Points scored for knocking out a GX Pokemon.
use_prize_cards: If True, use classic prize card mechanic instead of points.
prize_selection_random: If True, prize cards are taken randomly (classic).
If False, player chooses which prize to take.
"""
count: int = 4
per_knockout_basic: int = 1
per_knockout_stage_1: int = 1
per_knockout_stage_2: int = 1
per_knockout_ex: int = 2
per_knockout_v: int = 2
per_knockout_vmax: int = 3
per_knockout_gx: int = 2
use_prize_cards: bool = False
prize_selection_random: bool = True
def points_for_knockout(self, stage: PokemonStage) -> int:
"""Get the number of points scored for knocking out a Pokemon of the given stage.
Args:
stage: The PokemonStage of the knocked out Pokemon.
Returns:
Number of points to score.
"""
stage_map = {
PokemonStage.BASIC: self.per_knockout_basic,
PokemonStage.STAGE_1: self.per_knockout_stage_1,
PokemonStage.STAGE_2: self.per_knockout_stage_2,
PokemonStage.EX: self.per_knockout_ex,
PokemonStage.V: self.per_knockout_v,
PokemonStage.VMAX: self.per_knockout_vmax,
PokemonStage.GX: self.per_knockout_gx,
}
return stage_map.get(stage, self.per_knockout_basic)
class FirstTurnConfig(BaseModel):
"""Configuration for first turn restrictions.
These rules apply only to the very first turn of the game (turn 1 for player 1).
Attributes:
can_draw: Whether the first player draws a card on turn 1.
can_attack: Whether the first player can attack on turn 1.
can_play_supporter: Whether the first player can play Supporter cards on turn 1.
can_attach_energy: Whether the first player can attach energy on turn 1.
can_evolve: Whether the first player can evolve Pokemon on turn 1.
"""
can_draw: bool = True
can_attack: bool = True
can_play_supporter: bool = True
can_attach_energy: bool = False
can_evolve: bool = False
class WinConditionsConfig(BaseModel):
"""Configuration for win/loss conditions.
Each condition can be enabled or disabled independently. A player wins
when any enabled win condition is met.
Attributes:
all_prizes_taken: Win when a player scores the required number of points.
no_pokemon_in_play: Win when opponent has no Pokemon in play.
cannot_draw: Win when opponent cannot draw a card at turn start.
turn_limit_enabled: Enable maximum turn count (useful for AI matches).
turn_limit: Maximum number of turns before game ends. Each player's
turn counts as one turn (so 30 = 15 turns per player).
turn_timer_enabled: Enable per-turn time limits (multiplayer).
turn_timer_seconds: Seconds per turn before timeout (default 90).
game_timer_enabled: Enable total game time limit (multiplayer).
game_timer_minutes: Total game time in minutes.
"""
all_prizes_taken: bool = True
no_pokemon_in_play: bool = True
cannot_draw: bool = True
turn_limit_enabled: bool = True
turn_limit: int = 30
turn_timer_enabled: bool = False
turn_timer_seconds: int = 90
game_timer_enabled: bool = False
game_timer_minutes: int = 30
class StatusConfig(BaseModel):
"""Configuration for status condition effects.
Defines the damage values and removal mechanics for each status condition.
Attributes:
poison_damage: Damage dealt by Poison between turns.
burn_damage: Damage dealt by Burn between turns.
burn_flip_to_remove: If True, flip coin between turns; heads removes Burn.
sleep_flip_to_wake: If True, flip coin between turns; heads removes Sleep.
confusion_self_damage: Damage dealt to self on failed confusion flip.
"""
poison_damage: int = 10
burn_damage: int = 20
burn_flip_to_remove: bool = True
sleep_flip_to_wake: bool = True
confusion_self_damage: int = 30
class TrainerConfig(BaseModel):
"""Configuration for Trainer card rules.
Attributes:
supporters_per_turn: Maximum Supporter cards playable per turn.
stadiums_per_turn: Maximum Stadium cards playable per turn.
items_per_turn: Maximum Item cards per turn. None means unlimited.
tools_per_pokemon: Maximum Tool cards attachable to one Pokemon.
"""
supporters_per_turn: int = 1
stadiums_per_turn: int = 1
items_per_turn: int | None = None
tools_per_pokemon: int = 1
class EvolutionConfig(BaseModel):
"""Configuration for evolution rules.
Attributes:
same_turn_as_played: Can evolve a Pokemon the same turn it was played.
same_turn_as_evolution: Can evolve a Pokemon the same turn it evolved.
first_turn_of_game: Can evolve on the very first turn of the game.
"""
same_turn_as_played: bool = False
same_turn_as_evolution: bool = False
first_turn_of_game: bool = False
class RetreatConfig(BaseModel):
"""Configuration for retreat rules.
Attributes:
retreats_per_turn: Maximum number of retreats allowed per turn.
free_retreat_cost: If True, retreating doesn't require discarding energy.
"""
retreats_per_turn: int = 1
free_retreat_cost: bool = False
class RulesConfig(BaseModel):
"""Master configuration for all game rules.
This is the top-level configuration object that contains all rule settings.
Default values are based on Mantimon TCG house rules (Pokemon Pocket-inspired
with 40-card decks, separate energy deck, and 4 points to win).
For standard Pokemon TCG rules, override with:
RulesConfig(
deck=DeckConfig(min_size=60, max_size=60, energy_deck_enabled=False),
prizes=PrizeConfig(count=6, use_prize_cards=True),
first_turn=FirstTurnConfig(can_attack=False, can_attach_energy=True),
)
Attributes:
deck: Deck building configuration.
bench: Bench configuration.
energy: Energy attachment configuration.
prizes: Prize/scoring configuration.
first_turn: First turn restrictions.
win_conditions: Win/loss condition configuration.
status: Status condition effect configuration.
trainer: Trainer card rule configuration.
evolution: Evolution rule configuration.
retreat: Retreat rule configuration.
"""
deck: DeckConfig = Field(default_factory=DeckConfig)
bench: BenchConfig = Field(default_factory=BenchConfig)
energy: EnergyConfig = Field(default_factory=EnergyConfig)
prizes: PrizeConfig = Field(default_factory=PrizeConfig)
first_turn: FirstTurnConfig = Field(default_factory=FirstTurnConfig)
win_conditions: WinConditionsConfig = Field(default_factory=WinConditionsConfig)
status: StatusConfig = Field(default_factory=StatusConfig)
trainer: TrainerConfig = Field(default_factory=TrainerConfig)
evolution: EvolutionConfig = Field(default_factory=EvolutionConfig)
retreat: RetreatConfig = Field(default_factory=RetreatConfig)
@classmethod
def standard_pokemon_tcg(cls) -> "RulesConfig":
"""Create a configuration approximating standard Pokemon TCG rules.
Returns:
RulesConfig with settings closer to official Pokemon TCG.
"""
return cls(
deck=DeckConfig(
min_size=60,
max_size=60,
energy_deck_enabled=False,
),
prizes=PrizeConfig(
count=6,
use_prize_cards=True,
),
first_turn=FirstTurnConfig(
can_attack=False,
can_play_supporter=False,
can_attach_energy=True,
),
)