Design Documentation: - docs/ACTIVE_EFFECTS_DESIGN.md: Comprehensive design for persistent effect system - Data model (ActiveEffect, EffectTrigger, EffectScope, StackingMode) - Core operations (register, remove, query effects) - Integration points (damage calc, energy counting, retreat, lifecycle) - Effect categories from Pokemon Pocket card research (~372 cards) - Example implementations (Serperior, Greninja, Mr. Mime, Victreebel) - Post-launch TODO for generic modifier system Module README Files: - backend/app/core/README.md: Core engine overview and key classes - backend/app/core/effects/README.md: Effects module index and quick reference - backend/app/core/models/README.md: Models module with relationship diagram Minor cleanup: - Revert Bulbasaur weakness to Fire (was test change for Lightning) - Clean up debug output in game walkthrough
1024 lines
33 KiB
Python
1024 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,
|
|
ModifierMode,
|
|
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="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
|
|
# =============================================================================
|
|
|
|
|
|
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
|