The C# Battle Card Game Framework facilitates the process of developing a custom battle card game (such as Magic The Gathering, Pokémon and Hearthstone) in C#. It already provides a basic event-driven game loop and classes to derive from.
(see also the Official CSBCGF Homepage)
Check out how easy it is to setup your own battle card game using CSBCGF by playing the demo console application.
The Game class is the central object within the framework implementing the
IGame interface. It contains the whole state of a battle card match and
provides methods to alter its state:
- NextTurn: Ends the current turn and starts a new turn, activating the next player.
- Queue & Process: see section IAction.
There is also a IGameState interface which represents an IGame, but without
the ability to alter the game's state.
The Player class represents a participant in a game. It holds the player's
cards in four different collections:
- Deck: Source of cards that the player draws.
- Hand: Source of cards that the player might bring onto the board.
- Board: Location of monster cards that actively participate in the battle.
- Graveyard: Depot for discarded cards.
Implementing the IPlayer interface, the Player class provides methods
to interact with those cards:
- DrawCard: Draw a card from the deck.
- PlayMonster: Add a monster card from the player's hand to the player's board.
- PlaySpell: Play a spell card from the player's hand.
- Attack: Initiate a fight between a befriended and an opposing monster card.
The ICardCollection interface provides some useful methods to interact with a
collection of cards.
The ICard interface exposes the ManaValue property (i.e. the card's costs
in Mana) and a method IsCastable that checks whether this card can be
played (i.e. cast a spell card or add a monster card to the board) by the
active player.
The MonsterCard class is an abstract base class for monster cards that can
be played onto the board and attack other characters (i.e. monster cards or players).
The SpellCard class is an abstract base class for spell cards that have an
immediate effect once played from the player's hand. There are two subtypes:
- TargetlessSpellCard: A spell card that can be played without specifying a target character.
- TargetfulSpellCard: A spell card that can only be played by specifying a target character first.
Cards consist of CardComponents that combinedly make up the Card's
stats and behaviour. Those components can dynamically be added and removed
from the Card (e.g. to simulate certain enhancements or enchantments).
While SpellCardComponents only contain mana costs, Actions to be
executed when the card is played and Reactions (see IReaction section),
MonsterCardComponents also contain attack and life stats.
A Stat is an atomic value that is part of a game objects state. It provides
the following properties:
- Value: The current integer value.
- BaseValue: The base integer value is used to hold the
Stat's initial (e.g. life) or maximum (e.g. player mana) value.
This framework features the following IStat implementations:
- ManaCostStat: Represents the costs for playing a card.
- ManaPoolStat: Represents the mana available to a player.
- LifeStat: Represents the life points of the character.
- AttackStat: Represents the potential damage dealt in a fight with this character.
A character is a participant with an AttackStat and a LifeStat that
dies once its life reaches a value of zero. The IPlayer and IMonsterCard
interfaces inherit the ICharacter interface, i.e. within this framework
a character can either be a player or a monster card.
Changes to the game state should only be performed via actions (i.e. classes
implementing IAction)! More specifically only in the game.Execute method.
This method takes one or more IActions, adds them to the queue and
executes them one by one. IReactions triggered by those IActions
are being executed afterwards (which in turn can trigger further IReactions).
Since the game state might change before IAction.Execute is executed,
the IAction provides a check IAction.IsExecutable which is executed
immediately before IAction.Execute. If this method evaluates to false
the IAction will not be executed and simply discarded (i.e. also no
IReactions will be triggered).
An event is an abstract implementation of an IAction that does not execute
a game state change but rather serves as marker/trigger. One example would be
the StartOfTurnEvent which is triggered at the start of each turn.
Additionally, they may be used to mark the start and end of a certain series of
IActions and thereby provide useful information. E.g. before a player draws
a card, a StartDrawCardEvent is triggered. After the player has drawn the
card, a EndDrawCardEvent is triggered that also provides the drawn card.
Other useful Events to listen to and that are provided out of the box are:
[Start/End]AttackEvent: Provides attacking monster card and target character.[Start/End]PlayMonsterCardEvent: Provides monster card and board index.[Start/End]PlayTargetlessSpellCardEvent: Provides the spell card.[Start/End]PlayTargetfulSpellCardEvent: Provides the spell card and target character.
Cards implement the IReactive interface which means that implementations
of the IReaction interface can be added/removed to/from cards. After the
execution of each Action (via Game.Process method), this Action is fed into
the ReactBefore or ReactAfter method of every Action in every Card
and all Actions returned from this method are in turn again fed into the
action queue and subsequently processed.
The whole game state is meant to be serializable so that it can be sent from server to client and vice versa. Hence, the developer needs to ensure that their classes can be serialized and deserialized correctly. Here are some hints to achieve this:
If you use below mentioned annotations, make sure to import Newtonsoft.Json.
Example:
using Newtonsoft.Json;
If you implemented one or more constructors, also add one protected default
constructor without any implementation in its body.
In case this conflicts with your own parameterless public constructor, add
an optional dummy parameter to your constructor.
Example:
public class FarSight : TargetlessSpellCard
{
protected FarSight() {} // this is used for deserialization only
public FarSight(bool _ = true) : base(new FarSightComponent()) // this is used in your code
{
}
...
Make sure that the whole state that needs to be serialized is either contained
in protected/private member variables marked with JsonProperty or
public member variables/properties.
If you expose protected/private variables via public Getters / Setters,
annotate them with JsonIgnore so that they are not (de)serialized.
Example:
public abstract class Compound : ICompound
{
[JsonProperty]
protected List<ICardComponent> components = null!;
protected Compound() {}
public Compound(List<ICardComponent> components)
{
this.components = components;
}
[JsonIgnore]
public List<ICardComponent> Components {
get => components;
}
}
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:
- Call
game.Execute(IAction)multiple times orgame.ExecuteSequentially(List<IAction>). At each call theIActionitself plus all triggeredIReactions are executed if not forbidden byIAction.IsExecutable. Note that also the triggeredIReactions could in turn trigger furtherIReactions. - Call
game.ExecuteSimultaneously(List<IAction>)with allIActions. Now allIActions in the list will be executed first (if not forbidden byIAction.IsExecutable). Only after that has happened, allIReactions triggered by all thoseIActions will be executed. This goes on until noIReactions 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 theGetActionsto return theIActions to be performed once the corresponding spell card is played.TargetfulSpellCardComponent: Override theGetActionsto return theIActions to be performed once the corresponding spell card is played. Also override theGetPotentialTargetsmethod to provide a list of valid targets based on a given game state.
The C# Battle Card Game Framework was designed and implemented by Moritz Fink.