Programming Magic: the Gathering - §1 Mana
I would like to start posts about programming Magic: the Gathering (M: tG), and we will begin to regret with the simplest thing - with the concept of "mana". Mana is what all spells pay for. Despite the fact that there are only 5 types of mana in appearance, in fact, everything is a little more complicated. Let's try to figure it out.
First of all, there are not 5 types of mana. Even if we discard the “dual” mana (when you can pay either one or the other), then there is still “colorless” mana for which artifacts are bought, and which appears in the cost of many (most) spells. Also - if we are talking about "price" and not about payment, mana X appears, i.e. situation when the price is regulated by the rules.
Let's look at a few examples.
Ordinary colors
Base mana can be of five colors, and mix as you like. This is a direct hint that in any class that deals with mana, a model of these five colors should appear. Each element of the model, as happens with the straightforward application of OOP, pulls a few more. For example, a naive approach to implementing mana might look like this: While we play with the idea that the cost and availability of mana in a pool can be contained in one entity, you can dream up a little more. For example, how do I get a mana representation in a text string (like WUBRG for Sliver Legion)? Something like this:
class Mana
{
⋮
public int Blue { get; set; }
public bool IsBlue { get { return Blue > 0; } }
// и так далее
}
public string ShortString
{
get
{
StringBuilder sb = new StringBuilder();
if (Colorless > 0) sb.Append(Colorless);
if (Red > 0) sb.Append('R'.Repeat(Red));
if (Green > 0) sb.Append('G'.Repeat(Green));
if (Blue > 0) sb.Append('U'.Repeat(Blue));
if (White > 0) sb.Append('W'.Repeat(White));
if (Black > 0) sb.Append('B'.Repeat(Black));
if (HasX) sb.Append("X");
return sb.ToString();
}
}
This is how I illustrate the weakness of the model. If we did not know that there is dual mana (and we know something), then subsequent changes would cause an architectural apocalypse in our essence and everything with which it interacted. This is the first.
Secondly, writing the same thing 5+ times is bad. Imagine that you are implementing a method of paying for a certain mana cost from a pool. If you follow the same approach, you probably have to write something like this: The number of repetitions does not “roll over”, but it is certainly annoying. Let me remind you that there are no macros in C #.
public void PayFor(Mana cost)
{
if (cost.Red > 0) Red -= cost.Red;
if (cost.Blue > 0) Blue -= cost.Blue;
if (cost.Green > 0) Green -= cost.Green;
if (cost.Black > 0) Black -= cost.Black;
if (cost.White > 0) White -= cost.White;
int remaining = cost.Colorless;
while (remaining > 0)
{
if (Red > 0) { --Red; --remaining; continue; }
if (Blue > 0) { --Blue; --remaining; continue; }
if (Black > 0) { --Black; --remaining; continue; }
if (Green > 0) { --Green; --remaining; continue; }
if (White > 0) { --White; --remaining; continue; }
if (Colorless > 0) { --Colorless; --remaining; continue; }
Debug.Fail("Should not be here");
}
}
Colorless mana
Colorless mana is the first hint that each type of mana draws a domain-specific logic, which in principle is difficult to predict. For example, the map on the right is a typical example of a kinder surprise in working with such an inflexible domain as M: tG. Nevertheless, even using the same model (in C #), you can get some additional methods. For example, here is what the “converted value” property looks like: If you want something more serious, you can calculate whether the amount of mana satisfies a certain cost: Colorless mana, unlike color mana, lowers the degree of determinism, because we cannot play the spell automatically if, for example, we paid RG for a card worth 1, because it is not clear what color mana needs to pay.
public int ConvertedManaCost
{
get
{
return Red + Blue + Green + Black + White + Colorless;
}
}
public bool EnoughToPay(Mana cost)
{
if (Red < cost.Red || Green < cost.Green || White < cost.White ||
Blue < cost.Blue || Black < cost.Black)
return false;
// can we pay the colourless price?
return ((Red - cost.Red) + (Green - cost.Green) + (White - cost.White) +
(Blue - cost.Blue) + (Black - cost.Black) + Colorless) >= cost.Colorless;
}
Hybrid Mana
That's where it all starts ... after all, until that moment we thought that everything is very simple, and that for example you can take and predictably parse a string
2RG
and get an object of type Mana
. And here again - and new rules, not only the mechanics themselves, but also the notes. After all, how to spell the double mana symbol? Chances are as follows: {WB}{WB}{WB}
. Vootoot, and for this we already need a parser. Moreover, imagine that mana has bred like sleevers - purple, purple, and so on. Is it easy to add support for new mana to those pieces of code that I cited above? That's right - it's unrealistically difficult . A different approach is needed.
Before we look at this very “different approach”, it should be mentioned that cost and payment are two different things. More precisely, they are similar, but the cost, for example, may contain an icon
X
. For example, pay XG
and get X
life. In order not to produce essence, I still believe that the essence Mana
should be one. The situation with X
can be resolved singly ( public bool HasX
), or you can generalize a little bit, so if a card with a price suddenly appears XY
, we do not have to rewrite the entire logic. In addition, there are situations when for a certain X
one you can pay only with mana of a certain color. This also needs to be taken into account.About metaprogramming
It seems to me that metaprogramming is needed in this task, at least in order to avoid unnecessary code duplication and also to protect yourself from such cases when, for example, you suddenly need to add Observable support (say, through individual events) without rewriting each class property. C # is not suitable for such purposes (even considering that there is PostSharp). We need something that can take into account our goals, namely:
- Maintain color mana with an arbitrary number of colors. That is, for example, adding lilac mana should not break the system and should bring about only minor changes in the code.
- Maintain hybrid mana. I think that there will be no more mana built or more, so you can just make dual mana support for all existing colors. Moreover, when adding a new type of “primary” mana, additional actions for its “hybridization” are not necessary.
- Correctly maintain colorless mana as well as operations with it.
- Maintain standard mana notation, i.e. have a parser that can collect an object of type
Mana
from a string.
So, let's see how it is possible to gradually implement all of the above properties in a language that supports metaprogramming. Of course I'm talking about the Boo language. (Although there is still a Nemerle, but I'm not good at it.)
The colors are ordinary (attempt number 2)
Nb here and thereafter will be calculations in two languages at once - in Boo (what we wrote) and in C # (what Reflector saw in this). This was done in order to illustrate the actions of macros and meta-methods, because Boo itself, as you might guess, will not be transparent in this regard.
I would like to write "so, let's start with the simple", but simple, alas, will not be. To begin with, we will do two projects, namely
MagicTheGathering.Entities
for entities such asMana
. This assembly can later be usedILMerge
with other assemblies written in C # or F #.MagicTheGathering.Meta
for our meta-abstractions that will “collect” our entities.
Fields
Let's start with the support of forests: So, we hang attributes of different lands on our manaclass, starting with forests. What do we need from these attributes? First, they need to add the appropriate fields. It's simple: So, we have defined a constructor for the attribute that is called from our original entity. In the example above, we add a field to an existing class. This is done in three steps:
[ManaType("Green", "G", "Forest")]
class Mana:
public def constructor():
pass
class ManaTypeAttribute(AbstractAstAttribute):
colorName as string
colorAbbreviation as string
landName as string
public def constructor(colorName as StringLiteralExpression,
colorAbbreviation as StringLiteralExpression, landName as StringLiteralExpression):
self.colorName = colorName.Value
self.colorAbbreviation = colorAbbreviation.Value
self.landName = landName.Value
public override def Apply(node as Node):
AddField(node)
private def AddField(node as Node):
c = node as ClassDefinition
f = [|
$(colorName.ToLower()) as Int32
|]
c.Members.Add(f)
- First, we present the element to which the attribute is applied to the type
ClassDefinition
- Then, we create the field using a splice (we turn the contents of the string
colorName
into the real name of the field) and quoting (brackets[|
and|]
turn our construct into a property element - Add the collected property to the class.
Let's now compare the initial and final results:
Boo | C # |
[ManaType("Green", "G", "Forest")] | [Serializable] |
Direct and derived properties
Hmm, isn't that what we wanted? :) How about adding a simple property to this field? Elementary, Watson - you just need to change the definition
AddField()
:
Now let's create a test field, for example, let it return to us if the card is green, and if not. We will meet this property again, because it specifically interacts with hybrid cards. Here is my first attempt to implement it:
Implementing a derived property was also very simple. And here is how it all looks translated into C #:private def AddField(node as Node):
c = node as ClassDefinition
r = ReferenceExpression(colorName)
f = [|
[Property($r)]
$(colorName.ToLower()) as Int32
|]
c.Members.Add(f)
IsGreen
true
false
private def AddIndicatorProperty(node as Node):
c = node as ClassDefinition
r = ReferenceExpression(colorName)
f = [|
$("Is" + colorName) as bool:
get:
return ($r > 0);
|]
c.Members.Add(f)
[Serializable]
public class Mana
{
// Fields
protected int green;
// Properties
public int Green
{
get
{
return this.green;
}
set
{
this.green = value;
}
}
public bool IsGreen
{
get
{
return (this.Green > 0);
}
}
}
Interaction
Let's try to generate the total cost (converted mana cost). To do this, you need to implement colorless mana, which, in fact, is not so difficult. But how to auto-generate the sum of all mana + colorless? To do this, we will use the following approach:
- First, we will create a new attribute, and in it is a list of those properties that need to be summarized
class ManaSumAttribute(AbstractAstAttribute):
static public LandTypes as List = []
⋮ - Now, when creating any “land property”, we will write the name into this static property:
public def constructor(colorName as StringLiteralExpression,
⋮
ManaSumAttribute.LandTypes.Add(self.colorName) - Now use this property to create the sum:
class ManaSumAttribute(AbstractAstAttribute):
⋮
public override def Apply(node as Node):
c = node as ClassDefinition
root = [| Colorless |] as Expression
for i in range(LandTypes.Count):
root = BinaryExpression(BinaryOperatorType.Addition,
root, ReferenceExpression(LandTypes[i] as string))
p = [|
public ConvertedManaCost:
get:
return $root
|]
c.Members.Add(p)
Now let's check - add mountain support and see what is emitted for the property
ConvertedManaCost
. Here's what we get:
As you can see, everything works :)public int ConvertedManaCost
{
get
{
return ((this.Colorless + this.Green) + this.Red);
}
}
Hybrid Land Support
Okay, something started to work out for us. I add support for all lands to the code and cheers, now Boo auto-generates 10 properties for them, plus does the sum. What else is needed? Well, how about supporting hybrid land. This is of course more difficult, because you need to take all the pairs of existing lands. But it’s interesting, so why not give it a try?
The principle is the same as with the sum generator - we use a static field and fill it out with attributes of different mana. And then…. then a very complicated thing. In short - we are looking for all valid mana pairs and for them we create approximately the same properties as for ordinary, "of the same type" mana. I will not give the result because many properties are obtained :) Let's discuss what to do with properties like
class HybridManaAttribute(AbstractAstAttribute):
static public LandTypes as List = []
public override def Apply(node as Node):
mergedTypes as List = []
for i in range(LandTypes.Count):
for j in range(LandTypes.Count):
unless (mergedTypes.Contains(string.Concat(LandTypes[i], LandTypes[j])) or
mergedTypes.Contains(string.Concat(LandTypes[j], LandTypes[i])) or
i == j):
mergedTypes.Add(string.Concat(LandTypes[i], LandTypes[j]))
// each merged type becomes a field+property pair
c = node as ClassDefinition
for n in range(mergedTypes.Count):
name = mergedTypes[n] as string
r = ReferenceExpression(name)
f = [|
[Property($r)]
$(name.ToLower()) as int
|]
c.Members.Add(f)
IsGreen
in case of hybrid mana. After all, we can no longer keep them in the attributes of homogeneous mana, because at that time nothing is known about hybrid mana. Let's transfer them to a separate attribute. So, we need to use both hybrid and single properties to understand what color the card is.
Voila! In the code above, we find all types of mana that this type affects, and compare them with zero. This is not the most optimal way to calculate the property , but it works, although at the level of the Reflector it turns out bad such a mess.class ManaIndicatorsAttribute(AbstractAstAttribute):
public override def Apply(node as Node):
c = node as ClassDefinition
for i in range(ManaSumAttribute.LandTypes.Count):
basic = ManaSumAttribute.LandTypes[i] as string
hybridLands as List = []
for j in range(HybridManaAttribute.HybridLandTypes.Count):
hybrid = HybridManaAttribute.HybridLandTypes[j] as string
if (hybrid.Contains(basic)):
hybridLands.Add(hybrid)
rbasic = ReferenceExpression(basic.ToLower())
b = Block();
b1 = [| return true if $rbasic > 0 |]
b.Statements.Add(b1)
for k in range(hybridLands.Count):
rhybrid = ReferenceExpression((hybridLands[k] as string).ToLower())
b2 = [| return true if $rhybrid > 0 |]
b.Statements.Add(b2)
r = [|
$("Is" + basic):
get:
$b;
|]
c.Members.Add(r)
IsXxx
String representation and parser
For each simple type of mana, we have a string representation. This representation allows us to both read the string and get it. Let's start with a simple one - we get a string representation of mana, which we will issue through
ToString()
:
Well, we have almost everything, it remains only to add the most important thing - the mana description parser, i.e. so that the program can create the corresponding object from the line . Let's divide the manaparser into 3 parts - the statistics of the analysis of basic mana, hybrid mana, and "everything else." So, it’s not difficult to parse the basic mana:
You need to tinker with the hybrid mana so that the writing order of the mana ( or ) does not affect the parser. However, the solution is not very complicated:class ManaStringAttribute(AbstractAstAttribute):
public override def Apply(node as Node):
b = Block()
b1 = [|
sb.Append(colorless) if colorless > 0
|]
b.Statements.Add(b1)
for i in range(ManaTypeAttribute.LandTypes.Count):
land = ReferenceExpression((ManaTypeAttribute.LandTypes[i] as string).ToLower())
abbr = StringLiteralExpression(ManaTypeAttribute.LandAbbreviations[i] as string)
b2 = [|
sb.Append($abbr) if $land > 0;
|]
b.Statements.Add(b2)
for j in range(HybridManaAttribute.HybridLandTypes.Count):
land = ReferenceExpression((HybridManaAttribute.HybridLandTypes[j] as string).ToLower())
abbr = StringLiteralExpression("{" +
(HybridManaAttribute.HybridLandAbbreviations[j] as string) + "}")
b3 = [|
sb.Append($abbr) if $land > 0;
|]
b.Statements.Add(b3)
b3 = [|
sb.Append("X") if hasX
|]
m = [|
public override def ToString():
sb = StringBuilder();
$b
return sb.ToString()
|]
c = node as ClassDefinition
c.Members.Add(m)
2GG{RW}
// basic land cases are in a separate block
basicLandCases = Block()
for i in range(ManaTypeAttribute.LandTypes.Count):
name = ManaTypeAttribute.LandTypes[i] as string
abbr = ManaTypeAttribute.LandAbbreviations[i] as string
rAbbr = CharLiteralExpression(char.ToUpper(abbr[0]))
rName = ReferenceExpression(name)
case = [|
if (char.ToUpper(spec[i]) == $rAbbr):
m.$rName = m.$rName + 1
continue
|]
basicLandCases.Statements.Add(case);
RG
GR
// hybrid land cases are in a much smarter block
hybridLandCases = Block()
for i in range(HybridManaAttribute.HybridLandTypes.Count):
name = HybridManaAttribute.HybridLandTypes[i] as string
abbr = HybridManaAttribute.HybridLandAbbreviations[i] as string
// build an appreviation literal
abbr1 = StringLiteralExpression(abbr)
abbr2 = StringLiteralExpression(abbr[1].ToString() + abbr[0].ToString())
case = [|
if (s == $abbr1 or s == $abbr2):
m.$name = m.$name + 1
continue
|]
hybridLandCases.Statements.Add(case)
Well, then you can make the method itself as a set of cases. In addition to color mana, we add support for colorless mana, as well as a symbol
X
:
I will not give the result of this macro, because a lot of code is generated .// the method itself
method = [|
public static def Parse(spec as string) as Mana:
sb = StringBuilder()
cb = StringBuilder() // composite builder
inHybrid = false // set when processing hybrid mana
m = Mana()
for i in range(spec.Length):
if (inHybrid):
cb.Append(spec[i])
continue
if (char.IsDigit(spec[i])):
sb.Append(spec[i])
continue;
if (spec[i] == '{'):
inHybrid = true
continue
if (spec[i] == '}'):
raise ArgumentException("Closing } without opening") if not inHybrid
inHybrid = false
s = cb.ToString().ToUpper()
raise ArgumentException("Only two-element hybrids supported") if s.Length != 2
$hybridLandCases
raise ArgumentException("Hybrid mana " + s + " is not supported")
$basicLandCases
if (char.ToUpper(spec[i]) == 'X'):
m.HasX = true
continue;
|]
// add it
c = node as ClassDefinition
c.Members.Add(method)
Conclusion
Despite the fact that we didn’t make out a few cases, such as paying with a mana for a certain spell, I’m probably interrupted - partly because Firefox is already starting to fall from the number of characters in the textbox. I hope this post has illustrated how difficult it is to make extensible entities, and that sometimes metaprogramming is not optional. By the way, the full code (I can not vouch for its correctness at this stage) can be found here . Boo is ruthless.
Ah, yes, as for our essence, now it looks like this: That's really it. Comments welcome. ■
[ManaType("Green", "G", "Forest")]
[ManaType("Red", "R", "Mountain")]
[ManaType("Blue", "U", "Island")]
[ManaType("Black", "B", "Swamp")]
[ManaType("White", "W", "Plains")]
[ManaSum]
[HybridMana]
[ManaIndicators]
[ManaString]
[ManaParser]
class Mana:
[Property(Colorless)]
colorless as int
[Property(HasX)]
hasX as bool