Глава     8

Учебный пример: Пасьянс


Разделы

Содержание

     Программа для раскладывания карточного пасьянса проиллюстрирует всю мощь наследования и переопределения. В главах═3 и═4 встречались фрагменты этой программы, в частности абстракция игральной карты, представленная классом Card. Языком программирования этого учебного примера будет Java.

     Основное внимание будет уделено классу CardPile, абстрагирующему стопку игральных карт. Так как перекладывание карт из одной стопки в другую═≈ это основное действие пасьянса, то подклассы CardPile будут базовыми структурами данных при реализации пасьянса. Имеется множество стопок карт, и наследование вкупе с переопределением интенсивно используется для упрощения разработки этих компонент и обеспечения их единообразия.

8.1. Класс игральных карт Card

Разделы

     В предыдущих главах мы обсуждали абстрактный класс Card. Повторим некоторые важные моменты.

     Каждый экземпляр класса Card (листинг 8.1) наделен мастью и рангом. Чтобы предотвратить их изменение, поля данных (переменные экземпляра) объявлены закрытыми, и сделать что-либо с ними можно только посредством функций доступа.

     Значения полей масти и ранга устанавливаются конструктором класса. Кроме того, отдельная функция позволяет пользователям определять цвет карты. Значения целочисленных констант (определяемых в языке Java с помощью спецификаторов final static) заданы для черного и красного цветов, а также для мастей. Еще одна пара целочисленных констант определяет высоту и ширину карты.

     Есть важные причины для того, чтобы обращаться к масти и рангу только через функции доступа. Прямой доступ к этим полям следует запретить. Тогда поля масти и ранга могут быть прочитаны, но не модифицированы. (Соответствующая функция, используемая главным образом для изменения значений полей данных, часто называется мутатором (mutator).)

Листинг═8.1.
Описание класса card
class Card
{
  // конструктор
  Card (int sv, int rv)
   {
    s = sv; r = rv; faceup = false;
   }

  // доступ к атрибутам карты
  public int rank () 
   {  return r;  }

  public int suit () 
   {  return s;  }

  public boolean faceUp()
   {  return faceup;  }

  public void flip() 
   {  faceup = ! faceup;  }

  public int color()
   {
    if (suit() == heart || suit == diamond)
      return red;
    return black;
   }

  public void draw (Graphics g, int x, int y)
   {
    ...
   }

  // статические поля данных для цвета и масти
  final static int width 		=═50;
  final static int heigth 		=═70;
  final static int red   			=═0;
  final static int black  		=═1;
  final static int heart  		=═0;
  final static int spade  		=═1;
  final static int diamond 	=═2;
  final static int club  			=═3;

  // поля данных
  private boolean faceup;
  private int r;
  private int s;
}

     Итак, все действия, которые может выполнить карта (кроме установки и возврата состояния), ≈ это переворачивание и показ себя. Функция flip() состоит из одной строчки, которая просто обращает значение, содержащееся в переменной экземпляра faceup, на противоположное. Функция рисования draw() сложнее: она использует графические средства, предоставляемые стандартной библиотекой приложений Java. Библиотека приложений поставляет тип данных, называемый Graphics, который обеспечивает множество методов рисования линий и фигур, а также раскрашивание. В качестве аргумента функции рисования передается значение типа Graphics, а также целочисленные координаты, соответствующие верхнему левому углу карты.

     Черви и бубны нарисованы красным, а пики и крести═≈ черным. Штриховка рубашки выполнена желтым цветом. Фрагмент процедуры рисования игральной карты показан в листинге 8.2.

     Наиболее важная особенность абстракции игральной карты═≈ это стиль, при котором каждая карта ответственна за хранение в себе всей информации и поведения, к ней относящихся. Карта знает и свое значение, и то, как себя нарисовать. Таким образом, информация инкапсулирована и изолирована от приложения, использующего игральные карты. Если, например, программа перенесена на новую платформу, использующую другие графические средства, то изменить нужно будет только метод draw внутри самого класса.

Листинг═8.2.
Процедура рисования игральной карты
class Card
{
  ...
  public void draw (Graphics g, int x, int y)
   {
    String names[] = {"A", "2", "3", "4", "5", "6",
                      "7", "8", "9", "10", "J", "Q", "K"};

    // Очистить прямоугольник, нарисовать границу
    g.clearRect(x, y, width, height);
    g.setColor(Color.black);
    g.drawRect(x, y, width, height);

    // нарисовать тело карты
    if (faceUp) // лицевой стороной вверх
     {
      if (color() == red) g.setColor(Color.red);
      else g.setColor(Color.blue);

      g.drawString(names[rank()], x+3, y+15);
      if (suit() == heart)
       {
        g.drawLine(x+25, y+30, x+35, y+20);
        g.drawLine(x+35, y+20, x+45, y+30);
        g.drawLine(x+45, y+30, x+25, y+60);
        g.drawLine(x+25, y+60, x+5, y+30);
        g.drawLine(x+5, y+30, x+15, y+20);
        g.drawLine(x+15, y+20, x+25, y+30);
       }
      else if (suit() == spade )
       {  ...  }
      else if (suit() == diamond )
       {  ...  }
      else if (suit() == club )
       {
        g.drawOval(x+20, y+25,═10,═10);
        g.drawOval(x+25, y+35,═10,═10);
        g.drawOval(x+15, y+35,═10,═10);
        g.drawOval(x+23, y+45, x+20, y+55);
        g.drawOval(x+20, y+55, x+30, y+55);
        g.drawOval(x+30, y+55, x+27, y+45);
       }
     }
    else // картинкой вниз
     {
        g.setColor(Color.yellow);
        g.drawLine(x+15, y+5, x+15, y+65);
        g.drawLine(x+35, y+5, x+35, y+65);
        g.drawLine(x+5, y+20, x+45, y+20);
        g.drawLine(x+5, y+35, x+45, y+35);
        g.drawLine(x+5, y+50, x+45, y+50);
     }
   }
}

8.2. Связные списки

Разделы

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

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

     В абстракции связного списка задействованы два класса. Класс LinkedList═≈ это ╚фасад╩ списка, то есть класс, с которым взаимодействует пользователь. В действительности значения хранятся в экземплярах класса List. Обычно пользователь даже не догадывается о существовании класса List. Оба класса показаны в лист.═8.3.

     Так как контейнер данных на основе связного списка является абстракцией общего назначения и ничего не знает о типе объекта, который он будет содержать, то тип данных, приписываемый объекту-значению, ≈ это класс всех объектов Object. Переменная, объявленная с типом данных Object (в частности, поле данных value в классе Link), является полиморфной ≈ она может содержать значение любого типа.

     Класс LinkedList обеспечивает: добавление элемента в список, проверку списка на наличие в нем элементов, доступ к первому элементу списка, удаление первого элемента списка.

Листинг═8.3.
Классы Link и LinkedList
class Link
{ 
  public Link (Object newValue, Link next)
   {
    valueField = newValue; nextLink = next;
   }

  public Object value ()
   {  return valueField;  }

  public Link next ()
   {  return nextLink;  }

  private Object valueField;
  private Link nextLink;
}

class LinkedList
{
  public LinkedList ()
   {  firstLink = null;  }

  public void add (Object newValue)
   {  firstLink = new Link(newValue, firstLink);  }

  public boolean empty ()
   {  return firstLink == null;  }

  public Object front ()
   {
    if (firstLink == null)
      return null;
    return firstLink.value();
   }

  public void pop ()
   {
    if (firstLink != null)
      firstLink = firstLink.next();
   }

  public ListIterator iterator()
   {  return new ListIterator (firstLink);  }

  private Link firstLink;
}

     В более общем случае мы хотели бы предоставить пользователю нашей абстракции связного списка способ для перебора величин, содержащихся в списке, без необходимости их удаления и без знания детальной информации о внутренней структуре списка (в данном случае без сведений о классе Link). Как мы увидим в главе═16, такие возможности часто обеспечиваются разработчиками класса ╚список╩ через доступ к специальной разновидности объектов, называемых итераторами. Итератор скрывает детали представления контейнера данных и обеспечивает простой интерфейс для доступа к значениям в порядке очереди. Итератор для связного списка показан в листинг═8.4. С его помощью цикл записывается следующим образом:

ListIterator itr = aList.iterator();
while (! Itr.atEnd() )
{
   ... do something list itr.current() ...
   itr.next();
}

     Обратите внимание на то, как сам список возвращает итератор в результате вызова метода и как использование итератора позволяет избежать упоминания о связных полях списка.

Листинг═8.4.

Листинг═8.4.
Класс ListIterator
class ListIterator
{
  public ListIterator (Link firstLink)
   {
    currentLink = firstLink;
   }

  public boolean atEnd ()
   {
    return currentLink == null;
   }

  public void next ()
   {
    if (currentLink != null)
      currentLink = currentLink.next();
   }

  public Object current ()
   {
    if (currentLink == null)
      return null;
    return currentLink.value();
   }

  private Link currentLink:
}

8.3. Правила пасьянса

Разделы

     Версия пасьянса, которую мы будем описывать, известна под названием ╚Косынка╩ (или Klondike). Бесчисленные вариации этой игры делают ее, возможно, наиболее распространенной версией пасьянса, так что когда вы говорите слово ╚пасьянс╩, многие люди думают о ╚косынке╩. Версия, которую мы будем использовать здесь, описана в книге [Morehead═1949]. В упражнениях мы рассмотрим некоторые распространенные разновидности этого пасьянса.

     Расположение карт показано на рис.═8.1. Используется одна стандартная колода из═52 карт. Расклад пасьянса (tableau) состоит из═28 карт в═7 стопках. Первая стопка состоит из═1 карты, вторая═≈ из═2 и т. д. до═7. Верхняя карта в каждой стопке изначально лежит картинкой вверх; все остальные═≈ картинкой вниз.

     Стопки мастей (иногда называемые основаниями (foundations)) строятся от тузов до королей по мастям. Они создаются сверху расклада по мере того, как нужные карты становятся доступными. Цель игры═≈ сложить все═52 карты в основания по мастям.

     Те карты, которые не выложены в стопки, изначально находятся в колоде (deck). Карты там лежат картинкой вниз, они достаются из колоды по одной и кладутся картинкой вверх в промежуточную стопку (discard pile). Оттуда они перемещаются на расклад или в основания. Карты достаются из колоды, пока она не опустеет. Игра заканчивается, если дальнейшие перемещения карт невозможны.

     Карты кладутся в стопки расклада только на карту следующего по старшинству ранга и противоположного цвета. Карта переносится в основание, если она той же масти и следует по старшинству за верхней картой одного из оснований (или если основание пустое и карта является тузом). Пустые промежутки, возникающие в раскладе во время игры, заполняются только королями.

     Самая верхняя карта промежуточной стопки всегда доступна. Существует только одна возможность переместить более одной карты═≈ положить целый набор открытых карт расклада (называемый последовательностью (build)) в другую стопку расклада. Это можно сделать, если самая нижняя карта последовательности может быть по правилам положена на самую верхнюю карту в стопке назначения. Наша первоначальная игра не будет поддерживать перемещение последовательностей, но мы обсудим это в качестве возможного расширения. Самая верхняя карта расклада всегда лежит картинкой вверх. Если карта удаляется из расклада, оставляя на вершине закрытую карту, то последнюю можно открыть═≈ перевернуть ее картинкой вверх.

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

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

8.4. Стопки карт═≈ наследование в═действии

Разделы

     Значительная часть поведения, которое мы связываем со стопкой карт, является общим для всех типов стопок в игре. Например, каждая стопка содержит связный список карт; операции добавления и удаления элементов из этого связного списка тоже похожи. Другие операции, которым приписано поведение ╚по умолчанию╩ от класса CardPile, иногда переопределяются для разных подклассов. Класс CardPile показан в листинге═8.5.

     Каждая стопка карт содержит координаты своего верхнего левого угла, а также связный список карт в стопке. Все эти значения устанавливаются конструктором класса. Поля данных объявлены как protected и таким образом доступны только методам класса (или его подкласса).

     Три функции top(), pop() и empty(), манипулирующие списком карт, используют интерфейс, предоставляемый классом LinkedList. Новая карта добавляется в список путем вызова addCard(Card). Она модифицируется внутри подклассов. Обратите внимание: метод класса front() связного списка возвращает значение типа Object. Оно должно быть преобразовано к типу данных Card в функциях top() и pop().

Листинг═8.5.
Описание класса CardPile
class CardPile
{
  CardPile (int x1,int y1)
   {
    x = x1; y = y1; cardFile = new LinkedList();
   }

  public Card top()
   {
    return (Card) cardList.front();
   }

  public boolean empty()
   {
    return cardList.empty();
   }

  public Card pop()
   {
    Card result = (Card) cardList.front();
    cardList.pop();
    return result;
   }

  // нижеследующие иногда переопределяются
  public boolean includes (int tx, int ty)
   {
    return x <= tx && tx <= x + Card.width &&
      y <= ty && ty <= y + Card.height;
   }

  public void select (int tx, ty)  { }

  public void display (Graphics G)
   {
    g.setColor(Color.black);
    if (cardList.empty())
      g.drawRect(x, y, Card.width, Card.height);
    else
      top().draw(g, x, y);
   }

  public boolean canTake (Card aCard)
   {  return false;  }

  // координаты стопки карт
  protected int x;
  protected int y;
  protected LinkedList cardList;
}

     Оставшиеся пять операций являются типичными с точки зрения нашей абстракции стопки игральных карт. Однако они различаются в деталях в каждом отдельном случае. Например, функция canTake(Card) запрашивает, можно ли положить карту в данную стопку. Карта может быть добавлена к основанию, только если она следует по старшинству и имеет ту же масть, что и верхняя карта основания (или если карта═≈ туз, а стопка пуста). С другой стороны, карта может быть добавлена в стопку расклада, только если═1) цвет карты противоположен цвету текущей верхней карты в стопке и═2) карта имеет следующее по рангу младшее значение, чем верхняя карта в стопке или═3) стопка пуста, а карта является королем.

     Действия пяти виртуальных функций, определенных в классе CardPile, могут быть охарактеризованы так:

includes
определяет, содержатся ли координаты, переданные в качестве аргументов, внутри границ стопки. Действие по умолчанию просто проверяет самую верхнюю карту стопки. Для стопки DeckPile это действие переопределено как проверка всех карт, содержащихся в стопке.
canTake
сообщает, можно ли положить данную карту в стопку. Только стопка DeckPile и основания SuitPile могут принимать карты, поэтому действие по умолчанию═≈ вернуть ╚нет╩. В двух вышеупомянутых классах стопок карт это действие переопределяется.
addCard
добавляет карту к списку карт (к стопке). Для промежуточной стопки карт DiscardPile это действие переопределяется так, чтобы гарантировать, что карта лежит картинкой вверх.
display
отображает на экране стопку карт. По умолчанию этот метод просто показывает самую верхнюю карту стопки, но для класса стопок расклада TablePile он заменяется на показ колонки карт. При этом отображается верхняя половина каждой скрытой карты. Так что из всех карт такой стопки наиболее далеко отстоящими оказываются самая первая и самая последняя карты. Это позволяет определить границы, занимаемые стопкой карт.
select
выполняет действие в ответ на щелчок мыши. Функция вызывается, когда пользователь выбирает стопку карт щелчком мышью в области стопки. По умолчанию не делается ничего, но для стопок расклада TablePile, колоды DeckPile и промежуточной стопки DiscardPile оно переопределяется на операцию розыгрыша верхней карты, если это возможно.

     Следующая таблица иллюстрирует пользу наследования. Даны пять операторов и пять классов, так что имеется═25 потенциальных методов, которые мы должны были бы определить. Используя наследование, мы должны реализовать только═13 методов. Более того, нам гарантировано, что каждая стопка будет реагировать одинаковым образом на похожие запросы.

    
  CardPile SuitPile DeckPile DiscardPile TablePile
includes *       *
canTake * *     *
addCard *     *  
display *       *
select *   * * *

8.4.1. Основание SuitPile

Стопки карт═≈ наследование в═действии

     Мы детально рассмотрим каждый из подклассов CardPile, заостряя внимание на различных свойствах объектно-ориентированного программирования по мере их проявления. Самый простой подкласс═≈ это основания SuitPile. Он показан в листинге═8.6. Стопка лежит в верхнем углу стола, в ней находятся карты одной масти от туза до короля.

Листинг═8.6.
Класс SuitPile
class SuitPile extends CardPile
{
  SuitPile (int x, int y)
   {
    super(x, y);
   }

  public boolean canTake (Card aCard)
   {
    if (empty())
      return aCard.rank() ==═0;
    Card topCard = top();
    return (aCard.suit() == topCard.suit()) &&
        (aCard.rank() ==═1 + topCard.rank());
   }
}

     Класс SuitPile определяет только два метода. Его конструктор берет два целочисленных аргумента и не делает ничего, кроме вызова конструктора надкласса CardPile. Обратите внимание на ключевое слово super, указывающее родительский класс. Метод canTake определяет, можно или нет поместить карту в стопку. Перемещение карты законно, если стопка пуста и эта карта ≈ туз или если эта карта той же масти, что и верхняя карта в стопке, и ее ранг ≈ следующий по старшинству (например, тройка пик может быть положена только на двойку пик).

     Все остальное поведение стопки SuitPile такое же, как и у общей стопки карт. При выборе мышью основание не выполняет никаких действий. Когда карта добавляется, она просто вставляется в связный список. Для отображения стопки на экране рисуется только верхняя карта.

8.4.2. Колода DeckPile

Стопки карт═≈ наследование в═действии

     Класс DeskPile (листинг═8.7) обслуживает исходную колоду карт. Она отличается от стопки карт общего типа двумя моментами. При конструировании экземпляра вместо пустой стопки класс создает полную колоду из═52 карт, вставляя их в случайном порядке в связный список. Подпрограмма random библиотеки языка Java генерирует случайную величину с двойной точностью в диапазоне от═0 до═1. Она преобразуется в случайное целое число во время процесса тасования колоды.

     Метод select вызывается, когда щелчок мыши производится над колодой DeskPile. Если она пуста, то ничего не происходит. В противном случае верхняя карта удаляется из колоды и добавляется в промежуточную стопку.

     В языке Java нет глобальных переменных. Когда значение используется несколькими объектами классов (такими, как разные стопки карт в нашем пасьянсе), переменная объявляется с ключевым словом static. Как мы увидим в главе═20, при этом создается одна копия статической переменной, которая доступна всем экземплярам. В данной программе статические переменные применяются для хранения различных стопок карт. Они будут содержаться в экземпляре класса Solitare, который мы опишем впоследствии. Для доступа к ним мы используем полностью специфицированное имя, которое кроме имени переменной включает название класса. Это показано в методе select (листинг═8.8), который обращается к переменной Solitare.discardPile для доступа к промежуточной стопке.

Листинг═8.7.
Класс DeckPile
class DeckPile extends CardPile
{
  DeckPile (int x, int y)
   {
    // сначала инициализируется надкласс
    super(x, y);

    // затем создается новая колода
    // сначала она кладется в локальную стопку
    CardPile pileOne = new CardPile(0,═0);
    CardPile pileTwo = new CardPile(0,═0);
    int count =═0;
    for (int i =═0; i <═4; i++)
     {
      pileOne.addCard(new CArd(i, j));
      count++;
     }

    // затем случайно вытаскивается карта
    for (; count >═0; count--)
     {
      int limit = ((int)(Math.random() *═1000))% count;

      // перемещается вниз в случайное место
      for (int i =═0; i < limit; i++)
        pileTwo.addCard(pileOne.pop());

      // потом добавляется карта отсюда
      addCard(pileOne.pop());

      // затем колоды складываются обратно
      while (! pileTwo.empty())
        pileOne.addCard(pileTwo.pop());
     }
   }

  public void select(int tx, int ty)
   {
    if (empty())
      return;

    Solitaire.discardPile.addCard(pop());
   }
}

8.4.3. Промежуточная стопка DiscardPile

Стопки карт═≈ наследование в═действии

     Класс DiscardPile (см. листинг═8.8) интересен тем, что он демонстрирует две совершенно разные формы наследования. Метод select замещает или переопределяет поведение, по умолчанию обеспечиваемое классом CardPile. Новый код при вызове (то есть при нажатии кнопки мыши в области стопки) проверяет, может ли верхняя карта быть перемещена на какое-нибудь основание или на одну из стопок расклада. Если карта не может быть перемещена, она остается в промежуточной стопке.

     Метод addCard демонстрирует другой тип переопределения. Здесь поведение уточняет функциональность надкласса. То есть полностью отрабатывается поведение надкласса и, кроме того, добавляется новое поведение. В данном случае новый код гарантирует, что когда карта лежит в промежуточной стопке, она всегда будет смотреть картинкой вверх. После того как это условие удовлетворено, путем посылки сообщения для псевдопеременной super вызывается код надкласса, который добавляет карту в стопку.

     Другая форма уточнения возникает для конструкторов различных подклассов. До того как конструктор выполнит свои собственные действия, каждый из них должен вызвать конструктор надкласса, дабы гарантировать, что предок инициализировался должным образом. Конструктор предка вызывается через псевдо-переменную super; он вызывается как функция внутри конструктора дочернего класса. В главе═11 мы поговорим подробнее о различии между замещением и уточнением при переопределении методов.

Листинг═8.8.
Класс DiscardPile
class discardPile extends CardPile
{
  DiscardPile (int x, int y)
   {
    super (x, y);
   }

  public void addCard (Card aCard)
   {
    if (! aCard.faceUp())
      aCard.flip();
    super.addCard(aCard);
   }

  public void select (int tx, int ty)
   {
    if (empty())
      return;
    Card topCard = pop();

    for (int i =═0; i <═4; i++)
     {
      if (Solitaire.suitPile[i].canTake(topCard))
       {
        Solitaire.suitPale[i].addCard(topCard);
        return;
       }
     }

    for (int i =═0; i <═7; i++)
     {
      if (Solitaire.tableau[i].canTake(topCard))
       {
        Solitaire.tableau[i].addCard(topCard);
        return;
       }
     }

    // никто не может ее использовать,

    // положим ее назад
    addCard(topCard);
   }
}

8.4.4. Стопка расклада TablePile

Стопки карт═≈ наследование в═действии

     Наиболее сложный из подклассов класса CardPile═≈ это тот, который используется для хранения стопок расклада TablePile. Он показан в листингах 8.9 и 8.10. Стопки расклада отличаются от стопок карт общего назначения следующими моментами:

Листинг═8.9.
Класс TablePile, часть I
class TablePile extends CardPile
{
  TablePile (int x, int y, int c)
   {
    // инициализация надкласса
    super(x, y);

    // затем инициализируется наша стопка карт
    for (int i =═0; i < c; i++)
     {
      addCard(Solitaire.deckPile.pop());
     }

    // верхняя карта открывается
    top.flip();
   }

  public boolean cantake (Card aCard)
   {
    if (empty())
      return aCard.rank() ==═12;

    Card topCard = top();

    return (aCard.color() != topCard.color()) &&
           (aCard.rank() == topCard.rank()═≈═1);
   }

  public boolean includes (int tx, int ty)
   {
    // не проверяет нижнюю границу

    return x < = tx && tx<= x + Card.width &&
           y <= ty;
   }

  private int stackDisplay(Graphics g, ListIterator itr)
   {
    int localy;
    if (itr.atEnd())
      return y;

    Card aCard = (Card) itr.current;
    itr.next;
    localy = stackDisplay(g, itr);
    aCard.draw(g, x, localy);

    return localy +═35;
   }
...

Листинг═8.10.
Класс TablePile, часть II
class TablePile extends CardPile
{
  ...
  public void select (int tx, int ty)
   {
    if (empty())
      return;

    // если карта закрыта, перевернуть
    Card topCard = top();
    if (! topCard.faceUp())
     {
       topCard.flip();
       return;
     }

    // иначе смотрим, можно ли ее положить в основание
    topCard = pop();
    for (int i =═0; i <═4; i++)
     {
      if (Solitaire.suitPile[i].canTake(topCard))
       {
        Solitaire.suitPile[i].addCard(topCard);
        return;
       }
     }

    // нельзя ли положить в другую стопку расклада
    for (int i =═0; i <═7; i++)
     {
      if (Solitaire.tableau[i].canTake(topCard))
       {
        Solitaire.tableau[i].addCard(topCard);
        return;
       }
     }

    // иначе кладем обратно
    addCard(topCard);
   }

  public void display (Graphics g)
   {
    stackDisplay(g, cardList.iterator());
   }
}

8.5. Полиморфная игра

Разделы

     Как мы уже видели в ╚Задаче о восьми ферзях╩ (глава═5), среда для всех приложений на языке Java обеспечивается классом Applet. Для создания нового приложения программист определяет подклассы Applet, переопределяя при этом различные методы. Класс Solitare, который является центральным классом нашего приложения, показан в листинге═8.11.

     Мы ранее уже отмечали, что переменные, которые хранят общие для всех объектов данные, объявляются с ключевым словом static. Такие поля инициализируются в методе init класса1.

     Массивы в языке Java═≈ это нечто, отличное от массивов в большинстве других языков программирования. Java различает для массивов три действия: объявление, распределение и присваивание. Заметьте, что объявление

Листинг═8.11.
Класс Solitaire
public class Solitaire extends Applet
{
  static DeckPile deckPile;
  static DisacrdPile discardPile;
  static TablePile tableau [ ];
  static SuitPile suitPile [ ];
  static CardPile allPiles [ ];

  public void init()
   {
    // сначала отводим место под массивы
    allPiles = new CardPile[13];
    suitPile = new SuitPile[4];
    tableau = new TablePile[7];

    // затем заполняем их данными
    allPiles[0] = deckPile = new DeckPile(335,═5);
    allPiles[1] = discardPile =  new DiscardPile(268,═5);

    for (int i =═0; i <═4; i++)
     {
      allPiles[2+i] = suitPile[i] =
         new SuitPile(15 +═60 * i,═5);
     }

    for (int i =═0; i <═7; i++)
     {
      allPiles[6+i] = tableau[i] =
         new TablePile(5 +═55 * i,═80, i+1);
     }
   }

  public void paint(Graphics g)
   {
    for (int i =═0; i <═13; i++)
     {  allPiles[i].display(g);  }
   }

  public boolean mouseDown(Event evt, int x, int y)
   {
    for (int i =═0; i <═13; i++)
     {
      if (allPiles[i].includes(x, y))
       {
        allPiles[i].select(x, y);
        repaint();

        return true;
       }
     }

    return true;
   }
}

     показывает только то, что объекты являются массивами; про их границы ничего не говорится. Один из первых шагов процедуры инициализации═≈ выделение места под три массива (основания, стопки расклада и массив allPiles, который мы рассмотрим ниже). Команда new отводит память для этих массивов, но не присваивает никаких значений их элементам.

     Следующий шаг═≈ создание колоды DeskPile. Вспомните, что конструктор этого класса генерирует и перетасовывает полную колоду из═52 карт. Промежуточная стопка DiscardPile создается аналогичным образом. Затем в цикле порождаются и инициализируются четыре основания SuitPile, а второй цикл создает и инициализирует стопки расклада TablePile. Вспомните, что при инициализации стопок расклада карты берутся из колоды и вставляются в стопку расклада.

     Массив allPiles используется для представления всех═13 стопок карт. Заметьте, что как только создается очередная стопка, ей тут же присваивается ячейка в этом массиве, равно как и соответствующая статическая переменная. Мы воспользуемся этим массивом для иллюстрации еще одного аспекта наследования. Следуя принципу подстановки, allPiles объявлен как массив из элементов с типом данных CardPile, но на самом деле он содержит стопки карт разнообразного вида.

     Данный массив используется в ситуациях, когда различия между типами стопок карт не важны. Например, в процедуре перерисовки экрана каждую стопку просто просят самостоятельно перерисовать себя. Похожим образом при щелчке мышью опрашивается каждая стопка, не содержит ли она указанную точку экрана. Если да, то стопка выделяется. Среди них есть семь стопок расклада, четыре основания, промежуточная стопка и колода. Более того, фактический код, исполняемый в ответ на вызов методов select и includes, может различаться в зависимости от типа обрабатываемой стопки.

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

8.6. Создание более сложной игры

Разделы

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

     Другие альтернативные правила описаны в упражнениях.

Упражнения

Разделы

  1. Этот пасьянс был преднамеренно создан как можно более простым. Небогатый набор возможностей слегка раздражает, не правда ли? Его легко расширить за счет добавления кода. Возможные новые свойства:
    1. верхняя карта стопки расклада не должна перемещаться в другую стопку расклада, если под ней имеется открытая карта;
    2. целая последовательность не может перемещаться, если самая нижняя карта ≈ король и не осталось закрытых карт.
    Для каждого случая опишите, какие процедуры требуют изменений, и составьте для них исправленный код.
  2. Ниже следуют общеизвестные вариации ╚косынки╩. Опишите для каждой из них, какие части пасьянса должны быть изменены.
    1. Если пользователь щелкнул мышью по пустой стопке колоды, то промежуточная стопка перемещается (возможно, с перемешиванием) назад в колоду. Таким образом пользователь может перебирать колоду неоднократно.
    2. Карты могут быть передвинуты из оснований назад в стопку расклада.
    3. Карты вытягиваются из колоды по три сразу и располагаются в промежуточной стопке в обратном порядке. Как и прежде, в игре участвует только самая верхняя карта в промежуточной стопке. Если в колоде остается меньше трех карт, то все оставшиеся карты перемещаются в промежуточную стопку. (На практике эта разновидность часто сочетается с вариантом а, обеспечивая многоразовый проход колоды.)
    4. То же, что вариант в, но в игре принимает участие любая из трех карт промежуточной стопки. (Это требует небольшого изменения во внешнем виде карточного стола, и больших исправлений в классе промежуточной стопки.)
    5. На пустую стопку расклада может быть положена любая фигурная карта (король, дама, валет), а не только король.
  3. Пасьянс ╚thumb and pouch╩ похож на ╚косынку╩ за исключением того, что карта может быть положена на другую карту следующего по старшинству ранга и любой масти за исключением ее собственной. Так, девятку пик разрешается класть на десятку крестей, но не на десятку пик. Эта разновидность значительно увеличивает шансы на победу. (Согласно Морехеду [Morehead═1949], шансы на победу в ╚Klondike╩ составляют═1 к 30, тогда как в ╚thumb and pouch╩═≈1═к═4.) Опишите, какие фрагменты программы требуют приспособления к новому варианту.

Учебный пример: Пасьянс