📜 ⬆️ ⬇️

Programming Magic: the Gathering - §1 Mana


I want to start posts about programming Magic: the Gathering (M: tG), and we will start with the simplest thing - the concept of "mana". Mana is what all spells are paid for. Despite the fact that it looks like only 5 types of mana, in fact everything is a bit more complicated. Let's try to figure it out.



Firstly, mana is not at all 5 types. Even if you discard the "double" 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.

Consider a few examples.
')

Ordinary colors


Basic mana can be five colors, and mix as you like. This is a direct hint that in any class that deals with mana there should be a model of these five colors. Each element of the model, as is the case with the straightforward application of OOP, pulls a few more extras. For example, a naive approach to implementing mana might look like this:



class Mana
{

public int Blue { get; set; }
public bool IsBlue { get { return Blue > 0; } }
//
}

While we are playing with the idea that the cost and the presence of mana in a pool can be contained in one entity, we can still fantasize a little bit. For example, how to get a representation of mana in a text line (for example, WUBRG for Sliver Legion)? Like this:

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 a double mana (and we know), then subsequent changes would have caused 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 for paying for a certain mana cost from a pool. If you follow the same approach, you probably have to write something like this:

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" );
}
}

The number of repetitions does not "rolls over", but certainly annoying. Let me remind you that in C # there are no macros.

Colorless mana


Colorless mana is the first hint that each type of mana draws upon itself domain-specific logic, which in principle is difficult to predict. For example, the card on the right is a typical example of a kinder surprise when working with an inflexible domain like M: tG. However, even using the same model (in C #), you can get several additional methods. For example, here’s what the “converted value” property looks like:



public int ConvertedManaCost
{
get
{
return Red + Blue + Green + Black + White + Colorless;
}
}

If you want something more serious, then you can calculate whether the mana amount satisfies a certain cost:

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;
}

Colorless mana, in contrast to color, reduces 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 to pay.

Hybrid mana


This is where it all begins ... because up to this point we thought that everything is very simple, and that you can for example take and predictably 2RG line and get an object like Mana . And here again - and the new rules, not only the mechanics, but also the records. After all, how to write a double mana symbol? Most likely this way: {WB}{WB}{WB} . Vooot, and for this you need a parser.

Moreover, imagine that mana has multiplied like slivers — purple, purple, and so on appeared. Is it easy to add support for new mana in those pieces of code that I quoted above? That's right - it's unrealistically difficult . Need a different approach.

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 X icon. For example, pay XG and get X life. In order not to produce entities, I still think that the essence of Mana should be one. The situation with X can be razrulit single ( public bool HasX ), but you can summarize a little, so if suddenly there is a card with a value of XY , we do not have to rewrite all the logic. In addition, there are situations when you can pay for a certain X only with mana of a certain color. This also needs to be taken into account.

Pro metaprogramming


It seems to me that in this task you need metaprogramming, 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 if PostSharp is available). We need something that can take into account our goals, namely:


So, let's see how you can gradually implement all the above properties in a language that supports metaprogramming. Of course, I'm talking about the language of Boo. (Although there is still Nemerle, but I am not strong in it.)

Ordinary colors (attempt number 2)


Nb here and on will go the calculations in two languages ​​at once - on Boo (what we wrote) and on C # (what Reflector saw in this). This is done in order to illustrate the actions of macros and meta-methods, since Boo itself, as you can guess, will not be transparent in this regard.

I would like to write "so, let's start with a simple one," but it will not be easy, alas. To begin with, we will do two projects, namely


Fields


Let's start with the support of forests:



[ManaType( "Green" , "G" , "Forest" )]
class Mana:
public def constructor ():
pass

So, we put on our manaclass attributes of different lands, starting with the forests. What do we need from these attributes? First, they need to add the appropriate fields. It's simple:

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)

So, we have defined a constructor for an attribute that is called from our original entity. In the example above, we add a field to an already existing class. This is done in three steps:


Let's now compare the original and final result:

BooC #
[ManaType( "Green" , "G" , "Forest" )]
class Mana:
public def constructor ():
pass

[Serializable]
public class Mana
{
// Fields
protected int green;
}


Direct and derived properties


Hmm, isn't this what we wanted? :) How about building a simple property over this field? Elementary, Watson - you only need to change the definition of AddField() :

private def AddField(node as Node):
c = node as ClassDefinition
r = ReferenceExpression(colorName)
f = [|
[Property($r)]
$(colorName.ToLower()) as Int32
|]
c.Members.Add(f)

Let's now create a test field, for example, let IsGreen return us true if the map is green and false if not. This property we will meet again, because It interacts specifically with hybrid maps. Here is my first attempt to implement it:

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)

Implementing a derived property was also very easy. And this is how it all looks translated to C #:

[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 auto-generate the total cost (converted mana cost). To do this, you need to realize a colorless mana that, in fact, is not so difficult. But how to autogenerate the sum of all the colors of mana + colorless? To do this, we apply the following approach:

  1. First, we will create a new attribute, and in it - a list of those properties that need to be summarized
    class ManaSumAttribute(AbstractAstAttribute):
    static public LandTypes as List = []


  2. Now, when creating any "property of the earth", we will write the name in this static property:
    public def constructor (colorName as StringLiteralExpression,

    ManaSumAttribute.LandTypes.Add( self .colorName)

  3. And now we use this property to create a 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 support for the mountains (mountain) and see what is being emitted for the ConvertedManaCost property. Here is what we get:

public int ConvertedManaCost
{
get
{
return (( this .Colorless + this .Green) + this .Red);
}
}

As you can see, everything works :)

Hybrid land support


Okay, we started getting something. I add support for all lands to the code and cheers, now Boo autogenerates 10 properties for them, plus it does the sum. What else is needed? Well, how about supporting hybrid land. This is certainly more difficult, because need to take all the pairs of existing lands. But it’s interesting, so why not try it?

The principle is the same as with the amount generator - use a static field and fill it 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 normal, “same type” mana.

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)

I will not give the result, because many properties are obtained :) Let's better discuss what to do with properties like IsGreen in the case of hybrid mana. After all, we can no longer keep them in the attributes of uniform mana, because At that time, nothing was known about the hybrid mana. Let's move them to a separate attribute. So, we need to use both hybrid and single properties in order to understand the color of the map.

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)

Voila! In the code above, we find all the types of mana that this type affects, and compare them to zero. This is not the most optimal way to calculate the IsXxx property, but it works, although at the Reflector level such a mess is obtained.

String representation and parser


For each simple type of mana, we have a string representation. This view allows us to both read a string and receive it. Let's start with a simple one - we get a string representation of the mana, which we will issue via ToString() :

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)

Well, we have almost everything, all that remains is to add the most important thing - the mana description parser, that is so that the program can create the corresponding object from the 2GG{RW} line. Let's divide the manaparser into 3 parts - the analysis of base mana, hybrid mana, and "everything else". So, basic mana is not difficult to disassemble:

// 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);

You need to tinker with hybrid mana, so that the order of writing mana ( RG or GR ) does not affect the parser. However, the solution is not very complicated:

// 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 do the method itself as a set of cases. In addition to color mana, we add support for colorless mana, as well as the X symbol:

// 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)

I will not give the result of this macro, since a lot of code is generated.

Conclusion


Despite the fact that we haven’t disassembled several cases, such as paying a mana for a certain spell, I’ll probably stop - partly because Firefox is already starting to fall on the number of characters in the textbox. I hope that this post has illustrated how difficult it is to make extensible entities, and the fact 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.

Oh yes, as far as our essence is concerned, now it looks like this:



[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

This is really all. Comments welcome. ■

Source: https://habr.com/ru/post/72721/


All Articles