netlib.narod.ru< Назад | Оглавление | Далее >

Пятнашки

Пришла пора написать игру. Ну, что-то вроде головоломки. Она была изобретена в 1870-х, вероятно, знаменитым американским создателем головоломок Сэмом Ллойдом (1841 – 1911). В течение некоторого времени она была весьма популярна, особенно в Европе, и была известна под разными именами, например, «пятнашки» в России и Jeu de Taquin во Франции.

В своем классическом виде игра состоит из пятнадцати квадратных блоков, пронумерованных от 1 до 15. Квадраты находятся в рамке 4 × 4, и одно место остается пустым. Квадраты можно передвигать внутри рамки в горизонтальном или вертикальном направлении на пустую позицию, изменяя таким образом позицию и самого пустого места.

Сэм Ллойд располагал все квадраты в правильном порядке, за исключением двух последних (с номерами 14 и 15), которые были переставлены местами. Он предлагал 1 000 долларов тому, кто сумеет упорядочить квадраты. Награды никто так и не получил, поскольку в такой позиции головоломка не имеет решения 1.

Эта головоломка была воплощена в одной из первых компьютерных игр, написанных для Apple Macintosh, которая называлась PUZZLE. Также она входила в состав ранних версий SDK Microsoft Windows под именем MUZZLE и представляла собой единственный пример программы в SDK, написанной не на С, а на Microsoft Pascal. Обе эти программы отображали 15 квадратов в упорядоченном виде и меню для их перемешивания. Вам следовало восстановить исходное состояние или расположить квадраты в другом порядке, например, не построчно, а колонками. Поскольку мы еще не работали с меню, моя программа будет перемешивать квадраты при запуске. (Тут нам понадобится таймер.)

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

JeuDeTaquinTile.cs

  //------------------------------------------------
  // JeuDeTaquinTile.cs (C) 2001 by Charles Petzold
  //------------------------------------------------
  using System;
  using System.Drawing;
  using System.Windows.Forms;

  class JeuDeTaquinTile: UserControl
  {
      int iNum;

      public JeuDeTaquinTile(int iNum)
      {
          this.iNum = iNum;
          Enabled = false;
      }
      protected override void OnPaint(PaintEventArgs pea)
      {
          Graphics grfx = pea.Graphics;

          grfx.Clear(SystemColors.Control);

          int cx = Size.Width;
          int cy = Size.Height;
          int wx = SystemInformation.FrameBorderSize.Width;
          int wy = SystemInformation.FrameBorderSize.Height;

          grfx.FillPolygon(SystemBrushes.ControlLightLight, 
               new Point[] {new Point( 0, cy), new Point(0, 0), 
                            new Point(cx,  0), new Point(cx - wx, wy), 
                            new Point(wx, wy), new Point(wx, cy - wy)});

          grfx.FillPolygon(SystemBrushes.ControlDark,
               new Point[] { new Point(cx, 0), new Point(cx, cy), 
                             new Point(0, cy), new Point(wx, cy - wy), 
                             new Point(cx - wx, cy - wy), 
                             new Point(cx - wx, wy)});

          Font font = new Font("Arial", 24);
          StringFormat strfmt = new StringFormat();
          strfmt.Alignment = strfmt.LineAlignment = StringAlignment.Center;

          grfx.DrawString(iNum.ToString(), font, SystemBrushes.ControlText,
                          ClientRectangle, strfmt);
      }
  }

Программа, создающая фишки и передвигающая их, немного сложнее. Она создает элементы управления для фишек (и соответствующим образом устанавливает размеры клиентской области) переопределяя метод OnLoad класса Form. Метод OnLoad вызывается вскоре после первого отображения формы на экране; исходя из собственного опыта, отмечу, что создавать объект Graphics и устанавливать размер клиентской области лучше именно в методе OnLoad, а не в конструкторе. Обработка OnLoad завершается вызовом защищенного метода Randomize — он перемешивает фишки при помощи таймера.

JeuDeTaquin.cs

  //--------------------------------------------
  // JeuDeTaquin.cs (C) 2001 by Charles Petzold
  //--------------------------------------------
  using System;
  using System.Drawing;
  using System.Windows.Forms;

  class JeuDeTaquin: Form
  {
      const int          nRows = 4;
      const int          nCols = 4;
      Size               sizeTile;
      JeuDeTaquinTile[,] atile = new JeuDeTaquinTile[nRows, nCols];
      Random             rand;
      Point              ptBlank;
      int                iTimerCountdown;

      public static void Main()
      {
          Application.Run(new JeuDeTaquin());
      }
      public JeuDeTaquin()
      {
          Text = "Jeu de Taquin";
          FormBorderStyle = FormBorderStyle.Fixed3D;
      }
      protected override void OnLoad(EventArgs ea)
      {
          // Вычисление размеров фишек и формы

          Graphics grfx = CreateGraphics();

          sizeTile   = new Size((int)(2 * grfx.DpiX / 3),
                                (int)(2 * grfx.DpiY / 3));
          ClientSize = new Size(nCols * sizeTile.Width,
                                nRows * sizeTile.Height);
          grfx.Dispose();

          // Создание фишек

          for (int iRow = 0; iRow < nRows; iRow++)
          for (int iCol = 0; iCol < nCols; iCol++)
          {
              int iNum = iRow  * nCols + iCol + 1;

              if (iNum == nRows * nCols)
                  continue;

              JeuDeTaquinTile tile = new JeuDeTaquinTile(iNum);
              tile.Parent = this;
              tile.Location = new Point(iCol * sizeTile.Width,
                                        iRow * sizeTile.Height);
              tile.Size = sizeTile;

              atile[iRow, iCol] = tile;
          }
          ptBlank = new Point(nCols - 1, nRows - 1);

          Randomize();
      }
      protected void Randomize()
      {
          rand = new Random();
          iTimerCountdown = 64 * nRows * nCols;

          Timer timer    = new Timer();
          timer.Tick    += new EventHandler(TimerOnTick);
          timer.Interval = 1;
          timer.Enabled  = true;
      }
      void TimerOnTick(object obj, EventArgs ea)
      {
          int x = ptBlank.X;
          int y = ptBlank.Y;

          switch(rand.Next(4))
          {
          case 0:  x++;  break;
          case 1:  x--;  break;
          case 2:  y++;  break;
          case 3:  y--;  break;
          }
          if (x >= 0 && x < nCols && y >= 0 && y < nRows)
              MoveTile(x, y);

          if (--iTimerCountdown == 0)
          {
              ((Timer)obj).Stop();
              ((Timer)obj).Tick -= new EventHandler(TimerOnTick);
          }
      }
      protected override void OnKeyDown(KeyEventArgs kea)
      {
          if (kea.KeyCode == Keys.Left && ptBlank.X < nCols - 1)
              MoveTile(ptBlank.X + 1, ptBlank.Y);

          else if (kea.KeyCode == Keys.Right && ptBlank.X > 0)
              MoveTile(ptBlank.X - 1, ptBlank.Y);

          else if (kea.KeyCode == Keys.Up && ptBlank.Y < nRows - 1)
              MoveTile(ptBlank.X, ptBlank.Y + 1);

          else if (kea.KeyCode == Keys.Down && ptBlank.Y > 0)
              MoveTile(ptBlank.X, ptBlank.Y - 1);

          kea.Handled = true;
      }
      protected override void OnMouseDown(MouseEventArgs mea)
      {
          int x = mea.X / sizeTile.Width;
          int y = mea.Y / sizeTile.Height;

          if (x == ptBlank.X)
          {
              if (y < ptBlank.Y)
                  for (int y2 = ptBlank.Y - 1; y2 >= y; y2--)
                      MoveTile(x, y2);

              else if (y > ptBlank.Y)
                  for (int y2 = ptBlank.Y + 1; y2 <= y; y2++)
                      MoveTile(x, y2);
          }
          else if (y == ptBlank.Y)
          {
              if (x < ptBlank.X)
                  for (int x2 = ptBlank.X - 1; x2 >= x; x2--)
                      MoveTile(x2, y);

              else if (x > ptBlank.X)
                  for (int x2 = ptBlank.X + 1; x2 <= x; x2++)
                      MoveTile(x2, y);
          }
      }
      void MoveTile(int x, int y)
      {
          atile[y, x].Location = new Point(ptBlank.X * sizeTile.Width,
                                           ptBlank.Y * sizeTile.Height);

          atile[ptBlank.Y, ptBlank.X] = atile[y, x];
          atile[y, x] = null;
          ptBlank = new Point(x, y);
      }
  }

Программа просто обрабатывает ввод с клавиатуры и мыши, приводя к вызову метода MoveTile, представленного в конце листинга.

Двухмерный массив atile содержит объекты-фишки. Например, объект хранящийся в элементе atile[3,l], представляет собой фишку из четвертого ряда второй колонки. Один из элементов массива atile всегда хранит значение null. Этот элемент соответствует пустой клетке на доске. Поле ptBlank также хранит координату пустой клетки. Пустышка — как я ее называю — управляет интерфейсом пользователя; так что ptBlank играет главную роль в коде пользовательского интерфейса. Любая перемещаемая фишка должна соседствовать с пустой клеткой и перемещаться на ее место.

Однако при использовании мыши не обязательно щелкать по фишке, расположенной по соседству с пустой клеткой. Если вы щелкните по фишке из ряда или колонки, в которой есть пустышка, программа разом переместит несколько фишек, произведя несколько вызовов метода MoveTile. Этот метод физически перемещает фишку (устанавливая ее свойство Location согласно положению пустышки) и соответствующим образом изменяет массив atile и поле ptBlank.

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

Вот как выглядела эта программа, когда я наполовину собрал головоломку:


Рис. 10.7.

Создание собственного элемента управления оказалось полезным уже для третьей программы в книге. Как вам, несомненно, известно, Windows и Windows Forms .NET Framework содержат массу готовых элементов управления: кнопки, надписи, текстовые поля, списки, полосы прокрутки и многие другие. Мы подробно исследуем их в главе 12.



1Впервые математический анализ был применен к пятнашкам в 1879 г. в одной из статей American Journal of Mathematics. Лежащие в основе этой игры математические принципы были обобщены в книге Джеймса Ньюмена «Мир математики» (James R. Newman. The World of Mathematics, New York: Simon and Schuster, 1956, 4: 2429-2432). Этот четырехтомник был переиздан в 1988 г. издательством Tempus Books и в 2000 г. — издательством Dover Books. О пятнашках рассказывается на страницах 2405 – 2408 издания Tempus.


netlib.narod.ru< Назад | Оглавление | Далее >

Сайт управляется системой uCoz