Skip to main content

A Python engine for MTG-like card games.

Project description

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
  2. Core Concepts
  3. Card Definition Reference
  4. Mana System
  5. Abilities & Effects
  6. Triggers
  7. Combat System
  8. Techniques
  9. Group (Tribes)
  10. Loading Cards & Building Decks
  11. Public API Reference
  12. Appendix: Complete Card Examples

1. Quick Start

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

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

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.
techniques set[Technique] no set() Passive creature keywords. See Techniques.
group str no None Tribal identifier. See Group.

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

# A simple creature
card_id: goblin_01
name: Goblin Warrior
type: creature
mana:
  fire: 1
attack: 2
defense: 2
# A mana card (always free to play)
card_id: fire_mana_01
name: Fire Mana
type: mana
mana:
  fire: 1
# 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

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.).

# 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:

# 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:

# 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.

# +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 Card 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

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

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.
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.

# 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

# 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

- 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

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

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.

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.

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

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:

from ptg.techniques import has_flying, has_reach, can_block

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

9. Group (Tribes)

Cards can belong to a group via the group field (e.g., vampire, witch, soldier). Group information enables two features:

9.1 Conditional triggers by group

Use require_group on an alteration to make a triggered ability fire only when a card of that group is present in a specific zone:

abilities:
  - name: Pack Leader
    ability_type: triggered
    alterations:
      - effect: buff_card
        target: self
        start_trigger: on_card_enter_battlefield
        require_group: witch
        require_in_zone: battlefield
        require_zone_owner: owner
        params:
          attack_increase: 1

Works alongside require_card_id, with AND semantics (if both are specified, both must be present). Uses the same require_in_zone and require_zone_owner fields.

9.2 Mass effects by group

Use group on an alteration to apply the effect to all cards of that group in a specific zone, instead of a single target:

abilities:
  - name: Witch Hunt
    ability_type: activated
    alterations:
      - effect: destroy_card
        target: opponent       # whose zone to search
        group: witch            # which group to affect
        from_zone: battlefield  # optional, defaults to battlefield

The target must resolve to a player ID (owner or opponent). If it resolves to a card ID (e.g., self), the engine finds that card's owner automatically.

from_zone defaults to battlefield but can be set to graveyard, hand, or deck to target cards in other zones.

9.3 YAML fields reference

Field Location Purpose
group Card definition The card's tribal identifier.
group Alteration Apply the effect to all cards of this group.
require_group Alteration Trigger conditionally on presence of this group.
from_zone Alteration Zone to search when using group or require_group.

9.4 Example card

card_id: witch_coven_01
name: Witch Coven
type: creature
group: witch
mana:
  any: 3
attack: 2
defense: 3
abilities:
  - name: Sisterhood
    description: When this enters, if you control another Witch, gain +1/+1.
    ability_type: triggered
    alterations:
      - effect: buff_card
        target: self
        start_trigger: on_card_enter_battlefield
        require_group: witch
        require_in_zone: battlefield
        require_zone_owner: owner
        params:
          attack_increase: 1
          defense_increase: 1

10. 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)

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

Loading workflow

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:

# 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)

11. Public API Reference

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]

12. Appendix: Complete Card Examples

Creature with no abilities

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

Creature with a triggered ability

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

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)

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

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

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

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

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

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

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

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

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

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

Deck 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

Creature with group and conditional trigger

card_id: witch_coven_01
name: Witch Coven
type: creature
group: witch
mana:
  any: 3
defense: 3
attack: 2
abilities:
  - name: Sisterhood
    description: When this enters, if you control another Witch, gain +1/+1.
    ability_type: triggered
    mana_requirement: {}
    alterations:
      - effect: buff_card
        target: self
        start_trigger: on_card_enter_battlefield
        require_group: witch
        require_in_zone: battlefield
        require_zone_owner: owner
        params:
          attack_increase: 1
          defense_increase: 1

Spell that destroys all enemies of a group

card_id: witch_hunt_01
name: Witch Hunt
description: Destroy all enemy Witches.
type: spell
mana:
  fire: 3
abilities:
  - name: Purge
    description: Destroy all enemy Witches.
    ability_type: activated
    mana_requirement: {}
    alterations:
      - effect: destroy_card
        target: opponent
        group: witch

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ptg-0.6.2.tar.gz (77.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ptg-0.6.2-py3-none-any.whl (60.1 kB view details)

Uploaded Python 3

File details

Details for the file ptg-0.6.2.tar.gz.

File metadata

  • Download URL: ptg-0.6.2.tar.gz
  • Upload date:
  • Size: 77.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.11

File hashes

Hashes for ptg-0.6.2.tar.gz
Algorithm Hash digest
SHA256 cf3599e48fe10195052a7fba08fb125ce2b4ad1f3acb96a7a4f5b36940e2ed08
MD5 826041770c601e78c679523625c427df
BLAKE2b-256 2b9df89e220cf58341809b2a8586a1b3ebef3f66d6951f9305b8c8dd60c96867

See more details on using hashes here.

File details

Details for the file ptg-0.6.2-py3-none-any.whl.

File metadata

  • Download URL: ptg-0.6.2-py3-none-any.whl
  • Upload date:
  • Size: 60.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.11

File hashes

Hashes for ptg-0.6.2-py3-none-any.whl
Algorithm Hash digest
SHA256 12f9e645b40d1e68b7d295307e71f81f06ab3d47378021a05a77eeeedb561f1c
MD5 d87b8d60e45193cc7eee9da203288528
BLAKE2b-256 f461bf72e594994ca678a4c980cae58ebee215fc382fda3f44463dd7b05e956a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page