Tutorial

This tutorial will guide you through the first steps towards the implementation your custom battle card game using the C# Battle Card Game Framework (also referred to as CSBCGF).

In order to take away as much as possible from this tutorial, we highly recommend to write the provided code and not just copy-paste it. This leads to a much deeper learning experience.

0. Prerequisites

This tutorial assumes that you have at least some basic programming experience. Being aware of some fundamental principles of object-oriented programming or even knowledge with respect to the C# programming language will of course facilitate your learning experience.

We recommend using Visual Studio but of course you may use any other IDE that you like. This tutorial declares steps as to be done in Visual Studio.

1. Setup

We start by opening an IDE of our choice and creating a new console project. The C# Battle Card Game Framework is available as NuGet package. So we select Project -> Manage NuGet Packages... from Visual Studio's menu and search for "csbcgf". Click the checkbox next to the C# Battle Card Game Framework package and click Add Package. Now we are all setup to use the CSBCGF in our code.

Add NuGet package
Add the package to your project via NuGet Package Manager.

2. The Basics

Before we are actually implementing our first class with the CSBCGF, we should get a quick overview on the framework's basic game structure and mechanics.

2.1 Game Structure

Most battle card game have a pretty similar setup and we try to resemble this within the CSBCGF. A game consists of an arbitrary amount of players (but usually two). Each player has a deck (initially) containing all his/her cards. Each player also has a hand that can hold a certain amount of cards, a board with a certain amount of slots - which can in turn be occupied by a card or not - and a graveyard where cards can be discarded to.

This basic setup can easily achieved with a few lines of code:

using csbcgf;

namespace csbcgftutorial
{
    class MainClass
    {
        public static void Main(string[] args)
        {
            IGame myGame = new Game();

            IPlayer myPlayer1 = new Player();
            IPlayer myPlayer2 = new Player();

            myGame.Players.Add(myPlayer1);
            myGame.Players.Add(myPlayer2);
        }
    }
}

A player has a life stat and a mana pool stat that we can access as follows:

Console.WriteLine("Life stat of player 1 = " + myPlayer1.LifeValue
                + " / " + myPlayer1.LifeBaseValue);
Console.WriteLine("Mana pool stat of player 1 = " + myPlayer1.ManaValue
                + " / " + myPlayer1.ManaBaseValue);

You should notice that each stat consists of a value and a base value. In the case of the life stat, the base value represents a players full life and the value his/her current life points. In the case of the mana pool stat, the base value represents how much mana is available to the player at the start of each turn and the value counts how much of it is still left.

2.2 Cards

The most important ingredients of each battle card game naturally are the cards. Those can be categorized into two different kinds: monster cards & spell cards. Both have a mana cost stat that represents the amount of mana a player has to spend in order to bring the card into play.

Spell cards are played from a player's hand, alter the game's state, i.e. they have an effect and are afterwards discarded to the player's graveyard.

Monster cards are played from a player's hand onto his/her side of the board. They have a life stat and an attack stat. A player can attack an opponent player or one of his/her monster cards on the board with one of his/her monster cards on the board. This decreases the life stats of the defending and attacking character (i.e. player or monster) by the amount of attack points of the other character respectively. Once a monster card's life drops to zero it is discarded to the owning player's graveyard. If a player's life drops to zero this player looses the game.

Let us start by initializing a simple monster card that costs one mana and has one attack point and one life point respectively:

IMonsterCard myMonsterCard = new MonsterCard(
    mana: 1,
    attack: 1,
    life: 1
);

2.3 Card Collections

Card Collections provide a convenient way to hold multiple cards in one place. There are three different types:

  • Hand: Represents a player's hand
  • Deck: Represents a player's deck
  • Board: Represents a player's board with slots for monster cards

Before starting a game the player's deck should be set up in the following way:

IDeck deck = new Deck();

for (int i = 0; i < 20; ++i)
{
    IMonsterCard myMonsterCard = new MonsterCard(mana: 1, attack: 1, life: 1);
    deck.Push(myMonsterCard);
}

2.4 Players

Players must be initialized with the deck that we previously setup:

List<IPlayer> players = new List<IPlayer>();
for (int i = 0; i < 2; ++i)
{
    IDeck deck = new Deck();
    // TODO: Push some cards into the deck
    players.Add(new Player(deck));
}

2.5 Game

Finally, we can create and start a game involving those players:

IGame game = new Game(players);
game.StartGame(initialHandSize: 1, initialPlayerLife: 2);

By calling the StartGame method, every player receives the given number (initialHandSize) of cards from their deck to their hand. Each player starts with initialPlayerLife life points. At this point the game is on and the first turn has started. The (randomly chosen) active player (game.ActivePlayer) may now perform a set of actions that will be discussed later on. The turns ends with a call to game.NextTurn().

During a turn the players may use their resources to perform following actions:

Summon a monster

The active player spends the monster card's mana costs and transfers it from his/her hand to the board (onto the specified slot):

IPlayer activePlayer = game.ActivePlayer;
Console.WriteLine("Active player's mana = " + activePlayer.ManaValue);
IMonsterCard goblin = (IMonsterCard)activePlayer.Hand[0];
if(goblin.IsSummonable(game))
{
    activePlayer.CastMonster(game, goblin, 0);
    Console.WriteLine("Active player's mana = " + activePlayer.ManaValue);
}

Cast a spell

The active player spends the spell card's mana costs, the card's effects are played and the card moves to the graveyard. Since we have not discussed spell cards in detail at this point in the tutorial, we will just leave the following snippet here for demonstration purpose and come back to it later:

ISpellCard spell = (ISpellCard)activePlayer.Hand[1];
if(spell.IsCastable(game))
{
    if(spell is ITargetlessSpellCard targetlessSpell)
    {
        activePlayer.CastSpell(game, targetlessSpell);
    }
    else if (spell is ITargetfulSpellCard targetfulSpell)
    {
        activePlayer.CastSpell(game, targetfulSpell, goblin);
    }
}

Attack

If the active player possesses a monster on his/her board that has not been summoned in this turn and not attacked yet in this turn, then this monster may attack an opposing player or a monster on an opponent's board. Attacking a player reduces this player's life by the attack points of the attacking monster. Attacking another monster reduces both monsters' lifes by the other monster's attack points.

if(goblin.IsReadyToAttack)
{
    goblin.Attack(game, game.NonActivePlayers[0]);
    // OR
    goblin.Attack(game, anotherGoblin);
}

During the game you may want to check if a certain player is still alive:

Console.WriteLine("First player is still alive: " + game.Players[0].IsAlive);

3. Advanced Concepts

This section provides a more detailed view on some of the concepts of the CSBCGF which not only serve to gain insights on how the framework works internally, but also may be used to customize a game according to the developer's specifications.

3.1 Card Components

One of CSBCGF's benefits is that it treats cards as compounds made up of smaller parts, the so called card components. A card can thus dynamically be modified by adding or removing certain components.

Console.WriteLine("Goblin's life = " + goblin.LifeValue); // 1
ICardComponent extraLifeComponent = new MonsterCardComponent(0, 0, 1);
goblin.Components.Add(extraLifeComponent);
Console.WriteLine("Goblin's life = " + goblin.LifeValue); // 2
goblin.Components.Remove(extraLifeComponent);
Console.WriteLine("Goblin's life = " + goblin.LifeValue); // 1

In this example we have effectively just increased / decreased the monster's life points, but you could also modify mana and attack points, or even add / remove effects as components.

3.2 Actions & Events

Every modification of the game's state should only be performed using IActions. More specifically only via Game.Execute method. This method takes one or more actions, adds them to the queue and executes them one by one. IReactions triggered by those IActions are being executed afterwards (and may in turn trigger further reactions). A simple action that is already implemented in the CSBCGF is the AddCardToBoardAction:

public class AddCardToBoardAction : Action
{
    public readonly IBoard Board;
    public ICard Card;
    public int BoardIndex;

    public AddCardToBoardAction(IBoard board, ICard card, int boardIndex, bool isAborted = false)
            : base(isAborted)
    {
        Board = board;
        Card = card;
        BoardIndex = boardIndex;
    }

    public override object Clone(){...}

    public override void Execute(IGame game)
    {
        Board.AddAt(BoardIndex, Card);
    }

    public override bool IsExecutable(IGameState gameState)
    {
        return Card != null && Board.IsFreeSlot(BoardIndex);
    }
}

So this action just adds a card to the player's board, but by overriding the IsExecutable method, we can tell the framework if it even makes sense to execute it (e.g. if the board slot has been occupied by another card right before this action is executed, you cannot add the card anymore and thus you do not want the action to be executed).

IReactions are quite similar, but they only provide one method to be overridden to execute code right before or after an action is executed. Take for example the DrawCardOnStartOfTurnEventReaction:

public class DrawCardOnStartOfTurnEventReaction : Reaction
{
    public override object Clone(){...}

    public override void ReactTo(IGame game, IActionEvent actionEvent)
    {
        if (actionEvent.IsAfter(typeof(StartOfTurnEvent)))
        {
            game.Execute(new DrawCardAction(game.ActivePlayer));
        }
    }
}

Note that also here we do not alter the game state in any other way than calling game.Execute with an action. Also we react to a StartOfTurnEvent which is a built-in Event. Events are simple markers without any implementation that are triggered at certain points so that reactions may be triggered. The built-in events are StartOfGameEvent, EndOfGameEvent, StartOfTurnEvent and EndOfTurnEvent.

3.3 Effects

Using all that we covered in this tutorial, we are now able to implement a spell card that deals a defined amount of damage to a target character:

public class DamageSpellCard : TargetfulSpellCard
{
    protected uint damage;

    public DamageSpellCard(uint damage)
        : this(damage, new List<ICardComponent>(), new List<IReaction>())
    {
        Components.Add(new DamageSpellCardComponent((int)damage, damage));
    }

    protected DamageSpellCard(
        uint damage,
        List<ICardComponent> components,
        List<IReaction> reactions
        ) : base(components, reactions)
    {
        this.damage = damage;
    }

    public override object Clone(){...}

    public class DamageSpellCardComponent : TargetfulSpellCardComponent
    {
        private readonly uint damage;

        public DamageSpellCardComponent(int mana, uint damage)
            : this(damage, new ManaCostStat(mana, mana), new List<IReaction>())
        {
        }

        public DamageSpellCardComponent(
            uint damage,
            ManaCostStat manaCostStat,
            List<IReaction> reactions
            ) : base(manaCostStat, reactions)
        {
            this.damage = damage;
        }

        public override void Cast(IGame game, ICharacter target)
        {
            game.Execute(new ModifyLifeStatAction(target, -(int)damage));
        }

        public override HashSet<ICharacter> GetPotentialTargets(IGameState gameState)
        {
            HashSet<ICharacter> targets = new HashSet<ICharacter>();
            foreach (IPlayer player in gameState.Players)
            {
                targets.Add(player);
                player.Board.AllCards.ForEach(c => targets.Add((ICharacter)c));
            }
            return targets;
        }

        public override object Clone(){...}
    }
}

3.4 Demo Project

Have a look at the demo project to view and play a simple battle card game console application implemented with the CSBCGF. You can find further information in the readme. Additionally, the code for this tutorial is also on Github.

4. FAQ

When should I use card components to assemble cards?

As often as possible. You can of course change the card's mana costs (ManaValue) or reactions (AddReaction/RemoveReaction) directly, but this might lead to unexpected side effects. It is good practice to always add/remove IReactions/ IStats as components.

How to properly execute multiple actions?

Apparently there are two ways to execute multiple IActions:

  1. Call Game.Execute(IAction) multiple times. At each call the IAction itself plus all triggered IReactions are executed if not forbidden by IAction.IsExecutable. Note that also the triggered IReactions could in turn trigger further IReactions.
  2. Call Game.Execute(List<IAction>) with all IActions. Now all IActions in the list will be executed first (if not forbidden by IAction.IsExecutable). Only after that has happened, all IReactions triggered by all those IActions will be executed. This goes on until no IReactions have been triggered anymore.

So there is definitely a difference between both approaches. Consider e.g. a fight between two monster cards, where you want both monsters to lower each others life stats 'simultaneously'. If you pick the first approach, the first monster to attack would maybe kill the second monster and - as reaction - send it to the graveyard. Now the attack from the second monster could not take place anymore and the first monster leaves the fight unharmed. In this case you should definitely go with the second approach, which would modify the life stats of both monsters first, before sending them to the graveyard.

How can I implement a custom spell card?

You can either inherit from TargetfulSpellCard or TargetlessSpellCard based on whether your card should feature ISpellCardComponent that require a target to be selected (ITargetful) or not (ITargetless). If at least one component requires a target you have to inherit from TargetfulSpellCard.

How can I implement a custom spell card component?

You can either inherit from TargetfulSpellCardComponent or TargetlessSpellCardComponent based on whether your component requires a target to be selected (ITargetful) or not (ITargetless).

  • TargetlessSpellCardComponent: Override the GetActions to return the IActions to be performed once the corresponding spell card is played.
  • TargetfulSpellCardComponent: Override the GetActions to return the IActions to be performed once the corresponding spell card is played. Also override the GetPotentialTargets method to provide a list of valid targets based on a given game state.