Как составить программу крестики нолики в с

Игра крестики нолики на Си

Задача заключается в реализации игры крестики-нолики на языке программирования Си++.  Программа должна создавать поле для игры в крестики-нолики. Игроки поочерёдно вводят номер строки и столбца, в котором они хотят сделать ход. Игра проходит таким образом до тех пор, пока кто-либо из игроков не победит или не произойдёт ничья. Если клетка уже занята, то программа не должна дать сделать некорректный ход. Если кто-либо победит, программа должна уведомить об этом, после чего пользователь должен нажать кнопку, чтобы выйти из программы.

Краткое описание алгоритма программы крестики нолики на си. При запуске программы показывается игровое поле, после чего первый игрок должен ввести номер строки и столбца клетки, на которой он хочет сделать ход. Затем такие же действия должен совершить второй игрок. Программа считывает номер строки и столбца, введённые пользователем, проверяет, занято ли выбранное место, если занято, то предлагает сделать ход заново, если нет, то ход будет сделан. Если кто-либо из игроков победил, программа выводит сообщение о том, кто из игроков победил, после чего пользователь нажимают любую кнопку, чтобы выйти из программы.

Переменные и массивы. Массив a[i][j] отвечает за номер строки и столбца каждой клетки. i – номер строки клетки, j – номер столбца клетки. Подробно о работе с массивами в Си

Если a[i][j] =0 то данная клетка свободна

Если a[i][j] =1 то данная клетка занята ноликом

Если a[i][j] =2 то данная клетка занята крестиком

Переменные igra и res отвечают за состояние игры, если эти переменные равны нулю, то пока никто не победил, если 1 – победил нолик, если 2 – победил крестик. В переменную hi записывается ход игрока, например, «12» — первая строка, второй столбец. str и sto отвечают за номер выбранных строки и столбца. ver – проверяет возможность хода, если ход невозможен – 0, если возможен – 1.

В программе присутствует четыре процедуры. Подробно  о процедурах в Си.

nul() обнуляет находящиеся элементы каждой клетки на поле, это нужно для начала игры. Если клетка была пустой, то процедура оставит её пустой, если клетка была занята крестиком или ноликом, процедура уберёт их оттуда.

Алгоритм процедуры

процедура обнуления  массива

visual() предназначена для визуализации поля при каждом ходе. Если клетка пуста, то ничего в ней не нарисуется, если на клетке сделали ход, то процедура нарисует этот ход крестик или нолик.

Алгоритм процедуры визаулизации поля

процедура визуализации

Процедура hod(n). Процедура запрашивает номер строки и столбца в клетки, в которую ходит игрок и назначает на выбранную клетку нолик или крестик.  n – номер игрока, который должен ходить в данный момент. Если n = 1 то ходит нолик, если n = 2, то ходит крестик.

Для  проверки корректности хода, клетка должна быть пуста и номер должен быть один из номеров таблицы 3 на 3

if (a[str][sto]==0 && (hi==11 || hi==12 || hi==13 || hi==21 || hi==22 || hi==23 || hi==31 || hi==32 || hi==33 )) (ver=1);

Алгоритм процедуры ход.

процедура  хода

proverka() при каждом ходе проверяет, не победил ли какой-либо игрок, и возвращает одно из возможных значений. Если proverka() = 0, то никто пока не победил или случилась ничья, если proverka() = 1, то победил нолик, если 2 – крестик.

При проверке мы проверяем в цикле for  все строки и ищем есть в какой либо строке  все нолики

// пробегаем по всем строкам       

    for (i=1; i<=3; i++)

    {

       // проверка  все ли в строке нолики

        if (a[i][1]==1 && a[i][2]==1 && a[i][3]==1 ) {res=1;}

 }

Подробно о цикле for в Си

Аналогично идет проверрка по всем столбцам и проверка диагоналей

 // проверка диагоналей

      if (a[1][1]==1 && a[2][2]==1 && a[3][3]==1 )  {res=1;}

      if (a[1][3]==1 && a[2][2]==1 && a[3][1]==1 )  {res=1;}

Алгоритм проверки

Полный код программы.

#include <stdio.h>
#include <conio.h>

int a[3][3];// объявляем  текущее поле  3 строки и 3 столбца. если  нет ничего то поле 0, если нолик,  то  1, если крестик то 2 

// обнуление поля

void nul()
{ int i,j;

//  пробегаем по всем строкам
 for(i=1; i<=3; i++)
 {
// пробегаем по всем столбцам
  for (j=1;j<=3;j++)
  {
      a[i][j]=0;
  }  
 }
}

// вывод  поля на экран

void visual()
{
int i,j;
printf (»  1 2 3n»);

//  пробегаем по всем строкам  
 for(i=1; i<=3; i++)
 {
  printf («%d», i);  
// пробегаем по всем столбцам      
 for (j=1;j<=3;j++)
  {   
      if (a[i][j]==0){printf(«| «);};
      if (a[i][j]==1){printf(«|O»);};
      if (a[i][j]==2){printf(«|X»);};
  }

// переходим на следующую строку 

  printf («|n»);   
  printf («________n»);
 }      
 }

// Ход игрока n — номер игрока На выходе: 0 — 

void hod (int n)
{
int hi;// ход игрока
int sto; //  номер столбца в ходе
int str; // номер строки в ходе
int ver=0; // проверка на корректность хода, если нельзя сделать такой ход то 0, если можно то 1
// запрашиваем ход,  пока не будет введен корректный ход

while (ver==0)
{
// ввод хода
printf («Ваш ход. Введите номер строки и столбца. n»);  
scanf(«%d», &hi);

// определяем номер столбца — это последняя цифра в ходе
sto=hi%10;

// определяем номер строки хода — это первая цифра
str=(hi-sto)/10;

// проверка корректности хода, клетка должна быть пуста и номер должен быть один из номеров таблицы 3 на 3
if (a[str][sto]==0 && (hi==11 || hi==12 || hi==13 || hi==21 || hi==22 || hi==23 || hi==31 || hi==32 || hi==33 )) (ver=1);
}

// если  ходил игрок 1 то поле хода  1
if ( n==1) {a[str][sto]=1;}
// если  ходил игрок 2  то поле хода 2
if ( n==2) {a[str][sto]=2;} 
}

// Проверка  Если ни один игрок не выиграл то 0, если выиграл нолик то 1 если выиграл крестик то 2
int proverka()
{   int i;
    int res; // значение функции. Если ни один игрок не выиграл то 0, если выиграл нолик то 1 если выиграл крестик то 2
    res=0;
   // проверяем выигрыш первого игрока

   // пробегаем по всем строкам       
    for (i=1; i<=3; i++)
    {
        // проверка  все ли в строке нолики
        if (a[i][1]==1 && a[i][2]==1 && a[i][3]==1 ) {res=1;}
 }

   // пробегаем по всем столбцам     
    for (i=1; i<=3; i++)
    {
        // проверка все ли в столбце нолики
        if (a[1][i]==1 && a[2][i]==1 && a[3][i]==1 ) {res=1;}
    }

    // проверка диагоналей
      if (a[1][1]==1 && a[2][2]==1 && a[3][3]==1 )  {res=1;}
      if (a[1][3]==1 && a[2][2]==1 && a[3][1]==1 )  {res=1;}

   // проверяем выигрыш второго игрока
   // пробегаем по всем строкам       
      for (i=1; i<=3; i++)
    {
    // проверка  все ли в строке крестики
        if (a[i][1]==2 && a[i][2]==2 && a[i][3]==2 ) {res=2;}
    }

    // пробегаем по всем столбцам
    for (i=1; i<=3; i++)
    {
     // проверка все ли в столбце крестики 
        if (a[1][i]==2 && a[2][i]==2 && a[3][i]==2 ) {res=2;}
    }

      // проверка диагоналей
      if (a[1][1]==2 && a[2][2]==2 && a[3][3]==2 )  {res=2;}
      if (a[1][3]==2 && a[2][2]==2 && a[3][1]==2 )  {res=2;}   
    return res;
}

main()
{
int igra;// состояние игры если победил нолик то 1 , если победил крестик то 2 если никто пока не победил то 0
// обнуляем очищаем поле
nul();
// отображаем игровое поле
visual();
igra=0;

// пока никто не победил продолжается игра

while (igra==0)
{
// ход 1 го игрока нолика
hod(1);
// отображаем игровое поле
visual();
// проверка на победу первого игрока
igra=proverka();
// если первый игрок не победил , то ходит второй игрок

if (igra==0)
{
// ход второго игрока
hod(2);
// отображаем игровое поле
visual();
// проверка на победу второго игрока
igra=proverka();}

}

// отображение результатов игры
if (igra==1){printf («Победил ноликn»);};
if (igra==2){printf («Победил крестикn»);};
getch();
}

Задание для самостоятельной работы.

Расширьте игровое поле до размеров 5×5 клеток.

Вернуться к содержанию

Поделиться

Нет комментариев. Ваш будет первым!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
// Tic-Tac-Toe 2.0
// Plays the game of tic-tac-toe against a human opponent
// Uses pointers instead of refernces for function parameters
 
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
 
using namespace std;
 
// global constants
const char X = 'X';
const char O = 'O';
const char EMPTY = ' ';
const char TIE = 'T';
const char NO_ONE = 'N';
 
// function prototypes
void instructions();
char askYesNo(string question);
int askNumber(string question, int high, int low = 0);
char humanPiece();
char opponent(char piece);
void displayBoard(const vector<char>* const pBoard);
char winner(const vector<char>* const pBoard);
bool isLegal(const vector<char>* const pBoard, int move);
int humanMove(const vector<char>* const pBoard, char human);
int computerMove(vector<char> board, char computer);
void announceWinner(char winner, char computer, char human);
 
// main function
int main()
{
    int move;
    const int NUM_SQUARES = 9;
    vector<char> board(NUM_SQUARES, EMPTY);
 
    instructions();
    char human = humanPiece();
    char computer = opponent(human);
    char turn = X;
    displayBoard(&board);
 
    while (winner(&board) == NO_ONE)
    {
        if (turn == human)
        {
            move = humanMove(&board, human);
            board[move] = human;
        }
        else
        {
            move = computerMove(board, computer);
            board[move] = computer;
        }
        displayBoard(&board);
        turn = opponent(turn);
    }
 
    announceWinner(winner(&board), computer, human);
 
    return 0;
}
 
// functions
void instructions()
{
    cout << "Welcome to the ultimate man-machine showdown: Tic-Tac-Toe.n";
    cout << "--where human brain is pit against silicon processornn";
 
    cout << "Make your move known by entering a number, 0 - 8.  The numbern";
    cout << "corresponds to the desired board position, as illustrated:nn";
 
    cout << "       0 | 1 | 2n";
    cout << "       ---------n";
    cout << "       3 | 4 | 5n";
    cout << "       ---------n";
    cout << "       6 | 7 | 8nn";
 
    cout << "Prepare yourself, human.  The battle is about to begin.nn";
}
 
char askYesNo(string question)
{
    char response;
    do
    {
        cout << question << " (y/n): ";
        cin >> response;
    } while (response != 'y' && response != 'n');
 
    return response;
}
 
int askNumber(string question, int high, int low)
{
    int number;
    do
    {
        cout << question << " (" << low << " - " << high << "): ";
        cin >> number;
    } while (number > high || number < low);
 
    return number;
}
 
char humanPiece()
{
    char go_first = askYesNo("Do you require the first move?");
    if (go_first == 'y')
    {
        cout << "nThen take the first move.  You will need it.n";
        return X;
    }
    else
    {
        cout << "nYour bravery will be your undoing... I will go first.n";
        return O;
    }
}
 
char opponent(char piece)
{
    if (piece == X)
    {
        return O;
    }
    else
    {
        return X;
    }
}
 
void displayBoard(const vector<char>* const pBoard)
{
    cout << "nt" << (*pBoard)[0] << " | " << (*pBoard)[1] << " | " << (*pBoard)[2];
    cout << "nt" << "---------";
    cout << "nt" << (*pBoard)[3] << " | " << (*pBoard)[4] << " | " << (*pBoard)[5];
    cout << "nt" << "---------";
    cout << "nt" << (*pBoard)[6] << " | " << (*pBoard)[7] << " | " << (*pBoard)[8];
    cout << "nn";
}
 
char winner(const vector<char>* const pBoard)
{
    // all possible winning rows
    const int WINNING_ROWS[8][3] = { { 0, 1, 2 },
    { 3, 4, 5 },
    { 6, 7, 8 },
    { 0, 3, 6 },
    { 1, 4, 7 },
    { 2, 5, 8 },
    { 0, 4, 8 },
    { 2, 4, 6 } };
    const int TOTAL_ROWS = 8;
 
    // if any winning row has three values that are the same (and not EMPTY),
    // then we have a winner
    for (int row = 0; row < TOTAL_ROWS; ++row)
    {
        if (((*pBoard)[WINNING_ROWS[row][0]] != EMPTY) &&
            ((*pBoard)[WINNING_ROWS[row][0]] == (*pBoard)[WINNING_ROWS[row][1]]) &&
            ((*pBoard)[WINNING_ROWS[row][1]] == (*pBoard)[WINNING_ROWS[row][2]]))
        {
            return (*pBoard)[WINNING_ROWS[row][0]];
        }
    }
 
    // since nobody has won, check for a tie (no empty squares left)
    if (count(pBoard->begin(), pBoard->end(), EMPTY) == 0)
        return TIE;
 
    // since nobody has won and it isn't a tie, the game ain't over
    return NO_ONE;
}
 
inline bool isLegal(int move, const vector<char>* pBoard)
{
    return ((*pBoard)[move] == EMPTY);
}
 
int humanMove(const vector<char>* const pBoard, char human)
{
    int move = askNumber("Where will you move?", (pBoard->size() - 1));
    while (!isLegal(move, pBoard))
    {
        cout << "nThat square is already occupied, foolish human.n";
        move = askNumber("Where will you move?", (pBoard->size() - 1));
    }
    cout << "Fine...n";
    return move;
}
 
int computerMove(vector<char> board, char computer)
{
    unsigned int move = 0;
    bool found = false;
 
    //if computer can win on next move, that’s the move to make
    while (!found && move < board.size())
    {
        if (isLegal(move, &board))
        {
            //try move
            board[move] = computer;
            //test for winner
            found = winner(&board) == computer;
            //undo move
            board[move] = EMPTY;
        }
 
        if (!found)
        {
            ++move;
        }
    }
 
    //otherwise, if opponent can win on next move, that's the move to make
    if (!found)
    {
        move = 0;
        char human = opponent(computer);
 
        while (!found && move < board.size())
        {
            if (isLegal(move, &board))
            {
                //try move
                board[move] = human;
                //test for winner
                found = winner(&board) == human;
                //undo move
                board[move] = EMPTY;
            }
 
            if (!found)
            {
                ++move;
            }
        }
    }
 
    //otherwise, moving to the best open square is the move to make
    if (!found)
    {
        move = 0;
        unsigned int i = 0;
 
        const int BEST_MOVES[] = { 4, 0, 2, 6, 8, 1, 3, 5, 7 };
        //pick best open square
        while (!found && i <  board.size())
        {
            move = BEST_MOVES[i];
            if (isLegal(move, &board))
            {
                found = true;
            }
 
            ++i;
        }
    }
 
    cout << "I shall take square number " << move << endl;
    return move;
}
 
void announceWinner(char winner, char computer, char human)
{
    if (winner == computer)
    {
        cout << winner << "'s won!n";
        cout << "As I predicted, human, I am triumphant once more -- proofn";
        cout << "that computers are superior to humans in all regards.n";
    }
 
    else if (winner == human)
    {
        cout << winner << "'s won!n";
        cout << "No, no!  It cannot be!  Somehow you tricked me, human.n";
        cout << "But never again!  I, the computer, so swear it!n";
    }
 
    else
    {
        cout << "It's a tie.n";
        cout << "You were most lucky, human, and somehow managed to tie me.n";
        cout << "Celebrate... for this is the best you will ever achieve.n";
    }
}

Доброго времени суток, друзья. В этой статье мы создадим с вами всем хорошо известную игру «крестики-нолики» с помощью языка C# и Windows Forms. Мы обойдемся без программирования графики и использования спрайтов: основной дизайн игры будет выстроен при помощи стандартных элементов управления для Windows Forms. Мы также напишем с вами простенький «игровой движок» для этой игры (по сути это будет просто отдельный класс на C#, и его при желании можно переиспользовать, например, с другим дизайном формы и т.д.). Этот «движок» мы будем использовать на нашей основной форме, которая будет отображаться сразу при запуске игры.

Наша игра «крестики-нолики» будет поддерживать следующие фичи:

  • игра в двух режимах — «человек против компьютера» и «человек с человеком»
  • всегда первым ходит 1-й игрок
  • первый игрок ходит «крестиками», второй игрок ходит «ноликами»
  • ведение и отображение счёта для каждого из игроков
  • отображение очередности хода («кто сейчас ходит?»)
  • возможность сбросить счёт в любой момент, оставшись в текущем режиме игры
  • возможность сбросить счёт игры и выбрать другой режим

В конце данной статьи вы также сможете скачать готовый проект для Microsoft Visual Studio вместе с самой игрой в виде исполняемого exe-файла.

Итак, приступим.

UI. Создание главной формы для игры и элементов управления

Начнём написание нашей игры с создания нового проекта на языке C# с типом «Приложение Windows Forms (.NET Framework)». Имя проекта выбираем TicTacToe (так в переводе звучит название игры «крестики-нолики»).

После создания проекта нужно переименовать стандартную форму Form1, создаваемую в проекте по умолчанию, в FrmTicTacToe. После этого в окне «Обозреватель решений» у вас должно выйти примерно следующее (на другие классы вроде Cell.cs, GameEngine.cs пока не смотрите, их мы рассмотрим отдельно, чуть позже):

Теперь выберите нашу главную форму FrmTicTacToe и установите для неё следующие свойства:

  • BackColor —  MidnightBlue (при выборе цвета используйте готовое значение цвета на вкладке «Интернет»)
  • FormBorderStyle — None
  • Text — TicTacToe
  • AutoScaleMode — Font
  • StartPosition — CenterScreen
  • Size — 585; 327
  • Name — FrmTicTacToe

У нас готова главная форма, и сейчас нам нужно создать игровое поле, где мы и будем играть в «крестики-нолики».

Для этого перетащите на главную форму FrmTicTacToe новый элемент Panel, который вы найдете во вкладке «Панель элементов» в левой части Microsoft Visual Studio:

Для этого нового элемента Panel установите следующие свойства:

  • BackColor — Indigo (при выборе цвета используйте готовое значение цвета на вкладке «Интернет»)
  • Location — 12; 12
  • Size — 99; 96
  • Name — panelCell0_0

Как нетрудно догадаться, это прототип клетки для нашего игрового поля, куда мы в дальнейшем будем ставить «крестик» или «нолик». Теперь нам нужно скопировать этот элемент 8 раз (т.е. общее количество элементов Panel должно выйти 9 — по три в каждом ряду). Для копирования панелей используйте комбинации Ctrl+C, Ctrl+V или воспользуйтесь пунктами «Копировать», «Вставить» в контекстном меню для элемента.

У вас должна выйти примерно следующая картина:

 

Когда элементы Panel размещены подобным образом, вы заметите, что при копировании им были назначены какие-то произвольные имена. Нужно по очереди выбрать каждую панель и установить её свойство Name, чтобы получился следующий порядок:

 

Когда мы именуем все элементы Panel в таком порядке, мы фактически организуем матрицу размером 3 x 3 элемента, а в имя каждой панели помещаем метаинформацию об индексах элемента матрицы: так, panelCell0_0 соответствует элементу [0][0] матрицы, т.е. это элемент в самом первом ряду и самом первом столбце. Аналогично элемент panelCell2_1 соответствует элементу матрицы [2][1], расположенному в третьем ряду и втором столбце (нумерация рядов и столбцов идёт с нуля).

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

Найдите в панели элементов Label («метка») и поместите их на форму, пока что в количестве 7 штук. Это будут наши индикаторы для игры. Задайте меткам следующие свойства:

1) Метка «Новая Игра:»

  • Font — Franklin Gothic Medium; 24pt
  • ForeColor — White
  • Text — Новая игра:
  • AutoSize — True
  • Location — 359; 71
  • Size — 186; 37
  • Name — labelNewGameTitle

2) Метка с именем первого игрока

  • Font — Franklin Gothic Medium; 18pt
  • ForeColor — White
  • Text — Игрок:
  • AutoSize — True
  • Location — 327; 255
  • Size — 82; 30
  • Name — labelPlayer1Name

3) Метка с именем второго игрока

  • Font — Franklin Gothic Medium; 18pt
  • ForeColor — White
  • Text — Компьютер:
  • AutoSize — True
  • Location — 327; 292
  • Size — 143; 30
  • Name — labelPlayer2Name

4) Метка со счётом первого игрока

  • Font — Franklin Gothic Medium; 18pt
  • ForeColor — Gold
  • Text — 0
  • AutoSize — True
  • Location — 532; 255
  • Size — 27; 30
  • Name — labelPlayer1Score

5) Метка со счётом второго игрока

  • Font — Franklin Gothic Medium; 18pt
  • ForeColor — Fuchsia
  • Text — 0
  • AutoSize — True
  • Location — 532; 292
  • Size — 27; 30
  • Name — labelPlayer2Score

6) Метка «Ходит»

  • Font — Franklin Gothic Medium; 18pt
  • ForeColor — White
  • Text — Ходит:
  • AutoSize — True
  • Location — 327; 331
  • Size — 80; 30
  • Name — labelNowTurnIs

7) Метка с названием игрока, чей сейчас ход

  • Font — Franklin Gothic Medium; 18pt
  • ForeColor — White
  • Text — ?
  • AutoSize — True
  • Location — 413; 331
  • Size — 25; 30
  • Name — labelWhooseTurn

В результате у вас должен быть следующий вид главной формы для игры (я специально пока «размыл» те кнопки, которых у вас ещё нет, их мы добавим уже буквально в следующем абзаце):

Теперь пришло время для кнопок, как я уже сказал, мы не будем использовать стандартный элемент Button для кнопок, а сделаем их плоскими. Для этого кнопки создадим с помощью уже известного элемента Panel, а внутрь каждой панели поместим метку Label с соответствующим текстом для кнопки.

Итак, перетащите из панели элементов на главную форму:

  • 5 элементов Panel (это будут заготовки для 5 кнопок «Сброс», «Новая игра», «Игрок против компьютера», «Игрок против игрока» и «X» — для выхода из игры)
  • 5 элементов Label (это будут названия для кнопок) — перетаскивайте каждый элемент в отдельную панель

Установите для каждой пары элементов Panel и Label следующие свойства:

1) Кнопка «Сброс»

Panel:

  • BackColor — SlateBlue
  • Location — 327; 12
  • Size — 61; 38
  • Name — panelReset

 Label внутри Panel:

  • BackColor — SlateBlue
  • Font — Franklin Gothic Medium; 12pt
  • ForeColor — Cyan
  • Text — Сброс
  • AutoSize — True
  • Location — 4; 9
  • Size — 52; 21
  • Name — labelReset

2) Кнопка «Новая игра»

Panel:

  • BackColor — SlateBlue
  • Location — 394; 12
  • Size — 98; 38
  • Name — panelNewGame

 Label внутри Panel:

  • BackColor — SlateBlue
  • Font — Franklin Gothic Medium; 12pt
  • ForeColor — Cyan
  • Text — Новая игра
  • AutoSize — True
  • Location — 3; 9
  • Size — 91; 21
  • Name — labelNewGame

3) Кнопка «Игрок против компьютера»

Panel:

  • BackColor — SlateBlue
  • Location — 336; 114
  • Size — 236; 60
  • Name — panelPlayerVsCpu

 Label внутри Panel:

  • BackColor — SlateBlue
  • Font — Franklin Gothic Medium; 12pt
  • ForeColor — Cyan
  • Text — Игрок против компьютера
  • AutoSize — True
  • Location — 26; 20
  • Size — 197; 21
  • Name — labelPlayerVsCpu

4) Кнопка «Игрок против игрока»

Panel:

  • BackColor — SlateBlue
  • Location — 336; 180
  • Size — 236; 60
  • Name — panelPlayerVsPlayer

 Label внутри Panel:

  • BackColor — SlateBlue
  • Font — Franklin Gothic Medium; 12pt
  • ForeColor — Cyan
  • Text — Игрок против игрока
  • AutoSize — True
  • Location — 42; 20
  • Size — 158; 21
  • Name — labelPlayerVsPlayer

5) Кнопка выхода из игры в форме буквы X

Panel:

  • BackColor — Indigo
  • Location — 520; 12
  • Size — 52; 42
  • Name — panelCloseButton

 Label внутри Panel:

  • Font — Orbitron; 14,25pt; style=Bold
  • ForeColor — White
  • Text — X
  • AutoSize — True
  • Location — 14; 11
  • Size — 25; 22
  • Name — labelClose

В итоге у вас должно получиться следующее расположение всех элементов на главной форме:

 

Пусть сейчас расположение кнопок не смущает — это лишь их начальное расположение на форме, в дальнейшем мы установим нужные координаты местоположения кнопок и индикаторов при загрузке формы.

UI. Программирование интерфейса и событий на главной форме

В этой части статьи мы запрограммируем базовое поведение размещённых элементов формы. Например, мы сделаем так, чтобы при наведении курсора на кнопки происходил эффект смены цвета фона для кнопки, а при выводе курсора из области кнопки — цвет фона возвращался к первоначальному.

Кнопка выхода из игры

Сначала мы запрограммируем поведение кнопки выхода — ведь она нам сразу пригодится для тестирования внешнего вида и поведения нашей формы, чтобы мы без труда могли завершить программу.

Выберите панель с именем panelCloseButton и в окне «Свойства» нажмите на иконку «молнии» — это раздел «События» для данной панели. Сделайте по двойному клику напротив таких событий как Click, MouseEnter, MouseLeave, чтобы для каждого из них сгенерировались соответствующие методы-обработчики.

Теперь выберите метку с именем labelClose и таким же образом сгенерируйте для неё методы-обработчики для событий Click и MouseEnter.

Далее создадим в коде главной формы отдельный метод ButtonMouseEnter(Panel buttonPanel) и заполним сгенерированные методы следующим образом:

        private void ButtonMouseEnter(Panel buttonPanel) {
            buttonPanel.BackColor = Color.Purple;
            Cursor = Cursors.Hand;
        }        

        private void panelCloseButton_Click(object sender, EventArgs e) {
            Application.Exit();
        }

        private void panelCloseButton_MouseEnter(object sender, EventArgs e) {
            ButtonMouseEnter(panelCloseButton);
        }

        private void panelCloseButton_MouseLeave(object sender, EventArgs e) {
            panelCloseButton.BackColor = Color.Indigo;
            Cursor = Cursors.Default;
        }

        private void labelClose_Click(object sender, EventArgs e) {
            Application.Exit();
        }

        private void labelClose_MouseEnter(object sender, EventArgs e) {
            ButtonMouseEnter(panelCloseButton);
        }

Обработчики panelCloseButton_Click и labelClose_Click выполняют одно и то же действие Application.Exit(), что позволяет завершить игру, независимо от того, в какой именно части панели мы кликнули — на метке или на самой области панели.

Метод ButtonMouseEnter нужен для того, чтобы поменять цвет фона панели для кнопки выхода и поменять стандартный курсор «стрелка» на курсор «руки». Его мы вызываем при введении курсора мыши либо в область панели для кнопки, либо в область самой метки с надписью «X».

В событии panelCloseButton_MouseLeave мы возвращаем курсор обратно на «стрелку» и возвращаем цвет фона на первоначальный — Color.Indigo.

Можете теперь запустить приложение и протестировать форму в действии. Сейчас вы должны заметить эффект при наведении курсора на кнопку выхода из игры. Попробуйте нажать на кнопку — приложение должно завершить работу.

Программируем эффект наведения курсора на клетки игрового поля

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

Для этого по очереди выделите каждую панель для клеток игрового поля и, аналогично тому как делали выше, создайте методы-обработчики для событий Click, MouseEnter и MouseLeave.

Напишем два новых метода CellMouseOver(object sender) и CellMouseOut(object sender) — они и будут выполнять основную работу по смене фона для каждой клетки поля, и их мы будем вызывать из методов-обработчиков для событий MouseEnter и MouseLeave для каждой клетки:

        private void CellMouseOver(object sender) {
            if (sender is Panel) {
                Panel panelCell = (Panel)sender;
                panelCell.BackColor = Color.Purple;
                Cursor = Cursors.Hand;
            }
        }

        private void CellMouseOut(object sender) {
            if (sender is Panel) {
                Panel panelCell = (Panel)sender;
                panelCell.BackColor = Color.Indigo;
                Cursor = Cursors.Default;
            }
        }

        private void panelCell0_0_MouseEnter(object sender, EventArgs e) {
            CellMouseOver(sender);
        }

        private void panelCell0_0_MouseLeave(object sender, EventArgs e) {
            CellMouseOut(sender);
        }

        private void panelCell0_1_MouseEnter(object sender, EventArgs e) {
            CellMouseOver(sender);
        }

        private void panelCell0_1_MouseLeave(object sender, EventArgs e) {
            CellMouseOut(sender);
        }

        private void panelCell0_2_MouseEnter(object sender, EventArgs e) {
            CellMouseOver(sender);
        }

        private void panelCell0_2_MouseLeave(object sender, EventArgs e) {
            CellMouseOut(sender);
        }

        private void panelCell1_0_MouseEnter(object sender, EventArgs e) {
            CellMouseOver(sender);
        }

        private void panelCell1_0_MouseLeave(object sender, EventArgs e) {
            CellMouseOut(sender);
        }

        private void panelCell1_1_MouseEnter(object sender, EventArgs e) {
            CellMouseOver(sender);
        }

        private void panelCell1_1_MouseLeave(object sender, EventArgs e) {
            CellMouseOut(sender);
        }

        private void panelCell1_2_MouseEnter(object sender, EventArgs e) {
            CellMouseOver(sender);
        }

        private void panelCell1_2_MouseLeave(object sender, EventArgs e) {
            CellMouseOut(sender);
        }

        private void panelCell2_0_MouseEnter(object sender, EventArgs e) {
            CellMouseOver(sender);
        }

        private void panelCell2_0_MouseLeave(object sender, EventArgs e) {
            CellMouseOut(sender);
        }

        private void panelCell2_1_MouseEnter(object sender, EventArgs e) {
            CellMouseOver(sender);
        }

        private void panelCell2_1_MouseLeave(object sender, EventArgs e) {
            CellMouseOut(sender);
        }

        private void panelCell2_2_MouseEnter(object sender, EventArgs e) {
            CellMouseOver(sender);
        }

        private void panelCell2_2_MouseLeave(object sender, EventArgs e) {
            CellMouseOut(sender);
        }

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

Далее мы создадим пустой метод FillCell(Panel panel, int row, int column), который будет в дальнейшем заполнять клетку игрового поля либо «крестиком», либо «ноликом», а также вызовем этот метод из каждого метода-обработчика события Click для всех клеток поля. При этом из каждого обработчика мы передаем координаты клетки — т.е. индекс ряда и столбца (вспомним о том, что наше игровое поле есть матрица размерностью 3×3 элемента):

        private void FillCell(Panel panel, int row, int column) {
                // код метода мы напишем позже, пока здесь будет пусто.
        }

        private void panelCell0_0_Click(object sender, EventArgs e) {
            FillCell((Panel)sender, 0, 0);
        }

        private void panelCell0_1_Click(object sender, EventArgs e) {
            FillCell((Panel)sender, 0, 1);
        }

        private void panelCell0_2_Click(object sender, EventArgs e) {
            FillCell((Panel)sender, 0, 2);
        }

        private void panelCell1_0_Click(object sender, EventArgs e) {
            FillCell((Panel)sender, 1, 0);
        }

        private void panelCell1_1_Click(object sender, EventArgs e) {
            FillCell((Panel)sender, 1, 1);
        }

        private void panelCell1_2_Click(object sender, EventArgs e) {
            FillCell((Panel)sender, 1, 2);
        }

        private void panelCell2_0_Click(object sender, EventArgs e) {
            FillCell((Panel)sender, 2, 0);
        }

        private void panelCell2_1_Click(object sender, EventArgs e) {
            FillCell((Panel)sender, 2, 1);
        }

        private void panelCell2_2_Click(object sender, EventArgs e) {
            FillCell((Panel)sender, 2, 2);
        }

К содержимому метода FillCell мы вернёмся чуть позже, когда у нас будет готов класс нашего игрового движка.

Программируем визуальные эффекты для кнопок «Игрок против компьютера» и «Игрок против игрока»

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

Для этого выбираем панель panelPlayerVsCpu и создаем для неё методы-обработчики для событий Click, MouseEnter, MouseLeave.

Для метки labelPlayerVsCpu создаём методы-обработчики для событий Click и MouseEnter.

Аналогично поступаем с панелью panelPlayerVsPlayer и соответствующей ей меткой labelPlayerVsPlayer — для панели создаём обработчики для событий Click, MouseEnter, MouseLeave, а для метки — обработчики для событий Click и MouseEnter.

Поскольку мы хотим, чтобы кнопки у нас меняли стиль единообразно, мы создадим два общих метода для смены стиля кнопок:

  • RegularButtonMouseOver(Panel panelButton, Label labelButtonText) — будет вызываться при наведении курсора мыши на кнопку. В параметрах метода — панель и метка, для которой надо поменять стиль.
  • RegularButtonMouseOut(Panel panelButton, Label labelButtonText) — будет вызываться при выводе курсора мыши за область кнопки. В параметрах метода — также панель и метка, для которой надо поменять стиль.

Ниже код для этих методов и их вызов из методов-обработчиков для панелей и меток, отвечающих за кнопки:

        private void RegularButtonMouseOver(Panel panelButton, Label labelButtonText) {
            panelButton.BackColor = Color.Purple;
            labelButtonText.ForeColor = Color.Yellow;
            Cursor = Cursors.Hand;
        }

        private void RegularButtonMouseOut(Panel panelButton, Label labelButtonText) {
            panelButton.BackColor = Color.SlateBlue;
            labelButtonText.ForeColor = Color.Cyan;
            Cursor = Cursors.Default;
        }

        private void panelPlayerVsCpu_MouseEnter(object sender, EventArgs e) {
            RegularButtonMouseOver(panelPlayerVsCpu, labelPlayerVsCpu);
        }

        private void panelPlayerVsCpu_MouseLeave(object sender, EventArgs e) {
            RegularButtonMouseOut(panelPlayerVsCpu, labelPlayerVsCpu);
        }

        private void labelPlayerVsCpu_MouseEnter(object sender, EventArgs e) {
            RegularButtonMouseOver(panelPlayerVsCpu, labelPlayerVsCpu);
        }

        private void panelPlayerVsPlayer_MouseEnter(object sender, EventArgs e) {
            RegularButtonMouseOver(panelPlayerVsPlayer, labelPlayerVsPlayer);
        }

        private void panelPlayerVsPlayer_MouseLeave(object sender, EventArgs e) {
            RegularButtonMouseOut(panelPlayerVsPlayer, labelPlayerVsPlayer);
        }

        private void labelPlayerVsPlayer_MouseEnter(object sender, EventArgs e) {
            RegularButtonMouseOver(panelPlayerVsPlayer, labelPlayerVsPlayer);
        }

Обработчики для событий Click мы пока оставим пустыми — как для панелей, так и для меток с названиями кнопок, вернёмся к ним чуть позже.

Программируем визуальные эффекты для кнопок «Новая игра» и «Сброс»

Кнопки «Новая игра» и «Сброс» будут в том же стиле, как и «Игрок против компьютера» и «Игрок против игрока», поэтому для их стилизации мы используем уже написанные методы RegularButtonMouseOver, RegularButtonMouseOut

Выбираем панель с именем panelReset и создаём для неё обработчики событий Click, MouseEnter, MouseLeave.

Выбираем метку с именем labelReset и создаём для неё обработчики событий Click и MouseEnter.

То же самое проделываем для панели panelNewGame и метки labelNewGame.

Теперь для стилизации этих двух кнопок осталось вызывать нужные методы:

        private void panelReset_MouseEnter(object sender, EventArgs e) {
            RegularButtonMouseOver(panelReset, labelReset);
        }

        private void panelReset_MouseLeave(object sender, EventArgs e) {
            RegularButtonMouseOut(panelReset, labelReset);
        }

        private void labelReset_MouseEnter(object sender, EventArgs e) {
            RegularButtonMouseOver(panelReset, labelReset);
        }

        private void panelNewGame_MouseEnter(object sender, EventArgs e) {
            RegularButtonMouseOver(panelNewGame, labelNewGame);
        }

        private void panelNewGame_MouseLeave(object sender, EventArgs e) {
            RegularButtonMouseOut(panelNewGame, labelNewGame);            
        }

        private void labelNewGame_MouseEnter(object sender, EventArgs e) {
            RegularButtonMouseOver(panelNewGame, labelNewGame);
        }

Также пока оставим методы-обработчики события Click для этих панелей и меток пустыми.

Программируем видимость и расположение визуальных элементов

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

Давайте в код формы добавим следующий метод SetPlayersLabelsAndScoreVisible(bool visible), который будет отвечать за отображение/скрытие имён игроков, метки с указанием хода, а также кнопок «Сброс» и «Новая игра»:

        private void SetPlayersLabelsAndScoreVisible(bool visible) {
            labelPlayer1Name.Visible = visible;
            labelPlayer1Score.Visible = visible;
            labelPlayer2Name.Visible = visible;
            labelPlayer2Score.Visible = visible;
            labelNowTurnIs.Visible = visible;
            labelWhooseTurn.Visible = visible;

            panelNewGame.Visible = visible;
            panelReset.Visible = visible;
        }

Как видите, метод довольно прост, он принимает единственный параметр visible, которым мы задаём свойства Visible для всех нужных меток и панелей-кнопок.

Теперь выберем главную форму FrmTicTacToe в конструкторе и двойным кликом по ней перейдем в созданный метод-обработчик события Load, т.е. первичной загрузки формы. Разместим там следующий код:

        private void FrmTicTacToe_Load(object sender, EventArgs e) {
            labelPlayer1Name.Text = "?";
            labelPlayer2Name.Text = "?";
            SetPlayersLabelsAndScoreVisible(false);
        }

Здесь изначальные названия игроков будут равны некоторому неопределённому значению «?», а также мы вызываем метод SetPlayersLabelsAndScoreVisible со значением аргумента false, что говорит о том, что при загрузке главной формы лишние элементы управления будут изначально скрыты.

Напишем в коде главной формы ещё один метод ShowMainMenu(bool show):

        private void ShowMainMenu(bool show) {
            labelNewGameTitle.Visible = show;
            panelPlayerVsCpu.Visible = show;
            panelPlayerVsPlayer.Visible = show;
        }

Этот метод будет скрывать или отображать кнопки «Игрок против компьютера» и «Игрок против игрока», а также метку «Новая игра:». 

Добавим также в код главной формы следующий метод UpdateControls():

        private void UpdateControls() {
            ShowMainMenu(false);

            labelPlayer1Name.Text = "Игрок 1"; // engine.GetCurrentPlayer1Title();
            labelPlayer2Name.Text = "Игрок 2"; // engine.GetCurrentPlayer2Title();
            labelWhooseTurn.Text = "Ход игрока N"; // engine.GetWhooseTurnTitle();

            labelPlayer1Name.Top = labelNewGameTitle.Top;
            labelPlayer1Score.Top = labelPlayer1Name.Top;
            labelPlayer2Name.Top = labelPlayer1Name.Top + 37;
            labelPlayer2Score.Top = labelPlayer2Name.Top;
            labelNowTurnIs.Top = labelPlayer2Name.Top + 37;
            labelWhooseTurn.Top = labelNowTurnIs.Top;

            panelNewGame.Left = labelNowTurnIs.Left + 30;
            panelNewGame.Top = labelNowTurnIs.Bottom + 15;

            panelReset.Left = panelNewGame.Right + 15;
            panelReset.Top = panelNewGame.Top;

            SetPlayersLabelsAndScoreVisible(true);
        }

Метод устанавливает местоположение меток с именами игроков, счётом каждого игрока, индикатора хода и кнопок «Новая игра» и «Сброс» на форме, а также вызывает методы ShowMainMenu и SetPlayersLabelsAndScoreVisible. Также обратите внимание, что мы пока устанавливаем имена игроков в захардкоженные строки «Игрок 1», «Игрок 2», а индикатор хода в «Ход игрока N». В этих же строках кода идут комментарии с вызовом методов игрового движка, который позже будет определять корректные названия для текущих игроков и верное значение индикатора хода.

Создаём игровой движок

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

Для этого добавим к нашему проекту 2 новых класса и назовём их Cell и GameEngine. Добавить класс можно, кликнув правой кнопкой мыши на проекте TicTacToe и выбрав в контекстном меню  пункт Добавить → Класс…, как показано ниже:

 

Класс Cell для клетки игрового поля

Класс Cell будет хранить информацию о координатах клетки игрового поля и предоставит нам несколько полезных методов. Ниже показан его код (можно избавиться от всех импортов, добавляемых в класс при его создании — они нам будут не нужны):

namespace TicTacToe {
    internal class Cell {
        public int Row { get; set; }
        public int Column { get; set; }

        private Cell(int row, int column) {
            Row = row;
            Column = column;
        }

        public static Cell From(int row, int column) {
            return new Cell(row, column);
        }

        public static Cell ErrorCell() {
            return new Cell(-1, -1);
        }

        public bool IsErrorCell() {
            return Row == -1 && Column == -1;
        }

        public bool IsValidGameFieldCell() {
            return Row >= 0 && Row <= 2 && Column >= 0 && Column <= 2;
        }
    }
}

Как видим, у класса всего два целочисленных поля Row и Column, которые хранят индекс ряда и столбца в массиве игровых клеток (индексы начинаются с 0). У него есть параметризованный приватный конструктор Cell(int row, int column), который создаёт объект класса с заданными индексами ряда и столбца.

Статический метод From(int row, int column) будет позволять быстро создать объект класса по заданным индексам.

Статический метод ErrorCell() будет возвращать специальную, «ошибочную» клетку, которую мы сможем использовать в возвращаемых значениях методов игрового движка. Признак возврата «ошибочной» клетки будет нам говорить о том, что нужная/искомая клетка на игровом поле не была найдена и требуется сделать какое-то действие.

Метод IsErrorCell() проверяет текущие индексы ряда и столбца для объекта «клетка» и возвращает true, только если оба индекса равны -1.

Метод IsValidGameFieldCell() проверяет, являются ли текущие значения индексов для ряда и столбца допустимыми. Если да, то метод будет возвращать true, иначе false.

Теперь перейдем к написанию класса GameEngine самого игрового движка.

Класс GameEngine для игрового движка

В коде класса GameEngine мы сразу оставим импорт пространства имён System и добавим необходимый импорт для пространства имён System.Drawing, которое нам пригодится в дальнейшем для работы с системными цветами. Остальные неиспользуемые импорты можно удалить.

Добавим в класс два перечисления (enum) с именами GameMode и WhooseTurn:

using System;
using System.Drawing;

namespace TicTacToe {
    internal class GameEngine {
        internal enum GameMode {
            None,
            PlayerVsPlayer,
            PlayerVsCPU
        }

        internal enum WhooseTurn {
            Player1Human,
            Player2Human,
            Player2CPU
        }
    }
}

Первое перечисление задаёт текущий выбранный режим игры (None — игра не началась, PlayerVsPlayer — играют 2 человека, PlayerVsCPU — играет человек с компьютером), второй будет отвечать за индикатор текущего хода (Player1Human — ходит 1-й игрок человек, Player2Human — ходит 2-й игрок человек, Player2CPU — ходит 2-й игрок компьютер).

Добавим в наш класс GameEngine два поля Mode и Turn и установим первичные значения режима игры и того, чей сейчас ход:

        private GameMode Mode { get; set; } = GameMode.None;        
        private WhooseTurn Turn { get; set; } = WhooseTurn.Player1Human;

Добавим поле, которое будет хранить имя игрока-победителя в игре:

        private string Winner { get; set; } = "";

Добавим поля, которые будут содержать количество побед каждого игрока и количество раз, когда случилась ничья:

        private int player1Score = 0;
        private int player2Score = 0;
        private int numberOfDraws = 0;

Также введём символьные константы для обозначения незанятой клетки игрового поля (EMPTY_CELL), занятой крестиком (X_MARK) и ноликом (O_MARK). Также зададим строковые константы, содержащие имена игроков:

        const char EMPTY_CELL = '-';
        const char X_MARK = 'X';
        const char O_MARK = 'O';

        public const string PLAYER_HUMAN_TITLE = "Игрок";
        public const string PLAYER_CPU_TITLE = "Компьютер";

Теперь зададим двухмерный массив символов с именем gameField, который будет представлять наше игровое поле размером 3×3 клетки и по умолчанию инициализируем каждый элемент признаком незанятной клетки (EMPTY_CELL):

        private char[][] gameField = new char[][] {
            new char[] { EMPTY_CELL, EMPTY_CELL, EMPTY_CELL },
            new char[] { EMPTY_CELL, EMPTY_CELL, EMPTY_CELL },
            new char[] { EMPTY_CELL, EMPTY_CELL, EMPTY_CELL }
        };

Создадим несколько полезных методов в классе, которые впоследствии будем использовать:

        public GameMode GetCurrentMode() {
            return Mode;
        }

        public bool IsGameStarted() {
            return Mode != GameMode.None;
        }

        public WhooseTurn GetCurrentTurn() {
            return Turn;
        }

        public string GetWinner() {
            return Winner;
        }

        public bool IsPlayer1HumanTurn() {
            return Turn == WhooseTurn.Player1Human;
        }

        public void SetPlayer1HumanTurn() {
            Turn = WhooseTurn.Player1Human;
        }

        public void ResetScore() {
            player1Score = 0;
            player2Score = 0;
            numberOfDraws = 0;
        }

        public void PrepareForNewGame() {
            Mode = GameMode.None;
            ResetScore();
        }

Кратко их описание:

  • GetCurrentMode() — возвращает значение текущего режима игры, хранящейся в поле Mode
  • IsGameStarted() — возвращает true, если игра началась, т.е. значение Mode не равно GameMode.None
  • GetCurrentTurn() — возвращает текущее значение хода Turn для одного из игроков
  • GetWinner() — возвращает строку, содержащую имя игрока-победителя в игре
  • IsPlayer1HumanTurn() — возвращает true, если сейчас ход 1го игрока-человека
  • SetPlayer1HumanTurn() — устанавливает значение текущего хода Turn в значение WhooseTurn.Player1Human, т.е. ход передаётся 1-му игроку
  • ResetScore() — сбрасывает счёт обоих игроков в 0, а также обнуляет счётчик игр, сыгранных вничью
  • PrepareForNewGame() — сбрасывает все счётчики, а также устанавливает значение режима игры в GameMode.None. Подготавливает движок к новой игре.

Теперь добавим в движок метод с именем StartGame, который будет начинать новую игру в одном из двух возможных режимах:

        public void StartGame(GameMode gameMode) {
            if (gameMode == GameMode.None) {
                return;
            }

            ResetScore();

            Mode = gameMode;
            Turn = WhooseTurn.Player1Human;
        }

Логика метода простая: если параметр gameMode равен GameMode.None, то ничего не делаем и возвращаемся из метода. Если же передан правильный режим игры, то сбрасываем все счётчики с помощью вызова ResetScore(), устанавливаем режим движка Mode в значение, переданное при вызове метода, а далее, в зависимости от режима, в котором началась игра, устанавливаем текущий ход для 1-го игрока (т.к. у нас всегда начинает 1-й игрок, играющий за «крестики», независимо от режима игры).

Добавим в движок 2 метода, которые будут возвращать название 1-го и 2-го игроков — эти методы нам пригодятся для вывода в соответствующие метки на главной форме:

        public string GetCurrentPlayer1Title() {
            switch (Mode) {
                case GameMode.PlayerVsCPU:
                    return PLAYER_HUMAN_TITLE;
                case GameMode.PlayerVsPlayer:
                    return PLAYER_HUMAN_TITLE + " 1";                    
            }
            return "";
        }

        public string GetCurrentPlayer2Title() {
            switch (Mode) {
                case GameMode.PlayerVsCPU:
                    return PLAYER_CPU_TITLE;
                case GameMode.PlayerVsPlayer:
                    return PLAYER_HUMAN_TITLE + " 2";
            }
            return "";
        }

Логика методов также довольно простая — в зависимости от текущего режима игры, мы возвращаем значение ранее определённых в классе движка констант с именами игроков. Только если выбран режим игры «Игрок против игрока», то добавляем к константе PLAYER_HUMAN_TITLE цифру 1 или 2, чтобы различать игроков.

Далее добавим метод GetCurrentMarkLabelText, который будет возвращать текущую метку «X» или «O» в зависимости от того, чей сейчас ход:

        public string GetCurrentMarkLabelText() {
            if (IsPlayer1HumanTurn()) {
                return X_MARK.ToString();                
            } else {
                return O_MARK.ToString();                
            }
        }

Добавим ещё один метод GetCurrentMarkLabelColor, который вернёт системный цвет для крестиков и ноликов. Пусть крестики у нас будут окрашены в золотой цвет (Color.Gold), а нолики — в розовый (Color.Fuchsia):

        public Color GetCurrentMarkLabelColor() {
            if (IsPlayer1HumanTurn()) {
                return Color.Gold;
            } else {
                return Color.Fuchsia;
            }
        }

Добавим геттеры для получения счёта для 1го и 2го игроков:

        public int GetPlayer1Score() {
            return player1Score;
        }

        public int GetPlayer2Score() {
            return player2Score;
        }

Также напишем два метода, которые, в зависимости от текущего режима (Mode) и очередности хода (Turn), вернут строку, содержащую имя игрока. GetWhooseTurnTitle() будет возвращать имя игрока, кто ходит в текущий момент времени. GetWhooseNextTurnTitle() — вернёт имя игрока, чей будет следующий ход:

        /// <summary>
        /// Возвращает строку с именем игрока, чей ход в данный момент
        /// </summary>
        /// <returns>строка с именем игрока</returns>
        public string GetWhooseTurnTitle() {
            switch (Mode) {
                case GameMode.PlayerVsCPU:
                    return Turn == WhooseTurn.Player1Human ? PLAYER_HUMAN_TITLE : PLAYER_CPU_TITLE;
                case GameMode.PlayerVsPlayer:
                    return Turn == WhooseTurn.Player1Human ? PLAYER_HUMAN_TITLE + " 1" : PLAYER_HUMAN_TITLE + " 2";
            }
            return "";
        }

        /// <summary>
        /// Возвращает строку с именем игрока, для которого будет следующий ход
        /// </summary>
        /// <returns>строка с именем игрока</returns>
        public string GetWhooseNextTurnTitle() {
            switch (Mode) {
                case GameMode.PlayerVsCPU:
                    return Turn == WhooseTurn.Player1Human ? PLAYER_CPU_TITLE : PLAYER_HUMAN_TITLE;
                case GameMode.PlayerVsPlayer:
                    return Turn == WhooseTurn.Player1Human ? PLAYER_HUMAN_TITLE + " 2" : PLAYER_HUMAN_TITLE + " 1";
            }
            return "";
        }

Теперь добавим следующие методы в наш игровой движок:

  • ClearGameField() — для очистки всего игрового поля. Под очисткой мы понимаем пробег по нашей матрице gameField размером 3×3 клетки и заполнение каждой клетки значением пустой клетки (EMPTY_CELL)
  • MakeTurnAndFillGameFieldCell(int row, int column) — для осуществления хода игроком — т.е. для заполнения клетки игрового поля одним из маркеров — X_MARK для крестиков и O_MARK для ноликов, а также смены значения поля Turn, отвечающего за ход

Ниже представлен код этих методов:

        /// <summary>
        /// Очищает игровое поле, заполняя каждую клетку признаком
        /// пустой клетки (по умолчанию это символ '-')
        /// </summary>
        public void ClearGameField() {
            for (int row = 0; row < 3; row++) {
                for (int col = 0; col < 3; col++) {
                    gameField[row][col] = EMPTY_CELL;                    
                }
            }
        }

        public void MakeTurnAndFillGameFieldCell(int row, int column) {
            if (IsPlayer1HumanTurn()) {
                gameField[row][column] = X_MARK;
                if (Mode == GameMode.PlayerVsCPU) {
                    Turn = WhooseTurn.Player2CPU;                    
                } else if (Mode == GameMode.PlayerVsPlayer) {
                    Turn = WhooseTurn.Player2Human;                    
                }
            } else {
                gameField[row][column] = O_MARK;
                Turn = WhooseTurn.Player1Human;
            }
        }

Теперь давайте разберём то, как будет действовать компьютер в режиме игры «Игрок против компьютера». Т.е. фактически сейчас мы проработаем с вами стратегию хода компьютером и заложим её в наш игровой движок.

Ход компьютера разобьём на три фазы или стратегии, которые будут применяться c каждым ходом компьютера, по очереди:

  • Стратегия 1 («атакующая») — если до победы компьютеру остаётся всего лишь один ход — (не важно — по горизонтальным, вертикальным или диагональным клеткам), то нужно непременно ставить «нолик» в ту свободную клетку, которая приведёт компьютер к победе. Тем самым компьютер выиграет человека. Это стратегия с первым приоритетом.
  • Стратегия 2 («защитная») — если же стратегия 1 не сработала, и на игровом поле нет таких клеток, которые сразу приведут компьютер к победе, то компьютеру нужно применить «защитную» стратегию. Это означает, что ему нужно проверить — «а не выиграет ли меня человек своим следующим ходом?». Т.е. компьютер должен помешать игроку-человеку победить, ставя «нолик» в клетки, ведущие игрока-человека к победе. У этой стратегии второй приоритет выполнения.
  • Стратегия 3 («произвольный ход») — применяется, когда неуспешны стратегии 1 и 2. Обычно это характерно для самых первых ходов игры, когда ни один из игроков не имеет выигрышную позицию, ведущую следующим ходом к победе. Мы здесь не будем усложнять и применим рандомизацию — т.е. компьютер будет вычислять произвольную свободную клетку поля и ставить туда «нолик». Это стратегия с наименьшим приоритетом.

Вроде всё понятно, осталось реализовать все три стратегии в нашем движке. Но сперва обратим внимание на некоторую общность между 1-й и 2-й стратегиями — фактически они обе проверяют позиции заполненных клеток — либо на предмет «атаки», либо на предмет «защиты». Разница лишь в том, клетки с какими фигурами («крестиками» или «ноликами») нужно проверять на предмет того, что по горизонтали, вертикали или диагонали кому-то из игроков остался всего лишь 1 ход до победы.

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

  • Cell GetHorizontalCellForAttackOrDefence(char checkMark) — метод будет находить клетку по горизонтали, которую нужно «атаковать» для победы или в которую нужно ставить «нолик» при защитной стратегии. Алгоритм метода: бежим двумя циклами по всем клеткам игрового поля — по рядам, сверху-вниз. Для очередного ряда во внутреннем цикле получаем сумму заполненных клеток с определённой фигурой (признак фигуры мы получаем во входном параметре checkMark). Перед внутренним циклом мы задаем переменную int freeCol = -1, которая будет хранить индекс столбца со свободной клеткой в текущем ряду. По выходу из внутреннего цикла по столбцам мы проверяем два признака: если сумма фигур checkMark в текущем ряду равна 2, и при этом индекс столбца со свободной клеткой не равен -1, то это значит, что стратегия сработала, и нам нужно вернуть клетку с индексами текущего ряда и столбца, равного freeCol.
  • Cell GetVerticalCellForAttackOrDefence(check checkMark) — метод аналогичен предыдущему, только пробег по игровому полю уже осуществляем по столбцам, сверху-вниз, поэтому внешний цикл организован по столбцам, а внутренний — по рядам. Вместо freeCol теперь у нас переменная freeRow, и в неё мы пытаемся сохранить индекс ряда со свободной клеткой (если такая есть). Принцип действия такой же, только теперь мы считаем сумму фигур checkMark по столбцам, и в случае срабатывания стратегии вернём клетку с индексом ряда freeRow и текущего столбца, в котором находимся.
  • Cell GetDiagonalCellForAttackOrDefence(check checkMark) — метод похож на два предыдущих, но с некоторыми отличиями: во-первых, у нас теперь только один цикл по рядам, а индекс столбца, который выведет нас на клетку, принадлежащую одной или второй диагонали, мы вычисляем по формуле. Под первой диагональю будем понимать ту, что проходит через клетки с индексами (0; 0), (1; 1), (2; 2). Легко догадаться, что формула вычисления индекса столбца по индексу ряда следующая: <column> = row, где row — текущий ряд, а <column> — вычисляемый индекс столбца. Вторая же диагональ проходит через клетки с индексами (0; 2), (1; 1), (2; 0). И в этом случае индекс столбца для клетки, принадлежащей диагонали, вычисляется так: <column> = 2 — row, где row — текущий ряд, а <column> — вычисляемый индекс столбца. Мы также должны хранить две отдельных суммы для каждой диагонали, в которые будем добавлять 1, если текущая просматриваемая клетка равна интересующей нас фигуре checkMark. И отдельно храним пары индексов freeRow1, freeCol1 и freeRow2, freeCol2 для свободной клетки по первой и второй диагонали. Как и в двух других методах, мы проверяем, что если сумма интересующих фигур в диагонали равна 2, и при этом на диагонали есть свободная клетка, то мы вернём её координаты.

Ниже представлен код этих трёх методов:

        private Cell GetHorizontalCellForAttackOrDefence(char checkMark) {
            for (int row = 0; row < 3; row++) {
                int currentSumHorizontal = 0;
                int freeCol = -1;
                for (int col = 0; col < 3; col++) {
                    if (gameField[row][col] == EMPTY_CELL) {
                        freeCol = col;
                    }
                    currentSumHorizontal += gameField[row][col] == checkMark ? 1 : 0;
                }

                if (currentSumHorizontal == 2 && freeCol >= 0) {
                    return Cell.From(row, freeCol);
                }
            }
            return Cell.ErrorCell();
        }

        private Cell GetVerticalCellForAttackOrDefence(char checkMark) {            
            for (int col = 0; col < 3; col++) {
                int currentSumVert = 0;
                int freeRow = -1;
                for (int row = 0; row < 3; row++) {
                    if (gameField[row][col] == EMPTY_CELL) {
                        freeRow = row;
                    }
                    currentSumVert += gameField[row][col] == checkMark ? 1 : 0;
                }

                if (currentSumVert == 2 && freeRow >= 0) {
                    return Cell.From(freeRow, col);
                }
            }
            return Cell.ErrorCell();
        }

        private Cell GetDiagonalCellForAttackOrDefence(char checkMark) {
            // диагональ 1:
            // * - -
            // - * -
            // - - *
            // координаты клеток: (0; 0), (1; 1), (2; 2)
            // формула для вычисления столбца по ряду: <column> = row

            // диагональ 2:
            // - - *
            // - * -
            // * - -
            // координаты клеток: (0; 2), (1; 1), (2, 0)
            // формула для вычисления столбца по ряду: <column> = 2 - row

            int diagonal1Sum = 0;
            int diagonal2Sum = 0;
            int freeCol1 = -1, freeRow1 = -1;
            int freeCol2 = -1, freeRow2 = -1;
            for (int row = 0; row < 3; row++) {
                diagonal1Sum += gameField[row][row] == checkMark ? 1 : 0;
                diagonal2Sum += gameField[row][2 - row] == checkMark ? 1 : 0;

                if (gameField[row][row] == EMPTY_CELL) {
                    freeCol1 = row;
                    freeRow1 = row;
                }
                if (gameField[row][2 - row] == EMPTY_CELL) {
                    freeCol2 = 2 - row;
                    freeRow2 = row;
                }

                if (diagonal1Sum == 2 && freeRow1 >= 0 && freeCol1 >= 0) {
                    return Cell.From(freeRow1, freeCol1);
                } else if (diagonal2Sum == 2 && freeRow2 >= 0 && freeCol2 >= 0) {
                    return Cell.From(freeRow2, freeCol2);
                }
            }

            return Cell.ErrorCell();
        }

Теперь, когда универсальные методы для «атакующей» и «защитной» стратегий для компьютера готовы, нам остаётся их использовать.

Напишем в классе движка следующий код:

        private Cell ComputerTryAttackHorizontalCell() {
            return GetHorizontalCellForAttackOrDefence(O_MARK);
        }

        private Cell ComputerTryAttackVerticalCell() {
            return GetVerticalCellForAttackOrDefence(O_MARK);
        }

        private Cell ComputerTryAttackDiagonalCell() {
            return GetDiagonalCellForAttackOrDefence(O_MARK);
        }

        private Cell ComputerTryDefendHorizontalCell() {
            return GetHorizontalCellForAttackOrDefence(X_MARK);
        }

        private Cell ComputerTryDefendVerticalCell() {
            return GetVerticalCellForAttackOrDefence(X_MARK);
        }

        private Cell ComputerTryDefendDiagonalCell() {
            return GetDiagonalCellForAttackOrDefence(X_MARK);
        }

        private Cell ComputerTryAttackCell() {
            // Пытаемся атаковать по горизонтальным клеткам
            Cell attackedHorizontalCell = ComputerTryAttackHorizontalCell();
            if (!attackedHorizontalCell.IsErrorCell()) {
                return attackedHorizontalCell;
            }

            // Пытаемся атаковать по вертикальным клеткам
            Cell attackedVerticalCell = ComputerTryAttackVerticalCell();
            if (!attackedVerticalCell.IsErrorCell()) {
                return attackedVerticalCell;
            }

            // Пытаемся атаковать по диагональным клеткам
            Cell attackedDiagonalCell = ComputerTryAttackDiagonalCell();
            if (!attackedDiagonalCell.IsErrorCell()) {
                return attackedDiagonalCell;
            }

            // Нет приемлемых клеток для атаки - возвращаем спецклетку с признаком ошибки
            return Cell.ErrorCell();
        }

        private Cell ComputerTryDefendCell() {
            // Пытаемся защищаться по горизонтальным клеткам
            Cell defendedHorizontalCell = ComputerTryDefendHorizontalCell();
            if (!defendedHorizontalCell.IsErrorCell()) {
                return defendedHorizontalCell;
            }

            // Пытаемся защищаться по вертикальным клеткам
            Cell defendedVerticalCell = ComputerTryDefendVerticalCell();
            if (!defendedVerticalCell.IsErrorCell()) {
                return defendedVerticalCell;
            }

            // Пытаемся защищаться по диагональным клеткам
            Cell defendedDiagonalCell = ComputerTryDefendDiagonalCell();
            if (!defendedDiagonalCell.IsErrorCell()) {
                return defendedDiagonalCell;
            }

            // Нет приемлемых клеток для обороны - возвращаем спецклетку с признаком ошибки
            return Cell.ErrorCell();
        }

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

  • ComputerTryAttackCell() — пытается «атаковать» свободную клетку для выигрыша компьютера, по очереди проверяя клетки по горизонтали, вертикали и диагонали. Если клетки для атаки не найдены, возвращается спецклетка с индексами (-1; -1), что означает, что атакующая стратегия не сработала.
  • ComputerTryDefendCell() — пытается «защитить» клетку от ближайшего выигрыша игроком-человеком, по очереди проверяя клетки по горизонтали, вертикали и диагонали. Если клетки для защиты не найдены, возвращается спецклетка с индексами (-1; -1), что означает, что защитная стратегия не сработала.

Теперь, когда мы запрограммировали «атакующую» и «защитную» стратегии компьютера, осталась последняя стратегия — выбор произвольной клетки.

Реализуем её в методе ComputerTrySelectRandomFreeCell():

        private Cell ComputerTrySelectRandomFreeCell() {
            Random random = new Random();
            int randomRow, randomCol;
            const int max_attempts = 1000;  // кол-во попыток можно настроить по вкусу. чем больше, тем вероятнее может произойти подвисание, когда 
                                            // свободных клеток на поле всё меньше, и рандомный перебор и поиск свободной не приносит быстрого результата
            int current_attempt = 0;
            do {
                randomRow = random.Next(3);
                randomCol = random.Next(3);
                current_attempt++;
            } while (gameField[randomRow][randomCol] != EMPTY_CELL && current_attempt <= max_attempts);

            if (current_attempt > max_attempts) {
                // мы не смогли выбрать рандомную свободную клетку за 1000 попыток, поэтому выбираем вручную
                // ближайшую клетку простым перебором по всем клеткам игрового поля
                for (int row = 0; row < 3; row++) {
                    for (int col = 0; col < 3; col++) {
                        if (gameField[row][col] == EMPTY_CELL) {
                            // клетка свободна, сразу возвращаем её
                            return Cell.From(row, col);
                        }
                    }
                }
            }

            return Cell.From(randomRow, randomCol);
        }

Этот метод генерирует произвольный индекс ряда (randomRow) и столбца (randomRow) в цикле — до тех пор, пока сгенерированные индексы не дают нам свободную от «крестиков» и «ноликов» клетку. Во избежание ситуации, когда мы так и не сможем рандомно найти свободную клетку мы предусматриваем максимальное количество попыток поиска свободной клетки, которое задаём в константе max_attempts. Каждую итерацию цикла мы увеличиваем «счётчик попыток подбора свободной клетки», хранимый в переменной current_attempt.

По выходу из цикла мы проверим — исчерпался ли счётчик попыток подбора. Если да, мы задействуем самую элементарную стратегию подбора свободной для хода клетки — просто бежим по рядам и столбцам по игровому полю до тех пор, пока не найдем свободную клетку (EMPTY_CELL) и её же вернем из метода.

Но, если сработала логика произвольного подбора свободной клетки, то последней строкой метода мы вернём найденную клетку с индексами (randomRow, randomCol).

Теперь давайте добавим следующие два метода в движок — IsAnyFreeCell(), который вернёт true, если на поле осталась хоть одна свободная клетка, а также метод MakeComputerTurnAndGetCell(), который и делает «ход компьютера» — т.е. применяет все три стратегии по очереди и возвращает координаты клетки, выбранной компьютером:

        /// <summary>
        /// Возвращает true, если есть хотя бы одна незанятая клетка на игровом поле и false в противном случае
        /// </summary>
        /// <returns>true при наличии хотя бы одной свободной клетки на поле, иначе false</returns>
        public bool IsAnyFreeCell() {            
            for (int row = 0; row < 3; row++) {
                for (int col = 0; col < 3; col++) {
                    if (gameField[row][col] == EMPTY_CELL) {
                        return true;
                    }
                }
            }
            return false;
        }

        public Cell MakeComputerTurnAndGetCell() {
            // Стратегия 1 - компьютер пытается сначала атаковать, если ему до победы остался всего лишь один ход
            Cell attackedCell = ComputerTryAttackCell();
            if (!attackedCell.IsErrorCell()) {
                return attackedCell;
            }

            // Стратегия 2 - если нет приемлемых клеток для атаки, компьютер попытается найти клетки, которые нужно защитить,
            // чтобы предотвратить победу человека
            Cell defendedCell = ComputerTryDefendCell();
            if (!defendedCell.IsErrorCell()) {
                return defendedCell;
            }

            // Стратегия 3 - у комьютера нет приемлемых клеток для атаки и защиты, поэтому ему нужно выбрать произвольную свободную клетку
            // для его очередного хода
            if (IsAnyFreeCell()) {
                Cell randomFreeCell = ComputerTrySelectRandomFreeCell();
                return randomFreeCell;
            }

            return Cell.ErrorCell();
        }

Друзья, позади уже много кода, и нам осталась самая малость — для завершения написания движка нам нужно реализовать в нём две последние вещи:

  • логику определения, что произошла ничья — мы должны проверить, что на поле не осталось больше свободных клеток и, если это так, то увеличить счётчик для игр вничью
  • логику определения, что произошла победа — для этого мы должны подсчитать сумму одинаковых фигур по горизонтали, вертикали и диагоналям. Если эта сумма равна 3, то в зависимости от фигуры провозгласить, какой игрок победил и увеличить переменную его счёта.

Давайте добавим эту логику в игровой движок:

Метод IsDraw() — вернёт true, если произошла ничья, а также увеличит счётчик игр вничью:

        /// <summary>
        /// Возвращает true и увеличивает счётчик ничьих, если произошла очередная ничья.        
        /// </summary>
        /// <returns>true, если произошла ничья, в противном случае false</returns>
        public bool IsDraw() {
            bool isNoFreeCellsLeft = !IsAnyFreeCell();
            if (isNoFreeCellsLeft) {                
                numberOfDraws++;
            }
            return isNoFreeCellsLeft;
        }

Три метода, которые проверяют признак победы одного из игроков:

  • CheckWinOnHorizontalCellsAndUpdateWinner() — проверит победителя по горизонтальным клеткам и обновит поле Winner, а также увеличит соответствующий счётчик побед
  • CheckWinOnVerticalCellsAndUpdateWinner() — — проверит победителя по вертикальным клеткам и обновит поле Winner, а также увеличит соответствующий счётчик побед
  • CheckWinOnDiagonalCellsAndUpdateWinner() — проверит победителя по диагональным клеткам и обновит поле Winner, а также увеличит соответствующий счётчик побед
        /// <summary>
        /// Проверяет наличие победы какого-либо из игроков по горизонтальным клеткам игрового поля
        /// </summary>
        /// <returns></returns>
        private bool CheckWinOnHorizontalCellsAndUpdateWinner() {
            for (int row = 0; row < 3; row++) {
                int sumX = 0; int sumO = 0;
                for (int col = 0; col < 3; col++) {
                    sumX += gameField[row][col] == X_MARK ? 1 : 0;
                    sumO += gameField[row][col] == O_MARK ? 1 : 0;
                }
                if (sumX == 3) {
                    // X победили
                    Winner = Mode == GameMode.PlayerVsPlayer ? PLAYER_HUMAN_TITLE + " 1" : PLAYER_HUMAN_TITLE;
                    player1Score++;
                    return true;
                } else if (sumO == 3) {
                    // O победили
                    Winner = Mode == GameMode.PlayerVsPlayer ? PLAYER_HUMAN_TITLE + " 2" : PLAYER_CPU_TITLE;
                    player2Score++;
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Проверяет наличие победы какого-либо из игроков по вертикальным клеткам игрового поля
        /// </summary>
        /// <returns></returns>
        private bool CheckWinOnVerticalCellsAndUpdateWinner() {
            for (int col = 0; col < 3; col++) {
                int sumX = 0; int sumO = 0;
                for (int row = 0; row < 3; row++) {
                    sumX += gameField[row][col] == X_MARK ? 1 : 0;
                    sumO += gameField[row][col] == O_MARK ? 1 : 0;
                }

                if (sumX == 3) {
                    // X победили
                    Winner = Mode == GameMode.PlayerVsPlayer ? PLAYER_HUMAN_TITLE + " 1" : PLAYER_HUMAN_TITLE;
                    player1Score++;
                    return true;
                } else if (sumO == 3) {
                    // O победили
                    Winner = Mode == GameMode.PlayerVsPlayer ? PLAYER_HUMAN_TITLE + " 2" : PLAYER_CPU_TITLE;
                    player2Score++;
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Проверяет наличие победы какого-либо из игроков по диагональным клеткам игрового поля
        /// </summary>
        /// <returns></returns>
        private bool CheckWinOnDiagonalCellsAndUpdateWinner() {
            int diag1sumX = 0, diag2sumX = 0;
            int diag1sumO = 0, diag2sumO = 0;
            for (int row = 0; row < 3; row++) {
                if (gameField[row][row] == O_MARK) {
                    diag1sumO++;
                }
                if (gameField[row][row] == X_MARK) {
                    diag1sumX++;
                }
                if (gameField[row][2 - row] == O_MARK) {
                    diag2sumO++;
                }
                if (gameField[row][2 - row] == X_MARK) {
                    diag2sumX++;
                }
            }

            if (diag1sumX == 3 || diag2sumX == 3) {
                Winner = Mode == GameMode.PlayerVsPlayer ? PLAYER_HUMAN_TITLE + " 1" : PLAYER_HUMAN_TITLE;
                player1Score++;
                return true;
            } else if (diag1sumO == 3 || diag2sumO == 3) {
                Winner = Mode == GameMode.PlayerVsPlayer ? PLAYER_HUMAN_TITLE + " 2" : PLAYER_CPU_TITLE;
                player2Score++;
                return true;
            }

            return false;
        }

Наконец, остался последний метод для нашего движка — IsWin(), который по очереди вызывает три предыдущих и возвращает true, если была победа, в противном случае возвращает false:

        /// <summary>
        /// Возвращает true, если кто-то из игроков выиграл
        /// </summary>
        /// <returns>true, если какой-то из игроков выиграл, иначе false</returns>
        public bool IsWin() {
            if (CheckWinOnHorizontalCellsAndUpdateWinner()) {
                return true;
            }

            if (CheckWinOnVerticalCellsAndUpdateWinner()) {
                return true;
            }

            if (CheckWinOnDiagonalCellsAndUpdateWinner()) {
                return true;
            }

            return false;
        }

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

UI + GameEngine. Подключаем готовый игровой движок к главной форме приложения

Возвратимся на главную форму FrmTicTacToe для нашей игры. В самое начало класса добавляем новое поле engine класса GameEngine и инициализируем наш движок с помощью оператора new:

using System;
using System.Drawing;
using System.Windows.Forms;

namespace TicTacToe {
    public partial class FrmTicTacToe : Form {
        // подключаем наш игровой движок:
        private GameEngine engine = new GameEngine();
        
        // ... ранее написанный код формы ...
    }
}

Добавим к главной форме также следующие новые методы:

  • GetPanelCellControlByCell(Cell cell) — получает по экземпляру клетки cell название элемента Panel, отвечающего за эту клетку игрового поля. Затем среди всех элементов формы находит панель с данным названием и возвращает её в виде экземпляра класса Panel.
  • ClearGameField() — вызывает очистку игрового поля через одноимённый метод игрового движка, а также очищает всё внутреннее содержимое панели, представляющей клетку
        private Panel GetPanelCellControlByCell(Cell cell) {            
            if (cell == null || !cell.IsValidGameFieldCell()) {
                return null;
            }
            string panelCtrlName = "panelCell" + cell.Row + "_" + cell.Column;
            foreach (Control ctrl in this.Controls) {
                if (ctrl.Name.Equals(panelCtrlName) && ctrl is Panel) {
                    return (Panel)ctrl;
                }
            }

            return null;
        }

        private void ClearGameField() {
            engine.ClearGameField();

            for (int row = 0; row < 3; row++) {
                for (int col = 0; col < 3; col++) {
                    Panel panelCell = GetPanelCellControlByCell(Cell.From(row, col));
                    if (panelCell != null) {
                        panelCell.Controls.Clear();
                    }
                }
            }

            engine.SetPlayer1HumanTurn();
            labelWhooseTurn.Text = engine.GetWhooseTurnTitle();
        }

Помните метод FillCell(Panel panel, int row, int column) на главной форме, который мы добавили, но оставили пустым? Пришло время наполнить его логикой, поскольку наш игровой движок уже готов.

Напишем в нём следующий код:

        private void FillCell(Panel panel, int row, int column) {
            if (!engine.IsGameStarted()) {
                // если игра не началась, не рисовать ничего на игровом поле и просто вернуться
                return; 
            }

            Label markLabel = new Label();
            markLabel.Font = new Font(FontFamily.GenericMonospace, 72, FontStyle.Bold);
            markLabel.AutoSize = true;
            markLabel.Text = engine.GetCurrentMarkLabelText();
            markLabel.ForeColor = engine.GetCurrentMarkLabelColor();

            labelWhooseTurn.Text = engine.GetWhooseNextTurnTitle();

            engine.MakeTurnAndFillGameFieldCell(row, column);

            panel.Controls.Add(markLabel);

            if (engine.IsWin()) {
                // Движок вернул результат, что произошла победа одного из игроков
                MessageBox.Show("Победа! Выиграл " + engine.GetWinner(), "Крестики-Нолики", MessageBoxButtons.OK, MessageBoxIcon.Information);
                labelPlayer1Score.Text = engine.GetPlayer1Score().ToString();
                labelPlayer2Score.Text = engine.GetPlayer2Score().ToString();
                ClearGameField();
            } else if (engine.IsDraw()) {
                // Движок вернул результат, что произошла ничья
                MessageBox.Show("Ничья!", "Крестики-Нолики", MessageBoxButtons.OK, MessageBoxIcon.Information);
                ClearGameField();
            } else {
                // Ещё остались свободные клетки на поле. Если ход компьютера - вызываем движок для определения клетки, которую
                // выберет комьютер для хода
                if (engine.GetCurrentTurn() == GameEngine.WhooseTurn.Player2CPU) {
                    Cell cellChosenByCpu = engine.MakeComputerTurnAndGetCell();
                    if (!cellChosenByCpu.IsErrorCell()) {
                        Panel panelCell = GetPanelCellControlByCell(cellChosenByCpu);
                        if (panelCell != null) {
                            FillCell(panelCell, cellChosenByCpu.Row, cellChosenByCpu.Column);
                        } else {
                            // что-то пошло не так, мы не смогли найти верный элемент Panel по клетке, выбранной компьютером
                            // покажем ошибку
                            MessageBox.Show(
                                "Произошла ошибка: выбранная компьютером клетка не должна быть равна null!",
                                "Крестики-Нолики",
                                MessageBoxButtons.OK,
                                MessageBoxIcon.Error
                            );
                        }
                    } else {
                        // что-то пошло не так, движок вернул спецклетку, хотя такого быть не должно.
                        // покажем ошибку
                        MessageBox.Show(
                            "Произошла ошибка: компьютер не смог выбрать клетку для хода!", 
                            "Крестики-Нолики", 
                            MessageBoxButtons.OK, 
                            MessageBoxIcon.Error
                        );
                    }
                }
            }
        }

Как можно видеть, метод FillCell динамически создаёт новую метку (Label) с определённым шрифтом, текстом и стилем. Это и есть маркер «крестика» или «нолика» для заполнения клетки игрового поля, которой является определённая панель (Panel), передаваемая методу в параметре panel. Координаты же клетки нужны для маркировки клетки игрового поля в самом движке — чтобы пометить эту клетку как занятую.

Метод также проверяет, произошла ли победа кого-то из игроков сразу после заполнения клетки игрового поля. Если да, то выводится сообщение о победе игрока, обновляется текст со счётом игроков и производится очистка игрового поля. Если победы не было, но была ничья, то также выводится сообщение и очищается игровое поле. Наконец, если не было победы и не произошла ничья, то в случае режима игры «Игрок против компьютера» мы вызываем метод движка MakeComputerTurnAndGetCell(), который выполнит расчёт логики компьютера и выберет клетку для хода, которую мы тут же заполним с помощью этого же метода FillCell. Если же играют два человека, то просто они по очереди ставят «крестики» и «нолики» до тех пор, пока кто-то не выиграет, либо не произойдет ничья.

Добавим пару методов: ResetGame() — для сброса игры и StartNewGame() — для начала новой игры:

        private void ResetGame() {
            ClearGameField();
            engine.StartGame(engine.GetCurrentMode());
            labelPlayer1Score.Text = engine.GetPlayer1Score().ToString();
            labelPlayer2Score.Text = engine.GetPlayer2Score().ToString();
            UpdateControls();
        }

        private void StartNewGame() {
            ClearGameField();
            engine.PrepareForNewGame();
            
            labelPlayer1Score.Text = engine.GetPlayer1Score().ToString();
            labelPlayer2Score.Text = engine.GetPlayer2Score().ToString();

            ShowMainMenu(true);
            SetPlayersLabelsAndScoreVisible(false);
        }

Главная форма — последние штрихи

В методе UpdateControls() главной формы мы теперь можем раскомментировать строки, убрав хардкод для имён игроков и индикатора хода, обращаясь к нашему игровому движку:

        private void UpdateControls() {
            // ...
            labelPlayer1Name.Text = engine.GetCurrentPlayer1Title();
            labelPlayer2Name.Text = engine.GetCurrentPlayer2Title();
            labelWhooseTurn.Text = engine.GetWhooseTurnTitle();
            // ...
        }

Заполним пустые обработчики для события Click при нажатии на кнопку «Новая игра»:

        private void panelNewGame_Click(object sender, EventArgs e) {
            StartNewGame();
        }

        private void labelNewGame_Click(object sender, EventArgs e) {
            StartNewGame();
        }

Аналогичным образом заполним пустые обработчики для события Click при нажатии на кнопку «Сброс»:

        private void panelReset_Click(object sender, EventArgs e) {
            ResetGame();
        }

        private void labelReset_Click(object sender, EventArgs e) {
            ResetGame();
        }

Также добавим простой метод StartNewGameInSelectedMode(GameEngine.GameMode selectedMode) и заполним пустые обработчики для события Click для кнопок «Игрок против компьютера» и «Игрок против игрока»:

        private void StartNewGameInSelectedMode(GameEngine.GameMode selectedMode) {
            engine.StartGame(selectedMode);
            UpdateControls();
        }

        private void panelPlayerVsCpu_Click(object sender, EventArgs e) {
            StartNewGameInSelectedMode(GameEngine.GameMode.PlayerVsCPU);
        }

        private void panelPlayerVsPlayer_Click(object sender, EventArgs e) {
            StartNewGameInSelectedMode(GameEngine.GameMode.PlayerVsPlayer);
        }

        private void labelPlayerVsCpu_Click(object sender, EventArgs e) {
            StartNewGameInSelectedMode(GameEngine.GameMode.PlayerVsCPU);
        }

        private void labelPlayerVsPlayer_Click(object sender, EventArgs e) {
            StartNewGameInSelectedMode(GameEngine.GameMode.PlayerVsPlayer);
        }

Ну вот и всё, мы закончили написание игры «Крестики-Нолики» на языке C# с использованием Windows Forms. Можете теперь запустить игру и попробовать её в одном из доступных режимов.

Примерно так выглядит игровой процесс в нашей игре:

 

Ссылка на архив с проектом игры для Microsoft Visual Studio и исполняемыми файлами игры: здесь

Вы здесь

Пример программы игры в крестики-нолики

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

Матрица для игры в крестики-нолики имеет вид двумерного массива символов 3 на 3. Пользователь всегда играет крестиками, а компьютер — ноликами. Когда ходит пользователь, «X» помещается в указанную позицию матрицы. Когда наступает очередь ходить компьютеру, он сканирует матрицу и помещает «О» в пустое место матрицы. (Это достаточно тупая игра и можно получить определенное удовольствие, немного ее улучшив.) Если компьютер не может найти пустой ячейки, он выводит результат игры и завершает работу программы. Игровая матрица инициализируется так, что в начале игры она содержала пробелы. Ниже показана программа крестики-нолики:

#include <stdio.h>
#include <stdlib.h>

/* простая программа игры в крестики-нолики */
#define SPACE ‘ ‘
char matrix[3][3] = { /* матрица для крестиков-ноликов */
{SPACE, SPACE, SPACE},
{SPACE, SPACE, SPACE},
{SPACE, SPACE, SPACE}
} ;
void get_computer_move(void), get_player_move(void);
void disp_matrix(void);
char check (void);
int main()
{
char done;
printf(«This is the game of Tic-Tac-Toe.n»);
printf(«You will be playing against the computer.n»);
done = SPACE;
do {
disp_matrix(); /* вывод игровой доски */
get_player_move(); /* ходит игрок */
done = check(); /* проверка на победу */
if (done!=SPACE) break; /* победитель */
get_computer_move(); /* ходит компьютер */
done=check(); /* проверка на победу */
} while(done==SPACE);
if(done==’X’) printf(«You won!n»);
else printf(«I won!!!!n»);
disp_matrix(); /* отображение результирующего положения */
return 0;
}

/* ввод хода игрока */
void get_player_move(void)
{
int x, у;
printf(«Enter coordinates for your X.n»);
printf(«Row? «);
scanf («%d», &x);
printf(«Column? «);
scanf(«%d», &y);
х—; y—;
if (x<0 || y<0 || x>2 || y>2 || matrix[x] [y] !=SPACE)
{
printf(«Invalid move, try again.n»);
get_player_move();
}
else matrix[x][y]=’X’;
}

/* ход компьютера */
void get_computer_move(void)
{
register int t;
char *p;
p = (char *) matrix;
for (t=0; *p!=SPACE && t<9; ++t) p++;
if(t==9)
{
printf(«drawn»);
exit(0); /* game over */
}
else *p = ‘O’;
}

/* отображение игровой доски */
void disp_matrix(void)
{
int t;
for(t=0; t<3; t++)
{
printf(» %c | %c | %c», matrix[t][0], matrix[t][1], matrix[t][2]);
if(t!=2) printf(«n-|-|-n»);
}
printf(«n»);
}

/* проверка на победу */
char check(void)
{
int t;
char *p;
for(t=0; t<3; t++) { /* проверка строк */
p = &matrix[t] [0];
if (*p==* (p+1) && * (p+1)==*(p+2)) return *p;
}
for(t=0; t<3; t++) { /* проверка столбцов */
p = &matrix[0][t];
if(*p==*(p+3) && *(p+3)==*(p+6)) return *p;
}

/* проверка диагоналей */
if(matrix[0] [0]==matrix [1] [1] && matrix[1] [1]==matrix [2] [2] )
return matrix[0][0];
if(matrix[0][2]==matrix[1][1] && matrix[1][1]==matrix[2] [0])
return matrix[0][2];
return SPACE;
}

Массив инициализируется пробелами, поскольку пробелы используются для отображения вакантного состояния функциями get_player_move() и get_computer_move(). Тот факт, что пробелы используются вместо нулевых символов, упрощает функцию отображения матрицы — disp_matrix(), позволяя выводить содержимое массива на экран без преобразований. Обратим внимание, что процедура get_player_move() вызывает рекурсию в случае ввода неправильного значения. Это пример использования рекурсий для упрощения подпрограммы и уменьшения количества кода, необходимого для реализации функции.

В главном цикле каждый раз при вводе хода вызывается функция check(). Данная функция определяет, выиграна ли игра и, если да, то кем. Функция check() возвращает «X» в случае выигрыша пользователя, или «О», если выиграл компьютер. В противном случае она возвращает пробел. check() сканирует строки, столбцы и диагонали с целью поиска выигрышной конфигурации.

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

крестики нолики код на c#

Доброго времени суток. На данной странице я приведу пример создания всеми любимой логической игры под названием «крестики-нолики». Так как программирую я в основном на C#, то и представленная программа будет так же на данном языке программирования. Но даже если Вы не знакомы с C#, а программируете на C++, Java, Delphi или на любом другом языке, данный пост в любом случае поможет Вам, так как тут я приведу алгоритм работы такой программы.

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

Всю программу можно поделить на две большие части:

  1. Компьютер ходит первым(крестики)
  2. Компьютер ходит вторым(нолики)

Далее я опишу стратегию игры компьютера, которого мы собираемся сделать непобедимым.

Стратегия игры «крестики-нолики»

Основополагающие правила

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

  1. Можно ли выиграть? Если да, то компьютер выигрывает!
  2. Угрожает ли нам опасность проиграть следующим ходом? Если да, то закрываем такую возможность!

Компьютер ходит первым

Компьютер свой первый ход делает в центр. Тут другого варианта нет. Дальше ходит пользователь, у которого есть только два варианта: либо сходить в угол, либо сходить на сторону. Если пользователь ходит в угол, то компьютер отвечает ходом в противоположный угол. Если же пользователь ходит на сторону, то компьютер должен сходить в один из углов, которые расположены дальше всего от хода пользователя. Таких углов, если Вы понимаете о чем я, всего два. Разницы в какой из них сходить нет. Если эти противоположные углы заняты, компьютер ходит в любое свободное место(это условие актуально и если пользователь сходил в угол, и если он сходил на сторону). Данное правило ходить как можно дальше от предыдущего хода соперника актуально до конца игры, в которой компьютер сходил первым.

Компьютер ходит вторым

Когда компьютер ходит вторым, а пользователь, соответственно, первым, может возникнуть несколько ситуаций:

  1. Первый ход в центр. Тогда ходим до конца игры в любой из углов, а если это невозможно, то в любую свободную клетку.
  2. Первый ход в угол. Отвечаем ходом в центр. Следующие ходы — в любой из свободных углов, иначе — в свободные клетки.
  3. Первый ход в сторону. Ситуация аналогичная предыдущей: по максимуму закрываем углы, после чего ходим туда, где осталось место.

Вот такая стратегия игры в «крестики-нолики». Она не слишком сложная, и теперь мы попытаемся ее реализовать.

Реализация программы «крестики-нолики»

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

Проверка на возможность победы

Перед тем как писать код, необходимо уяснить смысл ее работы. Выигрышных комбинаций всего 8. Можете посмотреть скриншоты: я специально выделил и указал номер этих комбинаций(они понадобятся нам). Эти номера комбинаций будет хранить переменная cherta, которая сразу после выигрыша отдаст команду нарисовать выигрышную черту.

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

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

  • Изначально матрица обнуляется и это значит, что все 9 клеток пусты. Перед каждый ходом нужно проверять, пустая ли клетка, и можно ли на нее сходить. Полезно, пользуйтесь.
  • Для обозначения компьютера я использовал цифру 1.
  • Для обозначения пользователя я использовал цифру 2.

Далее нам необходимо будет создать специальный метод, который будет отрисовывать игровое поле, используя данные этой матрицы(метод paint, который будет мелькать в коде). А вот собственно и код данного метода:

[code language=»csharp»]if (((a[0, 0] + a[0, 1] + a[0, 2]) == 2) && (a[0, 0] == 1 || a[0, 1] == 1 || a[0, 2] == 1))
{
for (int j = 0; j < 3; j++)
{
if (a[0, j] == 0)
{
a[0, j] = 1;
}
}
win = 1;
paint();
cherta = 4;
}
else
{
if (((a[1, 0] + a[1, 1] + a[1, 2]) == 2) && (a[1, 0] == 1 || a[1, 1] == 1 || a[1, 2] == 1))
{
for (int j = 0; j < 3; j++)
{
if (a[1, j] == 0)
{
a[1, j] = 1;
}
}
win = 1;
paint();
cherta = 5;
}
else
{
if (((a[2, 0] + a[2, 1] + a[2, 2]) == 2) && (a[2, 0] == 1 || a[2, 1] == 1 || a[2, 2] == 1))
{
for (int j = 0; j < 3; j++)
{
if (a[2, j] == 0)
{
a[2, j] = 1;
}
}
win = 1;
paint();
cherta = 6;
}
else
{
if (((a[0, 0] + a[1, 0] + a[2, 0]) == 2) && (a[0, 0] == 1 || a[1, 0] == 1 || a[2, 0] == 1))
{
for (int i = 0; i < 3; i++)
{
if (a[i, 0] == 0)
{
a[i, 0] = 1;
}
}
win = 1;
paint();
cherta = 1;
}
else
{
if (((a[0, 1] + a[1, 1] + a[2, 1]) == 2) && (a[0, 1] == 1 || a[1, 1] == 1 || a[2, 1] == 1))
{
for (int i = 0; i < 3; i++)
{
if (a[i, 1] == 0)
{
a[i, 1] = 1;
}
}
win = 1;
paint();
cherta = 2;
}
else
{
if (((a[0, 2] + a[1, 2] + a[2, 2]) == 2) && (a[0, 2] == 1 || a[1, 2] == 1 || a[2, 2] == 1))
{
for (int i = 0; i < 3; i++)
{
if (a[i, 2] == 0)
{
a[i, 2] = 1;
}
}
win = 1;
paint();
cherta = 3;
}
else
{
if (((a[0, 0] + a[1, 1] + a[2, 2]) == 2) && (a[0, 0] == 1 || a[1, 1] == 1 || a[2, 2] == 1))
{
if (a[0, 0] == 0)
a[0, 0] = 1;
if (a[1, 1] == 0)
a[1, 1] = 1;
if (a[2, 2] == 0)
a[2, 2] = 1;
win = 1;
paint();
cherta = 7;
}
else
{
if (((a[2, 0] + a[1, 1] + a[0, 2]) == 2) && (a[2, 0] == 1 || a[1, 1] == 1 || a[0, 2] == 1))
{
if (a[2, 0] == 0)
a[2, 0] = 1;
if (a[1, 1] == 0)
a[1, 1] = 1;
if (a[0, 2] == 0)
a[0, 2] = 1;
win = 1;
paint();
cherta = 8;
}
}
}
}
}
}
}
}[/code]

Тут так же появляется переменная win, которая хранит значение победившей стороны. Я использовал только два значения: 0-ничья, 1 — победа компьютера. В моей программе нет даже поздравления пользователя с возможной победой, так как это невозможно.

Проверка на возможность проигрыша следующим ходом

[code language=»csharp»]if ((a[0, 0] + a[0, 1] + a[0, 2]) == 4 && a[0, 0]!=1 && a[0, 1]!=1 && a[0, 2]!=1) //1-4-7 — защита
{
for (int j = 0; j < 3; j++)
{
if (a[0, j] == 0)
{
a[0, j] = 1;
hdpc = false;
paint();
}
}
}
else
{
if ((a[1, 0] + a[1, 1] + a[1, 2]) == 4 && a[1, 0] != 1 && a[1, 1] != 1 && a[1, 2] != 1) //2-5-8 — защита
{
for (int j = 0; j < 3; j++)
{
if (a[1, j] == 0)
{
a[1, j] = 1;
hdpc = false;
paint();
}
}
}
else
{
if ((a[2, 0] + a[2, 1] + a[2, 2]) == 4 && a[2, 0] != 1 && a[2, 1] != 1 && a[2, 2] != 1) //3-6-9 — защита
{
for (int j = 0; j < 3; j++)
{
if (a[2, j] == 0)
{
a[2, j] = 1;
hdpc = false;
paint();
}
}
}
else
{
if ((a[0, 0] + a[1, 0] + a[2, 0]) == 4 && a[0, 0] != 1 && a[1, 0] != 1 && a[2, 0] != 1) //1-2-3 — защита
{
for (int i = 0; i < 3; i++)
{
if (a[i, 0] == 0)
{
a[i, 0] = 1;
hdpc = false;
paint();
}
}
}
else
{
if ((a[0, 1] + a[1, 1] + a[2, 1]) == 4 && a[0, 1] != 1 && a[1, 1] != 1 && a[2, 1] != 1) //4-5-6 — защита
{
for (int i = 0; i < 3; i++)
{
if (a[i, 1] == 0)
{
a[i, 1] = 1;
hdpc = false;
paint();
}
}
}
else
{
if ((a[0, 2] + a[1, 2] + a[2, 2]) == 4 && a[0, 2] != 1 && a[1, 2] != 1 && a[2, 2] != 1) //7-8-9 — защита
{
for (int i = 0; i < 3; i++)
{
if (a[i, 2] == 0)
{
a[i, 2] = 1;
hdpc = false;
paint();
}
}
}
else
{
if ((a[0, 0] + a[1, 1] + a[2, 2]) == 4 && a[0, 0] != 1 && a[1, 1] != 1 && a[2, 2] != 1) //1-5-9 — защита
{
if (a[0, 0] == 0)
a[0, 0] = 1;
if (a[1, 1] == 0)
a[1, 1] = 1;
if (a[2, 2] == 0)
a[2, 2] = 1;
hdpc = false;
paint();
}
else
{
if ((a[2, 0] + a[1, 1] + a[0, 2]) == 4 && a[2, 0] != 1 && a[1, 1] != 1 && a[0, 2] != 1) //3-5-7 — защита
{
if (a[2, 0] == 0)
a[2, 0] = 1;
if (a[1, 1] == 0)
a[1, 1] = 1;
if (a[0, 2] == 0)
a[0, 2] = 1;
hdpc = false;
paint();
}
}
}
}
}
}
}
}[/code]

Как видите, смысл защиты от возможного поражения является почти что точной копией предыдущего метода. Тут стоит лишь напомнить и уточнить несколько вещей:

  • Ходы противника мы отмечаем значением 2. Для этого используется массив a.
  • Метод paint вызывает отрисовку на основе данных матрицы.
  • Переменная булевого типа hdpc(hod pc, ход компьютера) принимает значение false, если пришла очередь ходить пользователю.

Дирижер всех ходов компьютера

Я использовал несколько переменных, которые сохраняли некоторое значение, на основании которых можно было бы понять, чей сейчас ход и кто ходил первым. Очень важным моментом всей программы является необходимость передавать управление то пользователю, то компьютеру. Для этого я рекомендую простой способ: лично я использовал элемент panel, который выполняет роль игрового поле. С помощью булевой переменной hdpc, я либо разрешал принимать нажатие мышкой пользователя на panel, либо нет, что обеспечивало только своевременный ход пользователя. Так же сразу после хода пользователя я запускал на действие специальный метод, который я образно назвал дирижером всех ходом компьютера. В чем это определение очень даже подходит для него, ведь именно здесь будут приниматься решения, какой метод активировать, то есть именно тут можно увидеть всю стратегию данной игры:

[code language=»csharp»]nichia(); //проверяем, если хотя бы одно свободное место на поле. Если да, то ходим. Если нет, то вывод сообщения о ничье. Данный метод предлагаю написать вам самим, он довольно прост.
pobeda(); //это тот самый метод для проверки возможности выигрыша
if (win == 0) //как помните мы использовали переменную win, чтобы обозначить победу компьютера. Если компьютер еще не выиграл, то продолжаем
{
zachita(); //этот метод мы так же описали. Компьютер защищается от возможного проигрыша
if (hdpc == true) //до сих пор ход компьютера, а это значит компьютер пока не может выиграть и поражением никто не пугает, а значит можно ходить по стратегии
{
if (pc == 1) //данная переменная хранит информацию о том, кто первым ходил. Если первым ходил компьютер, то значение равно 1, если пользователь — то 2.
{
krestiki(); //так называется метод, который выполняет ход в самый отдаленный угол от предыдущего хода пользователя
}
else //компьютер ходит вторым
{
if (xfir == 1 && yfir == 1) //пользователь первым ходом сходил в центр
{
ugol(); //ищем любой свободный угол и ходим туда, если это невозможно, ходим в любую свободную клетку
}
else //пользователь первым ходом не сходил в центр
{
if (pervhod) //данная булевая переменная хранит true, если пользователь собирается сделать первый ход в этой партии
{
hod1(); //ходим в центр
pervhod = false;
}
else
{
ugol(); //ходим в свободный угол
}
}
}
} //конец противоположного хода
nichia();
}
else
{
winner();
}[/code]

В данной методе, который я заботливо уложил комментариями, и хранится вся стратегия игры в «крестики-нолики». Нам же остается дописать все необходимые методы.

Ход в противоположный от предыдущего хода угол

Данный метод, который в представленном коде был назван как krestiki, выполняет ход в противоположный от предыдущего шага пользователя угол. Чтобы выполнить данную операцию, мы должны знать, куда последним шагом сходил пользователь. Именно для этих целей я использую специальные переменные. Парочку из них(xfir и yfir — сократил от x first и y first) я уже использовал в предыдущем методе. Данные переменный запоминают только первый ход пользователя. Следующие переменные, которые вы встретите, будут хранить каждый последний шаг пользователя.

[code language=»csharp»]if (xlast == 0 && ylast == 0) //если 0,0
{
if (a[2, 2] == 0)
{
a[2, 2] = 1;
hdpc = false;
paint();
}
else
{
random();
}
}
else
{
if (xlast == 2 && ylast == 0) //2.0
{
if (a[0, 2] == 0)
{
a[0, 2] = 1;
hdpc = false;
paint();
}
else
{
random();
}
}
else
{
if (xlast == 0 && ylast == 2) //0.2
{
if (a[2, 0] == 0)
{
a[2, 0] = 1;
hdpc = false;
paint();
}
else
{
random();
}
}
else
{
if (xlast == 2 && ylast == 2) //2.2
{
if (a[0, 0] == 0)
{
a[0, 0] = 1;
hdpc = false;
paint();
}
else
{
random();
}
}
else
{
if (xlast == 0 && ylast == 1) //0.1
{
if (a[2, 0] == 0)
{
a[2, 0] = 1;
hdpc = false;
paint();
}
else
{
if (a[2, 2] == 0)
{
a[2, 2] = 1;
hdpc = false;
paint();
}
else
{
random();
}
}
}
else
{
if (xlast == 1 && ylast == 0) //1.0
{
if (a[0, 2] == 0)
{
a[0, 2] = 1;
hdpc = false;
paint();
}
else
{
if (a[2, 2] == 0)
{
a[2, 2] = 1;
hdpc = false;
paint();
}
else
{
random();
}
}
}
else
{
if (xlast == 2 && ylast == 1) //2.1
{
if (a[0, 0] == 0)
{
a[0, 0] = 1;
hdpc = false;
paint();
}
else
{
if (a[0, 2] == 0)
{
a[0, 2] = 1;
hdpc = false;
paint();
}
else
{
random();
}
}
}
else
{
if (xlast == 1 && ylast == 2) //1.2
{
if (a[0, 0] == 0)
{
a[0, 0] = 1;
hdpc = false;
paint();
}
else
{
if (a[2, 0] == 0)
{
a[2, 0] = 1;
hdpc = false;
paint();
}
else
{
random();
}
}
}
}
}
}
}
}
}
}[/code]

В данном методе мы знакомимся с новым методом random, который отвечает, надеюсь вы догадались, за абсолютно рандомный(случайный шаг). Помните, в стратегии есть и такие ходы. Данный метод я так же оставлю для реализации Вам.

Ход в любой из углов

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

[code language=»csharp»]if (a[0, 0] == 0)
{
a[0, 0] = 1;
hdpc = false;
paint();
}
else
{
if (a[2, 0] == 0)
{
a[2, 0] = 1;
hdpc = false;
paint();
}
else
{
if (a[0, 2] == 0)
{
a[0, 2] = 1;
hdpc = false;
paint();
}
else
{
if (a[2, 2] == 0)
{
a[2, 2] = 1;
hdpc = false;
paint();
}
else
{
random();
}
}
}
}[/code]

Данный метод сделан на основе предыдущего метода, разница лишь в том, что в нем отсутствуют лишние проверки. Тут все просто: находим пустой угол — ходим, не находим — ходим рандомно.

Я понимаю, что приведенная часть это только половина всей программы. Но это основная половина всей программы. В ближайшее время я напишу еще одну статью, в которой опишу «рисовательную» часть этой программы, а так же укажу как отловить ход пользователя на panel`и. Напомню, что все это будет на языке C#. Так же напомню, что мы только что писали код программы, которая имитирует игру компьютера с пользователем в «Крестики-нолики»

Update: Боюсь у меня нет времени и еще больше нет желания заново разбираться в игре, которую я некогда написал и поэтому, скорее всего, обещанная рисовательная часть программы и прочие фишки никогда не увидят свет на страницах сайта About-windows.ru. В связи с этим просто предлагаю вам скачать исходник этой игры и дальше уже самим разобраться в ее коде. Прошу не судить строго по поводу «красоты» программирования.

Ссылка на скачивание игры Крестики и нолики.

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

Понравилась статья? Поделить с друзьями:
  • Как найти цифру в списке python
  • Как найти пероксид натрия
  • Повтори как найти площадь квадрата
  • Как составить кроссворд с одним ключевым словом
  • Как найти жучки на работе