SpiritPTCGO
SpiritPTCGO is a Python server emulator for the NOW sunseted Pokemon Trading Card Game Online (PTCGO) client.
Prequisites & Warning
-
This server was developed using the 2023 client (2.95.0.5815)
- Do note that if your copy of the client is not that version, there MAY be in incompatibility issues.
-
Make sure you own a copy of the PTCGO client beforehand for this server to run with!
Gallery
In Game
Collection
Versus
Environment Setup
WARNING: These instructions are meant for a Windows 11 Machine and have only been tested on Windows.
- Clone the repository
- Install Python 3.10+
- It is important to
Add to PATHwhen you are installing Python. (You can check by running this command in your terminalpython --v)
- It is important to
- Navigate to the project in a terminal and generate a python venv via:
python -m venv venv - Activate your venv by running this command in the same terminal:
./venv/Scripts/activate - Now run the command:
pip install -r requirements.txt
Server Setup
-
Navigate to your PTCGO installation folder and find the
Pokemon Trading Card Game Online_Data\cake.cfgfile. Edit the file to point thehostname,versionURL, andassetURLto your local machine:hostname=127.0.0.1 versionURL=http://127.0.0.1:8000/ assetURL=http://127.0.0.1:8000/(Leave the AppSecrets and version string at the bottom)
-
Generate the SSL Certificates
python spirit/network/generate_cert.pyDO NOT install this certificate into your Windows Trusted Root Store. If the certificate is trusted by Windows, the game's validator will actually reject it due to a Unity hostname parsing bug with IP addresses. As long as the
.crtand.keyfiles are next to the server script, the Python server will use them, and the client will accept them. -
Run the database initialization script to create the local SQLite database (
ptcgo_server.db) using SQLAlchemy ORM and seed the default test account (Username:brandon/ Password:password):python spirit/database/setup_db.py -
Run the main Python script from the root directory to start both the TCP (Game) and HTTP (Asset/MOTD) servers:
$env:PYTHONPATH=(Get-Location).Path; python -m spirit.mainDo note that on the first initial run, the server will download any initial asset bundles it needs determined by the
game/scripts/andassets/cards/directories (more info on card injections below) -
(Optional) If you have original game cache files (like UI elements, logos, etc.) that you want the server to serve:
- You can copy them into
spirit/assets/externalCache/. - OR set the
PTCGO_CACHE_DIRenvironment variable to point to your original game cache folder before running the server.
- You can copy them into
-
Run the
Pokemon Trading Card Game Online.exein your client's installation folder and you should be able to see logs in your servers as you login!
Hosting for Others (Remote Play)
The steps above are all you need for local play on a single machine (the client and server on the same PC). If you want friends to connect over the internet or a LAN, there's one extra knob: spirit/config.py.
The reason is that the login handshake redirects the client to a follow-up address mid-connection. Loopback/LAN peers get echoed back whatever address they dialed, so 127.0.0.1 "just works" locally, but a remote client that gets pointed at 127.0.0.1 will silently disconnect before it ever logs in. So for remote hosting you have to tell the server its real public address.
- On the server machine, set
PUBLIC_HOSTto your public IP or domain. Either editspirit/config.py:...or leave the file alone and just export the env var before launching:PUBLIC_HOST = os.environ.get("SPIRIT_PUBLIC_HOST", "your.public.ip.or.domain")$env:SPIRIT_PUBLIC_HOST="your.public.ip.or.domain" - Make sure the HTTP port (
8000) and TCP game port (39389) are forwarded/open on your router and firewall. - On each connecting machine, point that same public address into
cake.cfginstead of127.0.0.1:hostname=your.public.ip.or.domain versionURL=http://your.public.ip.or.domain:8000/ assetURL=http://your.public.ip.or.domain:8000/
Hosting on a VPS? See
deploy/Not needed for local development only for reliable remote hosting.
Custom Card Creation
SpiritPTCGO supports a modular card injection system. You add a custom card by dropping an image and a python script into the designated folders, and the server handles the Unity AssetBundle generation automatically on startup. A card is two things: a data definition (its name, HP, cost, weakness, etc.) and, if you want it to actually do something in a match, a bit of scripted behavior. We'll cover both.
1. Place the Card Image
Save your card art as a 1024x1024 PNG in the following directory:
spirit/assets/cards/<SET_CODE>/<NAME>_<NUMBER>.png
Example: spirit/assets/cards/CUSTOM/LugiaV_1.png
IMPORTANT: It's important to note that my card bundling script will add padding to images to make them 1024x1024, so using card images from say a popular pokemon tcg api would work ;)
2. Create the Card Script
Create a Python script to define the card's data and attributes:
spirit/game/scripts/cards/<SET_CODE>/<NAME>_<NUMBER>.py
Example: spirit/game/scripts/cards/CUSTOM/LugiaV_1.py
from spirit.game.data_utils import PokemonCardDef, Ability, Attack
from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities, AbilityTypes
# Define the card object
card = PokemonCardDef(
guid="a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d", # any unique UUID -- run: python -c "import uuid; print(uuid.uuid4())"
key="CUSTOM", # I typically will leave this as the same as set_code
name="Lugia V",
collector_number=1,
set_code="CUSTOM",
rarity=Rarities.RareUltra,
hp=220,
elements=[PokemonTypes.COLORLESS],
stage=PokemonStage.BASIC,
retreat_cost=2,
family_id=249, # I typically put the pokedex national number
weakness_type=PokemonTypes.LIGHTNING, # optional; defaults to no weakness
abilities=[
Attack(
title="Aero Ball",
game_text="This attack does 20 damage for each Energy attached to this Pokémon.",
cost={PokemonTypes.COLORLESS: 2},
damage=20,
damage_operator="x"
)
]
)
That is a complete, valid card. If it evolved from something you'd add evolves_from="Lugia V" (the literal name it evolves from) and bump the stage; a Stage 1 or 2 needs a lower stage of the family already in play to evolve onto.
2b. Making Attacks and Abilities Actually Do Something
The block above defines the card, but the client itself does no rules logic — the server decides what every attack does. Each Attack (and Ability) takes an optional effect=, and how you set it decides the behavior:
- Leave
effectoff entirely: the engine treats it as a vanilla attack and just deals the printeddamage(auto-applying weakness ×2 / resistance −30 against the opponent's Active) and ends the turn. Perfectly fine for plain hitters. effect=unimplemented: the card has real effect text you haven't scripted yet. Base damage still resolves, but the server logs a warning so you know it's a stub. Handy for getting a whole set playable-at-base quickly and coming back later.effect=<an async function>: full scripted behavior. You write anasync def effect(ctx):coroutine, andctxis your handle on the whole game (seespirit/game/session/effects.pyfor the complete API).
Here's Aero Ball code. The printed text says "20 damage for each Energy attached," so instead of a flat number we count the energy and deal it ourselves:
from spirit.game.data_utils import PokemonCardDef, Attack, unimplemented
from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities
async def aero_ball(ctx):
attached = ctx.attached_energies(ctx.source) # every Energy on this Pokémon
await ctx.deal_damage(20 * len(attached)) # weakness/resistance auto-apply vs the Active
card = PokemonCardDef(
guid="a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d",
key="CUSTOM",
name="Lugia V",
collector_number=1,
set_code="CUSTOM",
rarity=Rarities.RareUltra,
hp=220,
elements=[PokemonTypes.COLORLESS],
stage=PokemonStage.BASIC,
retreat_cost=2,
family_id=249,
abilities=[
Attack(
title="Aero Ball",
game_text="This attack does 20 damage for each Energy attached to this Pokémon.",
cost={PokemonTypes.COLORLESS: 2},
effect=aero_ball
)
]
)
ctx gives you everything you need to express real card text. A few of the common ones:
ctx.source/ctx.attacker— the Pokémon using the attackctx.defender/ctx.opponent_active()— the opponent's Activectx.deal_damage(amount=None)— deal damage (printeddamageif you omit the amount); handles weakness/resistance vs the Active for youctx.heal(amount, target=None)ctx.draw_cards(count)ctx.flip_coins(count, title)→ a list of bools (True= heads)ctx.ask_yes_no(prompt)→ bool, andctx.choose(prompt, buttons)→ the chosen indexctx.attached_energies(pokemon),ctx.my_bench(),ctx.opponent_bench(),ctx.discard_stadium()
The entire API list (searching decks, moving cards between zones, special conditions, passives) is in effects.py.
Trainers work the same way. An ItemCardDef / SupporterCardDef / StadiumCardDef takes the same effect= coroutine:
from spirit.game.data_utils import ItemCardDef, unimplemented
from spirit.game.attributes import Rarities
async def field_notes(ctx):
"""Draw 2 cards."""
await ctx.draw_cards(2)
card = ItemCardDef(
guid="d73ca4da-dd21-f428-8051-264ab564587c",
key="CUSTOM",
name="Field Notes",
collector_number=2,
set_code="CUSTOM",
rarity=Rarities.Common,
effect=field_notes
)
A couple of things worth knowing as you go deeper: an Ability can be a passive body (ability_type=AbilityTypes.POKE_BODY with a passive=), a triggered ability (trigger=Triggers.ON_PLAY), or an activated one — and both cards and trainers take a condition= to gate when they're even offered (Ultra Ball needing two cards to discard, etc.). Read references from an existing card under spirit/game/scripts/cards/ since the two starter decks are fully scripted.
If you want to see what's already scripted versus stubbed across a set, there's a tool:
python -m spirit.tools.effect_coverage
3. Get the Card Into a Collection
A fresh account only starts with the two starter decks and some booster packs. To give yourself your custom card in for testing, start the server and open the Admin Dashboard at http://127.0.0.1:8000/admin. Log in with the seeded admin account (brandon / password), go to the Accounts tab, and hit Grant All Cards on your account.
4. Run the Server
Start the server as usual. On startup, the AutoBundle system detects the new script, finds the corresponding PNG, and generates a .unity3d AssetBundle automatically in spirit/assets/.
$env:PYTHONPATH=(Get-Location).Path; python -m spirit.main
Custom Cosmetic Creation
SpiritPTCGO supports an advanced Dynamic Cosmetic Asset Injection system. You can add fully custom card sleeves, gameplay coins, and 3D deck boxes by placing your PNG textures in the designated folders. The server will dynamically expand the master asset templates and generate the client-prefixed AssetBundles automatically on startup.
1. Place your Custom Textures
Save your custom textures as PNGs under the respective subdirectories inside spirit/assets/products/:
- Sleeves: Place in
spirit/assets/products/custom_sleeves/- Recommended Resolution:
512x512or256x256pixels (Square1:1).
- Recommended Resolution:
- Coins: Place in
spirit/assets/products/custom_coins/- Recommended Resolution:
256x256pixels (Square1:1). Transparent background outside the circular border is recommended.
- Recommended Resolution:
- Deck Boxes: Place in
spirit/assets/products/custom_deckboxes/- Recommended Resolution:
512x512pixels (Square1:1). This texture folds directly over the 3D model's UV layout.
- Recommended Resolution:
Note: Filenames must be lowercase and use underscores/numbers (e.g., my_cool_sleeve.png). The filename (without extension) becomes the logical asset ID. Non-standard dimensions are automatically scaled to the proper size on server boot.
2. Update your Product Definition
To assign your custom cosmetics to players or set them as defaults, use their lowercase filenames as the logical image_url property in your product definitions (located inside spirit/game/scripts/products/):
Example: In spirit/game/scripts/products/noset/basic_sleeve.py
from spirit.game.data_utils import SleeveDef
product = SleeveDef(
guid="e079c0d3-b934-4fbd-b021-545106c75693", # Client Default Sleeve GUID
key="NoSet",
name="Basic Sleeve",
image_url="my_cool_sleeve" # Points directly to your custom 'my_cool_sleeve.png'!
)
3. Sync the Database
To apply any updated product properties and synchronize the items to existing players:
python spirit/database/seed_collection.py
4. Run the Server
On boot, the server automatically reads your templates, allocates unique PathIDs, dynamically appends your custom textures to the unified master bundles, and registers them in the asset manifest for the client:
$env:PYTHONPATH=(Get-Location).Path; python -m spirit.main
Your custom cosmetics are now ready to be equipped and rendered in game!
Custom Booster Pack & Theme Deck Creation
SpiritPTCGO supports the exact same advanced dynamic appending system for Booster Packs and PCD/Theme Decks. You can customize the look of pack foils and deck boxes in the Shop and Opening scenes by placing your PNG textures in the designated folders.
1. Place your Custom Textures
Save your custom textures as PNGs under the respective subdirectories inside spirit/assets/products/:
- Booster Packs: Place in
spirit/assets/products/custom_packs/- Image Specifications (to prevent stretching): The game client renders booster packs using a square
512x512transparent canvas. To prevent the vertical booster pack art from being stretched horizontally, your image must be laid out on a transparent square canvas as follows:- Canvas Dimensions:
512x512pixels (RGBA transparent background). - Booster Art Position: The visible booster pack artwork should be scaled to a height of
496pixels, with exactly16pixels of top padding (stretching from Y=16 to Y=512). - Horizontal Alignment: Center the artwork horizontally. It should have a width of roughly
270to285pixels (depending on your source artwork's aspect ratio), leaving transparent padding on the left and right.
- Canvas Dimensions:
- Image Specifications (to prevent stretching): The game client renders booster packs using a square
- Theme Decks (PCDs): Place in
spirit/assets/products/custom_pcds/- Recommended Resolution:
512x512pixels (Square1:1). This texture represents the flat box skin used by the client's PCD renderer.
- Recommended Resolution:
Note: Filenames must be lowercase (e.g., xy_breakthrough_booster.png). The filename (without extension) becomes the logical asset ID inside the client packs/pcdBoxes bundle.
2. Update your Product Definition
To link your custom booster or deck art to the item, use its lowercase filename as the image_url property in your product scripts (located inside spirit/game/scripts/products/):
Example: In spirit/game/scripts/products/xy8/booster_7df512a8.py (Breakthrough Booster Pack)
from spirit.game.data_utils import BoosterPackDef
product = BoosterPackDef(
guid="7df512a8-8f81-432d-ae52-9d3df3902341",
key="xy8",
name="BREAKthrough Booster Pack",
image_url="xy_breakthrough_booster" # Points directly to your custom 'xy_breakthrough_booster.png'!
)
3. Run the Server
On boot, the server reads packs.template and pcdBoxes.template, dynamically appends your custom textures to the unified master bundles, updates the client-side mappings, and serves them automatically in game!
$env:PYTHONPATH=(Get-Location).Path; python -m spirit.main
Custom Versus Season Rewards
SpiritPTCGO features a configuration-driven Versus Season Reward System that allows server administrators to easily customize, add, or schedule different seasons and tiers of rewards (Trainer Coins, Booster Packs, Cards, Deck Boxes, Sleeves, etc.) without writing any code.
1. Edit the Seasons Configuration
All versus seasons are defined inside the following JSON file:
spirit/database/json_data/versus_seasons.json
You can edit this file to modify the active season, update the start/end timestamps, adjust the point thresholds, or add custom rewards.
Example season block:
[
{
"seasonID": "Season1",
"startTime": 0,
"endTime": 4102444800000,
"description": {
"id": "SpiritPTCGO Season 1"
},
"tiers": [
{
"rewards": {
"10": [
{
"name": "5 Tokens",
"rewardType": "Tokens",
"rewardAmount": 5,
"rewardCurrency": "prizeTrainerCoin"
}
]
}
},
{
"rewards": {
"50": [
{
"name": "1 Pack",
"rewardType": "Product",
"rewardAmount": 1,
"rewardProductID": "your-booster-pack-archetype-guid"
}
]
}
}
],
"resetRewardID": ""
}
]
2. Supported Reward Types & Fields
Each reward object inside the rewards list supports these core fields:
name(string): A display name or label for the reward.rewardType(string): Tells the client how to process and render the reward:"Tokens": Used to award Trainer Coins. Ensure you also set"rewardCurrency": "prizeTrainerCoin"."Product": Used to award a specific product (e.g., booster pack, deck box, sleeve, coin, or card). Ensure you specify a valid"rewardProductID"."Currency": Used generically for event tickets/other.
rewardAmount(int): The quantity of the item to award.rewardProductID(string, optional): The Archetype/Product GUID of the specific item ifrewardTypeis"Product".rewardCurrency(string, optional): The currency string key ifrewardTypeis"Tokens"(usually"prizeTrainerCoin").


