Как стать автором
Обновить

Программирование Magic: the Gathering — §2 Карта

Время на прочтение8 мин
Количество просмотров3.2K
Продолжим наше обсуждение программирования Magic the Gathering. Сегодня мы обсудим то, как формируется объектная модель конкретной карты. Поскольку карты взаимдействуют со всеми участниками системы (с игроками, другими картами, и т.д.), мы также затронем вопросы реализации базового поведения карт. Как и предже, мы будем использовать экосистему .Net, хотя в будущем (намек) мы увидим использование неуправляемого С++. Также, для примеров мы воспользуемся картами 8й и поздних редакций.[1]

Предыдущие посты: §1


Вся экосистема M:tG реализует паттерн Наблюдатель, причем в такой неприятной форме, что говорить о каком-либо «привязывании данных» было бы нелепо. Поэтому, при первичном рассмотрении структуры карты можно попробовать создать голую, анемичную модель.

public class Card
{
  public string Name { get; set; }
  public Mana Cost { get; set; }
  ⋮
  // и так далее
}

К сожалению, меняя карту нам нужно помнить её начальное состояние. Например, Avatar of Hope изначально стоит , но когда у вас 3 жизни или меньше, он стоит не а просто . Поэтому у нас появляется дихотомия – нам нужен и прототип (начальное значение) и реальное значение «в игре». И так для каждого свойства.

В принципе, мы можем разнести этот функционал на два взаимосвязанных класса, которые будут отражать эти состояния:

// прототип карты
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 если в мане фигурирует .[2] Фактически, нет никаких проблем со считыванием стоимости использования карты. Что же касается стоимости использования возможностей, то помимо собственно маны у нас появляются дополнительные свойства, такие как RequiresTap. Мы это обсудим далее в посте.

Тип карты


Карта справа имеет тип, а точнее три. Тип как строка может быть записан как «Legendary Creature – Wizard», но поскольку есть карты которые активно манипулируют типами, мы также создадим коллекцию, которая может хранить список типов – во-первых, для быстрого поиска, во-вторых, для того чтобы иногда добавлять туда дополнительные типы.



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

Как вы уже наверное догадались, у карты есть список способностей, и собственно в игре пользователь может выбрать одну из них.

Итак, у способности есть текстовое описание, стоимость, флажок который показывает нужно ли поворачивать карту, и делегат который определяет что эта способность делает. Для Арканиса, его две «абилки» будут выглядеть так:

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

Способности не создаются магическим путем. Они считываются в текстовом формате, и разбираются с помощью обычных регулярных выражений. Использование маны – это тоже активируемая способность. Для того чтобы ее добавлять в модель, мы используем достаточно простой делегат.



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.

Продолжение следует!

Заметки


  1. Насколько я знаю, а точнее насколько мне подсказывает база данных, 8я редакция не русифицирована. На данный момент все примеры реализации карт представлены на английском языке, но это не значит что их нельзя русифицировать когда правила будут внедрены полностью. Парсер все равно удобнее писать на английском т.к. там слова не склоняются.
  2. На самом деле, тут непокрыт вариант когда стоимость, например . Эту задачу мы решим когда она станет актуальной.
Теги:
Хабы:
Всего голосов 51: ↑37 и ↓14+23
Комментарии65

Публикации

Истории

Работа

.NET разработчик
70 вакансий

Ближайшие события