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
- Quick Start
- Core Concepts
- Card Definition Reference
- Mana System
- Abilities & Effects
- Triggers
- Combat System
- Loading Cards & Building Decks
- Public API Reference
- 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,manacost,health,attack,defense, and optionalabilities. - CardInstance — one copy in play. Has a unique
uuid, currenthealth,attack,defense, and a reference back to itsCardDefinition.
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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ddfa4aee8fb4041920a9ea1f761e125a0ac68e2c7a641ce41845138ad440588d
|
|
| MD5 |
73680770470fc0ffd9cd42feb94e85bc
|
|
| BLAKE2b-256 |
d7eaf9b4adf159f967dd0c4063faa7b5f418210ff32f831f06ec080d735b31e0
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
989729fa597fa242d532b89f466734c81bea685f786bddb02c7e128038dd49c5
|
|
| MD5 |
26db5b0402d9a0fd1487aed73c8a12c1
|
|
| BLAKE2b-256 |
fc320324188867e003f3ffb21e4395d4df3bf6815b344991debb5ed06ef59d0d
|