Card Implementation Guide


#1

Update (22-03-2018): The repository and auto deployment process is now live! That means you can start coding cards! Please read this topic for more information.

Hello developers!

For the first time, I am starting accept contributions for card implementations. There is now a dedicated public git repository to host the card implementation code. Please read this guide carefully to learn how you can contribute card implementations.

Guidelines

Here is the stuff that you all need to know before you begin, please read them carefully.

  • We are using Groovy language to implement cards and their effects. It is a superset of Java, if you know Java than you’ll easily get used to it.
  • You’ll see each expansion has its own enum class. Please scroll down and look at how card implementation is structured.
  • Open sourcing the entire project is a really big thing for me. For now, you will only be able to test your changes remotely (not on your local machine). I plan to restructure and open source the entire game engine gradually.
  • Every time master branch is updated, the CI system will automatically build and deploy the code to https://dev.tcgone.net. It is a special UI that allows fast testing of code. Try it!
  • Please join our discord server and #dev room, ask any questions regarding implementation in there: https://discord.gg/JZP2qzU
  • Licensing: We are using Apache License 2.0 for the source code. You confirm you accept the terms the moment you send any contribution to the repository.

Instructions

  1. You’ll need a gitlab.com account to do merge (pull) requests. Then, fork the repository: https://gitlab.com/tcgone/engine-card-impl
  2. Do your changes under your repository and submit a merge request (pull request in github terms) to master, then ping me on discord or forum (for some reason gitlab doesn’t send emails for unassigned merge requests).
  3. I review your request and merge your changes to master. Please see and adhere to Contribution Rules below
  4. After your changes are merged to master, go to https://dev.tcgone.net and test your changes.
  5. After you’ve merged several good standing commits, you’ll be able to push directly to master branch, so no waiting for testing!

Repository Rules

  • Please DO NOT commit any other file than card implementation files like metadata files, IDE garbage, etc, your merge request will not be accepted.
  • Please also be careful to NOT let your IDE/editor to enforce some whitespace ruling to change whitespaces of files, it makes really hard to track real changes.
  • We are using NO MERGE COMMIT, ALL REBASE approach to track repository history, please always git pull --rebase --autostash to get changes from the server. I’ll force rebase any merge commits in the history.

Task List (up-to-date)

You can start coding the expansions with “Implement” status.

Expansion Next Step
SunMoonPromos Implement missing 100 and onwards
Deoxys Implement
Emerald Implement
Rewrite of BS, JU, FO, TR Quality Check
Rewrite of EXP, AQP, SKR Add Templates
Rewrite of RS, SS, TMTA, DR, HL Add Templates
Pokemod BS, JU, FO, TR Add templates

BaseSet, Jungle, Fossil, TeamRocket, Expedition, Aquapolis, Skyridge, RubySapphire, Sandstorm, TeamMagmaTeamAqua, Dragon, HiddenLegends were written in a legacy syntax and we need to rewrite these sets.

PokeMod sets have little changes to original ones. We’ll just keep them in separate classes and copy the cards from original sets.

Card Specification

Our cards are specified using a custom tailored Groovy DSL syntax.

Basic Pokemon

basic (this, hp:HP080, type:WATER, retreatCost:2) {
  weakness GRASS, X2
  resistance FIRE, MINUS20
  bwAbility "Ability Name", {
    text "Ability description"
    // any actionA, delayedA, getterA, onActivate, onDeactivate clauses
  }
  move "Move Name", {
    text "Move description. IMPORTANT: If the move has any damage text like 20, prepend it to text as: 20 damage."
    energyCost P, C, C
    attackRequirement { //check for requirements with assert clause
    }
    onAttack { //main attack phase, attack is now happening
      damage 20
      afterDamage { //this clause runs after damages are applied, many effects go here
      }
    }
}

Evolution Pokemon:

evolution (this, from: "...", ...) { // all other properties are same as basic
}

Example:

case DUSCLOPS_52:
return evolution (this, from:"Duskull", hp:HP090, type:PSYCHIC, retreatCost:3) {
  weakness DARKNESS
  resistance FIGHTING, MINUS20
  move "Night Roam", {
    text "Put 1 damage counter on each Pokémon in play (both yours and your opponent's)."
    energyCost P
    onAttack {
      all.each {
        directDamage 10, it
      }
    }
  }
  move "Ambush", {
    text "30+ damage. Flip a coin. If heads, this attack does 30 more damage."
    energyCost P, C, C
    onAttack {
      damage 30
      flip{damage 30}
    }
  }

};

Trainer

It starts with either basicTrainer or supporter or itemCard.

supporter (this) {
  text "Card text"
  onPlay {
    // on play effect(s)
  }
  playRequirement{
    // assertions
  }
};

Pokemon Tool

They stay attached to a pokemon, so they work a bit differently.

pokemonTool (this) {
  text "Card text"
  onPlay { reason->
    // activation effects, self denotes the pokemon, reason is the activation reason of the card
  }
  onRemoveFromPlay {
    // run when the card is being removed from the battleground, use it for unregistering delayed effects, etc
  }
  allowAttach { pcs->
    // (optional) use it to restrict the card to be attached to some specific pokemon, return boolean
    // example: pcs.pokemonEX would only allow this card to be attached to EX pokemon.
  }
}

Basic Energy

basicEnergy (this, GRASS)

Special Energy

Required closures: onPlay, onRemoveFromPlay
Optional closures: onMove, getEnergyTypesOverride
Example:

specialEnergy (this, [[C]]) {
	text "Prism Energy provides [C] Energy. If the Pokémon Prism Energy is attached to is a Basic Pokémon, Prism Energy provides every type of Energy but provides only 1 Energy at a time. (Doesn't count as a basic Energy card.)"
	onPlay {reason->
          // activation effects, self denotes the pokemon, reason is the activation reason of the card
	}
	onRemoveFromPlay {
          // run when the card is being removed from the battleground, use it for unregistering delayed 
	}
	onMove {to->
          // (optional) run when the card is being moved to a different pokemon. 
          // use to adjust some effects or discard if needed. 
	}
	getEnergyTypesOverride {
          // (optional) hard override for the provided energy types. 
          // use it for energy cards whose energy output can dynamically change
          // (such as prism energy only activates while it is attached to a basic pokemon)
		if(self==null || self.evolution)
			return [[C]]
		else
			return [[W, G, D, M, F, R, P, L, Y]]
	}
};

In-Game Constructs

Battleground

It holds every aspect of the game and is unique for a game. All effects and cards are tied to this instance and it can be accessible by bg.

PokemonCardSet (PCS)

This is the in-game Pokemon construct that holds information on card stack, damage, special conditions, abilities and the instance is saved/referenced in direct/delayed effects.

PcsList

A list of PokemonCardSet that has convenience methods for selection, etc. BenchSet is a specialized version of this.

CardList

Basically an array of cards, has some convenient methods.

Engine Features

Delayed Effects

An effect can either be direct or delayed. Direct effects (like switch, apply special condition) happens instantly. Delayed effects are like scheduled tasks and they listen to some events to run. For example, if we want to increase an attack damage before weakness and resistance, we do it like this:

delayed {
  before APPLY_ATTACk_DAMAGES, {
    // increase damage
  }
  after EVOLVE, self, {unregister()} //after this pokemon is evolved, unregister this effect
  after SWITCH, self, {unregister()} //after this pokemon is evolved, unregister this effect
  unregisterAfter 2 //auto unregister after 2 turns has passed (effectively means after your opponent's next turn or during the start of your next turn)
  register {} //do custom action on register
  unregister {} //do custom action on unregister
  def xyz = ... //you can define any variables with this effect's scope
}  

The given closure will be run every time after this event type. To stop this delayed effect, either call unregister() from inside, call unregisterAfter turns to set auto expire turn count, or assign this delayed effect to a variable then call unregister() method on it. You’ll find examples in the code repository.
List of EffectTypes

To control the lifecycle manually, create the delayed effect like def eff = delayed { ... }. Then eff.unregister() to unregister it.

The lifecycle will be auto managed if delayedA call was used inside an ability.

Getter Effects

Getter effects are basically modifying effects such as change weakness, retreat, energy type, pokemon type, list of playable moves, etc. List of Getters

User Input (Selections, Choices, Confirmations)

During the resolution of any kind of effect, user input is usually required, such as selecting cards, choosing pokemon, making choices.

  • Selecting cards can be done with the .select() call in CardList, which is basically used everywhere. The select() itself also returns a CardList so the calls can be chained if required.
  • list.select() - selects one card from list. It must contain at least one card or it will result in an engine error.
  • list.select(min: 0) - Selects at most 1 card. Returned list may be empty.
  • list.select(min: 1, max: 4) - Selects at least 1, at most 4 cards. Make sure list contains at least 1 element.
  • list.select(count: 3) - Selects 3 cards from list. If list contains less, it selects less. \
  • list.select(count: 3, "Information Text") - Information text is important for players to understand what are they selecting, please include.

To select pokemon, PcsList contains the selection methods: select(info).

To make a choice, call makeChoice(choices) or makeChoice(choices, labels) for custom labels. It will return one of the elements in choices array.

To just confirm something with yes and no, call confirm("Information") and it will return a boolean for you.

Important: Selection is directed to the player in the current thread (usually the current turn player). This works well if the selection is for a direct attack effect, play trainer, etc. For effects like Wishful Baton which returns energy to hand definitely needs to direct the selection to the owner of the card, not current turn player. In that case we add the PlayerType to the selection as self.owner or thisCard.player. For an example, check Wishful Baton card.

Actions

An action is the buttons you see in the lower left corner. They are automatically generated from the list of available moves, retreat and end turn. So we mainly use them to trigger abilities, although it’s possible to register actions for other reasons. For abilities, we use actionA clause that automatically matches its lifecycle with enclosing ability’s.

Abilities (incl. PokePower, PokeBody, etc)

An ability is always tied to a pokemon in play and it activates once the pokemon is in play. An ability is either:

  1. an action that the user can initiate during his/her turn (actionA pattern),
  2. a persistent delayed effect (delayedA pattern)
  3. a getter (modifier) effect (getterA pattern)
  4. one-off effect activated on play

Since the abilities in the older times are named differently, we use bwAbility for modern abilities, and pokemonPower, pokePower or pokeBody for the classic ones (did you know ‘Pokemon Power’ and ‘PokePower’ are different stuff?)

Abilities have special activation and deactivation flows, advanced stuff can be done by intercepting these points. Use onActivate and onDeactivate closures, respectively.

Moves

TODO

Assertions & Checks

Many trainer cards, moves and abilities need to be checked for some preconditions before they are allowed to run. Example: A Revive should not work when there’s no space on bench or there are no pokemon in discard. The game rules specifically disallow playing cards for no effect, so we should block them from happening.

We use assert keyword to check effects: assert condition : message(optional)

Example: assert my.bench.notFull : "Bench is full" asserts the bench to be not full, else it fails with “Bench is full” message.

We put them in either playRequirement of trainers, or attackRequirement of moves, or actionA clauses of abilities. When an assert statement is fails, the player sees the a warning text with message (if there is some) and no code in the same closure will run afterwards. This is a very cool feature of the language.

Common Effects

TcgStatics has all the common effects that you need.


When will we be able to test lost thunder on tcgone?
Allowing Voluntary Help For Coding
#2

Update (22-03-2018): The repository and auto deployment process is now live! That means you can start coding cards! The complete developer guide is still in the works but I believe you can inspect the existing card codes to have an idea.


pinned #3

#4

Updated instructions and guidelines. The path for contributions should be more clear now!

There are still many things to improve in the documentation, please comment on how I can improve it.


V26 Update: Crimson Invasion!
#5

Steps to code Pokemod Base Set to Team Rocket cards;

  1. cd src/tcgwars/logic
  2. Create a directory impl/pokemod
  3. Copy impl/gen1/BaseSet.groovy to impl/pokemod/PokemodBaseSet.groovy. Repeat for Jungle, Fossil and TeamRocket.
  4. Edit Pokemod*.groovy:
    • Append collection number to all enum names. Example: ALAKAZAM to ALAKAZAM_1. Remember to change them in below get implementation also.
    • Change getCollection method to either return POKEMOD_BASE_SET, POKEMOD_JUNGLE, POKEMOD_FOSSIL, POKEMOD_TEAM_ROCKET.
    • Add NEW cards to enum constants.
  5. In getImplementation method:
    • For UNMODIFIED cards, replace the groovy code with a copy from original set, like: return copy(BaseSet.XYZ, this). This is to prevent duplication of code.
    • For MODIFIED cards, edit the code to apply the modification. ZF_Goku can help pointing out what has changed.
    • For NEW cards, implement them from scratch.

Please contact #dev in Discord for more information


PokeMod Release Timeline Discussion
pinned #6

#7

Hello @developers ,

I have just updated the backend and CI/CD configuration of dev.tcgone.net. It is now 50% faster to build and restart after every push! Feel free to check it our by pushing to master.

EDIT: I am trying it and each push takes only ~20 seconds to be deployed! The app itself is also a lot faster due to improved runtime conditions.

Best