diff --git a/backend/references/console_testing.py b/backend/references/console_testing.py new file mode 100644 index 0000000..9145708 --- /dev/null +++ b/backend/references/console_testing.py @@ -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 + [] +""" + +# ============================================================================= +# 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}))")