Metadata-Version: 2.4
Name: ptg
Version: 0.2.0
Summary: A Python engine for MTG-like card games.
Author: Daniel Pérez Rodríguez
License-Expression: MIT
Project-URL: Repository, https://github.com/Dannyzimmer/PythonTheGathering
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Games/Entertainment :: Turn Based Strategy
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE.txt
Requires-Dist: pyyaml
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Dynamic: license-file

# PTG — Python The Gathering

A library for building MTG-like card games.  Define your cards in YAML,
assemble decks, and drive the game through a small set of pure, testable
actions — no side-effects, no global state.

---

## Table of Contents

1. [Quick Start](#1-quick-start)
2. [Core Concepts](#2-core-concepts)
3. [Card Definition Reference](#3-card-definition-reference)
4. [Mana System](#4-mana-system)
5. [Abilities & Effects](#5-abilities--effects)
6. [Triggers](#6-triggers)
7. [Combat System](#7-combat-system)
8. [Techniques](#8-techniques)
9. [Loading Cards & Building Decks](#9-loading-cards--building-decks)
10. [Public API Reference](#10-public-api-reference)
11. [Appendix: Complete Card Examples](#11-appendix-complete-card-examples)

---

## 1. Quick Start

Minimum working example — two players, hard‑coded cards, one turn.

```python
from ptg.api import (
    Game, Player, CardDefinition,
    CardType, ManaType, ManaPool, ManaRequirement,
    PlayCardAction, DeclareAttackAction,
)
from ptg.engine.types import CardAbility, AlterationData
from ptg.effects.draw import DrawCardEffect

# ── Card definitions ──────────────────────────────────────────
goblin    = CardDefinition("gob_01", "Goblin Warrior", "Fast.",
                           CardType.CREATURE,
                           mana={ManaType.FIRE: 1},
                           attack=2, defense=2)

fire_mana = CardDefinition("fire_01", "Fire Mana", "Gives 1 fire.",
                           CardType.MANA,
                           mana={ManaType.FIRE: 1})

# ── Players ───────────────────────────────────────────────────
alice = Player("Alice", [fire_mana] * 3 + [goblin] * 3, hand_size=3)
bob   = Player("Bob",   [fire_mana] * 3 + [goblin] * 3, hand_size=3)

game = Game([alice, bob])
game.start()
game.advance_phase()                                      # DRAW → MAIN

# Play mana, then a creature
for _ in range(3):
    cid = next(id for id in game.state.hands[alice.uuid]
               if game.state.all_cards[id].name == "Fire Mana")
    game.apply_action(PlayCardAction(), player_id=alice.uuid, card_id=cid)

goblin_id = next(id for id in game.state.hands[alice.uuid]
                 if game.state.all_cards[id].name == "Goblin Warrior")
game.apply_action(PlayCardAction(), player_id=alice.uuid, card_id=goblin_id)

# Attack and end turn (advances through ATTACK, DEFENSE, COMBAT, POSTCOMBAT, END → DRAW)
attacker_id = next(id for id in game.state.battlefield[alice.uuid]
                   if game.state.all_cards[id].name == "Goblin Warrior")
game.apply_action(DeclareAttackAction(), player_id=alice.uuid,
                  attacker_id=attacker_id)
game.advance_phase()   # MAIN → ATTACK
game.advance_phase()   # ATTACK → DEFENSE
game.advance_phase()   # DEFENSE → COMBAT (auto-resolve)
game.advance_phase()   # COMBAT → MAIN_POSTCOMBAT
game.advance_phase()   # MAIN_POSTCOMBAT → END → next player DRAW

print(f"Bob HP: {game.state.players[bob.uuid].health}")   # 18
```

---

## 2. Core Concepts

```mermaid
flowchart LR
    subgraph Setup
        YAML[Card YAML files] -->|yaml_loader| Def[CardDefinition]
        Def -->|deck_builder| Deck[Deck list]
    end

    subgraph Runtime
        Deck -->|Player.initialize| GS[GameState]
        GS --> Actions[Play / Attack / Defend / Activate]
        Actions -->|returns new| GS2[GameState]
        GS2 -->|check_victory| Winner
    end
```

### Card

A card has two representations:

- **CardDefinition** — the blueprint. Immutable. Lives in YAML or Python.
  Contains `card_id`, `name`, `type`, `mana` cost, `health`, `attack`, `defense`,
  and optional `abilities`.
- **CardInstance** — one copy in play. Has a unique `uuid`, current `health`,
  `attack`, `defense`, a `summoning_sick` flag that prevents attacking until the
  next turn, and a reference back to its `CardDefinition`.

### Player

Each player owns four **zones**:

| Zone | Contents | Notes |
|------|----------|-------|
| `deck` | Cards not yet drawn | Shuffled at game start. |
| `hand` | Cards available to play | Hidden from opponent. |
| `battlefield` | Creatures in play | Public. |
| `graveyard` | Discarded / destroyed cards | Public. |

A player also has `health`, a `mana_pool`, and a maximum hand size.

### Game

The `Game` object owns the `GameState` and the `EventBus`.  It provides
methods to advance turns, apply actions, resolve combat, and check victory.

### GameState

The entire game at one point in time — cards, zones, players, pending combats.
Every action returns a **new** `GameState`; the old one is never modified.

---

## 3. Card Definition Reference

### Fields

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `card_id` | `str` | yes | — | Unique identifier. |
| `name` | `str` | yes | — | Display name. |
| `description` | `str` | no | `""` | Flavour or tooltip text. |
| `type` | `CardType` | yes | — | See table below. |
| `mana` | `dict[ManaType, int]` | no | `{}` | Cost to play OR mana provided (for mana cards). |
| `attack` | `int` | no | `0` | Damage dealt when attacking. |
| `defense` | `int` | no | `0` | Creature toughness — its current life. Regenerates each turn. |
| `abilities` | `list[CardAbility]` | no | `None` | See [Abilities & Effects](#5-abilities--effects). |
| `techniques` | `set[Technique]` | no | `set()` | Passive creature keywords. See [Techniques](#8-techniques). |

### CardType

| Value | Meaning |
|-------|---------|
| `creature` | Stays on the battlefield. Can attack and block. Its `defense` is its current life; it regenerates each turn. Enters with summoning sickness. |
| `spell` | Resolves immediately, then goes to graveyard. |
| `mana` | Stays on the battlefield as a permanent.  Provides mana every turn.  Free to play. |

### Examples

```yaml
# A simple creature
card_id: goblin_01
name: Goblin Warrior
type: creature
mana:
  fire: 1
attack: 2
defense: 2
```

```yaml
# A mana card (always free to play)
card_id: fire_mana_01
name: Fire Mana
type: mana
mana:
  fire: 1
```

```yaml
# A spell that deals 3 damage
card_id: fireball_01
name: Fireball
type: spell
mana:
  fire: 2
abilities:
  - name: Explosion
    description: Deal 3 damage.
    ability_type: activated
    mana_requirement: {}
    alterations:
      - effect: damage_card
        start_trigger: null
        params:
          amount: 3
```

---

## 4. Mana System

### ManaType

| Type | Colour / flavour |
|------|-----------------|
| `fire` | Red |
| `earth` | Green |
| `water` | Blue |
| `air` | White |
| `any` | Generic — paid with leftover mana of any type |

### Playing mana

Mana cards are **free to play** and stay on the **battlefield** as permanents.
Playing a `Fire Mana` adds `1 fire` to your pool and to your `mana_sources`.
At the start of each of your turns, your pool is refilled from your sources.

You may only play **one mana card per turn**.  Effects that generate mana
(e.g. `gain_mana`) do not count toward this limit.

### Hand limit

Your maximum hand size is **7 cards**.  At the end of your turn, if you
have more than 7 cards, you discard down to 7 at random.

### First turn

The player who goes first skips their draw step on the first turn.

### The `ANY` type

When a requirement includes `any`, specific types are paid **first**, then
the `any` shortfall is covered from the remaining pool:

```
Pool:  {fire: 2, earth: 1}
Cost:  {fire: 1, any: 1}

1. Pay fire:  pool → {fire: 1, earth: 1}
2. Cover any: pool → {fire: 0, earth: 1}
```

### Cost fields in YAML

| Field | Where | Meaning |
|-------|-------|---------|
| `mana` | Top-level on a card | Cost to play the card (free for mana cards). |
| `mana_requirement` | Inside an `ability` | Cost to activate that ability. |

---

## 5. Abilities & Effects

An **ability** is a named power on a card.  An ability contains one or more
**alterations**; each alteration links an **effect** to a **target** with an
optional **trigger**.

### AbilityType

| Value | Behaviour |
|-------|----------|
| `triggered` | Fires automatically when its `start_trigger` event occurs.  No mana cost at trigger time. |
| `activated` | The player chooses when to use it and pays its `mana_requirement`. |

### Alteration structure

```yaml
alterations:
  - effect: <registered effect name>   # required
    target: <TargetSpec | null>        # default: null (see PendingTrigger below)
    start_trigger: <EventType | null>  # default: null
    end_trigger: <EventType | null>    # default: null
    trigger_self: <bool>               # default: true
    turn_duration: <int | null>        # default: null — number of turns the effect lasts
    params:                            # depends on the effect
      ...
```

### TargetSpec

| Value | Resolves to |
|-------|------------|
| `self` | The card that owns the ability. |
| `owner` | The player who controls the card. |
| `opponent` | The other player. |
| `choose` | The player must choose a target manually (see PendingTrigger). |
| `null` | Same as `choose` — the player must choose. |

### trigger_self

Controls whether a triggered ability only fires for events originating from
the owning card itself:

| Value | Behaviour |
|-------|----------|
| `true` (default) | The ability fires only when the event's `source` matches the owning `card_id`.  Events with `source=None` (turn events) are never filtered. |
| `false` | The ability fires for **any** event of the matching type, regardless of source.  Useful for global-scope triggers like "whenever a creature dies". |

### trigger_owner

Filters triggered abilities by the zone owner of the event. Works together
with `trigger_self` for fine-grained control:

| Value | Behaviour |
|-------|----------|
| `any` (default) | No filtering — triggers for events in any player's zone. |
| `owner` | Triggers only when `zone_owner` matches the card's controller. "When a creature enters YOUR battlefield…" |
| `opponent` | Triggers only when `zone_owner` does **not** match the card's controller. "When an opponent's creature dies…" |

Zone owner info (`zone_owner`) is automatically included in the `context` of
every zone-related event (`ON_CARD_ENTER_BATTLEFIELD`, `ON_CARD_DEATH`, etc.).

```yaml
# When an opponent's creature dies, draw a card
alterations:
  - effect: draw_card
    target: owner
    start_trigger: on_card_death
    trigger_self: false
    trigger_owner: opponent
    params:
      count: 1
```

### trigger_card_id

Filters triggered abilities by the **definition ID** (`card_id` field in YAML)
of the event's source card. When set, the ability only fires if the card that
triggered the event has the specified `card_id`.

Use together with `trigger_self: false` to create "tribal" triggers:

```yaml
# When a Goblin Warrior enters the battlefield, draw a card
alterations:
  - effect: draw_card
    target: owner
    start_trigger: on_card_enter_battlefield
    trigger_self: false
    trigger_card_id: goblin_01
    params:
      count: 1

# When opponent's Giant dies, heal 5
alterations:
  - effect: heal_player
    target: owner
    start_trigger: on_card_death
    trigger_self: false
    trigger_owner: opponent
    trigger_card_id: giant_01
    params:
      amount: 5
```

Events with `source=None` (turn events) are never matched by `trigger_card_id`.

### end_trigger

When an alteration specifies both `start_trigger` and `end_trigger`, the engine
automatically registers a second listener that **reverses** the effect by
negating numeric params when the end event fires:

```yaml
# Temporary +1/+1 until end of turn:
alterations:
  - effect: buff_card
    start_trigger: on_turn_start
    end_trigger: on_turn_end
    target: self
    params:
      attack_increase: 1
      defense_increase: 1
```

At `on_turn_end`, the engine applies `buff_card(attack_increase=-1, defense_increase=-1)`.
This works for any effect with numeric params (`damage_card`, `heal_player`,
`draw_card`, etc.).

### turn_duration

Sets a turn-based expiration for an effect.  The engine tracks the buff
internally and reverses it automatically when `current_turn` reaches
`expires_on_turn`.  `turn_duration: 1` means the effect is reversed at the
next turn start.

```yaml
# +2/+1 until the next turn start
alterations:
  - effect: buff_card
    start_trigger: on_card_enter_battlefield
    target: self
    turn_duration: 1
    params:
      attack_increase: 2
      defense_increase: 1
```

### Effect catalogue

| Effect name | Target type | Parameters | Description |
|------------|-------------|-----------|-------------|
| `damage_card` | Card | `amount: int` | Deals damage to a creature, reducing its defense. |
| `damage_player` | Player | `amount: int` | Deals damage to a player. |
| `heal_player` | Player | `amount: int` | Restores health to a player. |
| `buff_card` | Card | `attack_increase`, `defense_increase` (default 0) | Increases a creature's stats. |
| `buff_all` | Player | `attack_increase`, `defense_increase` (default 0) | Buffs every creature on that player's battlefield. |
| `draw_card` | Player | `count: int` (default 1) | Draws cards from the top of the deck. |
| `destroy_card` | Card | — | Sets a creature's health to 0. |
| `spend_mana` | Player | `mana_requirement: dict` | Deducts mana from the player's pool. |
| `gain_mana` | Player | `amounts: dict[ManaType, int]` | Adds temporary mana to a player's pool (does not increase mana_sources). |
| `spawn_creature` | Player | `template: CardDefinition` + `count: int` (default 1) | Creates copies of a creature on the battlefield. |
| `recruit` | Player | `card_definition_id: str`, `from_zone: ZoneType` | Moves a matching card from hand / deck / graveyard to the battlefield. |
| `return_to_hand` | Player | `card_uuid: str` | Removes a card from the battlefield and puts it back in the owner's hand. |
| `mill` | Player | `count: int` (default 1) | Moves cards from the top of the deck directly into the graveyard. |
| `random_discard` | Player | `count: int` (default 1) | Randomly discards cards from the player's hand into their graveyard. |
| `set_stats` | Card | `attack: int`, `defense: int` | Sets a creature's attack and/or defense to absolute values. |
| `recycle_graveyard` | Player | — | Shuffles all cards from the graveyard back into the deck. |
| `move_card` | Card | `from_zone: ZoneType`, `to_zone: ZoneType` | Moves a specific card from one zone to another within its owner. Example: `from_zone: hand, to_zone: graveyard`. |

### Example: triggered ability

```yaml
abilities:
  - name: Healing Aura
    description: When this enters, heal your hero for 3.
    ability_type: triggered
    mana_requirement: {}
    alterations:
      - effect: heal_player
        target: owner
        start_trigger: on_card_enter_battlefield
        params:
          amount: 3
```

### Example: activated ability

```yaml
abilities:
  - name: Fire Blast
    description: Pay 2 fire → deal 2 damage to opponent.
    ability_type: activated
    mana_requirement:
      fire: 2
    alterations:
      - effect: damage_player
        target: opponent
        params:
          amount: 2
```

### Dynamic parameter references

Params can reference the source card's current stats at runtime using:

| Syntax | Resolves to |
|--------|------------|
| `$attack` | The source card's current attack value. |
| `$defense` | The source card's current defense value. |

```yaml
abilities:
  - name: Life Drain
    description: When this enters, heal equal to its attack.
    ability_type: triggered
    mana_requirement: {}
    alterations:
      - effect: heal_player
        target: owner
        start_trigger: on_card_enter
        params:
          amount: $attack
```

### PendingTrigger — player-chosen targets

When a triggered ability has `target: null` or `target: choose`, the engine
cannot resolve the target automatically.  Instead of dropping the trigger, it
defers it by adding a `PendingTrigger` to `GameState.pending_triggers`.

The game server must:

1. After each action, check if `state.pending_triggers` is non-empty.
2. Prompt the player to select a target (creature or player).
3. Call `game.resolve_pending_trigger(trigger_index, chosen_target)` to apply
   the effect and register the `end_trigger` listener (if present).

The engine handles the full lifecycle — start, resolution, and end reversal —
automatically once the target is chosen.

```python
# Server-side flow
state = game.apply_action(PlayCardAction(), player_id=p1, card_id=card)

if state.pending_triggers:
    # Send trigger info to frontend for target selection
    trigger = state.pending_triggers[0]
    emit("choose_target", {
        "source_card_id": trigger.source_card_id,
        "trigger_index": 0,
    })

# After player picks target "g02":
state = game.resolve_pending_trigger(0, chosen_target=CardID("g02"))
# Engine applies effect and registers end_trigger listener automatically
```

### Example: spawn tokens

```yaml
# Using an inline template
- effect: spawn_creature
  target: owner
  start_trigger: on_card_enter_battlefield
  params:
    template:
      card_id: token_wasp
      name: Wasp
      type: creature
      defense: 1
      attack: 1
    count: 2

# Using a catalog reference (requires loading with a catalog)
- effect: spawn_creature
  target: owner
  start_trigger: on_card_death
  params:
    template_card_id: goblin_01
    count: 1
```

### Example: recruit from graveyard

```yaml
- effect: recruit
  target: owner
  start_trigger: on_card_enter_battlefield
  params:
    card_definition_id: goblin_01
    from_zone: graveyard
```

---

## 6. Triggers

Triggered abilities fire when a matching event is emitted.  The table below
lists every event in the engine.

| EventType (YAML value) | Emitted when … |
|------------------------|---------------|
| `on_card_enter_battlefield` | A creature is placed on the battlefield. |
| `on_card_death` | A creature's health drops to 0 (it is moved to the graveyard). |
| `on_card_receive_damage` | A creature takes damage. |
| `on_card_attack` | A creature is declared as an attacker. |
| `on_card_defense` | A creature is declared as a blocker. |
| `on_card_healed` | A creature is healed. |
| `on_card_draw` | A player draws one or more cards. |
| `on_card_enter_graveyard` | Any card enters the graveyard (creatures, spells). |
| `on_card_leave_battlefield` | A card is removed from the battlefield for any reason (death, bounce). |
| `on_card_leave_graveyard` | A card is removed from the graveyard (e.g., recruited to the battlefield). |
| `on_player_receive_damage` | A player takes damage. |
| `on_player_death` | A player's health drops to 0. |
| `on_player_mana_spent` | A player spends mana. |
| `on_player_mana_gained` | A player gains mana. |
| `on_player_healed` | A player is healed. |
| `on_turn_start` | A player's turn begins (after flags are reset). |
| `on_turn_end` | A player's turn ends. |
| `on_attack_declared` | An attack is declared (before blockers are chosen). |
| `on_defense_declared` | A blocker is assigned to an attacker. |
| `on_combat_resolved` | All pending combats are resolved for the turn. |
| `on_ability_activated` | A player activates an activated ability. |

### Event order during an attack

```mermaid
sequenceDiagram
    participant Attacker
    participant Blocker
    participant Engine

    Attacker->>Engine: DeclareAttackAction
    Engine-->>Engine: emit ON_ATTACK_DECLARED
    Blocker->>Engine: DeclareDefenseAction
    Engine-->>Engine: emit ON_DEFENSE_DECLARED
    Engine->>Engine: resolve_combat()
    Engine-->>Engine: emit ON_CARD_RECEIVE_DAMAGE
    Engine-->>Engine: emit ON_CARD_LEAVE_BATTLEFIELD (if any)
    Engine-->>Engine: emit ON_CARD_DEATH (if any)
    Engine-->>Engine: emit ON_PLAYER_RECEIVE_DAMAGE (spillover)
    Engine-->>Engine: emit ON_COMBAT_RESOLVED
```

---

## 7. Combat System

### Turn structure

```mermaid
flowchart TD
    Start([start]) --> DRAW[DRAW: auto-refresh mana, draw card]
    DRAW --> MAIN[MAIN: play cards, activate abilities]
    MAIN --> ATTACK[ATTACK: declare attackers]
    ATTACK --> DEFENSE[DEFENSE: declare blockers]
    DEFENSE --> COMBAT[COMBAT: auto-resolve damage]
    COMBAT --> MAIN2[MAIN_POSTCOMBAT: play cards]
    MAIN2 --> END[END: auto-switch player]
    END --> DRAW
```

### Direct attack (no blockers)

When no blockers are assigned, the attacker's full `attack` value hits the
defending player directly.

```python
game.apply_action(DeclareAttackAction(),
    player_id=alice.uuid, attacker_id=goblin_id)
game.resolve_combat()
# Bob loses attack value in health
```

### Blocked attack (damage exchange)

Both the attacker and the blocker deal damage to each other simultaneously.
Damage is applied directly to each card's `defense`.  A card dies when its
`defense` reaches 0.

```python
game.apply_action(DeclareDefenseAction(),
    player_id=bob.uuid, blocker_id=giant_id, combat_index=0)
game.resolve_combat()
```

### Multi-blocking

When multiple blockers are assigned to one attacker, the attacker distributes
its damage among blockers in the order chosen by the attacker. It must assign
lethal damage to each blocker before moving to the next. Every blocker deals
its full attack back to the attacker.

### Spillover (trample)

A creature with the **Trample** technique that is blocked may assign excess
damage (beyond what is lethal to all blockers) to the defending player.
Without Trample, excess damage is lost.

### Deathtouch

A creature dealt any amount of damage (>0) by a creature with **Deathtouch**
is destroyed, regardless of its defense.  Deathtouch also reduces the lethal
damage threshold to 1 for damage-assignment purposes (enabling the
deathtouch + trample combo).

### Flying & Reach

Creatures with **Flying** can only be blocked by other creatures with Flying
or by creatures with **Reach**.  Reach does not grant Flying — it only allows
the creature to block Flying attackers.

### Death & graveyard

Creatures that reach 0 health are moved from the battlefield to their owner's
graveyard, and an `on_card_death` event is emitted.

### Attack / block restrictions

**Summoning sickness**: a creature cannot attack or use activated abilities
on the turn it enters the battlefield.  The `summoning_sick` flag is set when
the creature enters and cleared at the start of its controller's next turn.
Creatures with the **Haste** technique ignore summoning sickness.

A creature that attacked during its controller's turn cannot block until the
beginning of that controller's **next** turn.  The `attacked_this_turn` flag
is set by `DeclareAttackAction` and reset by `_handle_draw` at turn start.
`DeclareDefenseAction` rejects any blocker with `attacked_this_turn=True`.

---

## 8. Techniques

Techniques are passive keywords on creatures that modify game rules.  They
are defined as a `set[Technique]` in the card's YAML or `CardDefinition`.

### Available techniques

| Technique | YAML value | Effect |
|-----------|-----------|--------|
| **Haste** | `haste` | Ignores summoning sickness. Can attack and use activated abilities the turn it enters. |
| **Flying** | `flying` | Can only be blocked by creatures with Flying or Reach. |
| **Reach** | `reach` | Can block creatures with Flying (does not grant Flying itself). |
| **Trample** | `trample` | Excess combat damage beyond blockers' lethal spills over to the defending player. |
| **Deathtouch** | `deathtouch` | Any amount of damage (>0) dealt to a creature is lethal. Reduces the lethal threshold to 1 for damage assignment. |

### YAML syntax

```yaml
card_id: sky_knight_01
name: Sky Knight
type: creature
mana:
  air: 2
attack: 3
defense: 2
techniques:
  - flying
  - haste
```

### Helper functions

The `ptg.techniques` package exports helpers for checking techniques
programmatically:

```python
from ptg.techniques import has_flying, has_reach, can_block

if can_block(blocker.card_definition, attacker.card_definition):
    game.apply_action(DeclareDefenseAction(), ...)
```

---

## 9. Loading Cards & Building Decks

### Card YAML files

Place one `.yaml` file per card in a directory:

```
cards/
  goblin.yaml
  fireball.yaml
  fire_mana.yaml
  ...
```

### Deck YAML files (optional)

```yaml
# decks/aggro.yaml
cards:
  goblin_01: 3
  fireball_01: 2
  fire_mana_01: 5
```

### Loading workflow

```python
from ptg.api import (
    load_cards_from_dir, build_catalog, load_deck, build_deck,
    Player, Game,
)

# 1. Load every card definition
all_cards = load_cards_from_dir("cards/")

# 2. Index by card_id for fast lookup
catalog = build_catalog(all_cards)

# 3. Build decks
alice_deck = load_deck(catalog, "decks/alice_aggro.yaml")
bob_deck   = build_deck(catalog, {"giant_01": 3, "fire_mana_01": 5, "heal_01": 2})

# 4. Create players
alice = Player("Alice", alice_deck, initial_health=20, hand_size=4)
bob   = Player("Bob",   bob_deck,   initial_health=20, hand_size=4)

game = Game([alice, bob])
game.start()
```

### Using a catalog for `template_card_id`

When a card references another card via `template_card_id`, pass the catalog
to the loader:

```python
# cards/hive_mind.yaml uses template_card_id: wasp_token_01
catalog = build_catalog(load_cards_from_dir("cards/"))
hive = load_card("cards/hive_mind.yaml", catalog=catalog)
```

---

## 10. Public API Reference

```python
from ptg.api import (
    # ── Core types ───────────────────────────────────────────
    CardType, ManaType, EventType, AbilityType, TargetSpec,
    Technique,
    ManaRequirement, ManaPool,
    # ── Engine ───────────────────────────────────────────────
    Game, GameState, Player,
    CardDefinition, CardAbility, AlterationData,
    PendingTrigger,
    # ── Actions ──────────────────────────────────────────────
    PlayCardAction, DeclareAttackAction,
    DeclareDefenseAction, OrderBlockersAction,
    ActivateAbilityAction,
    # ── I/O ──────────────────────────────────────────────────
    load_card, load_cards_from_dir,
    build_catalog, build_deck, load_deck,
)
```

### `Game` methods

The recommended way to control a turn is `advance_phase()`.  `begin_turn()`
and `end_turn()` are available for fine-grained manual control.

| Method | Returns | Description |
|--------|---------|-------------|
| `start()` | `GameState` | Initialises both players, draws starting hands, sets phase to `DRAW`. |
| `advance_phase()` | `GameState` | Moves to the next phase.  Executes automatic logic for `DRAW` / `COMBAT` / `END`. |
| `begin_turn()` | `GameState` | Manual jump to `DRAW` phase (legacy). |
| `end_turn()` | `GameState` | Manual jump to `END` phase (legacy). |
| `apply_action(action, **kwargs)` | `GameState` | Runs any action through the engine. |
| `resolve_combat()` | `GameState` | Processes all pending combats (called automatically in `COMBAT` phase). |
| `resolve_pending_trigger(index, chosen_target)` | `GameState` | Applies a deferred triggered ability to the player's chosen target and registers its `end_trigger` listener. |
| `check_victory()` | `Player \| None` | Returns the winner, or `None` if the game continues. |

| Property | Type | Description |
|----------|------|-------------|
| `current_phase` | `PhaseType \| None` | Current phase of the active player's turn. |
| `playable_phases` | `tuple[PhaseType, ...]` | Phases where the active player may play cards or activate abilities. |

### Action parameters

| Action | `kwargs` |
|--------|---------|
| `PlayCardAction` | `player_id`, `card_id` |
| `DeclareAttackAction` | `player_id`, `attacker_id` |
| `DeclareDefenseAction` | `player_id`, `blocker_id`, `combat_index` |
| `OrderBlockersAction` | `player_id`, `combat_index`, `ordered_blocker_ids` |
| `ActivateAbilityAction` | `player_id`, `card_id`, `ability_index`, `target_id` *(optional)* |

### I/O functions

| Function | Signature |
|----------|----------|
| `load_card` | `(path, catalog=None) → CardDefinition` |
| `load_cards_from_dir` | `(dir_path, catalog=None) → list[CardDefinition]` |
| `build_catalog` | `(cards) → dict[str, CardDefinition]` |
| `build_deck` | `(catalog, spec) → list[CardDefinition]` |
| `load_deck` | `(catalog, path) → list[CardDefinition]` |

---

## 11. Appendix: Complete Card Examples

### Creature with no abilities

```yaml
card_id: giant_01
name: Clumsy Giant
type: creature
mana:
  earth: 2
defense: 5
attack: 3
defense: 1
```

### Creature with a triggered ability

```yaml
card_id: priest_01
name: Healing Priest
description: When this enters, heal your hero for 3.
type: creature
mana:
  any: 2
defense: 3
attack: 1
defense: 1
abilities:
  - name: Healing Aura
    description: Heal your hero for 3.
    ability_type: triggered
    mana_requirement: {}
    alterations:
      - effect: heal_player
        target: owner
        start_trigger: on_card_enter_battlefield
        params:
          amount: 3
```

### Creature with an activated ability

```yaml
card_id: pyromancer_01
name: Pyromancer
type: creature
mana:
  fire: 2
defense: 3
attack: 2
defense: 0
abilities:
  - name: Fire Blast
    description: Pay 2 fire → deal 2 damage to opponent.
    ability_type: activated
    mana_requirement:
      fire: 2
    alterations:
      - effect: damage_player
        target: opponent
        params:
          amount: 2
```

### Creature that spawns tokens (inline template)

```yaml
card_id: hive_mind_01
name: Hive Mind
description: When this enters, create two 1/1 Wasps.
type: creature
mana:
  any: 3
defense: 2
attack: 1
defense: 0
abilities:
  - name: Swarm
    description: Spawn two Wasp tokens.
    ability_type: triggered
    mana_requirement: {}
    alterations:
      - effect: spawn_creature
        target: owner
        start_trigger: on_card_enter_battlefield
        params:
          template:
            card_id: token_wasp
            name: Wasp
            type: creature
            defense: 1
            attack: 1
          count: 2
```

### Creature that recruits from graveyard

```yaml
card_id: necromancer_01
name: Necromancer
description: When this enters, return a Goblin Warrior from your
             graveyard to the battlefield.
type: creature
mana:
  any: 4
defense: 3
attack: 2
defense: 1
abilities:
  - name: Dark Ritual
    description: Recruit a Goblin Warrior.
    ability_type: triggered
    mana_requirement: {}
    alterations:
      - effect: recruit
        target: owner
        start_trigger: on_card_enter_battlefield
        params:
          card_definition_id: goblin_01
          from_zone: graveyard
```

### Creature that mills the opponent

```yaml
card_id: mill_imp_01
name: Mill Imp
description: When this enters, mill 3 cards from your opponent's deck.
type: creature
mana:
  any: 2
defense: 2
attack: 1
defense: 0
abilities:
  - name: Mind Rot
    description: Mill 3 cards.
    ability_type: triggered
    mana_requirement: {}
    alterations:
      - effect: mill
        target: opponent
        start_trigger: on_card_enter_battlefield
        params:
          count: 3
```

### Creature that reacts to cards leaving the graveyard

```yaml
card_id: graveyard_watcher_01
name: Graveyard Watcher
description: Whenever a card leaves your graveyard, draw a card.
type: creature
mana:
  any: 3
defense: 3
attack: 2
defense: 1
abilities:
  - name: Soul Drain
    description: Draw a card.
    ability_type: triggered
    mana_requirement: {}
    alterations:
      - effect: draw_card
        target: owner
        start_trigger: on_card_leave_graveyard
        params:
          count: 1
```

### Spell

```yaml
card_id: fireball_01
name: Fireball
description: Deal 3 damage to any target.
type: spell
mana:
  fire: 2
abilities:
  - name: Explosion
    description: Deal 3 damage.
    ability_type: activated
    mana_requirement: {}
    alterations:
      - effect: damage_card
        params:
          amount: 3
```

### Creature that generates mana

```yaml
card_id: dark_ritualist_01
name: Dark Ritualist
description: When this enters, gain 2 fire mana until end of turn.
type: creature
mana:
  any: 2
defense: 2
attack: 1
defense: 0
abilities:
  - name: Dark Ritual
    description: Gain 2 fire mana this turn.
    ability_type: triggered
    mana_requirement: {}
    alterations:
      - effect: gain_mana
        target: owner
        start_trigger: on_card_enter
        params:
          amounts:
            fire: 2
```

### Creature that sets a creature's stats

```yaml
card_id: polymorphist_01
name: Polymorphist
description: When this enters, set another creature's stats to 1/1.
type: creature
mana:
  any: 3
defense: 2
attack: 2
defense: 1
abilities:
  - name: Polymorph
    description: Set target creature to 1/1.
    ability_type: triggered
    mana_requirement: {}
    alterations:
      - effect: set_stats
        target: null
        start_trigger: on_card_enter
        params:
          attack: 1
          defense: 0
```

### Creature that discards from opponent's hand

```yaml
card_id: mind_ripper_01
name: Mind Ripper
description: When this enters, your opponent discards a card at random.
type: creature
mana:
  any: 3
defense: 3
attack: 2
defense: 1
abilities:
  - name: Mind Rip
    description: Opponent discards a card at random.
    ability_type: triggered
    mana_requirement: {}
    alterations:
      - effect: random_discard
        target: opponent
        start_trigger: on_card_enter
        params:
          count: 1
```

### Spell that recycles the graveyard

```yaml
card_id: recycle_01
name: Recycle
description: Shuffle your graveyard into your deck.
type: spell
mana:
  any: 2
abilities:
  - name: Recycle
    description: Shuffle graveyard into deck.
    ability_type: activated
    mana_requirement: {}
    alterations:
      - effect: recycle_graveyard
        target: owner
```

### Mana card

```yaml
card_id: fire_mana_01
name: Fire Mana
description: Provides 1 fire mana.
type: mana
mana:
  fire: 1
```

### Deck YAML

```yaml
# decks/midrange.yaml
cards:
  goblin_01: 3
  giant_01: 2
  fireball_01: 2
  pyromancer_01: 2
  fire_mana_01: 5
  earth_mana_01: 6
```
