Add interactive console testing script for manual testing

Provides a ready-to-use REPL environment with:
- Pre-configured game state with two players and active Pokemon
- Sample card definitions (Pikachu, Charmander, Bulbasaur, Potion, Energy)
- Helper functions: make_ctx(), reset_game(), show_board()
- All imports ready (effects, models, config, RNG)

Usage: cd backend && uv run python -i references/console_testing.py
This commit is contained in:
Cal Corum 2026-01-25 00:52:35 -06:00
parent dba2813f80
commit 35bb001292

View File

@ -0,0 +1,353 @@
#!/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
# =============================================================================
from app.core.models.enums import (
CardType,
EnergyType,
ModifierMode,
PokemonStage,
PokemonVariant,
StatusCondition,
TrainerType,
TurnPhase,
)
from app.core.config import (
RulesConfig,
CombatConfig,
DeckConfig,
BenchConfig,
EnergyConfig,
PrizeConfig,
StatusConfig,
TrainerConfig,
)
from app.core.models.card import (
CardDefinition,
CardInstance,
Attack,
Ability,
WeaknessResistance,
)
from app.core.models.game_state import GameState, PlayerState, Zone
from app.core.models.actions import (
PlayPokemonAction,
EvolvePokemonAction,
AttachEnergyAction,
AttackAction,
RetreatAction,
PassAction,
parse_action,
)
from app.core.rng import SeededRandom, SecureRandom, create_rng
from app.core.effects.base import EffectContext, EffectResult, EffectType
from app.core.effects.registry import resolve_effect, list_effects
# Register all built-in effect handlers
import app.core.effects.handlers # noqa: F401
# =============================================================================
# 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}))")