
Programming Magic: the Gathering - §2 Map

Previous positions: §1
The entire M: tG ecosystem implements the Observer pattern, and in such an unpleasant form that it would be ridiculous to talk about any kind of “data binding”. Therefore, when you first examine the structure of the map, you can try to create a bare, anemic model. Unfortunately, changing the map we need to remember its initial state. For example, Avatar of Hope initially costs , but when you have 3 lives or less, it costs not but simply . Therefore, we have a dichotomy - we need both a prototype (initial value) and the real value “in the game”. And so for each property. In principle, we can divide this functionality into two interconnected classes that will reflect these states: Class
public class Card
{
public string Name { get; set; }
public Mana Cost { get; set; }
⋮
// и так далее
}








// прототип карты
class Card
{
public virtual string Name { get; set; }
⋮
}
// карта в игре, со всеми внутриигровыми изменениями
class CardInPlay : Card
{
public override string Name
{
⋮
}
public Card Prototype { get; set; }
// вот так создается "живая" карта
public CardInPlay(Card prototype)
{
Prototype = prototype; // ссылка на оригинал
Name = prototype.Name; // копия всех свойств - не под силу C# без AutoMapper :)
⋮
}
}
CardInPlay
implements one of the variations of the Decorator pattern, in which one class simultaneously inherits and aggregates another class. Having some idea of these two classes, let's look at the different properties of maps and how they can be implemented.
Map Name and Problem Æ
In principle, everything is clear with the name of the card - this is a line, it is saved, drawn on the screen, sometimes of course it can change (for example, when cloning), but basically it does not present any problems.
But there is a problem with databases that for some reason do not want to write the letter Æ, using capital AEs instead. This is not a big problem, we just need to use it
string.Replace()
when reading the card from the database.Card cost
We discussed mana in a previous post. The cost is described by standard notation, which is then parsed by the system. There are several variations in cost, in particular
- Zero cost (
)
- Regular Cost (
)
- Cost as a function of something (
)
The structure
Mana
is suitable for all cases, because it can count the amount of one or another mana, and also supports the property HasX
if it appears in mana 
RequiresTap
. We will discuss this later in the post.Card type

public string Type
{
get { return type; }
set
{
if (type != value)
{
type = value;
// create derived types
types.Clear();
string[] parts = type.Split(' ', '-', '–');
foreach (var part in parts.Select(p => p.Trim()).Where(p => p.Length > 0))
{
types.Add(part);
}
}
}
}
private ICollection types = new HashSet();
public ICollection Types
{
get
{
return types;
}
set
{
types = value;
}
}
HashSet
public bool IsLegend
{
get
{
return Types.Where(t => t.Contains("Legend")).Any();
}
}
rules
While Arkanis is hanging close to our screen, let's take it as an example. Arkanis has two activated abilities ("abilities"). Using all the benefits of OOP, we can again create an anemic model. As you probably already guessed, the card has a list of abilities, and in the game itself, the user can select one of them. So, the ability has a text description, cost, a checkbox that shows whether to turn the card, and a delegate who determines what this ability does. For Arkanis, his two “abilities” will look like this:
public sealed class ActivatedAbility
{
public string Description { get; set; }
public Mana Cost { get; set; }
public bool RequiresTap { get; set; }
public Action Effect { get; set; }
}
![]() | ![]() ![]() ![]() |
|
|

Action addManaGeneratingAbility =
mana => c.ActivatedAbilities.Add(new ActivatedAbility
{
Cost = 0,
RequiresTap = true,
Effect = (game, card) =>
game.CurrentPlayer.ManaPool.Add(Mana.Parse(mana)),
Description = "Tap to add " + mana + " to your mana pool."
});
Match m = Regex.Match(c.Text,
"{Tap}: Add {(.)} or {(.)} to your mana pool.");
if (m.Success)
{
addManaGeneratingAbility(m.Groups[1].Value);
addManaGeneratingAbility(m.Groups[2].Value);
}
Strength and Health
It would be simple if the cards had only numerical values for the strength and health of the card. Then, they could be made and everything would be openwork. In fact, in the prototype such values as, for example, can appear . Of course, in most cases, we just parse values, but in addition to fixed values, we have derivative values. This in turn means that we have an override of properties and which consider derived values. For example, for a Mortivore map , the structures look like this: Now, we can use s to create map properties .
Nullable
*/*

Power
Toughness
class Card
{
public Card()
{
⋮
GetPower = (game, card) => card.Power;
GetToughness = (game, card) => card.Toughness;
}
⋮
// содержит */*
public string PowerAndToughness { get; set; }
// содержат что угодно (скорее всего нули)
public virtual int Power { get; set; }
public virtual int Toughness { get; set; }
// а вот и методы подсчета
public Func GetPower { get; set; }
public Func GetToughness { get; set; }
}
Regex
m = Regex.Match(c.Text, c.Name + "'s power and toughness are each equal to (.+).");
if (m.Success)
{
switch (m.Groups[1].Value)
{
case "the number of creature cards in all graveyards":
c.GetPower = c.GetToughness = (game,card) =>
game.Players.Select(p => p.Graveyard.Count(cc => cc.IsCreature)).Sum();
break;
}
}
Conclusion
In this post I briefly described how the object model of maps looks. I deliberately left all the metaprogram delights “overboard” because with them the material would be less readable. I can only hint that some of the recurring aspects of the implementation of the Decorator pattern are too time-consuming - they must either be automated , or use advanced languages like Boo.
To be continued!
Notes
- ↑ As far as I know, or rather, as far as the database tells me , the 8th edition is not Russified. At the moment, all examples of implementation of maps are presented in English, but this does not mean that they cannot be Russified when the rules are fully implemented. Parser is still more convenient to write in English since there words are not inclined.
- ↑ In fact, here the option is uncovered when the cost, for example
. We will solve this problem when it becomes relevant.