All Articles

Modelling Hearthstone with GraphQL

Hearthstone is a collectible card video game created by Blizzard Entertainment. In this post I will use my understanding of the game and model it using GraphQL types. This will serve as a good exercise for architecting with GraphQL. Since GraphQL is a contract between the client and server, I will keep my type definitions agnostic. It will be up to each side to decide how they implement the game.

Players

Each player registers into a system which keeps track of the following attributes: a unique identifier, a rank which represents their standing, and a match history. They also have a card collection, a set of decks made up of their cards, and an in-game currency called dust which can craft more cards. There are also coins which the player accumulates for completing quests. Finally, in order to gain a rank, a certain amount of wins must be held to progress.

type Player {
  playerId: String!
  decks: [Deck]
  cards: [Card]
  dust: Int!
  coins: Int!
  matchHistory: [Match]
  rank: [RankTitle]
  winsNeededTillNextRank: Int
}

For the Player type, I used the ! type modifier to indicate that a field is non-nullable. A new player will have no decks, no match history, and no rank. The playerId, however, is mandatory for all players since it uniquely identifies a player system wide. GraphQL keeps no notion of uniqueness, that is all kept in the database. Furthermore, all players start with zero dust and coins when they first register.

The remaining fields are made of other types and enumerations. For instance, every player gains a rank when they decide to play in competitive mode. Each rank is represented by a card name:

enum RankTitle {
  ANGRYCHICKEN
  LEPERGNOME
  ...
  INNKEEPER
  LEGEND
}

As a player wins matches, they get closer to gaining a new RankTitle. There should be a mechanism for setting winsNeededTillNextRank, but that is independent of GraphQL.

A player who just started playing in competitive, is titled an ANGRYCHICKEN. Meanwhile, players that surpass the ranking system become LEGEND. For that reason, I used an enumeration to represent these progressing finite titles.

Decks

All decks have the following mandatory fields: a deck name, a hero, and cards.

type Deck {
  name: String!
  hero: HeroClass!
  cards: [Card]!
}

In a regular match, there is a thirty card limit per deck. With that said, GraphQL makes no assertions on how much data a field contains. This validation must be done at the implementation level. In addition, there are game modes that break this rule, which means I would like to keep the limit flexible.

A deck also has a hero class associated with it. Once again, I used an enumeration since there is a finite amount of hero classes in the game.

enum HeroClass {
  DRUID
  HUNTER
  MAGE
  PALADIN
  PRIEST
  ROGUE
  SHAMAN
  WARLOCK
  WARRIOR
}

In the game, a hero has an ability which they can use in a match. Even though the power is related to the hero, events in the game may change the hero ability. For that reason, I will manage the ability elsewhere. Since each hero has cards specific to them, it’s necessary to put the hero class within the deck.

Matches

A ranked match consists of two matched players, they both have the same fields, and thus, I made MatchedPlayer. A utility type to ease the definition of Match.

type Match {
  players: [MatchedPlayer]!
  matchKind: MatchKind!
}

Traditionally, there are only two players in a match. However, there are crazy game modes in Hearthstone where there are more than two players involved. For that reason, I defined an enumeration to represent the kinds of possible matches.

enum MatchKind {
  CASUAL
  CASUALWILD
  RANKED
  RANKEDWILD
  ADVENTURE
  ARENA
  BRAWL
}

Based on matchKind, I will let the underlying implementation conduct validations for player count. For instance, a RANKED match would be validated for two players.

There are also other validation considerations that must be conducted within MatchedPlayer. The implementation should also prevent players with the same playerId to be matched, since this condition is impossible.

type MatchedPlayer {
  playerId: String!
  wonCoinToss: Boolean
  health: Int!
  manaMax: Int!
  manaAvailable: Int!
  manaLocked: [ManaLockTurn]
  hand: [Card]
  ability: HeroAbility!
  hero: HeroClass!
  matchStatus: MatchStatus
  deck: Deck!
}

At the beginning of a Hearthstone match, a coin toss is made to decide who goes first, this is represented by wonCoinToss. Whenever the field is null, the coin toss has not yet occurred.

Players will fight one another until a player reaches zero health, this is represented by the health field.

Players expend mana each turn to invoke cards, manaMax is the amount of mana they can spend per turn. manaAvailable is the amount of mana they have left in a turn.

In the game, it is possible to temporarily prevent players from regenerating some of their mana, this is represented by manaLocked. Since locking mana is a condition that can occur over several turns. There is a helper type to help represent this game condition.

enum ManaLockTurn {
  amountLocked: Int!
}

When the field manaLocked is null, no mana is locked. Otherwise, for every ManaLockTurn, the amount specified by amountLocked will represent how many mana is unusable for a single turn. At the implementation level, after a turn is over an entry is dequeued from the array, and the next entry takes effect until the field is null.

Players will draw cards from their decks. Those cards are kept in their hand.

There are also two non-trivial design decisions happening in MatchedPlayer. I included a hero field within the type, even though Deck already has one. In Hearthstone, cards in a deck are restricted by the chosen hero. For example, WARLOCK cards are not allowed in a PRIEST deck; and vice-versa. But, there are special events within a match that could theoretically change the player’s hero. For that reason, I decided the hero is kept within the matched player.

Likewise, the hero’s ability can also be changed during the match, which is why it is kept separate from Hero, and within MatchedPlayer. A HeroAbility type is used to represent each ability in the game. Abilities have similarities with card, for that reason, the heroEffect field matches another type called CardEffect which will be explained later.

type HeroAbility {
  name: String!
  cost: Int!
  heroEffect: CardEffect!
}

In addition, the field matchStatus keeps track of the player’s end game condition.

enum MatchStatus {
  VICTORY
  DEFEAT
  ONGOING
}

The enumerated values should be self-explanatory. Note that, in Hearthstone, there is no DRAW condition. If both players lose all their health, both players lose.

Cards

To reach the end of a match, players use their cards against one another. Cards are more complex to model since they carry a lot of information.

type Card {
  class: HeroClass
  name: String!
  rarity: CardRarity!
  type: CardType!
  minionType: CardMinion
  cardAbilities: [CardAbility]
  cardEffects: [CardEffect]
  cardSet: CardSet!
  cost: Int!
  golden: Boolean!
}

The use of nullable types is necessary to account for all variation in cards. With that said, there are some field combinations which are not possible. For instance, a card of type SPELL cannot have a minionType. The implementation which validates this model will keep track of this condition.

For good measure, the implementation of all enums used in Card are listed below with a brief explanation.

The CardType represent the main type of the card.

enum CardType {
  MINION
  SPELL
  WEAPON
}

CardMinion represents a sub type of MINION card types. As previously stated, minions cannot be SPELLS or WEAPONS, but that’s up to the implementation to enforce.

enum CardMinion {
  BEAST
  DEMON
  DRAGON
  MECH
  MURLOC
  PIRATE
  TOTEM
  GENERAL
}

Cards may have zero to many abilities. Only cards of type MINION or WEAPON can have these card abilities. The implementation will decide that.

enum CardAbility {
  CHARGE
  BATTLECRY
  DEATHRATTLE
  ...
}

Cards belong to exactly one card set, this includes card expansions and promotional card sets.

enum CardSet {
  CLASSIC
  ...
}

Cards have various degrees of rarity, the rarer the card, the more valuable.

enum CardRarity {
  LEGENDARY
  EPIC
  RARE
  COMMON
  FREE
}

Card effects are the core of the game. Based on the card effect, a card (or hero ability) will do something in-game. There are many these, since there are many cards. Some effects are re-used among several cards, and other effects are specific to certain cards of rarer variety.

enum CardEffect {
  DEAL_ONE_DMG_ALL
  DEAL_ONE_DMG_TARGET
  ...
  GIVE_DIVINE_SHIELD_ALL
  GIVE_DIVINE_SHIELD_TARGET
  ...
  FREEZE_ALL
  FREEZE_LEFT_RIGHT
  FREEZE_TARGET
  ... 
  PRIEST_OF_THE_FIEST_EFFECT
  JUSTICAR_TRUEHEART_EFFECT
}

For instance, dealing one damage, freezing characters, or providing divine shield; are common effects within Hearthstone. However, certain cards tend to have unique abilities. When the card Justicar Trueheart is summoned her BATTLECRY card ability should activate the JUSTICAR_TRUEHEART_EFFECT card effect, an effect specific to her card. At an implementation level, I would probably keep a map of all abilities to a lambda that performs the corresponding ability.

Conclusion

In this post I’ve modelled players, decks, matches, and cards. To fully represent each as a concept, additional enumerations and helper types were needed. Even though not all game scenarios are covered, the core of the game is intact.

The beauty of GraphQL is that I can establish a base contract that can evolve over time, and still be useful for a server and client to implement. With a couple more iterations, a fully modelled game can be accomplished. By using GraphQL, I can fully immerse in architecture without having to worry about too many implementation details.

Published 24 Nov 2016