Top.Mail.Ru

С# и концепция ООП. Продолжение-2

На прошлом уроке мы разобрали основные архитектурные идеи игры, а также разработали несколько классов. Сегодня начнем писать интерфейсную часть. Итак, идем в проект «MafiaGame» и находим там форму:

Для начала назовем форму нормально, а не Form1. Для этого щелкаем по ней правой кнопкой мыши и во всплывшем меню выберем «Переименовать»:

Для чего мы переименовываем? Дело в том, что форма – это класс, на него, возможно, мы будем ссылаться в коде. А елси в коде будет имея “Form1” то это будет быдлокод. Поэтому, кодим правильно, называем идентификаторы нормально. Например, MainForm. Визуал студия у нас еще спросит, переименовать ли все ссылки, естественно, отвечаем «Да»:

Чтобы отредактировать форму, просто щелкнем на нее двойным кликом, откроется редактор форм:

 

Заголовок тоже неплохо бы изменить. Это можно сделать в обозревателе свойств, он находиться под обозревателем решений. За заголовок отвечает свойство «Text»:

Изменим его и у нас измениться заголовок:

Накидаем интерфейс. Для элементов будет с панели инструментов захватывать компоненты и перетаскивать на форму:

Если у вас нет панели элементов, то ее можно включить в меню «Вид»:

Итак, какие элементы нам понадобятся на форме? Во-первых, метка, чтобы отображать, кто вы (мафия, город, или кто-то иной). Также нужна будет вторая метка, чтобы отобразить статус игры (город спит, мафия выбирает жертву и так далее). Понадобиться кнопка, по которой вы будете переходить к следующему шагу игры. И рамка с радиокнопкам (GroupBox):

На GroupBox можно добавить не только радиокнопки, но и другие элементы. Мы, правда, пока ничего добавлять не будем, у нас это будет делаться программно, так как список действий может зависеть от того, кто вы, мафия, город или кто-то другой.

Итак, элементы мы добавили. Теперь, как это мы делали с формой, им надо присвоить нормальные имена. Это тоже можно сделать в обозревателе свойств. Выберем интересующий нас элемент и изменим его свойство Name:

Первая метка отображает информацию о том, кто вы (мафия, город, или кто-то еще). Поэтому обзовем ее lbWhoIsGamer:

lb – это префикс, который обозначает «label» — метка.

Дальше. Вторая метка. Ее мы назовем lbGameStatus. Кнопку назовем btnStep – ход. Группу – gpActions. Теперь выровняем элементы, и их свойствам Text присвоим нормальные значения:

Прежде чем мы пойдем дальше, нам надо решить важный вопрос: какой класс будет выполнять функционал игрока? Сам класс персонажа, или введем некий абстрактный класс Gamer, от которого будет наследоваться класс «Игрок – человек», осуществляющий связь с пользователем, и класс «Игрок компьютер», который будет рассчитывать ход для персонажа. С одной стороны, принцип SOLID требует, чтобы класс выполнял только одну обязанность (принцип единственной обязанности). В первой части этого цикла уроков я писал, что у нас будет класс персонажа и класс блока управления персонажами. С другой стороны, класс «Игрок – компьютер» должен уметь управлять любым персонажем? Или для каждого персонажа создать свой класс? Дублировать сущности тоже не хорошо – принцип Оккама действует и в программировании. Но если внимательно подумать, то у нас может быть несколько подходов к управлению персонажем. Вдруг мы захотим написать ИИ для игры в «Мафию». Или эмулятор игрока, который придерживается определенной стратегии игры. Или устроить соревнование между программистами, которые пишут ИИ для этой игры.

Но стоп. На прошлом уроке мы уже решили не заморачиваться, а навесить на класс персонажа исполнение хода, реализовав у класса MafiaUnit метод step, в котором он убивает случайного персонажа. Это была ошибка, но можно еще исправить, не так трудно вырезать этот кусок кода и перенести в другой класс. Но давайте сначала подумаем. Во-первых что тогда у нас будет делать метод step класса персонажа? Во-вторых, а он вообще нужен?

Идея состоит в следующем: из юнита вызываем метод связанного с ним геймера. Кто может быть геймером? Это может быть игрок-человек или искусственный интеллект. А моет и не быть связанного геймера. Тогда это будет у нас зомби. На него мы навесим примитивное тупое поведение, например, делать решение по рандому. Это самое зомби-поведение будет реализовывать метод default_step юнита, метод будет абстрактный.

Итак, погнали, реализуем default_step:

/// <summary>
/// Ход игрока по умолчанию (зомби-поведение)
/// </summary>
public abstract void default_step();

А сам метод step будет уже не абстрактный, и он будет вызывать при необходимости default_step. Но сначала нам надо создать абстрактный класс геймера:

    /// <summary>
    /// Абстрактный геймер
    /// </summary>
    public abstract class AbstractGamer
    {
        /// <summary>
        /// Ход игрока
        /// </summary>
        /// <param name="unit">Связанный с игроком персонаж</param>
        public abstract void step(AbstractUnit unit);
    }

Объявляем это поле у класса AbstractUnit:

/// <summary>
/// Геймер 
/// </summary>
public AbstractGamer gamer;

И тут возникает мысль: «А что если мы забудем проинициализировать поле gamer? Прога у нас это пропустит (мы же будем проверять на null), ничего не произойдет не будет выдано даже исключение. Таким образом, ошибка окажется незамеченной. Тогда логично передавать геймера в конструкторе, и пусть он будет всегда заполнен. Тогда нам не нужен никакой default_step, пусть все делает Gamer. Решено, убираем из AbstractUnit default_step, а в самом step просто вызываем step самого геймера:

        /// <summary>
        /// Ход игрока
        /// </summary>
        public virtual void step()
        {
            gamer.step(this);
        }

Слово this обозначает ссылку на самого себя (на тот объект юнита, из метод step которого выполняется в данный момент)

Теперь изменим конструктор класса AbstractUnit:

        /// <summary>
        /// Конструктор по фильтру следующего шага и ссылке на ядро
        /// </summary>
        /// <param name="a_core">Ссылка на ядро</param>
        /// <param name="a_next_unit_filter">Фильтр следующего шага</param>
        /// <param name="a_gamer">Геймер, связанный с персонажем</param>
        public AbstractUnit(Core a_core, IUnitFilter a_next_unit_filter, AbstractGamer a_gamer)
        {
            core = a_core;
            next_unit_filter = a_next_unit_filter;
            gamer = a_gamer;
        } 

Если мы теперь попробуем скомпилировать, то нас выдаст ошибку:

Исправляем ее:

        /// <summary>
        /// Конструктор по фильтру
        /// </summary>
        /// <param name="a_core">Ссылка на ядро</param>
        /// <param name="a_next_unit_filter">Фильтр</param>
        /// <param name="a_gamer">Геймер, связанный с персонажем</param>
        public MafiaUnit(Core a_core, IUnitFilter a_next_unit_filter, AbstractGamer a_gamer) : base(a_core, a_next_unit_filter, a_gamer)
        {
        }

Теперь нам надо создать игрока, который будет играть на стороне компьютера. Мы знаем, как он должен играть за мафию. А за других игроков? Будем в одном объекте реализовывать несколько алгоритмов в зависимости от того, за кого игрок играет? По идее, если это будет ИИ, то он должен получить набор правил и по ним сам определить, как играть. Но мы не ставим цель написать такой супер ИИ, по крайней мере, на данном этапе. Хотя, при проектировании архитектуры такую возможность тоже надо иметь ввиду. Итак, ключевой момент: пока у нас класс Gamer не будет автоматический определять, как какими юнитами управлять на основе правил игры. Это вопрос довольно отдаленной перспективы. А писать отдельный алгоритм и выбирать их через “switch … case” как-то не айс.

Итак, давайте думать. А кто сказал, что мы не можем для каждого юнита создать своего геймера? Казалось бы, это означает дублирование функционала… Вовсе нет. За управление юнитом по идее должен отвечать геймер, а не сам юнит. Итак, создаем геймера для управления мафией. Но прежде обратим внимание вот на такой нюанс: у нас в будущем может быть очень много разнообразных классов, хорошо бы сразу сгруппировать. Поэтому, давайте сначала создадим папку Gamers, для чего щелкнем

Папка у нас появится в дереве проекта:

И в ней мы создадим MafiaGamer:

    /// <summary>
    /// Игрок за мафию
    /// </summary>
    public class MafiaGamer : AbstractGamer
    {
        /// <summary>
        /// Шаг игры
        /// </summary>
        /// <param name="unit">Юнит</param>
        public override void step(AbstractUnit unit)
        {
            if (unit.GetType() != typeof(MafiaUnit)) throw new Exception("Этот объект Gamer может управлять только мафией");
            List<AbstractUnit> units = unit.core.get_alive_units();
            List<AbstractUnit> victims = units.FindAll(
                delegate (AbstractUnit item)
                {
                    return !(item is MafiaUnit);
                });
            int victim_index = Core.rnd.Next(victims.Count);
            unit.core.kill(victims[victim_index]);
        }
    }

Соответственно, метод step из класса MafiaUnit надо удалить, теперь он наследует его из абстрактного юнита.

Что дальше? Для начала, давайте создадим и остальных персонажей. Для них точно также сделаем папку Units:

Добавим туда заготовку комиссара и мирного жителя:

Таким образом, у нас есть три персонажа: мафия, комиссар и мирный житель. Пока на этом остановимся. Остальных добавим в ходе программирования игры.

Теперь нам надо заполнить список юнитов в классе Core. Но как это сделать, если каждому созданному экземпляру юнитам нам надо присвоить фильтр следующего хода? Надо реализовать эти фильтры. Создаем снова папку:

Начинаем реализовывать, для начала MafiaFilter:

    /// <summary>
    /// Фильтр хода после хода мафии
    /// </summary>
    public class MafiaFilter : IUnitFilter
    {
        /// <summary>
        /// список юнитов
        /// </summary>
        private new List<AbstractUnit> _units;

        /// <summary>
        /// Конструктор
        /// </summary>
        /// <param name="a_units">Список юнитов</param>
        public MafiaFilter(List<AbstractUnit> a_units)
        {
            _units = a_units;
        }

        /// <summary>
        /// Получить список персонажей, кому передается ход
        /// </summary>
        /// <param name="unit">Персонаж, для которого получаем список следующего хода</param>
        /// <returns>Список персонажей</returns>
        public List<AbstractUnit> get_next_unix(AbstractUnit unit)
        {
            return _units.FindAll(
                delegate (AbstractUnit item)
                {
                    return item is SheriffUnit;
                });
        }
    }

Реализуем SheriffFilter:

    /// <summary>
    /// Фильтр хода после хода комиссара
    /// </summary>
    public class SheriffFilter : IUnitFilter
    {
        /// <summary>
        /// список юнитов
        /// </summary>
        private new List<AbstractUnit> _units;

        /// <summary>
        /// Конструктор
        /// </summary>
        /// <param name="a_units">Список юнитов</param>
        public SheriffFilter(List<AbstractUnit> a_units)
        {
            _units = a_units;
        }

        /// <summary>
        /// Получить список персонажей, кому передается ход
        /// </summary>
        /// <param name="unit">Персонаж, для которого получаем список следующего хода</param>
        /// <returns>Список персонажей</returns>
        public List<AbstractUnit> get_next_unix(AbstractUnit unit)
        {
            return _units.FindAll(
                delegate (AbstractUnit item)
                {
                    return item is CitizenUnit;
                });
        }
    }

 

Но стоп. У нас, по сути, практически идентичный код. Это не правильно! Давайте сразу сделаем нормально. Для начала, создадим абстрактный класс AbstractFilter:

    /// <summary>
    /// Абстрактный класс фильтра
    /// </summary>
    public abstract class AbstractFilter
    {
        /// <summary>
        /// список юнитов
        /// </summary>
        protected new List<AbstractUnit> _units;

        /// <summary>
        /// Конструктор 
        /// </summary>
        /// <param name="a_units">Список юнитов</param>
        public AbstractFilter(List<AbstractUnit> a_units)
        {
            _units = a_units;
        }
    }

Теперь перепишем наши классы:

    /// <summary>
    /// Фильтр хода после хода мафии
    /// </summary>
    public class MafiaFilter : AbstractFilter, IUnitFilter
    {
        /// <summary>
        /// Конструктор  
        /// </summary>
        /// <param name="a_units">Список юнитов</param>
        public MafiaFilter(List<AbstractUnit> a_units) :  base(a_units) {}

        /// <summary>
        /// Получить список персонажей, кому передается ход
        /// </summary>
        /// <param name="unit">Персонаж, для которого получаем список следующего хода</param>
        /// <returns>Список персонажей</returns>
        public List<AbstractUnit> get_next_unix(AbstractUnit unit)
        {
            return _units.FindAll(
                delegate (AbstractUnit item)
                {
                    return item is SheriffUnit;
                });
        }
    }

    /// <summary>
    /// Фильтр хода после хода комиссара
    /// </summary>
    public class SheriffFilter : AbstractFilter, IUnitFilter
    {
        /// <summary>
        /// Конструктор  
        /// </summary>
        /// <param name="a_units">Список юнитов</param>
        public SheriffFilter(List<AbstractUnit> a_units) : base(a_units) { }

        /// <summary>
        /// Получить список персонажей, кому передается ход
        /// </summary>
        /// <param name="unit">Персонаж, для которого получаем список следующего хода</param>
        /// <returns>Список персонажей</returns>
        public List<AbstractUnit> get_next_unix(AbstractUnit unit)
        {
            return _units.FindAll(
                delegate (AbstractUnit item)
                {
                    return item is CitizenUnit;
                });
        }
    }

Ну, и наконец фильтр после хода мирного жителя:

    /// <summary>
    /// Фильтр хода после хода мирного жителя
    /// </summary>
    public class CitizenFilter : AbstractFilter, IUnitFilter
    {
        /// <summary>
        /// Конструктор  
        /// </summary>
        /// <param name="a_units">Список юнитов</param>
        public CitizenFilter(List<AbstractUnit> a_units) : base(a_units) { }

        /// <summary>
        /// Получить список персонажей, кому передается ход
        /// </summary>
        /// <param name="unit">Персонаж, для которого получаем список следующего хода</param>
        /// <returns>Список персонажей</returns>
        public List<AbstractUnit> get_next_unix(AbstractUnit unit)
        {
            return _units.FindAll(
                delegate (AbstractUnit item)
                {
                    return item is MafiaUnit;
                });
        }
    }

Теперь, казалось бы, можно заполнить список юнитов. Но нет. Еще нужно создать геймеров. Этим мы займемся на следующем уроке.

 

Comments

So empty here ... leave a comment!

Добавить комментарий

Sidebar