| Новости | |||||
|---|---|---|---|---|---|
|
| Техника | |
|---|---|
|
| Программирование | |||
|---|---|---|---|
|
| Дополнительно | ||
|---|---|---|
|
| Опросы |
|---|
| Реклама |
|---|
|
|
| Партнеры |
|---|
| Пишем игру Крестики-нулики 3х3 |
| Автор Albinos_X | ||
| 29.08.2009 г. | ||
![]() У многих начинающих программистов появляются желания опробовать себя в написании какой-либо игры. В том числе, некоторые преподаватели, дают задания в качестве Курсовой работы, написать какую-либо игру. Данная статья не является концептом в написании игр, в том числе и Крестиков-Нуликов, а является всего лишь небольшим примером (и может быть далеко не самым лучшим) того, как можно решить данный вопрос. ШАГ 1. Определимся «чего хотим». Определим, что мы хотим увидеть в конце нашей работы. Не будем задаваться на «навороты», поэтому определим простейший интерфейс, в который будет входить: 1. игровое поле, 2. возможность начала новой игры, 3. Возможность переключения режимов игры на 2 игрока и на 1-го игрока (второй компьютер), 4. Возможность изменения сложности игры с компьютером 5. Отображения текущего хода, количества сыгранных игр и победителя. Теперь необходимо определиться, как мы будем реализовывать игровое поле. Подходов и вариантов здесь можно использовать множество, начиная от использования банальных кнопок (TButton, TSpeedButton, TBitBtn), использования TPaintBox, рисования непосредственно на канве формы и т.д., и наконец, написанием собственного компонента. Остановлюсь в реализации заданного примера на использовании TImage, для показа простейшего примера работы с графикой. ШАГ 2. Набросаем форму. Итак, с тем, что мы хотим увидеть, мы определились. Теперь создаём форму, бросаем на форму MainMenu1, Panel1 – будет служить контейнером для Image, Image1 – непосредственно игровое поле, RadioGroup1 – выбор количества игроков, RadioGroup2 – выбор сложности игры, Label1 – будет показывать сколько игр сыграно, Label2 – будет отображать кто ходит, Label3 – будет отображать кто победил. Форма у нас принимает, приблизительно следующий вид: ![]() ШАГ 3. Определяемся со «структурой» программы. У нас будет 1 форма, к ней модуль, также создадим модули Graph, который будет у нас отвечать за работу с графикой, и модуль Logik, который будет отвечать за логику компьютера, проверку выигрыша игроков и другие логические операции. ШАГ 4. Подготовим рисунки игрового поля, крестика и нулика. Набросаем нужного размера игровое поле, крестик и нулик по отдельности. Для этого можно использовать любой имеющийся у Вас под рукой графический редактор. Загружаем сделанные рисунки в ресурс и сохраняем его в папке с разрабатываемой нами программой под именем Res.res. Останавливаться на работе с ресурсами не будем. Не забываем также подключить этот ресурс к модулю Graph. {$R Res.RES} ШАГ 5. Описываем работу с графикой. Так как мы будем всё рисовать, а рисовать мы будем заранее подготовленные рисунки, которые будем выгружать из ресурса, нам будет необходимо хранить эти снимки в переменных, при чём желательно в глобальных, т.к. каждый раз выгружать из ресурса нужную картинку не считаю целесообразным, поэтому объявляем глобальные переменные: var Bmp_temp : TBitmap; // для хранения текущего состояния поля Bmp_Pole : TBitmap; // снимок чистого поля Bmp_X : TBitmap; // снимок креста Bmp_O : TBitmap; // снимок нулика Теперь пишем процедуры и функции. Так как мы используем графику из ресурса, нам необходимо её выгрузить. Пишем процедуру, которая у нас будет выгружать картинки из ресурса: Function LoadResurce(Bmp:TBitmap; NameBitmap:String):Boolean; begin Result:=True; Bmp.LoadFromResourceName(HInstance,NameBitmap); Result:=False; end; Не забываем, что это всего лишь маленькая деталь, нам ещё нужно проинициализировать глобальные переменные (т.е. снимки поля и игровых фигур): // возвращает True, если что-то прошло не удачно Function CreateBmp:Boolean; begin Result:=True; // создаём витмапы Bmp_temp := TBitmap.Create; Bmp_Pole := TBitmap.Create; Bmp_X := TBitmap.Create; Bmp_O := TBitmap.Create; // загружаем ресурсы if LoadResurce(Bmp_Pole, 'BITMAP_3') thenExit; if LoadResurce(Bmp_O, 'BITMAP_4') then Exit; if LoadResurce(Bmp_X, 'BITMAP_5') then Exit; Bmp_temp.Height:=300; Bmp_temp.Width:=300; Bmp_temp.Canvas.StretchDraw(Bmp_temp.Canvas.ClipRect, Bmp_Pole); Result:=False; end; Теперь нам также нужно предусмотреть освобождение памяти, после окончания работы с программой: procedure DestroyBmp; begin Bmp_temp.Free; Bmp_Pole.Free; Bmp_X.Free; Bmp_O.Free; end; Предусмотрим процедуру очистки игрового поля: Procedure ClearPole; begin Bmp_temp.Canvas.StretchDraw(Bmp_temp.Canvas.ClipRect,Bmp_Pole); end; Так как пользователь будет щёлкать мышью по полю, нам будет необходимо вычислить, куда он попал, а точнее в какую клетку: // вычисление клетки в которую попал пользователь Function CalcClick(Height, Width, X, Y : Integer): TPoint; begin Result.X:=X div (Width div 3) + 1; Result.Y:=Y div (Height div 3) + 1; end; Написанная функция возвращает координаты клетки, в которую попал пользователь. Координаты у нас будут измеряться от 1. Не забываем также и о том, что нам нужно ещё и нарисовать сделанный пользователем ход: // рисование хода procedure StepBmp(Step:TPoint; O_X:Boolean); var X, Y : Integer; tmp:TBitmap; begin if O_X then tmp:=Bmp_X else tmp:=Bmp_O; X:=(Step.X-1)*100+50-(tmp.Width div 2); Y:=(Step.Y-1)*100+50-(tmp.Height div 2); Bmp_temp.Canvas.Draw(X,Y,tmp); end; Здесь переменная O_X, как Вы уже поняли, определяет, чем сделан ход, крестом или нуликом. Ну и напоследок, сделаем процедуру, которая будет обновлять содержимое Image, т.е. то, что будет видеть пользователь: procedure RefreshPole(Can:TCanvas); begin Can.StretchDraw(Can.ClipRect,Bmp_temp); end; Не забываем, что нам нужно написанные процедуры и функции описать в разделе interface модуля. ШАГ 6. Описываем логику Для хранения ходов мы будем использовать массив по размерности поля, в частности 3х3. Также нам необходимо для удобства массив, в котором мы будем хранить количество сделанных ходов по линиям, т.к. выигрышных линий для каждой клетки может быть только 4 (горизонталь, вертикаль и 2 диагонали), то это будет массив размерностью в 4 байта. Также нам нужно будет хранить, чей сейчас ход и на случай игры с компьютером, необходимо знать, чем ходит компьютер. Опишем типы и переменные: Const Dimens = 3; // размерность поля Type TMas_Pole = array[1..Dimens,1..Dimens] of Integer; { cspGoriz : Byte; cspVert : Byte; cspDiag1 : Byte; cspDiag2 : Byte; } TColStepPole = array[1..4] of Byte; ….. // здесь мы будем описывать написанные функции и процедуры …. var // 0 - clear, 1 - O, 2 - X Mas_Pole: TMas_Pole; // определяет чем ходит комп // False - O, True - X Komp_Logik : Boolean; O_X : Boolean; // чей ход false - 0, true - X Напишем первые процедуры и функции, а в частности процедуру очистки массива, записи хода пользователя: Procedure ClearMasPole; var i, j : Byte; begin for i:=1 to Dimens do for j:=1 to Dimens do Mas_Pole[i,j]:=0; end; // запись в массив и проверка, можно и туда ходить Function StepUser(Step:TPoint; O_X:Boolean):Boolean; begin Result:=false; if Mas_Pole[Step.X,Step.Y]=0 then begin Mas_Pole[Step.X,Step.Y]:=Integer(O_X)+1; Result:=True; end; end; Напишем функцию, которая будет проверять окончание игры и выигрыш (комментарии в коде): // определение победы и победителя // 0 - игра не закончена // 1 - победили 0 // 2 - победили Х // 3 - ничья function WinLogik:Byte; var i,j:Byte; h,w,x1,x2:Integer; b:Boolean; begin x1:=0; x2:=0; b:=false; Result:=0; for i:=1 to Dimens do begin h:=0; w:=0; for j:=1 to Dimens do begin // просматриваем одновременно строки и столбцы h:=(Mas_Pole[i,j] and 2)*10+(Mas_Pole[i,j] and 1)+h; w:=(Mas_Pole[j,i] and 2)*10+(Mas_Pole[j,i] and 1)+w; // проверяем наличие свободных клеток if Mas_Pole[j,i]=0 then b:=True; end; // просматриваем диагонали x1:=(Mas_Pole[i,i] and 2)*10+(Mas_Pole[i,i] and 1)+ x1; x2:=(Mas_Pole[i,(Dimens+1)-i] and 2)*10+(Mas_Pole[i,(Dimens+1)-i] and 1)+ x2; // проверяем просмотренные столбцы и строки if (h=3) or (w=3) then begin Result:=1; exit; end; if (h=60) or (w=60) then begin Result:=2; exit; end; end; // проверяем диагонали if (x1=3) or (x2=3) then begin Result:=1; exit; end; if (x1=60) or (x2=60) then begin Result:=2; exit; end; // если до сюда мы дошли, то // проверяем есть ли свободные клетки // если нет то возвращаем ничью = 4 if not b then Result:=3; end; Как Вы уже заметили, мы проверяем только выигрышные линии, к тому же, способ проверки не совсем стандартный, но позволяющий нам избежать использование дополнительных переменных для О и Х отдельно или прогонять алгоритм повторно для каждого игрока, что ускоряет работу самого алгоритма. Сам используемый способ, может чем-то напомнить метод проверки контрольных сумм. Теперь, попробуем реализовать функцию выбора, чем будет играть компьютер и простейшую игру компьютера. И в том и другом случае будем использовать генератор случайных чисел: // выбирает чем ходит комп // False - O, True - X function KompLogik : Boolean; begin Randomize; Result:=Boolean(Random(1000) mod 2); end; // подсчёт свободных клеток function CalcFreeStep:Byte; var i, j : Byte; begin Result:=0; for i:=1 to Dimens do for j:=1 to Dimens do Result:=Integer(Mas_Pole[i,j]=0)+Result; end; // играет комп, простая игра // клетки выбираются случайно Function EasyStepKomp:TPoint; var fr,tmp : Byte; i,j : Byte; begin tmp:=0; Randomize; fr:=Random(CalcFreeStep); if fr=0 then fr:=1; for i:=1 to Dimens do for j:=1 to Dimens do begin if (Mas_Pole[i,j]=0) then begin inc(tmp); if tmp=fr then begin Result.X:=i; Result.Y:=j; Break; end; end; end; end; В функции EasyStepKomp, во избежание зацикливания функции, когда случайные числа попадают только на занятые клетки, мы считаем количество свободных ходов, выбираем по полученному значению случайное число и последовательно выбираем эту позицию, начиная с крайней верхней клетки. Игру средней сложности компьютером сделаем на основе оценочной таблицы, оценка будет производиться поэтапно, т.е. сначала оцениваем, можем ли мы сделав этот ход выиграть. Потом оценим, может ли противник следующим ходом выиграть, тем самым предотвратить его выигрыш. Далее делаем анализ на возможность выигрыша через ход, т.е. если в минимум одной из полос, есть наш знак и нет знака противника. Такой же анализ сделаем и для противника, потом оценим количество выигрышных полос, которые мы можем занять, поставив на клетку свой знак. Далее можно ещё оценить и количество и длину доступных диагоналей. Итак, вот что у нас получится: // средняя игра компа // на основе оценочной таблицы // таблицу не делаем, оцениваем на лету function MediumStepComp(Mas_Pole: TMas_Pole; O_X, Komp_Logik:Boolean):TPoint; Type TMaxOcen=record mmoX:Byte; mmoY:Byte; end; TMasMaxOcen=array of TMaxOcen; var Mas_Ocen : Integer; i,j :Byte; tmp_max : Integer; MasMaxOcen:TMasMaxOcen; begin tmp_max:=1; SetLength(MasMaxOcen,0); // если комп ходит вторым, и если не занята центральная ячейка // необходимо её занять if (Komp_Logik<>O_X) and (Mas_Pole[2,2]=0) then begin Result.X:=2; Result.Y:=2; Exit; end; // производим оценку ячеек for i:=1 to Dimens do for j:=1 to Dimens do begin Mas_Ocen:=MasOcenCell(Mas_Pole, i, j,O_X); // производим сразу оценку максимумов if tmp_max<Mas_Ocen then begin // обнуляем массив до начального SetLength(MasMaxOcen,1); // записываем координаты максимума MasMaxOcen[0].mmoX:=i; MasMaxOcen[0].mmoY:=j; // изменяем на новый максимум tmp_max:=Mas_Ocen; end else begin // если максимум равен if tmp_max=Mas_Ocen then begin // добавляем значение в список координат максимумов SetLength(MasMaxOcen,Length(MasMaxOcen)+1); MasMaxOcen[Length(MasMaxOcen)-1].mmoX:=i; MasMaxOcen[Length(MasMaxOcen)-1].mmoY:=j; end; end; end; // выбираем случайное значение из масива координат максимумов Randomize; Mas_Ocen:=Random(Length(MasMaxOcen)-1); Result.X:=MasMaxOcen[Mas_Ocen].mmoX; Result.Y:=MasMaxOcen[Mas_Ocen].mmoY; SetLength(MasMaxOcen,0); end; // подсчёт занятых на диагонали/полосе определённым игроком клеток Function CalcStep(Mas_Pole: TMas_Pole; X,Y:Byte; O_X:Boolean; var EnemyColStepPole:TColStepPole):TColStepPole; var i :Byte; User :Integer; Enemy:Integer; Temp1_int,Tepm2_int:Integer; pit:TPoint; begin for i:=1 to 4 do begin Result[i]:=0; EnemyColStepPole[i]:=0; end; User:=Integer(O_X)+1; Enemy:=Integer(not O_X)+1; // смотрим горизонтали и вертикали for i:=1 to Dimens do begin if Mas_Pole[X,i]=User then inc(Result[1]); if Mas_Pole[i,Y]=User then inc(Result[2]); if Mas_Pole[X,i]=Enemy then inc(EnemyColStepPole[1]); if Mas_Pole[i,Y]=Enemy then inc(EnemyColStepPole[2]); end; pit:=LengthDiagonalPoleCell(X,Y); // смотрим диагонали Temp1_int:=((X-Y)*Integer((X-Y)>0)+1); Tepm2_int:=(Abs(X-Y)*Integer((X-Y)<0)+1); for i:=0 to Pit.X-1 do begin if Mas_Pole[Temp1_int+i,Tepm2_int+i]=User then inc(Result[3]); if Mas_Pole[Temp1_int+i,Tepm2_int+i]=User then inc(Result[4]); end; Temp1_int:=Dimens-(Dimens+1-X-Y)*Integer((Dimens+1-X-Y)>0); Tepm2_int:=Abs(Dimens+1-X-Y)*Integer((Dimens+1-X-Y)<0)+1; for i:=0 to Pit.Y-1 do begin if Mas_Pole[Temp1_int+i,Tepm2_int+i]=User then inc(Result[3]); if Mas_Pole[Temp1_int+i,Tepm2_int+i]=Enemy then inc(EnemyColStepPole[3]); end; For i:=0 to Pit.Y-1 do begin if Mas_Pole[Temp1_int-i,Tepm2_int+i]=User then inc(Result[4]); if Mas_Pole[Temp1_int-i,Tepm2_int+i]=Enemy then inc(EnemyColStepPole[4]); end; end; // считаем оценку для конкретного случая Function AttributionOcen(ColStepPole:TColStepPole; ColStep, ColStepEnemy, Ocen:Integer; EnemyColStepPole:TColStepPole):Integer; var i:Byte; begin Result:=0; for i:=1 to 4 do if (ColStepPole[i]=ColStep) and (EnemyColStepPole[i]=ColStepEnemy) then Result:=Result+Ocen; end; // оценка ячейки // X,Y - координаты оцениваемой ячейки Function MasOcenCell(Mas_Pole: TMas_Pole; X,Y:Byte; O_X:Boolean):Integer; var tmp, tmp_next:TColStepPole; LenDiag:TPoint; begin Result:=0; if Mas_Pole[X,Y]<>0 then Exit; Result:=1; {1. Оценка Клетки с точки зрения, если мы можем выиграть поставив в неё} // если ставим в неё то выигрываем tmp:=CalcStep(Mas_Pole, X,Y,O_X,tmp_next); Result:=AttributionOcen(tmp,2, 0,1000000, tmp_next)+Result; {//////////////////////////////////////////////////////////////////////} {2. Оценка предотвращение выигрыша противника} tmp:=CalcStep(Mas_Pole, X,Y,not O_X, tmp_next); Result:=AttributionOcen(tmp,2,0,100000, tmp_next)+Result; {3. Оценка ячейки с точки зрения, возможности выиграть, т.е. в полосе нет противника и один свой знак уже стоит, тут же чем больше пересекающихся полос, в которых присутствует такие варианты, оценка будет расти в число раз пересекающихся полос, что обеспечивает возможность установки вилок} tmp:=CalcStep(Mas_Pole, X,Y, O_X, tmp_next); Result:=AttributionOcen(tmp,1,0,10000, tmp_next)+Result; {4. Делаем тоже самое для противника, с целью предотвращение установки вилок противником, и перекрытия его ходов} tmp:=CalcStep(Mas_Pole, X,Y, not O_X, tmp_next); Result:=AttributionOcen(tmp,1,0,1000, tmp_next)+Result; {5. оцениваем число выигрышных полос, которые ещё не заняты} LenDiag:=LengthDiagonalPoleCell(X,Y); tmp:=CalcStep(Mas_Pole, X,Y, not O_X, tmp_next); if (LenDiag.X=3) or (LenDiag.Y=3) then // если есть диагонали = 3 ячейкам Result:=AttributionOcen(tmp,0,0,100*(LenDiag.X+LenDiag.Y), tmp_next)+Result else // если нет диагоналей = 3 ячейкам Result:=AttributionOcen(tmp,0,0,10, tmp_next)+Result; {****************************************************************} // оцениваем доступные диагонали Result:=(LenDiag.X-1)+(LenDiag.Y-1)+Result; end; // определение длины диагонали function LengthDiagonalPoleCell(X,Y:Byte):TPoint; begin // первая диагональ Result.X:=Dimens-ABS(X-Y); // вторая диагональ Result.Y:=Dimens-ABS((Dimens-X+1)-Y); end; Если Вы внимательно смотрели код, то обратили внимание, что мы на случай наличия нескольких максимумов, записываем их в массив, а потом случайно выбираем из этого массива координаты ячейки, на которую пойдём. Кроме того, Вы должны были заметить, что для оценки диагоналей у меня используется функция, автоматизирующая вычисление начальной ячейки диагонали, в зависимости от того на какую диагональ попала текущая позиция. Алгоритм естественно не является лучшим в решении данного вопроса, а является как я писал в самом начале статьи, всего лишь примером возможного способа реализации. ШАГ 6. Главный модуль Здесь уже ничего сложного нет. Описываем самые простейшие события и описываем необходимые глобальные переменные. В своём коде я решил уйти от объявления глобальных переменных, и буду использовать стандартные средства различных компонентов: // Image1.Tag:=X; - координаты мыши над Image // Panel1.Tag:=Y; - координаты мыши над Image // Form1.Tag - счётчик игр … // кнопка «Выход» в меню procedure TForm1.N4Click(Sender: TObject); begin Close; end; // создание формы procedure TForm1.FormCreate(Sender: TObject); begin if CreateBmp then begin MessageDlg('Не удалось инициировать игровое поле',mtError,[mbCancel],0); Halt; end; Image1.Picture.Bitmap:=Bmp_Temp; RadioGroup1Click(Sender); end; // уничтожение формы procedure TForm1.FormDestroy(Sender: TObject); begin DestroyBmp; end; Опишем клики мышкой по игровому полю: // т.к. поверку на выигрыш мы будем применять часто // вынесем её в отдельную функцию Function WinShowMessage:Boolean; var win:byte; s: string; begin Result:=False; win:=WinLogik; case win of 1 : s:='Победил "О"!!!'; 2 : s:='Победил "X"!!!'; 3 : s:='Ничья'; end; if (win>0) and (win<4) then begin Result:=True; Form1.Label3.Caption:=s; ShowMessage('Игра закончена.'+s); end; end; procedure TForm1.Image1Click(Sender: TObject); var Step:TPoint; begin // проверяем окончание игры if WinShowMessage then Exit; // вычисляем куда попали Step:=CalcClick(Image1.Height, Image1.Width, Image1.Tag, Panel1.Tag); if StepUser(Step, O_X) then begin StepBmp(Step, O_X); // обновляем рисунок RefreshPole(Image1.Canvas); Application.ProcessMessages; // проверяем окончание игры if WinShowMessage then Exit; end else exit; // от количества игроков if RadioGroup1.ItemIndex=0 then // 1 игрок begin // меняем игрока O_X:=not O_X; if O_X then Label2.Caption:='Ходит "X" (Компьютер)' else Label2.Caption:='Ходит "O" (Компьютер)'; case RadioGroup2.ItemIndex of 0 : Step:=EasyStepKomp; // простая 1 : Step:=MediumStepComp(Mas_Pole, O_X, not Boolean(Form1.Tag mod 2)); // Средняя // 2 : ComplexStepComp; // сложная end; StepUser(Step, O_X); StepBmp(Step, O_X); // обновляем рисунок RefreshPole(Image1.Canvas); // проверяем окончание игры WinShowMessage; end; {if RadioGroup1.ItemIndex=0 then} // меняем игрока O_X:=not O_X; if O_X then Label2.Caption:='Ходит "X"' else Label2.Caption:='Ходит "O"'; end; // запоминаем координаты мыши на полем procedure TForm1.Image1MouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); begin Image1.Tag:=X; Panel1.Tag:=Y; end; Теперь нам нужно описать событие начала новой игры и переходы между вариантами игры, т.е. один игрок или два игрока у нас играют: // новая игра procedure TForm1.N2Click(Sender: TObject); var Step:TPoint; begin ClearMasPole; ClearPole; RefreshPole(Image1.Canvas); Form1.Tag:=Form1.Tag+1; // Х и О ходят первыми по очереди O_X:=not Boolean(Form1.Tag mod 2); Label1.Caption:='Игра '+inttostr(Form1.Tag); if O_X then Label2.Caption:='Ходит "X"' else Label2.Caption:='Ходит "O"'; Label3.Caption:='Победитель не определён'; // т.к. началась новая игра необходимо проверить // кто ходит первый (если 1 игрок) if RadioGroup1.ItemIndex=0 then begin if (Komp_Logik=O_X) then begin Label2.Caption:=Label2.Caption+'(Компьютер)'; case RadioGroup2.ItemIndex of 0 : Step:=EasyStepKomp; // простая 1 : Step:=MediumStepComp(Mas_Pole, O_X, not Boolean(Form1.Tag mod 2)); // Средняя // 2 : Step:=ComplexStepComp; // средняя end; StepUser(Step, O_X); StepBmp(Step, O_X); // обновляем рисунок RefreshPole(Image1.Canvas); O_X:=not O_X; end; end; {if Komp_Logik=O_X then} end; // вариант игры (1, 2 игрока) procedure TForm1.RadioGroup1Click(Sender: TObject); begin Komp_Logik:=KompLogik; RadioGroup2.Enabled:=not Boolean(RadioGroup1.ItemIndex); N2Click(Sender); end; Как видите, ничего сложного здесь нет. Если у Вас есть замечания, вопросы или предложения прошу писать на почту или ICQ которые указаны в контактах. СУВ, Albinos_X (С) 2009 г. ВНИМАНИЕ: Коммерческая публикация данной статьи запрещена без согласования с автором.
Функция доступна только зарегистрированным пользователям. Powered by AkoComment 2.0! |
||



