Programming Magic: the Gathering - §2 Map
Continuing our discussion of Magic the Gathering programming . Today we will discuss how the object model of a particular map is formed. Since the cards interact with all participants of the system (with players, other cards, etc.), we will also touch upon the implementation of the basic behavior of the cards. As before, we will use the .Net ecosystem, although in the future (hint) we will see the use of unmanaged C ++. Also, for example, we will use cards 8 th and later editions [. 1 ]
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
Having some idea of these two classes, let's look at the different properties of maps and how they can be implemented.
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
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
The structure
The card on the right has a type, or rather three. A type as a string can be written as “Legendary Creature - Wizard”, but since there are cards that actively manipulate types, we will also create a collection that can store a list of types - firstly, for quick searching, and secondly, so that sometimes add additional types there. Used above because types of cards can not be repeated. Having such a set, we can, for example, create a property that checks whether the map is legendary or not.
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:
Abilities are not created magically. They are read in text format, and parsed using regular regular expressions. Mana use is also an activated ability. In order to add it to the model, we use a fairly simple delegate. Now in order to implement, say, a double ground like Shivan Oasis , you just need to find the appropriate text in the map rules and add the corresponding abilities.
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 .
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!
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 . [ 2 ] In fact, there are no problems with reading the cost of using the card. As for the cost of using opportunities, in addition to the mana itself, we have additional properties, such as RequiresTap
. We will discuss this later in the post.Card type
The card on the right has a type, or rather three. A type as a string can be written as “Legendary Creature - Wizard”, but since there are cards that actively manipulate types, we will also create a collection that can store a list of types - firstly, for quick searching, and secondly, so that sometimes add additional types there. Used above because types of cards can not be repeated. Having such a set, we can, for example, create a property that checks whether the map is legendary or not.
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; }
}
: Draw three cards. | : Return Arcanis the Omnipotent to its owner's hand. |
|
|
Abilities are not created magically. They are read in text format, and parsed using regular regular expressions. Mana use is also an activated ability. In order to add it to the model, we use a fairly simple delegate. Now in order to implement, say, a double ground like Shivan Oasis , you just need to find the appropriate text in the map rules and add the corresponding abilities.
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.