From f807a4a940a2fcf29c79e7d1bb2462fb91306be3 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Mon, 26 Jan 2026 14:48:49 -0600 Subject: [PATCH] Add interactive game walkthrough script for engine demonstration Creates a comprehensive interactive demo that walks through: - Card definition creation - Rules configuration - Deck building - Game initialization via GameEngine - Setup phase with Basic Pokemon placement - Full turn cycle (draw, main, attack, end phases) Uses colored terminal output and 'press Enter' prompts for step-by-step exploration of the core game engine. --- backend/references/game_walkthrough.py | 1028 ++++++++++++++++++++++++ 1 file changed, 1028 insertions(+) create mode 100644 backend/references/game_walkthrough.py diff --git a/backend/references/game_walkthrough.py b/backend/references/game_walkthrough.py new file mode 100644 index 0000000..94b8186 --- /dev/null +++ b/backend/references/game_walkthrough.py @@ -0,0 +1,1028 @@ +#!/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 uuid +from typing import Callable + +# Register effect handlers +import app.core.effects.handlers # noqa: F401 +from app.core import ( + ActionResult, + CardType, + EnergyType, + GameEngine, + GameState, + PokemonStage, + PokemonVariant, + RulesConfig, + StatusCondition, + TrainerType, + TurnPhase, + create_rng, +) +from app.core.models import ( + Action, + Attack, + AttachEnergyAction, + AttackAction, + CardDefinition, + CardInstance, + PassAction, + PlayPokemonAction, + 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="coin_flip_damage", + effect_params={"base_damage": 30, "bonus_on_heads": 0, "apply_paralysis": True}, + 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 +# ============================================================================= + + +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 = engine.create_game( + player_ids=["player1", "player2"], + decks=decks, + energy_decks=energy_decks, + card_registry=card_registry, + ) + + print_result(f"Game created! ID: {game.game_id[:8]}...") + print_result(f"Turn order: {game.turn_order}") + print_result(f"Current phase: {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(f"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(f"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(f"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_instance_id=energy_card.instance_id, + target_pokemon_id=active_pokemon.instance_id, + from_energy_zone=True, + ) + + result = 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 = engine.execute_action(game, game.current_player_id, action) + + if result.success: + print_result(f"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} 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: + run_walkthrough() + except KeyboardInterrupt: + print("\n\nWalkthrough interrupted. Goodbye!") + except Exception as e: + print(error(f"\nError: {e}")) + raise