mantimon-tcg/backend/references/console_testing.py
Cal Corum e7431e2d1f Move enums to app/core/enums.py and set up clean module exports
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.
2026-01-26 14:45:26 -06:00

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}))")