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.
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.
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.
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.
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.
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
);
Card Collections provide a convenient way to hold multiple cards in one place. There are three different types:
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);
}
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));
}
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:
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);
}
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);
}
}
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);
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.
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.
Every modification of the game's state should only be performed using IAction
s.
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. IReaction
s triggered
by those IAction
s 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).
IReaction
s 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.
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(){...}
}
}
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.
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.
Apparently there are two ways to execute multiple IActions:
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.
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.
You can either inherit from TargetfulSpellCardComponent or TargetlessSpellCardComponent based on whether your component requires a target to be selected (ITargetful) or not (ITargetless).