Architectural refactor to eliminate circular imports and establish clean module boundaries: - Move enums from app/core/models/enums.py to app/core/enums.py (foundational module with zero dependencies) - Update all imports across 30 files to use new enum location - Set up clean export structure: - app.core.enums: canonical source for all enums - app.core: convenience exports for full public API - app.core.models: exports models only (not enums) - Add module exports to app/core/__init__.py and app/core/effects/__init__.py - Remove circular import workarounds from game_state.py This enables app.core.models to export GameState without circular import issues, since enums no longer depend on the models package. All 826 tests passing.
333 lines
10 KiB
Python
333 lines
10 KiB
Python
#!/usr/bin/env python
|
|
"""Interactive console testing reference for Mantimon TCG core engine.
|
|
|
|
Usage:
|
|
cd backend && uv run python -i references/console_testing.py
|
|
|
|
This script sets up a complete game state with cards, players, and demonstrates
|
|
how to use the effects system interactively.
|
|
|
|
After running, you'll have access to:
|
|
- game: A GameState with two players and active Pokemon
|
|
- pikachu, charmander: CardInstance objects (active Pokemon)
|
|
- pikachu_def, charmander_def: CardDefinition objects
|
|
- rng: A SeededRandom for deterministic testing
|
|
- All imports ready to use
|
|
|
|
Quick Examples:
|
|
>>> resolve_effect("deal_damage", make_ctx({"amount": 30}))
|
|
>>> charmander.damage
|
|
30
|
|
>>> resolve_effect("apply_status", make_ctx({"status": "poisoned"}))
|
|
>>> charmander.status_conditions
|
|
[<StatusCondition.POISONED: 'poisoned'>]
|
|
"""
|
|
|
|
# =============================================================================
|
|
# PATH SETUP (ensures imports work when running from backend/)
|
|
# =============================================================================
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Add backend to path if needed (references/ is directly under backend/)
|
|
backend_dir = Path(__file__).resolve().parent.parent
|
|
if str(backend_dir) not in sys.path:
|
|
sys.path.insert(0, str(backend_dir))
|
|
|
|
# =============================================================================
|
|
# IMPORTS
|
|
# =============================================================================
|
|
|
|
# Register all built-in effect handlers
|
|
import app.core.effects.handlers # noqa: F401
|
|
from app.core.config import (
|
|
RulesConfig,
|
|
)
|
|
from app.core.effects.base import EffectContext
|
|
from app.core.effects.registry import list_effects
|
|
from app.core.enums import (
|
|
CardType,
|
|
EnergyType,
|
|
PokemonStage,
|
|
TrainerType,
|
|
TurnPhase,
|
|
)
|
|
from app.core.models.card import (
|
|
Attack,
|
|
CardDefinition,
|
|
CardInstance,
|
|
WeaknessResistance,
|
|
)
|
|
from app.core.models.game_state import GameState, PlayerState
|
|
from app.core.rng import SeededRandom
|
|
|
|
# =============================================================================
|
|
# SAMPLE CARD DEFINITIONS
|
|
# =============================================================================
|
|
|
|
pikachu_def = CardDefinition(
|
|
id="pikachu-001",
|
|
name="Pikachu",
|
|
card_type=CardType.POKEMON,
|
|
stage=PokemonStage.BASIC,
|
|
hp=60,
|
|
pokemon_type=EnergyType.LIGHTNING,
|
|
attacks=[
|
|
Attack(name="Gnaw", damage=10),
|
|
Attack(
|
|
name="Thunder Shock",
|
|
cost=[EnergyType.LIGHTNING],
|
|
damage=20,
|
|
effect_id="may_paralyze",
|
|
effect_description="Flip a coin. If heads, the Defending Pokemon is now Paralyzed.",
|
|
),
|
|
],
|
|
weakness=WeaknessResistance(energy_type=EnergyType.FIGHTING),
|
|
retreat_cost=1,
|
|
)
|
|
|
|
charmander_def = CardDefinition(
|
|
id="charmander-001",
|
|
name="Charmander",
|
|
card_type=CardType.POKEMON,
|
|
stage=PokemonStage.BASIC,
|
|
hp=70,
|
|
pokemon_type=EnergyType.FIRE,
|
|
attacks=[
|
|
Attack(name="Scratch", damage=10),
|
|
Attack(
|
|
name="Ember",
|
|
cost=[EnergyType.FIRE],
|
|
damage=30,
|
|
effect_id="discard_energy",
|
|
effect_params={"count": 1},
|
|
),
|
|
],
|
|
weakness=WeaknessResistance(energy_type=EnergyType.WATER),
|
|
retreat_cost=1,
|
|
)
|
|
|
|
bulbasaur_def = CardDefinition(
|
|
id="bulbasaur-001",
|
|
name="Bulbasaur",
|
|
card_type=CardType.POKEMON,
|
|
stage=PokemonStage.BASIC,
|
|
hp=70,
|
|
pokemon_type=EnergyType.GRASS,
|
|
attacks=[
|
|
Attack(name="Vine Whip", cost=[EnergyType.GRASS], damage=20),
|
|
],
|
|
weakness=WeaknessResistance(energy_type=EnergyType.FIRE),
|
|
resistance=WeaknessResistance(energy_type=EnergyType.WATER),
|
|
retreat_cost=1,
|
|
)
|
|
|
|
# Sample trainer card
|
|
potion_def = CardDefinition(
|
|
id="potion-001",
|
|
name="Potion",
|
|
card_type=CardType.TRAINER,
|
|
trainer_type=TrainerType.ITEM,
|
|
effect_id="heal",
|
|
effect_params={"amount": 30},
|
|
effect_description="Heal 30 damage from one of your Pokemon.",
|
|
)
|
|
|
|
# Sample energy card
|
|
fire_energy_def = CardDefinition(
|
|
id="fire-energy-001",
|
|
name="Fire Energy",
|
|
card_type=CardType.ENERGY,
|
|
energy_type=EnergyType.FIRE,
|
|
energy_provides=[EnergyType.FIRE],
|
|
)
|
|
|
|
# =============================================================================
|
|
# GAME STATE SETUP
|
|
# =============================================================================
|
|
|
|
# Create the game state
|
|
game = GameState(
|
|
game_id="test-game-001",
|
|
rules=RulesConfig(),
|
|
card_registry={
|
|
"pikachu-001": pikachu_def,
|
|
"charmander-001": charmander_def,
|
|
"bulbasaur-001": bulbasaur_def,
|
|
"potion-001": potion_def,
|
|
"fire-energy-001": fire_energy_def,
|
|
},
|
|
players={
|
|
"player1": PlayerState(player_id="player1"),
|
|
"player2": PlayerState(player_id="player2"),
|
|
},
|
|
turn_order=["player1", "player2"],
|
|
current_player_id="player1",
|
|
turn_number=1,
|
|
phase=TurnPhase.MAIN,
|
|
)
|
|
|
|
# Create card instances
|
|
pikachu = CardInstance(instance_id="pika-1", definition_id="pikachu-001")
|
|
charmander = CardInstance(instance_id="char-1", definition_id="charmander-001")
|
|
bulbasaur = CardInstance(instance_id="bulba-1", definition_id="bulbasaur-001")
|
|
|
|
# Set up the board
|
|
game.players["player1"].active.add(pikachu)
|
|
game.players["player2"].active.add(charmander)
|
|
game.players["player2"].bench.add(bulbasaur)
|
|
|
|
# Add some cards to decks
|
|
for i in range(5):
|
|
game.players["player1"].deck.add(
|
|
CardInstance(instance_id=f"p1-deck-{i}", definition_id="bulbasaur-001")
|
|
)
|
|
game.players["player2"].deck.add(
|
|
CardInstance(instance_id=f"p2-deck-{i}", definition_id="bulbasaur-001")
|
|
)
|
|
|
|
# Create RNG
|
|
rng = SeededRandom(seed=42)
|
|
|
|
|
|
# =============================================================================
|
|
# HELPER FUNCTIONS
|
|
# =============================================================================
|
|
|
|
|
|
def make_ctx(params: dict, **kwargs) -> EffectContext:
|
|
"""Create an EffectContext with sensible defaults.
|
|
|
|
Args:
|
|
params: Effect parameters (e.g., {"amount": 30})
|
|
**kwargs: Override defaults (source_player_id, source_card_id, target_card_id, etc.)
|
|
|
|
Returns:
|
|
EffectContext ready to pass to resolve_effect()
|
|
|
|
Example:
|
|
>>> ctx = make_ctx({"amount": 20})
|
|
>>> resolve_effect("deal_damage", ctx)
|
|
"""
|
|
defaults = {
|
|
"game": game,
|
|
"source_player_id": "player1",
|
|
"rng": rng,
|
|
"params": params,
|
|
}
|
|
defaults.update(kwargs)
|
|
return EffectContext(**defaults)
|
|
|
|
|
|
def reset_game():
|
|
"""Reset the game state to initial conditions."""
|
|
global pikachu, charmander, bulbasaur, rng
|
|
|
|
# Reset damage and status
|
|
pikachu.damage = 0
|
|
pikachu.status_conditions = []
|
|
pikachu.hp_modifier = 0
|
|
pikachu.damage_modifier = 0
|
|
pikachu.retreat_cost_modifier = 0
|
|
pikachu.attached_energy = []
|
|
|
|
charmander.damage = 0
|
|
charmander.status_conditions = []
|
|
charmander.hp_modifier = 0
|
|
charmander.damage_modifier = 0
|
|
charmander.retreat_cost_modifier = 0
|
|
charmander.attached_energy = []
|
|
|
|
bulbasaur.damage = 0
|
|
bulbasaur.status_conditions = []
|
|
|
|
# Reset RNG
|
|
rng = SeededRandom(seed=42)
|
|
|
|
print("Game state reset!")
|
|
|
|
|
|
def show_board():
|
|
"""Print the current board state."""
|
|
print("\n" + "=" * 50)
|
|
print("PLAYER 1 (current)")
|
|
p1 = game.players["player1"]
|
|
if p1.active:
|
|
poke = p1.get_active_pokemon()
|
|
poke_def = game.card_registry.get(poke.definition_id)
|
|
hp = poke_def.hp + poke.hp_modifier if poke_def else "?"
|
|
print(f" Active: {poke_def.name if poke_def else poke.definition_id}")
|
|
print(f" HP: {hp - poke.damage}/{hp} Damage: {poke.damage}")
|
|
print(f" Status: {[s.value for s in poke.status_conditions]}")
|
|
print(f" Energy: {poke.attached_energy}")
|
|
print(f" Bench: {len(p1.bench)} Pokemon")
|
|
print(f" Hand: {len(p1.hand)} cards, Deck: {len(p1.deck)} cards")
|
|
|
|
print("\nPLAYER 2 (opponent)")
|
|
p2 = game.players["player2"]
|
|
if p2.active:
|
|
poke = p2.get_active_pokemon()
|
|
poke_def = game.card_registry.get(poke.definition_id)
|
|
hp = poke_def.hp + poke.hp_modifier if poke_def else "?"
|
|
print(f" Active: {poke_def.name if poke_def else poke.definition_id}")
|
|
print(f" HP: {hp - poke.damage}/{hp} Damage: {poke.damage}")
|
|
print(f" Status: {[s.value for s in poke.status_conditions]}")
|
|
print(f" Energy: {poke.attached_energy}")
|
|
print(f" Bench: {len(p2.bench)} Pokemon")
|
|
print(f" Hand: {len(p2.hand)} cards, Deck: {len(p2.deck)} cards")
|
|
print("=" * 50 + "\n")
|
|
|
|
|
|
# =============================================================================
|
|
# DEMONSTRATION
|
|
# =============================================================================
|
|
|
|
if __name__ == "__main__":
|
|
print(__doc__)
|
|
print("\n" + "=" * 60)
|
|
print("AVAILABLE EFFECT HANDLERS")
|
|
print("=" * 60)
|
|
for name in sorted(list_effects()):
|
|
print(f" - {name}")
|
|
|
|
print("\n" + "=" * 60)
|
|
print("QUICK REFERENCE")
|
|
print("=" * 60)
|
|
print("""
|
|
# Deal raw damage (no weakness/resistance)
|
|
resolve_effect("deal_damage", make_ctx({"amount": 30}))
|
|
|
|
# Deal attack damage (with weakness/resistance)
|
|
resolve_effect("attack_damage", make_ctx({"amount": 30}, source_card_id="pika-1"))
|
|
|
|
# Apply status condition
|
|
resolve_effect("apply_status", make_ctx({"status": "poisoned"}))
|
|
|
|
# Heal damage
|
|
resolve_effect("heal", make_ctx({"amount": 20}))
|
|
|
|
# Draw cards
|
|
resolve_effect("draw_cards", make_ctx({"count": 3}))
|
|
|
|
# Coin flip damage
|
|
resolve_effect("coin_flip_damage", make_ctx({"damage_per_heads": 20, "flip_count": 3}))
|
|
|
|
# Bench damage
|
|
resolve_effect("bench_damage", make_ctx({"amount": 10}))
|
|
|
|
# Show board state
|
|
show_board()
|
|
|
|
# Reset everything
|
|
reset_game()
|
|
""")
|
|
|
|
print("=" * 60)
|
|
print("INITIAL BOARD STATE")
|
|
print("=" * 60)
|
|
show_board()
|
|
|
|
print("Ready for interactive testing!")
|
|
print("Try: resolve_effect('deal_damage', make_ctx({'amount': 30}))")
|