Make a CCG – Data Modeling

We started out by creating an architecture to allow systems to talk to each other, and still don’t have any systems. Even worse, we have no models for the systems to operate on! Let’s take a few steps back and start building up our game from the beginning.

Go ahead and open the project from where we left off, or download the completed project from last week here. I have also prepared an online repository here for those of you familiar with version control.

Design

Ideally, you would begin a project with some sort of design document. Sometimes it’s fun to doodle in code, but you can get a lot further, faster, on paper than you can while actually building stuff – especially when you end up going completely the wrong direction and have to undo work before you can move forward again.

In our case, I am using an existing game, Hearthstone, as if it were my design document. I can use the wiki to help clarify any fine points that aren’t obvious from playing. So what exactly are we making? I think the highest level overview is found here – the wiki’s Gameplay page. The opening paragraph sounds a bit like an advertisment, but the section titled as “Matches” seems to really start defining things for us.

At this point, what we are looking for are the “nouns” in the desgin doc. These are the items that are most easily represented by raw data in code such as in a structure or class. Here are the things that stand out to me:

  • Match – a collection of two opponents / players, with a concept of turn-based gameplay and therefore an idea of which player is current.
  • Player – a participant in a match with its own set of stats (like mana) and collection of cards in various positions like a “hand” or “deck”. May be controlled by a human or computer.
  • Card – an entity that is owned and operated by a player. Has a variety of types such as spells, weapons, minions, and even hero cards. Each type of card can have its own set of stats.

Notice that the “nouns” presented here all form a single hierarchy of data. If I have access to a match, I should have access to everything. If I save a match and restore it later, I will have put everything in place necessary to continue the game. If we use Firebase to store matches online, it will make it very convenient that all of the data we need is together like this.

We also have a general idea of what makes it a game – each player takes turns and plays cards toward the end goal of reducing their opponent hero’s hit points to zero. Our goal for this lesson will be to define enough models that all of this can be represented in code. We can polish and refine the implementation as we go, but this seems like a pretty good minimum to start building our systems around.

Match

We defined a match as a collection of two players, and with an ability to keep track of whose turn it is. This is a pretty simple model that can be defined as follows:

public class Match {
	public const int PlayerCount = 2;

	public List<Player> players = new List<Player> (PlayerCount);
	public int currentPlayerIndex;

	public Player CurrentPlayer {
		get {
			return players [currentPlayerIndex]; 
		}
	}

	public Player OpponentPlayer {
		get {
			return players [1 - currentPlayerIndex];
		}
	}

	public Match () {
		for (int i = 0; i < PlayerCount; ++i) {
			players.Add (new Player (i));
		}
	}
}

I added a const for the PlayerCount because programmers tend to get picky about what they call “magic numbers” appearing in code. We may be able to use this value in multiple places and are already using it by creating a List of Players at the correct capacity.

I use a simple int data type to represent whose turn it is. The “currentPlayerIndex” is the index of a player in the players list. I also created some convenience properties so you could easily know who the CurrentPlayer and OpponentPlayer are at any time, regardless of what the “currentPlayerIndex” has been set to.

You might have noticed that to fetch the opponent player that I index with 1 - currentPlayerIndex. This works because we have a zero-based list of only two players. When the current player is at index ‘0’, then the opponent is ‘1 – 0 = 1’. If the current player is at index ‘1’, then the opponent is ‘1 – 1 = 0’. Simple math.

I added a constructor for the Match object which populates the List of players with instances of a player. This must be done because creating a List with a capacity is not the same as filling a list with content.

Mana

Before we create the Player model, I want to take a quick detour and create another model for the player’s mana. Technically they could be implemented as stats directly on the player, but I prefer to keep the related fields separate for organization and more concise names. For example, the field on a “Player” might be “spentMana” whereas the same field in a “Mana” class could be simply “spent”.

public class Mana {
	public const int MaxSlots = 10;

	public int spent;
	public int permanent;
	public int overloaded;
	public int pendingOverloaded;
	public int temporary;

	public int Unlocked {
		get {
			return Mathf.Min (permanent + temporary, MaxSlots);
		}
	}

	public int Available {
		get {
			return Mathf.Min (permanent + temporary - spent, MaxSlots) - overloaded;
		}
	}
}

I have defined another constant – “MaxSlots” at ’10’ which is another game design requirement. The other fields are:

  • spent – The amount of mana slots that are used up in the current turn. This will reset to ‘0’ at the beginning of a new turn.
  • permanent – The number of earned mana slots. A player earns one per turn up to the max slot amount.
  • overloaded – The number of locked mana slots caused by certain over-powerful cards.
  • pendingOverloaded – The number of mana slots that will be locked next turn.
  • temporary – The number of temporarily available mana slots which can be awarded by spells.

I also created two convenience properties. Unlocked will be helpful to render the appropriate number of mana crystals in the UI. Available will also be helpful for the same purpose, but even more so it will help simplify the check of whether or not a card’s cost can be payed when we want to play it.

Control Modes

One of a player’s properties will be to determine what is in control. The three options I will want to support could include a local human player, a computer A.I. player, or an online opponent (remote player). Let’s create a separate enum file to define these types of options:

public enum ControlModes {
	Computer,
	Local,
	Remote
}

Zones

Although a player will hold a collection of cards, they are grouped by something called “zones” which are all different positions that a card can be placed in a game. Take a look at the image below for a reference, where the top half and bottom half of the screen are each controlled by a different player:

Hearthstone card positions

  • The hero appears at the very top and bottom center.
  • A weapon appears to the left of the top hero, but the lower hero does not have a weapon equipped.
  • The deck for each player is stacked along the right edge of the screen.
  • The hand appears in the upper left and lower right corners of the screen.
  • The battlefield (or board) represents the area where minions are summoned in front of each hero.
  • The secrets are visible only as a circled mark – the top hero has 2 secrets in play.
  • There is no visible indicator for the graveyard (discard pile), but it plays an important role regardless.

Each of these will be defined in an enum as follows:

public enum Zones {
	Hero,
	Weapon,
	Deck,
	Hand,
	Battlefield,
	Secrets,
	Graveyard
}

There will be many reasons to work with the cards in a specified “zone” of a player in the future. For instance, we know that there are going to be cards with special abilities. Different cards may apply to cards in different zones. Some cards may only apply to cards in your hand, while others may only target cards played to the table, etc. There isn’t really a “good” way to serialize a reference to a field of an instance when trying to make the database (or json etc) of our cards. In other words, I wouldn’t have an entry for a card ability that included something like "target" : "player.hand" because a “player” wouldn’t exist in the context yet. I could however use a string that would be parsed into an enum, and allow a system to determine the matching player and zone it needs to work with later.

Player

A player is more than just a collection of cards grouped by zones – each player will have certain other stats as well. For instance, a player keeps track of a mana pool that determines how many cards can be played on a given turn (since a card has a mana “cost” to play). If a player runs out of cards they will begin taking fatigue damage – the amount of damage goes up each turn and is specified per player. We also need to specify the different ways a player is controlled. We can define all of that as follows:

public class Player {
	public const int maxDeck = 30;
	public const int maxHand = 10;
	public const int maxBattlefield = 7;
	public const int maxSecrets = 5;

	public readonly int index;
	public ControlModes mode;
	public Mana mana = new Mana ();
	public int fatigue;

	public List<Card> hero = new List<Card> (1);
	public List<Card> weapon = new List<Card> (1);
	public List<Card> deck = new List<Card> (maxDeck);
	public List<Card> hand = new List<Card> (maxHand);
	public List<Card> battlefield = new List<Card> (maxBattlefield);
	public List<Card> secrets = new List<Card> (maxSecrets);
	public List<Card> graveyard = new List<Card> (maxDeck);

	public List<Card> this[Zones z] {
		get {
			switch (z) {
			case Zones.Hero:
				return hero;
			case Zones.Weapon:
				return weapon;
			case Zones.Deck:
				return deck;
			case Zones.Hand:
				return hand;
			case Zones.Battlefield:
				return battlefield;
			case Zones.Secrets:
				return secrets;
			case Zones.Graveyard:
				return graveyard;
			default:
				return null;
			}
		}
	}

	public Player (int index) {
		this.index = index;
	}
}

Note that in this case, some of the constants are actually restrictions in size, while others are just an initial capacity. For example, although all player decks begin at 30 cards, a special ability could cause a card to be cloned and added to the deck repeatedly, which could in theory allow the size of the deck to exceed its original size. Other constants like the number of cards to hold in a hand are a restriction, and if you try to draw a card while at the limit, then the card will instead move to the discard pile. That rule will have to be imposed as part of a system though.

You may be wondering why the hero and weapon zones were modeled in code as collections, since they seem to hold only a single card at a time. One reason is greater flexibility in design. For example, when fighting in a specially crafted single-player mission such as against Professor Putricide, there may be several phases for the enemy hero, each of which could be represented by a separate hero card in the collection.

Another reason to treat the hero and weapon zone as a collection (instead of just as a pointer to a single card), is that it makes the code a little simpler to write if we treat each position in an identical manner. For example, we are able to use a zone “indexer” even in this class to simplify the fetch of a collection based on a zone. Later when we model the Card class, we can add a Zones field so that it is easy to determine which collection it is contained by.

Card

The design for a card already calls for a variety of sub-types, but ideally we can treat all cards as a same base-type (this is called polymorphism). If there are any stats that are common among all of the sub-types, then we can define them in the base-type. Let’s take a look at the design of each:

Hearthstone card types

  • Hero – represents the player
  • Minion – summoned to battlefield
  • Spell – can do just about anything
  • Weapon – equipped by heroes

While I was browsing through the hearthstone wiki, I also stumbled across a link that had all of the Hearthstone card data. Of course, I don’t actually know for sure that the Hearthstone game used this directly, because someone could have generated it by another means. Either way, you can use that JSON sample to get up and running with actual content very quickly. I didn’t want to mess with such a large amount of data, so I took the liberty to break it down into the various sets. You can see those here. I won’t actually include them in the main project just in case there are copyright issues and if necessary it will be a lot easier to simply remove the links from here. Let’s take a look at a snippet from the CORE set for each of the four main kinds of cards, but note that even for the same kind of card the fields included in each entry may vary:

Here is a hero:

{
    "rarity" : "FREE",
    "name" : "Rexxar",
    "health" : 30,
    "id" : "HERO_05",
    "set" : "CORE",
    "collectible" : true,
    "playerClass" : "HUNTER",
    "dbfId" : 31,
    "cardClass" : "HUNTER",
    "type" : "HERO"
}

Here is a minion:

{
    "name" : "Oasis Snapjaw",
    "health" : 7,
    "attack" : 2,
    "flavor" : "His dreams of flying and breathing fire like his idol will never be realized.",
    "cardClass" : "NEUTRAL",
    "type" : "MINION",
    "rarity" : "FREE",
    "howToEarnGolden" : "Unlocked at Druid Level 51.",
    "artist" : "Ittoku",
    "cost" : 4,
    "id" : "CS2_119",
    "collectible" : true,
    "set" : "CORE",
    "playerClass" : "NEUTRAL",
    "dbfId" : 1370,
    "race" : "BEAST"
}

Here is a spell:

{
    "name" : "Shadow Word: Death",
    "flavor" : "If you miss, it leaves a lightning-bolt-shaped scar on your target.",
    "playRequirements" : {
      "REQ_TARGET_TO_PLAY" : 0,
      "REQ_MINION_TARGET" : 0,
      "REQ_TARGET_MIN_ATTACK" : 5
    },
    "cardClass" : "PRIEST",
    "type" : "SPELL",
    "rarity" : "FREE",
    "howToEarnGolden" : "Unlocked at Level 43.",
    "artist" : "Raymond Swanland",
    "howToEarn" : "Unlocked at Level 8.",
    "id" : "EX1_622",
    "collectible" : true,
    "set" : "CORE",
    "text" : "Destroy a minion with 5Â or more Attack.",
    "playerClass" : "PRIEST",
    "dbfId" : 1363,
    "cost" : 3
}

Here is a weapon:

{
    "name" : "Fiery War Axe",
    "attack" : 3,
    "durability" : 2,
    "flavor" : "During times of tranquility and harmony, this weapon was called by its less popular name, Chilly Peace Axe.",
    "cardClass" : "WARRIOR",
    "type" : "WEAPON",
    "rarity" : "FREE",
    "howToEarnGolden" : "Unlocked at Level 49.",
    "artist" : "Lucas Graciano",
    "howToEarn" : "Unlocked at Level 1.",
    "id" : "CS2_106",
    "collectible" : true,
    "set" : "CORE",
    "cost" : 2,
    "playerClass" : "WARRIOR",
    "dbfId" : 401
}

The fields in these snippets reveal a lot of features that may not have been obvious to implement, such as the fact that there are “golden” versions of cards, or that some cards are unlockable and can be earned. There may be cards that are not collectible – perhaps a boss card. A lot of the keys appeared on each of the four snippets, and others might appear on variants of the same kind of card, so it appears that there are actually a lot of features common to a base type of interface.

Even though I like having a good idea of the potential feature set of a project, I won’t actually model all of these fields now. I don’t know what all I will actually get around to implementing and I like to keep things simple. Also, I plan to take a few liberties and redesign the data here and there. So far I think the important shared features of a card could look something like this:

public class Card {
	public string id;
	public string name;
	public string text;
	public int cost;
	public int orderOfPlay = int.MaxValue;
	public int ownerIndex;
	public Zones zone = Zones.Deck;
}

The fields: id, name, text, and cost were all picked from the json snippets as things I wanted to include right away and which would be pretty applicable to all types of cards. The id should be unique so that we can fetch card data by this value from some sort of database to create instances of it at any time. The name is the title of the card and appears on the card itself. The text describes any special action a card can take, such as a spell causing damage to a target. The cost is the amount of mana required to play a card.

I also added a few fields of my own. The orderOfPlay will be populated dynamically as a user actually plays a card. The value of this field will be important in the resolution of events, because cards that are played first should also have their triggered events resolved first.

The ownerIndex and zone will make it easy for any system to know who owns a card and in what zone collection a card is located. For example, imagine a spell has been played with an ability that “Destroys” another targeted card. Instead of looping through all of the collections of all of the players until you find the container for the targeted card, you will be able to use the “ownerIndex” directly on a “match” object to get the correct player, and the “zone” can be used in the player’s indexer so that you will know what list of cards contains the targeted card. Now you can remove the targeted card from the zone it was in and place it in the Graveyard zone instead.

Card Sub-Types

The most common way that I have implemented polymorphism in the past was through class inheritance. It will be the same now – the four main kinds of card sub-types (Hero, Minion, Spell, & Weapon) will be implemented as sub classes of the Card class. However, there are some attributes that will be common among sub-types that weren’t common enough to put in the base-type.

For example, some types of cards will have a concept of hit-points (Minions and Heroes) – I would want an attack system and any kind of ability that causes damage to be able to apply to both kinds of cards without having to write specific implementations for each. But I also don’t want one of those types to inherit from the other – a Hero isn’t a “special” Minion.

One solution for this problem is to make both sub-types impelement an interface. You can then use the type of the interface in your shared code to handle all of the types of cards that implement the interface. Here are a few we can try out:

public interface IArmored {
	int armor { get; set; }
}

public interface ICombatant {
	int attack { get; set; }
	int remainingAttacks { get; set; }
	int allowedAttacks { get; set; }
}

public interface IDestructable {
	int hitPoints { get; set; }
	int maxHitPoints { get; set; }
}

Let’s go ahead provide a few starter implementations for our card sub-types while seeing how we can implement some shared (but not inherited) functionality via these interfaces:

public class Hero : Card, IArmored, ICombatant, IDestructable {
	// IArmored
	public int armor { get; set; }

	// ICombatant
	public int attack { get; set; }
	public int remainingAttacks { get; set; }
	public int allowedAttacks { get; set; }

	// IDesructable
	public int hitPoints { get; set; }
	public int maxHitPoints { get; set; }
}

public class Minion : Card, ICombatant, IDestructable {
	// ICombatant
	public int attack { get; set; }
	public int remainingAttacks { get; set; }
	public int allowedAttacks { get; set; }

	// IDestructable
	public int hitPoints { get; set; }
	public int maxHitPoints { get; set; }

	// Other
	public List<string> mechanics;
	public string race;
}

public class Spell : Card {

}

public class Weapon : Card {
	public int attack;
	public int durability;
}

The classes are likely not “done” – for example, a hero should have some concept of a hero power, and eventually I will probably even add additional features even to the base type such as abilities – but we can grow them as we write systems to support them too.

Summary

In a single lesson we have managed to “model” a pretty good description of what our CCG will be. From a match, to the players involved, and the cards that they own and play – everything is nicely contained in a single hierarchy of data that should be easy to persist in the future. We have enough in place to start creating a variety of new systems and even to determine the winner of a game – just watch for a hero’s hit points to drop to zero.

You can grab the project with this lesson fully implemented right here. If you are familiar with version control, you can also grab a copy of the project from my repository.

If you find value in my blog, you can support its continued development by becoming my patron. Visit my Patreon page here. Thanks!

Leave a Reply

Your email address will not be published. Required fields are marked *