Журнал LinuxFormat - перейти на главную

LXF92:Mono

Материал из Linuxformat
(Различия между версиями)
Перейти к: навигация, поиск
(Классификация объектов: оформление)
(Описываем игру: оформление)
Строка 113: Строка 113:
 
'''Clubs''' – трефы, они же крести, '''Spades''' – пики, '''Player''' – игрок, англ.]:
 
'''Clubs''' – трефы, они же крести, '''Spades''' – пики, '''Player''' – игрок, англ.]:
  
<code>
+
<source lang=csharp>
 
  using System;
 
  using System;
 
  namespace Geno {
 
  namespace Geno {
Строка 135: Строка 135:
 
  }
 
  }
 
  }
 
  }
</code>
+
</source>
  
 
Ну да, знаю, здесь куча пустых методов, но они проясняют структуру программы. Заметили, что мне пришлось объявить все переменные
 
Ну да, знаю, здесь куча пустых методов, но они проясняют структуру программы. Заметили, что мне пришлось объявить все переменные

Версия 11:11, 24 ноября 2008


Mono-Мания Программирование на современной платформе для новичков

Содержание

Mono: Объекты и обобщенные типы

Объектно-ориентированное программирование кое-кого пугает больше, чем школьников прививки, но Пол Хадсон собирается обойтись без боли.

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

Это не штуки, из которых можно делать свои приложения, а просто методы, которые пригодятся вам при программировании на Mono. Коль скоро вы это поняли, я покажу вам, как реализовать карточную игру с помощью названных двух методов.

Классификация объектов

ООП позволяет определять предметы в вашем программном коде и даже придавать им желаемое поведение. Вы уже использовали ООП, только не догадывались об этом. Создайте новый проект с именем Geno (еще один редкий персонаж Nintendo – уж простите!), и вы уви- дите, что MonoDevelop напишет код по умолчанию:

 class MainClass
 {
     public static void Main(string[] args)
   {
       Console.WriteLine(“Hello World!);
     }
 }

Здесь применяется ООП, и программа отлично работает, даже если вы не понимаете, что это значит (да и знать не хотите). Но теперь знайте: класс – это определение предмета, а объект – это экземпляр предмета. Ясно как ночь? Так вот: на вопросы «Какого цвета машина?», «Какая длина у машины?» или «Сколько у машины передач?» ответ будет «это зависит от»: что такое машина, представляют все, но каждая машина индивидуальна.

В терминах ООП, «машина» является классом. Но «машину вообще» увидеть нельзя: это абстрактное понятие. На самом деле мы видим «Форды», «Хонды» и так далее, то есть физические реализации класса «машина». Итак: машина, находящаяся на шоссе, это объект класса «машина». У нее есть цвет, длина, и вы знаете, сколько у нее передач, но это просто переменные свойства класса «машина». Другие машины, даже той марки, что и ваша, могут сильно отличаться; но все они машины.

Если для вас это все еще пустой звук, подождите: вы все поймете из кода! А пока MonoDevelop определил для нас класс MainClass, содержащий метод public static void Main(), который мы все время используем. Два странных слова – public и static – относятся к ООП.

Объект Geno

Измените MainClass на Geno, имя нашего проекта. Теперь замените строку Console.WriteLine() на следующую:

Geno game = new Geno();

Этот код создает объект класса Geno и присваивает его переменной game. Что представляет собой класс Geno? В данный момент он содержит только Main() и больше ничего – это просто пустая переменная. Но она рождает интересный вопрос: строка находится внутри метода Main(), который находится внутри класса Geno. Как может Geno создать сам себя? Или – основной вопрос философии: что появилось раньше, класс Geno или метод Main()?

Тут на помощь приходит слово ‘static’ – статический. Метод Main(), если вы помните, помечен как public static void, и на то есть причина: статические методы могут вызываться без экземпляра класса. Фактически они привязывают метод к классу, просто в организационных целях. Например, если в нашем классе был метод СменитьПередачу(), его применение не имело бы смысла без конкретного экземпляра машины, так как в противном случае, какую передачу надо менять? А как насчет вычисления тормозного пути машины, мчащейся со скоростью 100 км/ч? К машинам это имеет отношение, ноясно, что для этого не требуется реальный объект машины.

Таким образом, статический метод Main() может вызываться без существования класса Geno, и мы используем его для создания объекта Geno так, чтобы можно было играть в карточную игру. Объект Geno будет контролировать все аспекты игры, поэтому других объектов нам не понадобится. Но, ради интереса, мы добавим еще два класса: один будет отвечать за игроков, другой за карты. Логика отдается на откуп объекту Geno, так что классы игрока и карт предназначены просто для хранения данных.

Есть еще кое-что, что вам надо знать, прежде чем писать код. Иногда нужно, чтобы переменная принимала значение только из определенного набора. Например, набор данных для дней недели – воскресенье, понедельник, вторник и т.д. Для карточной игры требуется, чтобы каждая карта была определенной масти: червы, бубны, трефы или пики. C# позволяет определить масти карт как перечисление:

public enum Suits { Hearts, Diamonds, Clubs, Spades };

Описываем игру

Запрограммируем детскую игру: она, возможно, знакома вам как «Дама червей». Из карточной колоды извлекается одна дама (бубен), остается 51 карта. Карты сдаются всем игрокам, втемную. Игроки смотрят на свои карты и сбрасывают пары карт одинакового достоинства: например, если у игрока есть две десятки, то он кладет эти две десятки на стол. Дама червей не может быть использована как парная карта; игрок, которому она досталась, должен ее сохранить.

После того, как все игроки выкинули свои пары, первый игрок поворачивается к игроку справа и втемную забирает у него произвольную карту. Если в результате у игрока образовались парные карты, то он может их сбросить. Игра продолжается, и второй игрок поворачивается к игроку справа и вынимает карту – и так далее. В конечном счете, каждая карта должна найти себе пару, за исключением дамы червей, а игрок, у которого она на руках, проигрывает. [порусски эта игра называется «Акулина» или «Акулька», но непарная дама – пиковая, – прим.ред.]

Нам надо предусмотреть следующие функции:

  • Play() Эта функция начинает игру, после проведения необходимых настроек.
  • ShuffleCards() Перетасовать колоду (рандомизировать порядок карт).
  • RemovePlayerPairs() Поиск и удаление подходящей пары карты у игрока.
  • PrintResult() Печать результатов игры (у кого осталась червонная дама).


Также потребуется определить классы Player и Card, которые будут содержать информацию. Вот скелет будущего кода [для большей ясности: Suit – масть, Card – карта, Hearts – черви, Diamonds – бубны, Clubs – трефы, они же крести, Spades – пики, Player – игрок, англ.]:

 using System;
 namespace Geno {
 enum Suits { Hearts, Diamonds, Clubs, Spades };
 class Card {
    public int Val;
    public Suits Suit;
 }
 class Player {
    public int Score;
 }
 class Geno {
    static void Main(string[] args) {
        Geno game = new Geno();
        game.Play();
    }
    void Play() { }
    void ShuffleCards() { }
    void RemovePlayerPairs(int player) { }
    void PrintResults() { }
 }
 }

Ну да, знаю, здесь куча пустых методов, но они проясняют структуру программы. Заметили, что мне пришлось объявить все переменные внутри классов Player и Card как public? Это потому, что переменные внутри объекта доступны только внутри самого объекта, чтобы внешние части кода их не затрагивали. Но так как у нас довольно простая программа, класс Geno будет делать большую часть работы и использовать классы Player и Card просто для хранения значений. То, что эти переменные стали public, значит, что класс Geno может их читать и записывать [в серьезных программах так делать не рекомендуется – вместо этого следует определить методы, обеспечивающие доступ извне к закрытым переменным, – прим.ред.].

Введение в обобщенные типы

У нас есть класс Card, то есть мы можем определить значение карты (от 1 до 13) и ее масть. Но мы еще не создали сам объект «карты» и не знаем, где их хранить. Здесь приходят на помощь массивы: они позволяют хранить множество объектов в одной переменной. У Mono есть огромное количество типов массивов, но долгое время наиболее используемым был ArrayList. Он позволяет хранить в массиве любой тип объекта и обращаться к нему просто по индексу. Но здесь есть проблема: у каждой переменной в C# есть тип, будь то integer, string, Suit или любой другой. Так как ArrayList может содержать переменную любого типа, то вам всегда придется сообщать Mono, какой тип используется. Например:

int i = 1;
ArrayList numbers = new ArrayList();
numbers.Add(i);
int j = numbers[0];

Здесь будет ошибка – Mono не сможет преобразовать numbers[0] в integer, даже если мы знаем, что оно уже типа integer. Вместо этого надо написать:

int j = (int)numbers[0];

Префикс (int) значит «обращаться с numbers[0] как с целым числом», и наш код будет компилироваться правильно. Новые версии C#, включая поставляемую с Fedora Core 6, поддерживает новый способ программирования, известный как обобщенные типы (generics). И если вы когда-либо раньше использовали С++, то знакомы с термином «шаблон», а это почти тоже самое – только на вид гораздо легче!

Обобщенные типы – это произвольные массивы, которые принимают только один тип переменных. Вам уже не надо приписывать (int), чтобы вытаскивать целые числа из обобщенного списка – туда так и так можно помещать только целые числа. В Geno обобщенные типы будут использоваться для двух вещей: хранения карт и хранения игроков. У каждого игрока будет свой собственный список карт, так как карты из колоды раздаются именно игрокам.

Добавьте такие две строчки сразу под строкой class Geno:

List<Card> Cards = new List<Card>();
List<Player> Players = new List<Player>();

Этот синтаксис может затуманить мозги, но по-простому он гласит «Хочу, чтобы один список содержал переменные типа Card, а второй список содержал переменные типа Players». Вы также должны добавить строку до переменной Score в классе Player:

public List<Card> Cards = new List<Card>();

Устанавливаем игру

Оба наших списка Players и Cards пусты, поэтому первым заданием будет поместить 51 карту в колоду (помните, что мы убрали бубновую даму) и разместить несколько игроков. Самым простым способом вста- вить карты будет перебрать в цикле все масти ('Suits), и для каждой масти посчитать от одного до 13 так, чтобы получить все от тузов до королей [туз считается единицей]. Добавив все карты и всех игроков, завершаем установку, вызывая метод ShuffleCards() для перетасовки карт.

foreach (Suits suit in Enum.GetValues(typeof(Suits))) {
    for (int i = 1; i < 14; ++i) {
      Card card = new Card();
      card.Val = i;
      card.Suit = suit;
      if (card.Val == 12 && card.Suit == Suits.Diamonds) continue;
      Cards.Add(card);
    }
}
for (int i = 0; i < 4; ++i) {
    Player player = new Player();
    Players.Add(player);
}
ShuffleCards();

Вы видите, что надо вызвать просто Cards.Add(card), чтобы вставить карту в список из Cards; все очень просто. Метод ShuffleCards() пока ничего не делает, потому что он пустой. Нам необходимо пройтись по всему массиву Cards, вытащить отдельные карты и поместить их обратно в произвольную позицию. Тут не обойтись без генератора случайных чисел – вставьте эту строку перед static void Main:

Random Rand = new Random();

Теперь полная реализация ShuffleCards():

void ShuffleCards() {
  for (int i = 0; i < Cards.Count; ++i) {
      Card tmp = Cards[i];
      Cards.RemoveAt(i);
      Cards.Insert(Rand.Next(0, Cards.Count), tmp);
  }
}

Здесь показано несколько новых возможностей списков: у них есть свойство Count, которое возвращает число содержащихся в них элементов; они содержат метод RemoveAt(), который удаляет элемент в указанной позиции; и у них есть метод Insert(), который вставляет элемент в указанную позицию. Номер позиции определяется переменной Rand, которая может генерировать число между 0 и Crads.Count (количество карт в колоде).

Раздаем карты

Наша колода заполнена и стасована. Осталось сдать ее игрокам. Чтобы это сделать, будем давать им карты, пока колода не кончится. Это зна чит, что нам надо начать с игрока 0 (в C# списки начинаются с 0), сдать карту, перейти к игроку 1, сдать карту и так далее, пока не закончатся игроки; потом перейти снова к игроку 0. В коде этот алгоритм будет выглядеть вот так:

 int playernum = 0;
 while (Cards.Count > 0) {
    Players[playernum].Cards.Add(Cards[0]);
    Cards.RemoveAt(0);
    ++playernum;
    if (playernum == Players.Count) playernum = 0;
 }
 // удалять начальные пары
 for (int i = 0; i < Players.Count; ++i) {
    RemovePlayerPairs(i);
 }

В последней части (от комментариев и ниже) уже начинается логи ка игры: каждый игрок удаляет пары карт, которые оказались у него в начале игры.

Пишем логику

Метод RemovePlayerPairs() принимает на вход номер игрока и удаляет у него парные карты. Чтобы облегчить понимание, я разделил функцию на две, вот так:

void RemovePlayerPairs(int player) {
    while (TryPairRemove(player)) {
       ++Players[player].Score;
    }
}

Итак, номер игрока поступает в функцию и передается методу TryPairRemove(). Если этот метод вернул true, значит, была найдена пара; затем он вызывается снова. В конце концов будут найдены все пары, и цикл завершит свою работу.

Метод TryPairRemove() немного сложноват, так как ему надо у каждого игрока взять карту, перебрать остальные его карты на предмет совпадения, и если в паре ни одна из карт не является дамой червей удалить пару и вернуть true.

 bool TryPairRemove(int player) {
      Card card1;
      Card card2;
      Player thisplayer = Players[player];
      for (int i = 0; i < thisplayer.Cards.Count; ++i) {
          card1 = thisplayer.Cards[i];
          if (card1.Suit == Suits.Hearts && card1.Val == 12) continue;
          for (int j = i + 1; j < thisplayer.Cards.Count; ++j) {
               card2 = thisplayer.Cards[j];
                 if (card2.Suit == Suits.Hearts && card2.Val == 12) continue;
                 if (card1.Val == card2.Val) {
                        thisplayer.Cards.RemoveAt(j);
                        thisplayer.Cards.RemoveAt(i);
                        Console.WriteLine(“Player “ + (player + 1) + “ plays “ + CardName(card1) + “ and “ + CardName(card2));
                        return true;
               }
        }
    }
    return false;
 }

Некоторые комментарии по коду:

  • 1 j начинается с i+1: среди проверенных карт совпадений нет.
  • 2 Проверка совпадения с дамой червей делается и для card1, и для card2.
  • 3 Если найдено совпадение, сначала удаляется j, затем i. Удаление элемента из списка заставляет сдвигаться все элементы массива, чтобы закрыть пробел, поэтому первым надо удалять элемент с более высоким индексом.
  • 4 Метод CardName() будет описан далее.

Да, это солидный кусок кода, ведь мы должны дважды перебрать карты игрока. Метод CardName() очень прост и выдает названия карт, я не буду приводить его здесь – можете обратиться к исходному коду на диске.


Последний рывок

Оставшийся код обрабатывает основной игровой цикл: игроки выбирают карты и пытаются найти пары. Самой сложной частью является выбор игрока, у которого надо вытащить карту: код должен начать искать игрока «справа» от нас (то есть его номер должен быть больше нашего), но если он никого не находит, то начинает смотреть с начала списка. Если он вернулся обратно, значит, игра закончилась. Если найден подходящий игрок, у него выбирается карта, возможно, сбрасываются новые пары, и игра продолжается.

Этот код должен следовать до конца метода Play(). Он длинный, но не такой сложный, если разбить его на части вот так:


bool finished = false;
while (!finished) {
    int playersleft = Players.Count;
    for (int i = 0; i < Players.Count; ++i) {
          Player player = Players[i];
          if (player.Cards.Count == 0) {
               --playersleft;
               continue;
}

Здесь отслеживается число игроков, оставшихся в игре. Если это значение равно 1, то надо выходить; займемся этим позже. Сейчас цикл просто обрабатывает каждого игрока.

int playertouse = -1;
for (int j = i + 1; j < Players.Count; ++j) {
        if (Players[j].Cards.Count > 0) {
             playertouse = j;
            break;
        }
}
if (playertouse == -1) {
      for (int j = 0; j < Players.Count; ++j) {
        if (Players[j].Cards.Count > 0) {
              playertouse = j;
              break;
        }
    }
}

Этот блок ищет игрока, у которого нужно вытянуть карту: сперва над текущей позицией, потом с начала. Игроки, у которых нет карт, автоматически пропускаются.

</code>

if (playertouse == i) {
   finished = true;
     break;
 } else {
      int cardtochoose = Rand.Next(0, Players[playertouse].Cards.
 Count);
      Players[i].Cards.Add(Players[playertouse].
Cards[cardtochoose]);
      Players[playertouse].Cards.RemoveAt(cardtochoose);
     RemovePlayerPairs(i);
   }
 }

</code>

Теперь начинается самое интересное: если мы добрались сами до себя, значит, нет игроков, у которых можно взять карту. В противном случае, берем произвольную карту и вызываем метод RemovePlayerPairs(), для сброса подходящей пары.

 if (playersleft == 1) finished = true;
 }
 PrintResults();

Наконец, если игроков не осталось, завершаем цикл. Игра закончена, и PrintResult() выдает сообщение:

 void PrintResults() {
      Console.WriteLine(“”);
      for (int i = 0; i < Players.Count; ++i) {
      if (Players[i].Cards.Count > 0) {
         Console.WriteLine(“У игрока “ + (i + 1) + “ осталась дама червей!”);
         break;
      }
   }
 }

Если вы запустите программу, то увидите четырех компьютерных игроков, играющих около секунды. Теперь у вас есть понимание о классах и объектах, и вы можете извлечь преимущества обобщенных типов для хранения ваших объектов. Вы также узнали, как сделать несложную карточную игру – полпути к разработке покера или любой другой игры. LXF

Персональные инструменты
купить
подписаться
Яндекс.Метрика