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. Loading Cards & Building Decks
  9. Public API Reference
  10. 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, 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.

CardType

Value Meaning
creature Stays on the battlefield. Can attack and block. Its defense is its current life; it regenerates each turn.
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
    start_trigger: <EventType | null>  # default: null
    end_trigger: <EventType | null>    # default: null
    turn_duration: <int | null>        # default: null
    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.
null Chosen at runtime (spells, activated abilities).

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.

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

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 them. The engine assigns minimum lethal damage to each blocker in declaration order until no attack remains.

Spillover (trample)

If the attacker survives and all of its blockers die, any leftover damage hits the defending player.

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.


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

9. Public API Reference

from ptg.api import (
    # ── Core types ───────────────────────────────────────────
    CardType, ManaType, EventType, AbilityType, TargetSpec,
    ManaRequirement, ManaPool,
    # ── Engine ───────────────────────────────────────────────
    Game, GameState, Player,
    CardDefinition, CardAbility, AlterationData,
    # ── Actions ──────────────────────────────────────────────
    PlayCardAction, DeclareAttackAction,
    DeclareDefenseAction, 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).
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
ActivateAbilityAction player_id, card_id, ability_index

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]

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

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.1.13.tar.gz (45.3 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.1.13-py3-none-any.whl (44.7 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for ptg-0.1.13.tar.gz
Algorithm Hash digest
SHA256 ddfa4aee8fb4041920a9ea1f761e125a0ac68e2c7a641ce41845138ad440588d
MD5 73680770470fc0ffd9cd42feb94e85bc
BLAKE2b-256 d7eaf9b4adf159f967dd0c4063faa7b5f418210ff32f831f06ec080d735b31e0

See more details on using hashes here.

File details

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

File metadata

  • Download URL: ptg-0.1.13-py3-none-any.whl
  • Upload date:
  • Size: 44.7 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.1.13-py3-none-any.whl
Algorithm Hash digest
SHA256 989729fa597fa242d532b89f466734c81bea685f786bddb02c7e128038dd49c5
MD5 26db5b0402d9a0fd1487aed73c8a12c1
BLAKE2b-256 fc320324188867e003f3ffb21e4395d4df3bf6815b344991debb5ed06ef59d0d

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