Яндекс цитирования
 

Игры на Delphi

 

[ В начало раздела ]

Игра "Минное поле" или Сапер-2

дата публикации 25 июня 2001г.

Введение

Эту игру я написал еще в 1998 году, когда в стране ходили 2 и 3 версии Delphi. Зачем? Во-первых, я хотел научиться создавать игры, даже если у них есть лучшие реализации. Во-вторых, научиться динамически создавать визуальные объекты и управлять ими. Так что это был первый серьезный проект после "Пятнашек".

Итак, игра от Microsoft известна всем. Каждый играл в нее и добивался безусловного лидерства в классе "Профессионал" :))! Но если Вы желали создать свои правила, то замечали ее ограниченность. Например, нельзя было задавать крупные поля и большое число мин (для поля 10 на 20 максимально можно было выставить 171 мину, а что делать, если ты экстримал?).

Так вот, мне захотелось обойти и эту ограниченность, я оставлял лишь 10 свободных полей. Стало ужасно интересно попасть хоть раз в свободную точку!

Игра

1. Игра "Минное поле"

Игровые структуры

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

type
{ Хранение информации о ячейке игрового поля }
APlace = record
    Pos :byte;
    Val: word;
    btnPtr: TPanel;
end;
APlaceArray = array[1..1] of APlace; 
AMapArray = array[1..1] of byte;

Начало проектирования

На рисунке ниже Вы можете видеть внешний вид главной формы.

Внешний вид главной формы при проектировании

2. Внешний вид главной формы при проектировании

Основное игровое поле будет располагаться на компоненте Panel1 как в контейнере. Таймер Timer1 нужен для обработки информации о времени игры. Кроме того, на форме присутствуют панелька Panel2 для показа числа оставшихся мин и кнопка SB для быстрого начала игры (как и у "Сапера"). Да, еще и главное меню программы.

Создание игрового поля

Для создания игрового поля я написал дополнительную процедуру, размещающая объекты в динамическом массиве и самостоятельно настраивающая внешний вид формы в зависимости от заданных размеров по горизонтали и вертикали - makePlace.

...
  { резервируем память для игры }
  GetMem(mapState, numRows*numCols*sizeof(APlace));
  { расчитываем размеры для каждой кнопки }
  btnW:=16;  btnH:=16;
  for i:= 1 to numRows*numCols do
  begin
    { расчитываем параметры кнопок }
    btnL:= 3 + ((i-1) mod numCols) * (btnW+1);
    btnT:= 3+ ((i-1) div numCols) * (btnH+1);
    { создаем,помещаем и показываем кнопки }
    mapState^[i].BtnPtr:= TPanel.Create(Panel4);
    mapState^[i].BtnPtr.Parent:=Panel4;
    mapState^[i].BtnPtr.SetBounds(btnL,btnT,btnH,btnW);
    mapState^[i].BtnPtr.onClick:= Button1Click;
    mapState^[i].BtnPtr.onMouseDown:= Button1MouseDown;
  end;
...
  Form1.Height:= numRows*17 + 90;
  Form1.Width:= numCols*17 + 12;

После этого мне потребовалась еще одна процедура - generateMap. Она случайным образом расставляла на игровом поле мины. Для этого используется динамический массив map.

...
{ резервируем память для поля игры }
 GetMem(map, numRows*numCols*sizeof(byte));
 { генерируем карту }
 for i:=1 to NumRows*NumCols do begin
   map^[i]:=0;
   mapState^[i].Pos:=0;
   mapState^[i].Val:=3;
   mapState^[i].BtnPtr.BevelOuter:=bvRaised;
   mapState^[i].BtnPtr.Font.Color:=clBlack;
   mapState^[i].BtnPtr.Caption:='';
 end;
 for i:=1 to numMines do begin
   repeat
     a:=Random(numRows)+1;  b:=Random(numCols)+1;
   until (map^[(a-1)*numCols+b]=0);
   map^[(a-1)*numCols+b]:=1;
 end;
...

Обработка выбора полей

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

Напомню, что эти обработчики инициализируются в процедуре makePlace:

...
    mapState^[i].BtnPtr.onClick:= Button1Click;
    mapState^[i].BtnPtr.onMouseDown:= Button1MouseDown;
...

То есть, в нашем проекте должны присутствовать соответствующие обработчики. Что они должны делать? Первый из них - Button1Click - должен определить, для какой ячейки поля произошло событие, т.к. мы используем одномерный массив. Кроме того, в этом обработчике должен быть отслежен момент начала игры и старт отсчета времени для таймера.

Вот этот обработчик целиком:

procedure TForm1.Button1Click(Sender: TObject);
var
  i: word;
begin
  if gamego then begin
  { находим нажатую кнопку }
  for i:= 1 to numRows*numCols do
    if Sender = mapState^[i].btnPtr then begin
      if first then begin
        Timer1.Enabled:=TRUE; first:=FALSE;
      end;
      if mapState^[i].Pos=0 then
         doClick(i);  { выполняем процедуру для выбранной кнопки }
    end;
  end;
end;

Заметьте, что в приведенном фрагменте есть указание на выполнение функции doClick с параметром индекса ячейки игрового поля. Это и есть главное ядрышко нашего проекта, место, где отслеживается весь игровой цикл.

Что делает эта функция:

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

Вот функция doClick:

function TForm1.doClick(btnNum: word): boolean;
var k,ret: word;
    s: string;
begin
  doClick:= FALSE;

if (mapState^[btnNum].Val <> 0) then begin mapState^[btnNum].Val:=0; mapState^[btnNum].BtnPtr.BevelOuter:=bvNone; if (map^[btnNum]<>1) then begin k:=showMap(btnNum); if (k=0) then begin clearZero(btnNum); s:=''; end else s:=inttostr(k); end else begin s:='@'; mapState^[btnNum].Val:=0; mapState^[btnNum].BtnPtr.Caption:=s; mapState^[btnNum].BtnPtr.Font.Color:=clBlack; Timer1.Enabled:=FALSE; SB.Glyph.LoadFromFile('bulboff.bmp'); ret:=Application.MessageBox('Вы подорвались на мине!', 'Конец игры', 0); gamego:=FALSE; {подрывание} exit; end; mapState^[btnNum].BtnPtr.Caption:=s; mapState^[btnNum].BtnPtr.Font.Color:=ColorNum[k]; doClick:= TRUE; { действие выполнено } exit; end; end;

Для отображения числа мин рядом с ячейкой используется функция showMap, возвращающая для любой ячейки число мин.

{ функция, возвращающая число мин рядом с клеткой }
function TForm1.showMap(btnNum: word): word;
Label rc;
Var a,b,flag :byte;
    n :byte;
    i,j :word;
Begin
 a:=0;b:=0;
 flag:=0;
 i:=((btnNum-1) div numCols)+1;
 j:=btnNum-((i-1)*numCols);
 for n:=1 to 8 do begin
  case n of
    1: begin   a:=i; b:=j-1;    end;
    2: begin   a:=i-1; b:=j-1;  end;
    3: begin   a:=i-1; b:=j;    end;
    4: begin   a:=i-1; b:=j+1;  end;
    5: begin   a:=i; b:=j+1;    end;
    6: begin   a:=i+1; b:=j+1;  end;
    7: begin   a:=i+1; b:=j;    end;
    8: begin   a:=i+1; b:=j-1;  end;
  end;
  if ((a<1) or (a>numRows) or (b<1) or (b>numCols)) then goto rc;
  if map^[(a-1)*numCols+b]=1 then flag:=flag+1;
rc:
 end;
 ShowMap:=flag;
end;

Кроме того, для визуального эффекта разного цвета выводимого числа, используются константы:

const
  ColorNum :TColor =
    (clSilver,clBlue,clRed,clGreen,clYellow,clFuchsia,clLime);

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

{ процедура, выполняющая расчистку нулевых клеток }
procedure TForm1.clearZero(btnNum: word);
var pnt,temp: array[1..80,1..2] of word;
    cx,cy,points,temppoints,d :word;
    a,b :byte;
    n :byte;
    k,t,i,j,btn :word;
    s:string;
begin
 i:=((btnNum-1) div numCols)+1;
 j:=btnNum-((i-1)*numCols);
 pnt[1,1]:=i;  pnt[1,2]:=j;  points:=1; temppoints:=0;
repeat
  for t:=1 to points do begin
    cy:=pnt[t,1];  cx:=pnt[t,2];
    for n:=1 to 8 do begin
    case n of
      1: begin   a:=cy; b:=cx-1;    end;
      2: begin   a:=cy-1; b:=cx-1;  end;
      3: begin   a:=cy-1; b:=cx;    end;
      4: begin   a:=cy-1; b:=cx+1;  end;
      5: begin   a:=cy; b:=cx+1;    end;
      6: begin   a:=cy+1; b:=cx+1;  end;
      7: begin   a:=cy+1; b:=cx;    end;
      8: begin   a:=cy+1; b:=cx-1;  end;
    end;
   if ((a>0) and (a<=numRows) and (b>0) and (b<=numCols))then
   begin
    btn:=(a-1)*numCols+b;
    if(mapState^[btn].Val<>0) then
    begin
    k:=showMap(btn);
    mapState^[btn].Val:=0;
    mapState^[btn].BtnPtr.BevelOuter:=bvNone;
    if k=0 then s:='' else s:=inttostr(k);
    mapState^[btn].BtnPtr.Caption:=s;
    mapState^[btn].BtnPtr.Font.Color:=ColorNum[k];
    if k=0 then begin
       temppoints:=temppoints+1;
       temp[temppoints,1]:=a;  temp[temppoints,2]:=b;
    end;
    end;
   end;
    end;
  end; {for t...}
  points:=0;
  points:=temppoints; temppoints:=0;
  if points<>0 then begin
    for i:=1 to points do begin
      pnt[i,1]:=temp[i,1];  pnt[i,2]:=temp[i,2];
    end;
  end;
until (points=0);
end;

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

procedure TForm1.Button1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: integer);
var
  i: word;
begin
  { находим нажатую кнопку }
  for i:= 1 to numRows*numCols do
    if Sender = mapState^[i].btnPtr then begin
  if ((Button=mbRight) and (mapState^[i].Val<>0)) then begin
    if (mapState^[i].Val<>0) then begin
      if mapState^[i].Pos=0 then begin
         mapState^[i].Pos:=1;
         mapState^[i].BtnPtr.Caption:='!';
         inc(numPtrs);
      end
      else begin
         mapState^[i].Pos:=0;
         mapState^[i].BtnPtr.Caption:='';
         dec(numPtrs);
      end;
    end;
  end;
    end;
 Panel2.Caption:=inttostr(numMines-numPtrs);
 if gamego then checkCompleted; { проверка завершения }
end;

{ проверяем правильность флажков и окончание игры } procedure TForm1.checkCompleted; var i,k: word; ret: word; begin k:=0; for i:= 1 to numRows*numCols do begin if ((map^[i]=1) and (mapState^[i].Pos=1)) then k:=k+1; end; if ((k<>numMines) or (numPtrs>numMines)) then exit; Timer1.Enabled:=FALSE; ret:= Application.MessageBox('Ты верно отыскал мины!', 'Поздравляю', 0); gamego:= FALSE; end;

Настройка

После того, как я отладил работу программы на стандартном размере поля 10 на 10 при 10 минах, я решил ввести диалоговое окно "Настройка".

Диалоговое окно

3. Диалоговое окно "Настройка"

Пользователь может свободно установить желаемый размер поля, а также произвольное число мин, но не более, чем число полей - 10. Ну, надо же хоть что-то оставить! :))

...
  gamego:=FALSE;
  first:=TRUE;
  if formConfig.ShowModal = idOk then
  begin
    r:= formConfig.SpinEdit2.Value;
    c:= formConfig.SpinEdit3.Value;
    numMines:= formConfig.SpinEdit1.Value;
    if (numRows<>r) or (numCols<>c) then begin
      FreeMem(map, numRows*numCols*sizeof(byte));
      destroyPlace;  { освободить динамическую память }
      numRows:=r;  numCols:=c;
      makePlace;
    end;
    generateMap;
  end;
...

Как Вы можете здесь заметить, мне потребовалась еще процедура, освобождающая объекты в динамической памяти - destroyPlace.

  Form1.Height:= 16;
  Form1.Width:= 200;
  Form1.Caption:='Ждите!';
  for i:= 1 to numRows*numCols do
    mapState^[i].BtnPtr.Free;
  FreeMem(mapState, numRows*numCols*sizeof(APlace));

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

Заключение

Что ж, подробное рассмотрение процесса проектирования игры закончено. Кому-то, представленный мною материал покажется слишком трудным и непонятным, а кто-то и заинтересуется, как я в 98-мом году, постигая основы Delphi. Обилие представленного здесь кода не должно Вас смущать, я хотел подробнее "разжевать" этот проект для Вас.

Уверен, если Вы захотите, Вы сможете улучшить эту игру. Основной недостаток продемонстрированного здесь метода проектирования - работа с динамическими массивами "по старинке" методами GetMem и FreeMem. Сейчас, используя возможности Delphi 5 можно переписать интерфейс работы с памятью, воспользовавшись силой динамических массивов.

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

Исходники проекта "Минное поле" Вы можете взять здесь! (zip-архив)

Дополнительные ссылки по теме:

© Долгов Сергей

 

[ В начало раздела ]


 

 

Все для web-дизана!!! Бард-Путеводитель Много Всего CGI-Гид. Лучшие скрипты... WDH - WebDesignHelp - CGI, JAVA, APPLETS, TOP100! Раскрутка, увеличение посещаемости и индекса цитируемости в поисковых системах.

© 2000-2002 Долгов Сергей

dolgov_sergei@mail.ru

X