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},
                           health=2, attack=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.begin_turn()                                         # draw phase

# 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
game.apply_action(DeclareAttackAction(), player_id=alice.uuid,
                  attacker_id=game.state.battlefield[alice.uuid][0])
game.resolve_combat()
game.end_turn()

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).
health int no 0 Only meaningful for creatures.
attack int no 0 Only meaningful for creatures.
defense int no 0 Reduces incoming damage.
abilities list[CardAbility] no None See Abilities & Effects.

CardType

Value Meaning
creature Stays on the battlefield. Can attack and block.
spell Resolves immediately, then goes to graveyard.
mana Adds mana to your pool, then goes to graveyard. Free to play.

Examples

# A simple creature
card_id: goblin_01
name: Goblin Warrior
type: creature
mana:
  fire: 1
health: 2
attack: 2
defense: 0
# 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. Playing a Fire Mana adds 1 fire to your pool permanently. Mana stays in the pool until you spend it.

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 (defense is subtracted).
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.
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.

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

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
      health: 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, mana).
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([begin_turn]) --> Draw[Draw 1 card]
    Draw --> Main[Main phase: play cards, activate abilities]
    Main --> Attack[Declare attackers]
    Attack --> Defense[Declare blockers]
    Defense --> Resolve[resolve_combat]
    Resolve --> End([end_turn → next player])

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.

  • Attacker → blocker: damage is reduced by the blocker's defense.
  • Blocker → attacker: damage is reduced by the attacker's defense.
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

Method Returns Description
start() GameState Initialises both players and draws their starting hands.
begin_turn() GameState Resets attack flags, emits on_turn_start, draws a card.
end_turn() GameState Emits on_turn_end, switches to the next player.
apply_action(action, **kwargs) GameState Runs any action through the engine.
resolve_combat() GameState Processes all pending combats.
check_victory() Player | None Returns the winner, or None if the game continues.

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

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.0.tar.gz (38.8 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.0-py3-none-any.whl (38.2 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: ptg-0.1.0.tar.gz
  • Upload date:
  • Size: 38.8 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.0.tar.gz
Algorithm Hash digest
SHA256 30cb87ed8947455c55681e56eba23d3fa8f02e84cc7e60b57df5e877a35ba147
MD5 f2a85756b520b1d89eca7839b7e772bc
BLAKE2b-256 ee4c25889bdf6f0dd52f1e320a75e98cecf4bf8e3b86851c2642d31ddfec9244

See more details on using hashes here.

File details

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

File metadata

  • Download URL: ptg-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 38.2 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.0-py3-none-any.whl
Algorithm Hash digest
SHA256 cfb03f05c76cd0aca74bcf31f5e86535b8d1e10ac75660fdc81a2237bed6da9c
MD5 0e72ec8e6b252c948342ae9973d4d9e4
BLAKE2b-256 821695b582097a7e4dd9d5161c622971b80c35971c4163b3b26fbbf835007934

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