📜 ⬆️ ⬇️

Programming Magic: the Gathering - §2 Map

Let's continue our discussion of programming Magic the Gathering . Today we will discuss how the object model of a particular map is formed. Since the cards interact with all participants in the system (with players, other cards, etc.), we will also touch on the implementation of the basic card behavior. As before, we will use the .Net ecosystem, although in the future (hint) we will see the use of unmanaged C ++. Also, for examples we will use maps of the 8th and later editions. [ 1 ]

Previous posts: §1


The entire M: tG ecosystem implements the Observer pattern, and in such an unpleasant manner that it would be absurd to talk about any “data binding”. Therefore, when first considering the structure of the map, you can try to create a bare, anemic model.
')
public class Card
{
public string Name { get; set; }
public Mana Cost { get; set; }

//
}

Unfortunately, changing the map, we need to remember its initial state. For example, Avatar of Hope is originally worth but when you have 3 lives or less, it’s worth not but just . Therefore, we have a dichotomy - we need both a prototype (the initial value) and the real value “in the game”. And so for each property.

In principle, we can divide this functional into two interrelated classes that will reflect these states:

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

}
}

The CardInPlay class implements one of the variations of the Decorator pattern, in which one class inherits and aggregates another class at the same time.

Having some idea of ​​these two classes, let's consider different properties of maps and how they can be implemented.

Card name and problem Æ


In principle, everything is clear with the name of the card - it is a string, it is saved, drawn on the screen, sometimes, of course, it can change (for example, during cloning), but basically it presents no problems.

But there is a problem with databases that for some reason do not want to write the letter, using capital AE instead. This is not a big problem, we just need to use string.Replace() when reading the card from the database.

Card price


We discussed mana in the last post. The cost is described by standard notation, which is then parsed by the system. There are several cost variations, in particular


The structure of Mana is suitable for all cases, because she can count the number of one or another mana, and also supports the HasX property if the mane appears [ 2 ] In fact, there are no problems reading the cost of using the card. As for the cost of using the features, in addition to the mana itself, we have additional properties, such as RequiresTap . We will discuss this further in the post.

Card type


The map 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 maps that actively manipulate types, we will also create a collection that can store a list of types - firstly, for quick searching, secondly, in order to add additional types there.



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< string > types = new HashSet< string >();
public ICollection< string > Types
{
get
{
return types;
}
set
{
types = value ;
}
}

Above is used HashSet<T> . Card types 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 bool IsLegend
{
get
{
return Types.Where(t => t.Contains( "Legend" )).Any();
}
}

rules


While Arkanis is hanging close to us on the 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.

public sealed class ActivatedAbility
{
public string Description { get; set; }
public Mana Cost { get; set; }
public bool RequiresTap { get; set; }
public Action<Game, CardInPlay> Effect { get; set; }
}

As you may have guessed, the card has a list of abilities, and in the game itself, the user can choose one of them.

So, the ability has a text description, a cost, a checkbox that indicates whether the card should be rotated, and a delegate who determines what this ability does. For Arkanis, his two abilities will look like this:

: Draw three cards. : Return Arcanis Omnipotent to its owner's hand.
  • Description = Draw three cards
  • Cost =
  • RequiresTap = true
  • Effect = (game,card) => card.Owner.DrawCards(3)
  • Description = Return Arcanis Omnipotent to its owner's hand.
  • Cost =
  • RequiresTap = false
  • Effect = (game,card) => game.ReturnCardToOwnersHand(card)

Abilities are not created magically. They are read in text format, and parsed using regular regular expressions. Using mana is also an activated ability. In order to add it to the model, we use a fairly simple delegate.



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

Now, in order to realize, say, a double land like Shivan Oasis , you just need to find the appropriate text in the map rules and add the appropriate abilities.

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 Nullable<int> and everything would be laced. In fact, in the prototype may appear such values ​​as, for example, */* . Of course, in most cases, we just parse the values, but in addition to the fixed values, we have derivatives.

This in turn means that we have Power and Toughness override properties that consider derived values. For example, for a Mortivore map, the structures look like this:



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<Game, CardInPlay, int > GetPower { get; set; }
public Func<Game, CardInPlay, int > GetToughness { get; set; }
}

Now, to create map properties we can use Regex s.

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 what the object model of maps looks like. I deliberately left all the meta-program delights “overboard” because with them the material would be less readable. I can only hint that some of the repetitive aspects of the implementation of the Decorator pattern are too laborious - they need to either be automated or use advanced languages ​​like Boo.

To be continued!

Notes


  1. 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 the implementation of maps are in English, but this does not mean that they cannot be Russified when the rules are fully implemented. The parser is still more convenient to write in English. there words do not lean.
  2. In fact, there is an uncovered option when the cost, for example . We will solve this problem when it becomes relevant.

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


All Articles