
Предыдущие посты: §1
Вся экосистема M:tG реализует паттерн Наблюдатель, причем в такой неприятной форме, что говорить о каком-либо «привязывании данных» было бы нелепо. Поэтому, при первичном рассмотрении структуры карты можно попробовать создать голую, анемичную модель.
public class Card
{
public string Name { get; set; }
public Mana Cost { get; set; }
⋮
// и так далее
}
К сожалению, меняя карту нам нужно помнить её начальное состояние. Например, Avatar of Hope изначально стоит








В принципе, мы можем разнести этот функционал на два взаимосвязанных класса, которые будут отражать эти состояния:
// прототип карты
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
реализует одну из вариаций паттерна Декоратор, в которой один класс одновременно наследует и аггрегирует другой класс.Имея некоторое представление об этих двух классах, давайте рассмотрим разные свойства карт и то, как их можно реализовать.
Название карты и проблема Æ
В принципе, с названием карты все понятно – это строка, она сохраняется, рисуется на экране, иногда конечно она может изменяться (например при клонировании), но в основном она не представляет никаких проблем.
Зато есть проблема с базами данных, которые почему-то не хотят писать букву Æ, используя вместо этого заглавные AE. Это не большая проблема, просто нам нужно использовать
string.Replace()
при считывании карты из базы.Стоимость карты
Мы обсудили ману в прошлом посте. Стоимость описывается стандартной нотацией, которая потом парсится системой. Существует несколько вариаций стоимости, в частности
- Нулевая стоимость (
)
- Обычная стоимость (
)
- Стоимость как функция чего-то (
)
Структура
Mana
подходит для всех случаев, т.к. она умеет считать количество той или иной маны, а также поддерживает свойство HasX
если в мане фигурирует 
RequiresTap
. Мы это обсудим далее в посте.Тип карты

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;
}
}
Выше использован
HashSet<T>
т.к. типы карт не могу повторяться. Имея такой набор мы можем, например, создать свойство, которое проверяет, является ли карта легендарной или нет.public bool IsLegend
{
get
{
return Types.Where(t => t.Contains("Legend")).Any();
}
}
Правила
Пока у нас недалеко на экране висит Арканис, давайте возьмем его как пример. У Арканиса есть две активируемых способности («абилки»). Используя все блага ООП, мы опять можем создать анемичную модель.
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; }
}
Как вы уже наверное догадались, у карты есть список способностей, и собственно в игре пользователь может выбрать одну из них.
Итак, у способности есть текстовое описание, стоимость, флажок который показывает нужно ли поворачивать карту, и делегат который определяет что эта способность делает. Для Арканиса, его две «абилки» будут выглядеть так:
![]() |
![]() ![]() ![]() |
|
|

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."
});
Теперь для того чтобы реализовать, скажем, двойную землю вроде Shivan Oasis, нужно просто найти подходящий текст в правилах карты и добавить соответствующие способности.
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);
}
Cила и здоровье
Было бы просто, если бы карты имели только численные значения для силы и здоровья карты. Тогда, их можно было бы сделать
Nullable<int>
и все было бы ажурно. На самом же деле, в прототипе могут фигурировать такие значения как, например, */*
. Конечно, в большинстве случаев, мы просто парсим значения, но помимо фиксированных значений, у нас есть значения производные.
Power
и Toughness
которые считают производные значения. Например для карты Mortivore, структуры выглядят так: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; }
}
Теперь, для создания свойств карты мы можем использовать
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;
}
}
Заключение
В этом посте я кратко описал то, как выглядит объектная модель карт. Я специально оставил все метапрограммные изыски «за бортом» потому что с ними материал был бы менее читабельным. Мне остается лишь намекнуть на то, что некоторые повторяющиеся аспекты реализации паттерна Декоратор слишком трудоемки – их нужно либо автоматизировать, либо использовать продвинутые языки вроде Boo.
Продолжение следует!
Заметки
- ↑ Насколько я знаю, а точнее насколько мне подсказывает база данных, 8я редакция не русифицирована. На данный момент все примеры реализации карт представлены на английском языке, но это не значит что их нельзя русифицировать когда правила будут внедрены полностью. Парсер все равно удобнее писать на английском т.к. там слова не склоняются.
- ↑ На самом деле, тут непокрыт вариант когда стоимость, например
. Эту задачу мы решим когда она станет актуальной.