» Главная
eXcode.ru » Статьи » Delphi/Pascal » Игры
» Новости
» Опросы
» Файлы
» Журнал



Пользователей: 0
Гостей: 6





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




Введение

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

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

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

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

Разберемся сначала, как мы собираемся хранить наше игровое поле. Проектируя, я решил, что визуально ячейка поля будет выглядеть панелькой (объект 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;


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

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

Основное игровое поле будет располагаться на компоненте 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^.BtnPtr:= TPanel.Create(Panel4);
mapState^.BtnPtr.Parent:=Panel4;
mapState^.BtnPtr.SetBounds(btnL,btnT,btnH,btnW);
mapState^.BtnPtr.onClick:= Button1Click;
mapState^.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^:=0;
mapState^.Pos:=0;
mapState^.Val:=3;
mapState^.BtnPtr.BevelOuter:=bvRaised;
mapState^.BtnPtr.Font.Color:=clBlack;
mapState^.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^.BtnPtr.onClick:= Button1Click;
mapState^.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^.btnPtr then begin
if first then begin
Timer1.Enabled:=TRUE; first:=FALSE;
end;
if mapState^.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^.btnPtr then begin
if ((Button=mbRight) and (mapState^.Val<>0)) then begin
if (mapState^.Val<>0) then begin
if mapState^.Pos=0 then begin
mapState^.Pos:=1;
mapState^.BtnPtr.Caption:=′!′;
inc(numPtrs);
end
else begin
mapState^.Pos:=0;
mapState^.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 минах, я решил ввести диалоговое окно "Настройка".

Пользователь может свободно установить желаемый размер поля, а также произвольное число мин, но не более, чем число полей - 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^.BtnPtr.Free;
FreeMem(mapState, numRows*numCols*sizeof(APlace));


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

Заключение

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

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

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

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

© Долгов Сергей
К началу статьи





Добавил: LedWormДата публикации: 2005-06-02 21:56:15
Рейтинг статьи:4.33 [Голосов 6]Кол-во просмотров: 6899

Комментарии читателей

Всего комментариев: 5

2009-06-19 18:44:48
Артур
Вот идиоты, а вам никак не скомпилировать самим?

2008-12-12 09:06:05
HeRo
да, как???? ауууууууууууу...

2007-12-03 08:02:40
Юрий
"Исходники проекта "Минное поле" Вы можете взять здесь! (zip-архив)" Так ты и зарабатываешь?
А как насчет ...

2007-11-13 15:33:30
DartBob
И правдо, как. Нигде неть накой кнопы.

2007-10-17 14:31:30
Feniks
Как скачать исходник?
Ваше имя: *
Текст записи: *
Имя:

Пароль:



Регистрация

Какую музыку вы предпочитаете?
Techno
11% (29)
Rap
10% (25)
Rock
48% (126)
Trance
10% (27)
Pop
7% (17)
house
5% (13)
Классическую
7% (19)
Я не слушаю музыку
2% (4)

Проголосовало: 260
Звонок в службу технической поддержки:
-Как узнать есть ли у меня "WinCih"?
-Переведите дату на 26 апреля...
Рейтинг: 0/10 (0)
Посмотреть все анекдоты

 
eXcode.ru » Статьи » Delphi/Pascal » Игры