mantimon-tcg/backend/references/game_walkthrough.py
Cal Corum 5cf2198542 Add engine validation script with attack_coin_status effect handler
- Add attack_coin_status effect handler for coin-flip status conditions
  (e.g., Thunder Shock paralysis on heads)
- Create comprehensive engine_validation.py script (~1250 lines) that
  validates game engine behavior with 29 test cases:
  - Illegal moves (attack without energy, wrong turn, evolution rules)
  - Energy mechanics (attachment limits, cost validation)
  - Weakness calculation (+20 additive mode)
  - Status conditions (paralysis blocks actions, poison damage)
  - Knockout flow (points, forced actions, state cleanup)
  - Win conditions (4 points triggers game over)
- Update game_walkthrough.py Thunder Shock to use new effect handler
- Interactive prompts between sections (Enter to continue, q to quit)
- Uses seed=42 for deterministic, reproducible coin flips

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 00:15:12 -06:00

1023 lines
33 KiB
Python

#!/usr/bin/env python
"""Interactive game walkthrough demonstrating the Mantimon TCG engine.
Usage:
cd backend && uv run python references/game_walkthrough.py
This script creates a new game from scratch, narrates each step, and allows
the user to progress interactively through game setup and a simulated turn.
Press Enter to continue through each step, or 'q' to quit.
"""
# =============================================================================
# PATH SETUP
# =============================================================================
import sys
from pathlib import Path
backend_dir = Path(__file__).resolve().parent.parent
if str(backend_dir) not in sys.path:
sys.path.insert(0, str(backend_dir))
# =============================================================================
# IMPORTS
# =============================================================================
import asyncio
# Register effect handlers
import app.core.effects.handlers # noqa: F401
from app.core import (
CardType,
EnergyType,
GameEngine,
GameState,
PokemonStage,
RulesConfig,
TrainerType,
TurnPhase,
create_rng,
)
from app.core.models import (
AttachEnergyAction,
Attack,
AttackAction,
CardDefinition,
CardInstance,
WeaknessResistance,
)
# =============================================================================
# TERMINAL COLORS
# =============================================================================
class Colors:
"""ANSI color codes for terminal output."""
HEADER = "\033[95m"
BLUE = "\033[94m"
CYAN = "\033[96m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
RED = "\033[91m"
BOLD = "\033[1m"
UNDERLINE = "\033[4m"
END = "\033[0m"
def header(text: str) -> str:
"""Format text as a header."""
return f"{Colors.BOLD}{Colors.HEADER}{text}{Colors.END}"
def success(text: str) -> str:
"""Format text as success (green)."""
return f"{Colors.GREEN}{text}{Colors.END}"
def info(text: str) -> str:
"""Format text as info (cyan)."""
return f"{Colors.CYAN}{text}{Colors.END}"
def warning(text: str) -> str:
"""Format text as warning (yellow)."""
return f"{Colors.YELLOW}{text}{Colors.END}"
def error(text: str) -> str:
"""Format text as error (red)."""
return f"{Colors.RED}{text}{Colors.END}"
def bold(text: str) -> str:
"""Format text as bold."""
return f"{Colors.BOLD}{text}{Colors.END}"
# =============================================================================
# DISPLAY HELPERS
# =============================================================================
def clear_screen():
"""Clear the terminal screen."""
print("\033[2J\033[H", end="")
def print_divider(char: str = "=", width: int = 70):
"""Print a divider line."""
print(char * width)
def wait_for_continue() -> bool:
"""Wait for user to press Enter. Returns False if user wants to quit."""
try:
response = input(f"\n{info('[Press Enter to continue, or q to quit]')} ")
return response.lower() != "q"
except (KeyboardInterrupt, EOFError):
return False
def print_narration(text: str):
"""Print narration text with formatting."""
print(f"\n{Colors.CYAN}{text}{Colors.END}")
def print_action(text: str):
"""Print an action being taken."""
print(f" {Colors.YELLOW}>>> {text}{Colors.END}")
def print_result(text: str):
"""Print the result of an action."""
print(f" {Colors.GREEN} {text}{Colors.END}")
def show_game_state(game: GameState, engine: GameEngine):
"""Display the current game state in a nice format."""
print_divider()
print(header("GAME STATE"))
print_divider()
print(
f"\n{bold('Turn:')} {game.turn_number} | {bold('Phase:')} {game.phase.value} | {bold('Current Player:')} {game.current_player_id}"
)
for player_id in game.turn_order:
player = game.players[player_id]
is_current = player_id == game.current_player_id
marker = " *" if is_current else ""
print(f"\n{bold(f'Player: {player_id}{marker}')}")
print(f" Score: {player.score} / {game.rules.prizes.count} to win")
# Active Pokemon
active = player.get_active_pokemon()
if active:
card_def = game.get_card_definition(active.definition_id)
if card_def:
max_hp = card_def.hp + active.hp_modifier
current_hp = max_hp - active.damage
status = ", ".join(s.value for s in active.status_conditions) or "none"
energy = len(active.attached_energy)
print(
f" {bold('Active:')} {card_def.name} ({current_hp}/{max_hp} HP, {energy} energy, status: {status})"
)
else:
print(f" {bold('Active:')} {warning('None!')}")
# Bench
bench_names = []
for card in player.bench.cards:
card_def = game.get_card_definition(card.definition_id)
if card_def:
max_hp = card_def.hp + card.hp_modifier
current_hp = max_hp - card.damage
bench_names.append(f"{card_def.name} ({current_hp}/{max_hp})")
bench_str = ", ".join(bench_names) if bench_names else "empty"
print(f" {bold('Bench:')} {bench_str} ({len(player.bench)}/{game.rules.bench.max_size})")
# Hand and Deck
print(
f" {bold('Hand:')} {len(player.hand)} cards | {bold('Deck:')} {len(player.deck)} cards | {bold('Energy Zone:')} {len(player.energy_zone)} cards"
)
print()
print_divider()
def show_card(card_def: CardDefinition, indent: str = " "):
"""Display a card definition."""
print(f"{indent}{bold(card_def.name)} ({card_def.card_type.value})")
if card_def.card_type == CardType.POKEMON:
print(
f"{indent} Stage: {card_def.stage.value if card_def.stage else 'N/A'}, HP: {card_def.hp}"
)
print(f"{indent} Type: {card_def.pokemon_type.value if card_def.pokemon_type else 'N/A'}")
if card_def.attacks:
print(f"{indent} Attacks:")
for attack in card_def.attacks:
cost = ", ".join(e.value for e in attack.cost) if attack.cost else "free"
print(f"{indent} - {attack.name}: {attack.damage} damage (cost: {cost})")
if card_def.weakness:
print(f"{indent} Weakness: {card_def.weakness.energy_type.value}")
if card_def.resistance:
print(f"{indent} Resistance: {card_def.resistance.energy_type.value}")
print(f"{indent} Retreat Cost: {card_def.retreat_cost}")
elif card_def.card_type == CardType.TRAINER:
print(f"{indent} Type: {card_def.trainer_type.value if card_def.trainer_type else 'N/A'}")
if card_def.effect_description:
print(f"{indent} Effect: {card_def.effect_description}")
elif card_def.card_type == CardType.ENERGY:
provides = (
", ".join(e.value for e in card_def.energy_provides)
if card_def.energy_provides
else "N/A"
)
print(f"{indent} Provides: {provides}")
# =============================================================================
# CARD DEFINITIONS
# =============================================================================
def create_card_definitions() -> dict[str, CardDefinition]:
"""Create all card definitions for the demo game."""
cards = {}
# === POKEMON CARDS ===
# Pikachu - Basic Lightning
cards["pikachu-001"] = CardDefinition(
id="pikachu-001",
name="Pikachu",
card_type=CardType.POKEMON,
stage=PokemonStage.BASIC,
hp=60,
pokemon_type=EnergyType.LIGHTNING,
attacks=[
Attack(
name="Gnaw",
cost=[],
damage=10,
),
Attack(
name="Thunder Shock",
cost=[EnergyType.LIGHTNING],
damage=30,
effect_id="attack_coin_status",
effect_params={"status": "paralyzed"},
effect_description="Flip a coin. If heads, the Defending Pokemon is now Paralyzed.",
),
],
weakness=WeaknessResistance(energy_type=EnergyType.FIGHTING),
retreat_cost=1,
)
# Raichu - Stage 1 Lightning (evolves from Pikachu)
cards["raichu-001"] = CardDefinition(
id="raichu-001",
name="Raichu",
card_type=CardType.POKEMON,
stage=PokemonStage.STAGE_1,
evolves_from="Pikachu",
hp=100,
pokemon_type=EnergyType.LIGHTNING,
attacks=[
Attack(
name="Thunder Punch",
cost=[EnergyType.LIGHTNING],
damage=50,
),
Attack(
name="Thunderbolt",
cost=[EnergyType.LIGHTNING, EnergyType.LIGHTNING, EnergyType.COLORLESS],
damage=100,
effect_id="discard_energy",
effect_params={"count": "all"},
effect_description="Discard all Energy attached to this Pokemon.",
),
],
weakness=WeaknessResistance(energy_type=EnergyType.FIGHTING),
retreat_cost=1,
)
# Charmander - Basic Fire
cards["charmander-001"] = CardDefinition(
id="charmander-001",
name="Charmander",
card_type=CardType.POKEMON,
stage=PokemonStage.BASIC,
hp=70,
pokemon_type=EnergyType.FIRE,
attacks=[
Attack(
name="Scratch",
cost=[],
damage=10,
),
Attack(
name="Ember",
cost=[EnergyType.FIRE],
damage=40,
effect_id="discard_energy",
effect_params={"count": 1, "energy_type": "fire"},
effect_description="Discard 1 Fire Energy attached to this Pokemon.",
),
],
weakness=WeaknessResistance(energy_type=EnergyType.WATER),
retreat_cost=1,
)
# Squirtle - Basic Water
cards["squirtle-001"] = CardDefinition(
id="squirtle-001",
name="Squirtle",
card_type=CardType.POKEMON,
stage=PokemonStage.BASIC,
hp=60,
pokemon_type=EnergyType.WATER,
attacks=[
Attack(
name="Bubble",
cost=[EnergyType.WATER],
damage=20,
),
Attack(
name="Water Gun",
cost=[EnergyType.WATER, EnergyType.COLORLESS],
damage=40,
),
],
weakness=WeaknessResistance(energy_type=EnergyType.LIGHTNING),
retreat_cost=1,
)
# Bulbasaur - Basic Grass
cards["bulbasaur-001"] = 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=30,
),
Attack(
name="Leech Seed",
cost=[EnergyType.GRASS, EnergyType.COLORLESS],
damage=30,
effect_id="heal",
effect_params={"amount": 10, "target": "self"},
effect_description="Heal 10 damage from this Pokemon.",
),
],
weakness=WeaknessResistance(energy_type=EnergyType.FIRE),
resistance=WeaknessResistance(energy_type=EnergyType.WATER, value=-30),
retreat_cost=2,
)
# Rattata - Basic Normal
cards["rattata-001"] = CardDefinition(
id="rattata-001",
name="Rattata",
card_type=CardType.POKEMON,
stage=PokemonStage.BASIC,
hp=40,
pokemon_type=EnergyType.COLORLESS,
attacks=[
Attack(
name="Bite",
cost=[EnergyType.COLORLESS],
damage=20,
),
],
weakness=WeaknessResistance(energy_type=EnergyType.FIGHTING),
retreat_cost=0, # Free retreat!
)
# === TRAINER CARDS ===
cards["potion-001"] = 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.",
)
cards["professor-001"] = CardDefinition(
id="professor-001",
name="Professor's Research",
card_type=CardType.TRAINER,
trainer_type=TrainerType.SUPPORTER,
effect_id="draw_cards",
effect_params={"count": 7, "discard_hand": True},
effect_description="Discard your hand and draw 7 cards.",
)
# === ENERGY CARDS ===
for energy_type in [EnergyType.LIGHTNING, EnergyType.FIRE, EnergyType.WATER, EnergyType.GRASS]:
cards[f"{energy_type.value}-energy"] = CardDefinition(
id=f"{energy_type.value}-energy",
name=f"{energy_type.value.title()} Energy",
card_type=CardType.ENERGY,
energy_type=energy_type,
energy_provides=[energy_type],
)
return cards
# =============================================================================
# DECK BUILDERS
# =============================================================================
def build_player1_deck(
card_registry: dict[str, CardDefinition],
) -> tuple[list[CardInstance], list[CardInstance]]:
"""Build Player 1's main deck and energy deck (Lightning theme)."""
main_deck = []
energy_deck = []
# Add Pokemon to main deck
for i in range(4):
main_deck.append(CardInstance(instance_id=f"p1-pikachu-{i}", definition_id="pikachu-001"))
for i in range(3):
main_deck.append(CardInstance(instance_id=f"p1-raichu-{i}", definition_id="raichu-001"))
for i in range(2):
main_deck.append(CardInstance(instance_id=f"p1-rattata-{i}", definition_id="rattata-001"))
# Add trainers
for i in range(4):
main_deck.append(CardInstance(instance_id=f"p1-potion-{i}", definition_id="potion-001"))
for i in range(2):
main_deck.append(
CardInstance(instance_id=f"p1-professor-{i}", definition_id="professor-001")
)
# Pad to 40 cards with more Pokemon
while len(main_deck) < 40:
idx = len(main_deck)
main_deck.append(
CardInstance(instance_id=f"p1-pikachu-extra-{idx}", definition_id="pikachu-001")
)
# Energy deck (20 Lightning energy)
for i in range(20):
energy_deck.append(
CardInstance(instance_id=f"p1-lightning-{i}", definition_id="lightning-energy")
)
return main_deck, energy_deck
def build_player2_deck(
card_registry: dict[str, CardDefinition],
) -> tuple[list[CardInstance], list[CardInstance]]:
"""Build Player 2's main deck and energy deck (Fire theme)."""
main_deck = []
energy_deck = []
# Add Pokemon to main deck
for i in range(4):
main_deck.append(
CardInstance(instance_id=f"p2-charmander-{i}", definition_id="charmander-001")
)
for i in range(3):
main_deck.append(
CardInstance(instance_id=f"p2-bulbasaur-{i}", definition_id="bulbasaur-001")
)
for i in range(2):
main_deck.append(CardInstance(instance_id=f"p2-squirtle-{i}", definition_id="squirtle-001"))
# Add trainers
for i in range(4):
main_deck.append(CardInstance(instance_id=f"p2-potion-{i}", definition_id="potion-001"))
for i in range(2):
main_deck.append(
CardInstance(instance_id=f"p2-professor-{i}", definition_id="professor-001")
)
# Pad to 40 cards
while len(main_deck) < 40:
idx = len(main_deck)
main_deck.append(
CardInstance(instance_id=f"p2-charmander-extra-{idx}", definition_id="charmander-001")
)
# Energy deck (mix of Fire and Grass)
for i in range(12):
energy_deck.append(CardInstance(instance_id=f"p2-fire-{i}", definition_id="fire-energy"))
for i in range(8):
energy_deck.append(CardInstance(instance_id=f"p2-grass-{i}", definition_id="grass-energy"))
return main_deck, energy_deck
# =============================================================================
# MAIN WALKTHROUGH
# =============================================================================
async def run_walkthrough():
"""Run the interactive game walkthrough."""
clear_screen()
print_divider("*")
print(header(" MANTIMON TCG - INTERACTIVE GAME WALKTHROUGH"))
print_divider("*")
print_narration("""
Welcome to the Mantimon TCG game engine walkthrough!
This script will demonstrate how the game engine works by:
1. Creating card definitions
2. Building decks for two players
3. Initializing a new game
4. Walking through game setup (shuffle, draw, place basics)
5. Simulating a full turn with actions
Let's begin!
""")
if not wait_for_continue():
print("\nGoodbye!")
return
# =========================================================================
# STEP 1: Create Card Definitions
# =========================================================================
clear_screen()
print_divider()
print(header("STEP 1: Creating Card Definitions"))
print_divider()
print_narration("""
First, we create CardDefinition objects for all cards in the game.
These are immutable templates that define a card's properties:
- Name, type, HP, attacks
- Weaknesses, resistances, retreat cost
- Effect IDs for special abilities
Cards in play use CardInstance objects that reference these definitions.
""")
print_action("Creating card definitions...")
card_registry = create_card_definitions()
print_result(f"Created {len(card_registry)} card definitions")
print("\nSample cards:")
show_card(card_registry["pikachu-001"])
print()
show_card(card_registry["charmander-001"])
if not wait_for_continue():
return
# =========================================================================
# STEP 2: Create Rules Configuration
# =========================================================================
clear_screen()
print_divider()
print(header("STEP 2: Creating Rules Configuration"))
print_divider()
print_narration("""
The RulesConfig defines all game rules - deck sizes, prize counts,
per-turn limits, and more. Mantimon TCG uses house rules inspired by
Pokemon Pocket:
- 40-card main deck + 20-card energy deck
- 4 points to win (no prize cards)
- 1 energy attachment per turn (from energy zone)
- First turn player can attack
""")
print_action("Creating RulesConfig with default Mantimon rules...")
rules = RulesConfig()
print_result("Rules created!")
print(f"\n Deck size: {rules.deck.min_size} cards")
print(f" Energy deck: {rules.deck.energy_deck_size} cards")
print(f" Points to win: {rules.prizes.count}")
print(f" Bench size: {rules.bench.max_size}")
print(f" Energy per turn: {rules.energy.attachments_per_turn}")
print(f" Starting hand: {rules.deck.starting_hand_size} cards")
if not wait_for_continue():
return
# =========================================================================
# STEP 3: Build Decks
# =========================================================================
clear_screen()
print_divider()
print(header("STEP 3: Building Player Decks"))
print_divider()
print_narration("""
Each player needs a main deck and an energy deck.
The main deck contains Pokemon and Trainers.
The energy deck contains only Energy cards.
Player 1 is playing a Lightning deck with Pikachu and Raichu.
Player 2 is playing a mixed Fire/Grass deck.
""")
print_action("Building Player 1's deck (Lightning theme)...")
p1_deck, p1_energy = build_player1_deck(card_registry)
print_result(f"Main deck: {len(p1_deck)} cards, Energy deck: {len(p1_energy)} cards")
print_action("Building Player 2's deck (Fire/Grass theme)...")
p2_deck, p2_energy = build_player2_deck(card_registry)
print_result(f"Main deck: {len(p2_deck)} cards, Energy deck: {len(p2_energy)} cards")
if not wait_for_continue():
return
# =========================================================================
# STEP 4: Initialize Game Engine
# =========================================================================
clear_screen()
print_divider()
print(header("STEP 4: Initializing the Game Engine"))
print_divider()
print_narration("""
The GameEngine is the main orchestrator. It handles:
- Game creation and initialization
- Action validation and execution
- Turn and phase management
- Win condition checking
We create it with our rules and a random number generator.
""")
print_action("Creating SeededRandom (seed=42 for reproducibility)...")
rng = create_rng(seed=42)
print_result("RNG created")
print_action("Creating GameEngine with rules...")
engine = GameEngine(rules=rules, rng=rng)
print_result("Engine created!")
if not wait_for_continue():
return
# =========================================================================
# STEP 5: Create the Game
# =========================================================================
clear_screen()
print_divider()
print(header("STEP 5: Creating the Game"))
print_divider()
print_narration("""
Now we call engine.create_game() with:
- Player IDs
- Each player's decks (main + energy)
- The card registry
The engine will:
1. Validate deck legality
2. Shuffle both decks
3. Deal starting hands (7 cards)
4. Flip energy from energy deck to energy zone
5. Set up turn order
""")
print_action("Creating game with two players...")
decks = {
"player1": p1_deck,
"player2": p2_deck,
}
energy_decks = {
"player1": p1_energy,
"player2": p2_energy,
}
game_creation = engine.create_game(
player_ids=["player1", "player2"],
decks=decks,
energy_decks=energy_decks,
card_registry=card_registry,
)
game = game_creation.game
print_result(f"Game created! ID: {game_creation.game.game_id[:8]}...")
print_result(f"Turn order: {game_creation.game.turn_order}")
print_result(f"Current phase: {game_creation.game.phase.value}")
if not wait_for_continue():
return
# =========================================================================
# STEP 6: Setup Phase - Place Basic Pokemon
# =========================================================================
clear_screen()
print_divider()
print(header("STEP 6: Setup Phase - Placing Basic Pokemon"))
print_divider()
print_narration("""
During setup, each player must place at least one Basic Pokemon
as their Active Pokemon. They may also place Basic Pokemon on the bench.
The game is in SETUP phase until all players have placed their active.
Let's look at each player's hand and place their Pokemon.
""")
for player_id in game.turn_order:
player = game.players[player_id]
print(f"\n{bold(f'{player_id} hand:')}")
basics_in_hand = []
for card in player.hand.cards:
card_def = game.get_card_definition(card.definition_id)
if card_def:
card_type = f"({card_def.card_type.value})"
if card_def.card_type == CardType.POKEMON and card_def.stage == PokemonStage.BASIC:
basics_in_hand.append((card, card_def))
card_type = f"({card_def.card_type.value} - BASIC)"
print(f" - {card_def.name} {card_type}")
if basics_in_hand:
# Place first basic as active
card, card_def = basics_in_hand[0]
print_action(f"Placing {card_def.name} as {player_id}'s active Pokemon")
# In a real game this would be a SelectActiveAction during forced action
# For the demo, we manually move the card
player.hand.remove(card.instance_id)
player.active.add(card)
print_result(f"{card_def.name} is now active!")
# Place additional basics on bench (up to 2 more for demo)
for bench_card, bench_def in basics_in_hand[1:3]:
print_action(f"Placing {bench_def.name} on bench")
player.hand.remove(bench_card.instance_id)
player.bench.add(bench_card)
print_result(f"{bench_def.name} benched!")
# Advance to draw phase
game.phase = TurnPhase.DRAW
game.turn_number = 1
if not wait_for_continue():
return
# =========================================================================
# STEP 7: View Initial Game State
# =========================================================================
clear_screen()
print_divider()
print(header("STEP 7: Initial Game State"))
print_divider()
print_narration("""
Setup is complete! Both players have active Pokemon.
Let's view the game state before starting the first turn.
""")
show_game_state(game, engine)
if not wait_for_continue():
return
# =========================================================================
# STEP 8: Turn 1 - Draw Phase
# =========================================================================
clear_screen()
print_divider()
print(header("STEP 8: Turn 1 - Draw Phase"))
print_divider()
current_player = game.get_current_player()
print_narration(f"""
It's {game.current_player_id}'s turn!
The turn begins with the DRAW phase:
1. Draw a card from the deck
2. Flip an energy from the energy deck to the energy zone
In Mantimon TCG (Pokemon Pocket style), you get one energy per turn
automatically from your energy deck.
""")
# Draw a card
drawn_card = current_player.deck.draw()
if drawn_card:
current_player.hand.add(drawn_card)
card_def = game.get_card_definition(drawn_card.definition_id)
print_action("Drawing a card from deck...")
print_result(f"Drew: {card_def.name if card_def else drawn_card.definition_id}")
# Flip energy
flipped_energy = current_player.energy_deck.draw()
if flipped_energy:
current_player.energy_zone.add(flipped_energy)
card_def = game.get_card_definition(flipped_energy.definition_id)
print_action("Flipping energy from energy deck...")
print_result(f"Flipped: {card_def.name if card_def else flipped_energy.definition_id}")
game.phase = TurnPhase.MAIN
print_result("Advancing to MAIN phase")
print(f"\n{bold('Hand size:')} {len(current_player.hand)} cards")
print(f"{bold('Energy zone:')} {len(current_player.energy_zone)} energy available")
if not wait_for_continue():
return
# =========================================================================
# STEP 9: Turn 1 - Main Phase Actions
# =========================================================================
clear_screen()
print_divider()
print(header("STEP 9: Turn 1 - Main Phase"))
print_divider()
print_narration(f"""
In the MAIN phase, {game.current_player_id} can:
- Play Basic Pokemon to the bench
- Evolve Pokemon (not on first turn they're played)
- Attach energy from the energy zone (1 per turn)
- Play Trainer cards
- Use Pokemon abilities
- Retreat the active Pokemon
Let's attach energy to our active Pokemon!
""")
# Find energy in energy zone
active_pokemon = current_player.get_active_pokemon()
active_def = game.get_card_definition(active_pokemon.definition_id) if active_pokemon else None
if current_player.energy_zone.cards and active_pokemon:
energy_card = current_player.energy_zone.cards[0]
energy_def = game.get_card_definition(energy_card.definition_id)
print_action(
f"Attaching {energy_def.name if energy_def else 'energy'} to {active_def.name if active_def else 'active Pokemon'}..."
)
# Create and execute attach energy action
action = AttachEnergyAction(
energy_card_id=energy_card.instance_id,
target_pokemon_id=active_pokemon.instance_id,
from_energy_zone=True,
)
result = await engine.execute_action(game, game.current_player_id, action)
if result.success:
print_result(
f"Energy attached! {active_def.name if active_def else 'Active'} now has {len(active_pokemon.attached_energy)} energy"
)
else:
print(error(f"Failed: {result.message}"))
if not wait_for_continue():
return
# =========================================================================
# STEP 10: Turn 1 - Attack Phase
# =========================================================================
clear_screen()
print_divider()
print(header("STEP 10: Turn 1 - Attack Phase"))
print_divider()
print_narration(f"""
{game.current_player_id} advances to the ATTACK phase!
Their active {active_def.name if active_def else "Pokemon"} can use one of its attacks
if it has enough energy attached.
""")
game.phase = TurnPhase.ATTACK
if active_pokemon and active_def and active_def.attacks:
print(f"\n{bold('Available attacks:')}")
for i, attack in enumerate(active_def.attacks):
cost = ", ".join(e.value for e in attack.cost) if attack.cost else "free"
print(f" [{i}] {attack.name}: {attack.damage} damage (cost: {cost})")
# Check what attacks we can afford
attached_energy_count = len(active_pokemon.attached_energy)
print(f"\n{bold('Attached energy:')} {attached_energy_count}")
# Try the first attack (usually free or low cost)
chosen_attack = active_def.attacks[0]
print_action(f"Using {chosen_attack.name}!")
action = AttackAction(attack_index=0)
result = await engine.execute_action(game, game.current_player_id, action)
if result.success:
print_result("Attack successful!")
print_result(result.message)
# Show opponent's Pokemon state
opponent = game.get_opponent(game.current_player_id)
opp_active = opponent.get_active_pokemon()
if opp_active:
opp_def = game.get_card_definition(opp_active.definition_id)
max_hp = opp_def.hp + opp_active.hp_modifier if opp_def else 0
print_result(
f"Opponent's {opp_def.name if opp_def else 'Pokemon'}: {max_hp - opp_active.damage}/{max_hp}"
)
else:
print(error(f"Attack failed: {result.message}"))
if not wait_for_continue():
return
# =========================================================================
# STEP 11: End of Turn
# =========================================================================
clear_screen()
print_divider()
print(header("STEP 11: End of Turn"))
print_divider()
print_narration("""
The turn ends after the attack (or if the player passes).
End of turn processing:
1. Apply between-turn effects (poison damage, burn damage, etc.)
2. Check for knockouts
3. Award points for knockouts
4. Check win conditions
5. Switch to next player's turn
""")
# Pass to end turn (this triggers end of turn processing)
game.phase = TurnPhase.END
print_action("Processing end of turn...")
# Advance to next player
old_player = game.current_player_id
game.advance_turn()
print_result(f"Turn ended for {old_player}")
print_result(f"It's now {game.current_player_id}'s turn!")
print_result(f"Turn number: {game.turn_number}")
if not wait_for_continue():
return
# =========================================================================
# FINAL: Show Final State
# =========================================================================
clear_screen()
print_divider()
print(header("WALKTHROUGH COMPLETE!"))
print_divider()
print_narration("""
That concludes the game engine walkthrough!
You've seen how the engine:
- Creates and manages card definitions
- Builds and validates decks
- Handles game initialization
- Processes turn phases and actions
- Manages combat and damage
The game will continue with Player 2's turn, and play proceeds
until someone reaches 4 points (or another win condition is met).
""")
show_game_state(game, engine)
print(f"\n{success('Thanks for exploring the Mantimon TCG engine!')}")
print(
f"\nTo continue experimenting, run: {info('uv run python -i references/console_testing.py')}"
)
# =============================================================================
# ENTRY POINT
# =============================================================================
if __name__ == "__main__":
try:
asyncio.run(run_walkthrough())
except KeyboardInterrupt:
print("\n\nWalkthrough interrupted. Goodbye!")
except Exception as e:
print(error(f"\nError: {e}"))
raise