Доброго времени суток, друзья. В этой статье мы создадим с вами всем хорошо известную игру «крестики-нолики» с помощью языка 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 и исполняемыми файлами игры: здесь
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 |
using System; using System.Drawing; using System.Net; using System.Windows.Forms; namespace WindowsFormsApplication309 { public partial class Form1 : Form { private Game game; private PictureBox[,] pbs = new PictureBox[3,3]; private Image Cross; private Image Nought; public Form1() { InitializeComponent(); Init(); game = new Game(); Build(game); } void Init() { Cross = Image.FromStream(new WebClient().OpenRead("https://raw.github.com/christkv/tic-tac-toe/master/public/img/cross.png")); Nought = Image.FromStream(new WebClient().OpenRead("https://raw.github.com/christkv/tic-tac-toe/master/public/img/circle.png")); for (int i = 0; i < 3; i++) for (int j = 0; j < 3; j++) { pbs[i, j] = new PictureBox { Parent = this, Size = new Size(100, 100), Top = i * 100, Left = j * 100, BorderStyle = BorderStyle.FixedSingle, Tag = new Point(i, j), Cursor = Cursors.Hand, SizeMode = PictureBoxSizeMode.StretchImage }; pbs[i, j].Click += pb_Click; } new Button { Parent = this, Top = 320, Text = "Reset" }.Click += delegate { game = new Game(); Build(game); }; } private void Build(Game game) { for (int i = 0; i < 3; i++) for (int j = 0; j < 3; j++) pbs[i, j].Image = game.Items[i, j] == FieldState.Cross ? Cross : (game.Items[i, j] == FieldState.Nought ? Nought : null); } void pb_Click(object sender, EventArgs e) { game.MakeMove((Point) (sender as Control).Tag); Build(game); if(game.Winned) MessageBox.Show(string.Format("{0} is winner!", game.CurrentPlayer == 0 ? "Cross" : "Nought")); } } class Game { public FieldState[,] Items = new FieldState[3,3]; public int CurrentPlayer = 0; public bool Winned; public void MakeMove(Point p) { if (Items[p.X, p.Y] != FieldState.Empty) return; if (Winned) return; Items[p.X, p.Y] = CurrentPlayer == 0 ? FieldState.Cross : FieldState.Nought; if (CheckWinner(FieldState.Cross) || CheckWinner(FieldState.Nought)) { Winned = true; return; } CurrentPlayer ^= 1; } private bool CheckWinner(FieldState state) { for (int i = 0; i < 3; i++) { if (Items[i, 0] == state && Items[i, 1] == state && Items[i, 2] == state) return true; if (Items[0, i] == state && Items[1, i] == state && Items[2, i] == state) return true; } if (Items[0, 0] == state && Items[1, 1] == state && Items[2, 2] == state) return true; if (Items[0, 2] == state && Items[1, 1] == state && Items[2, 0] == state) return true; return false; } } enum FieldState { Empty, Cross, Nought } } |
Вот хочу вам показать, как создать такую несложную игру на языке C#.
Для начала сделаем саму форму. Нам нужно: игровое поле(9 кнопок), кнопка «Start»(для рестарта игры), 3 текстовых поля( указатель хода, номер игры, счет игры). Вот у меня получилось то, что вы видите на скрине.
Переходим к самому коду. У нас будет 3 функции: Game() — главная, которая отвечает за хода в игре; who_vin() — объявляет победителя; refresch() — очищает игровое поле, начинает новую игру.
Сначала объявляем глобальные переменные в классе:
public partial class Form1 : Form
{
int i; //счетчик ходов
int num_game = 0; // номер игры
int Ovin = 0; // количество побед О
int Xvin = 0; // количество побед Х
string[] arr = new string[10]; // массив, который хранит расположение Х и О на игровом поле
string t = «X»; // переменная которая показывает чей ход происходит
}
Дальше переходим к самим функциям:
public string Game(int j) // принимает номер клетки поля
{
string fakeT; // переменная следущего хода
if (i % 2 == 1) // узнает чей ход сделан (Х — непарный ход)
{
t = «X»;
fakeT = «O»;
}
else {
t = «O»;
fakeT = «X»;
}
fakeT = «Ходит « + fakeT;
arr[j] = t;
i += 1;
label1.Text = fakeT;// пишет кто следущий ходит
return t; // возвращает отметку на поле Х или О
}
public void who_vin()
{
if ((arr[1] == «X» & arr[2] == «X» & arr[3] == «X») | (arr[1] == «X» & arr[5] == «X» & arr[9] == «X») | (arr[1] == «X» & arr[4] == «X» & arr[7] == «X») | (arr[2] == «X» & arr[5] == «X» & arr[8] == «X») | (arr[3] == «X» & arr[6] == «X» & arr[9] == «X») | (arr[3] == «X» & arr[5] == «X» & arr[7] == «X») | (arr[4] == «X» & arr[5] == «X» & arr[6] == «X») | (arr[7] == «X» & arr[8] == «X» & arr[9] == «X»))
{ // за таких обстоятельств побеждает Х
Xvin += 1; // начисляем ему победу
label3.Text = «X : O\n» + Xvin.ToString() + » : « + Ovin.ToString(); // обновляем табло побед
MessageBox.Show(» X VIN!»); // выводим окошечко, которое говорит нам о победе
refresch();// чистим поле, начинаем новую игру
}
if ((arr[1] == «O» & arr[2] == «O» & arr[3] == «O») | (arr[1] == «O» & arr[5] == «O» & arr[9] == «O») | (arr[1] == «O» & arr[4] == «O» & arr[7] == «O») | (arr[2] == «O» & arr[5] == «O» & arr[8] == «O») | (arr[3] == «O» & arr[6] == «O» & arr[9] == «O») | (arr[3] == «O» & arr[5] == «O» & arr[7] == «O») | (arr[4] == «O» & arr[5] == «O» & arr[6] == «O») | (arr[7] == «O» & arr[8] == «O» & arr[9] == «O»))
{// за таких обстоятельств побеждает О, следущие действия аналогичны
Ovin += 1;
label3.Text = «X : O\n» + Xvin.ToString() + » : « + Ovin.ToString();
MessageBox.Show(» O VIN!»);
refresch();
}
if ((arr[1] != «») & (arr[2] != «») & (arr[3] != «») & (arr[4] != «») & (arr[5] != «») & (arr[7] != «») & (arr[8] != «») & (arr[9] != «»))
{// Ничья, тоесть все поле заполнено, а победителя нет
MessageBox.Show(» Ничья! :)»); // объявляем ничью
refresch();// чистим поле
}
}
public void refresch()
{
button1.Text = «»;
button2.Text = «»;
button3.Text = «»;
button4.Text = «»;
button5.Text = «»;
button6.Text = «»;
button7.Text = «»;
button8.Text = «»;
button9.Text = «»;// почистили каждую клетку поля
label1.Text = «»;//табло следующего хода
for (i = 0; i < 10; i += 1) // массив ходов
{
arr[i] = «»;
}
i = 1; // номер хода
num_game += 1; // добавляем к номеру игры
label2.Text = num_game + » игра»; // обновляем табло с номером игры
}
Функции готовы. Далее перейдем к командам, то есть нашему полю(кнопкам):
private void button10_Click(object sender, EventArgs e) // Кнопка «Start» чистит поле
{
refresch();
}
private void button1_Click(object sender, EventArgs e) // первая клетка поля
{
if (button1.Text == «X» | button1.Text == «O») // если клетка уже заполненая
{
MessageBox.Show(» Нельзя! :нуну:»);// предупреждаем, что нельзя
}
else { button1.Text = Game(1); who_vin(); }// если все хорошо, запускаем 2 функции. задаем клетке отметку Х или О
}
private void button2_Click(object sender, EventArgs e) // вторая клетка поля. действия аналогичны, и со следующими
{
if (button2.Text == «X» | button2.Text == «O»)
{
MessageBox.Show(» Нельзя! :нуну:»);
}
else { button2.Text = Game(2); who_vin(); }
}
private void button3_Click(object sender, EventArgs e)
{
if (button3.Text == «X» | button3.Text == «O»)
{
MessageBox.Show(» Нельзя! :нуну:»);
}
else { button3.Text = Game(3); who_vin(); }
}
private void button4_Click(object sender, EventArgs e)
{
if (button4.Text == «X» | button4.Text == «O»)
{
MessageBox.Show(» Нельзя! :нуну:»);
}
else { button4.Text = Game(4); who_vin(); }
}
private void button5_Click(object sender, EventArgs e)
{
if (button5.Text == «X» | button5.Text == «O»)
{
MessageBox.Show(» Нельзя! :нуну:»);
}
else { button5.Text = Game(5); who_vin(); }
}
private void button6_Click(object sender, EventArgs e)
{
if (button6.Text == «X» | button6.Text == «O»)
{
MessageBox.Show(» Нельзя! :нуну:»);
}
else { button6.Text = Game(6); who_vin(); }
}
private void button7_Click(object sender, EventArgs e)
{
if (button7.Text == «X» | button7.Text == «O»)
{
MessageBox.Show(» Нельзя! :нуну:»);
}
else { button7.Text = Game(7); who_vin(); }
}
private void button8_Click(object sender, EventArgs e)
{
if (button8.Text == «X» | button8.Text == «O»)
{
MessageBox.Show(» Нельзя! :нуну:»);
}
else { button8.Text = Game(8); who_vin(); }
}
private void button9_Click(object sender, EventArgs e)
{
if (button9.Text == «X» | button9.Text == «O»)
{
MessageBox.Show(» Нельзя! :нуну:»);
}
else { button9.Text = Game(9); who_vin(); }
}
Ну и на последок функция которая работает сразу при старте программы:
private void Form1_Load(object sender, EventArgs e)
{
refresch();// чистим поле перед игрой
label3.Text = «X : O\n» + Xvin.ToString() + » : « + Ovin.ToString(); // задаем счетчик
}
Вот и все.
Вот сам исходник:
Скачать
Крестики-нолики на C#: подробное руководство с примерами кода
Игра в крестики-нолики является одной из самых популярных игр на бумаге. Она проста в понимании, но в то же время предлагает ряд сложностей для тех, кто хочет реализовать ее в виде программы на языке C#. В этой статье мы рассмотрим процесс разработки крестиков-ноликов на C# и предоставим подробную документацию с примерами кода для каждого шага.
1. Создание проекта в Visual Studio
Прежде чем приступить к разработке игры, нам понадобится среда разработки. Мы будем использовать Visual Studio для создания нашего проекта на C#. Откройте Visual Studio и создайте новый проект типа «Windows Forms Application».
2. Дизайн формы игры
На форме игры мы будем использовать элементы управления для создания игрового поля. Добавьте на форму элемент TableLayoutPanel, это позволит легко располагать кнопки на игровом поле. Установите его свойство Rows и Columns на 3, чтобы создать поле 3×3.
3. Создание класса игры
Создайте новый класс под названием «Game». В этом классе мы будем хранить информацию о текущем состоянии игры и реализовывать логику игры. Добавьте следующие поля:
private char[,] board; private char currentPlayer;
Первое поле, `board`, будет представлять игровое поле. Оно будет представлять собой массив 3×3 символов, начальное значение которого будет пустым символом (‘ ‘). Второе поле, `currentPlayer`, будет хранить символ текущего игрока (‘X’ или ‘O’).
Добавьте также конструктор для класса, в котором будет происходить инициализация игры:
public Game() { board = new char[3, 3]; currentPlayer = 'X'; InitializeBoard(); } private void InitializeBoard() { for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { board[i, j] = ' '; } } }
4. Событие клика по кнопке на игровом поле
Добавьте обработчик события Click для кнопок на игровом поле. В этом обработчике мы будем обновлять состояние игры и отображать текущего игрока в соответствующей ячейке игрового поля.
private void Cell_Click(object sender, EventArgs e) { Button cell = (Button)sender; int row = (int)cell.Tag / 3; int col = (int)cell.Tag % 3; if (board[row, col] == ' ') { board[row, col] = currentPlayer; cell.Text = currentPlayer.ToString(); if (CheckWin()) { MessageBox.Show("Игрок " + currentPlayer + " победил!"); InitializeBoard(); } else if (CheckDraw()) { MessageBox.Show("Ничья!"); InitializeBoard(); } else { currentPlayer = currentPlayer == 'X' ? 'O' : 'X'; } } }
5. Проверка победы и ничьи
Добавьте два метода в класс Game: CheckWin() и CheckDraw(). Первый будет проверять, есть ли победитель, а второй — закончилась ли игра вничью.
private bool CheckWin() { // Проверка строк for (int i = 0; i < 3; i++) { if (board[i, 0] != ' ' && board[i, 0] == board[i, 1] && board[i, 0] == board[i, 2]) { return true; } } // Проверка столбцов for (int j = 0; j < 3; j++) { if (board[0, j] != ' ' && board[0, j] == board[1, j] && board[0, j] == board[2, j]) { return true; } } // Проверка диагоналей if (board[0, 0] != ' ' && board[0, 0] == board[1, 1] && board[0, 0] == board[2, 2]) { return true; } if (board[0, 2] != ' ' && board[0, 2] == board[1, 1] && board[0, 2] == board[2, 0]) { return true; } return false; } private bool CheckDraw() { for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { if (board[i, j] == ' ') { return false; } } } return true; }
6. Подключение обработчика события к кнопкам
Для каждой кнопки на игровом поле добавьте обработчик события Click, чтобы при клике по кнопке вызывался метод Cell_Click и выполнялась соответствующая логика игры.
button1.Click += Cell_Click; button2.Click += Cell_Click; button3.Click += Cell_Click; // ... button9.Click += Cell_Click;
7. Запуск игры
Добавьте код для создания экземпляра класса Game и запуска формы:
Game game = new Game(); Application.Run(new Form1());
Вот и все! Теперь у вас есть полностью функциональная игра в крестики-нолики на языке C#. Вы можете запустить приложение и начать играть.
Крестики-нолики — это отличный способ изучить основы программирования и взаимодействия с элементами управления в Windows Forms. Мы рассмотрели только базовую реализацию этой игры, но вы можете расширить ее функциональность, добавив возможность игры против компьютера или других игроков, а также сохранение и загрузку игрового прогресса.
Надеюсь, что этот материал был полезен и помог вам разобраться с разработкой игры в крестики-нолики на C#. Удачи в программировании!
Доброго времени суток, друзья. В этой статье мы создадим с вами всем хорошо известную игру «крестики-нолики» с помощью языка 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 и исполняемыми файлами игры: здесь
Подборка по базе: Макет персонализированной программы наставника.docx, Английские заимствования в русском языке.docx, окружайка анализ программы.docx, Методическая разработка практического занятия _Применение языка , макет персонализированной программы наставника.docx, Критерии оценки выполнения программы.docx, онлайн программы и контент.docx, макет персонализированной программы наставника.pdf, Макет персоналиированной программы наставника.docx, макет персонализированной программы наставника.docx
Факультет информационных технологий
Кафедра «Системы информационной безопасности»
Дисциплина «Алгоритмические языки»
КУРСОВАЯ РАБОТА
на тему:
«Разработка программы на языке С++»
Выполнил
студент гр. О-21-ИБ-2-ози-Б,
зач. кн. № 21.0402
_______________ Короткова К.В.
«__»_________ 2022 г.
Проверил
_______________ к.т.н., доц. А.П. Горлов
«__»_________ 2022 г.
Брянск
2022 г.
СОДЕРЖАНИЕ
ВВЕДЕНИЕ………………………………………………………………………………………………….. 3
1. Постановка задачи…………………………………………………………………………………….. 5
2. Описание программы………………………………………………………………………………… 6
2.1. Правила игры……………………………………………………………………………………. 6
2.2. Интерфейс программы………………………………………………………………………. 6
2.3. Общий принцип работы программы…………………………………………………… 8
2.4. Блок-схема алгоритма игры………………………………………………………………. 10
3. Контрольный пример……………………………………………………………………………….. 11
ЗАКЛЮЧЕНИЕ…………………………………………………………………………………………… 14
СПИСОК ЛИТЕРАТУРЫ……………………………………………………………………………. 15
ПРИЛОЖЕНИЕ А – листинг программы………………………………………………………. 16
ВВЕДЕНИЕ
Данная курсовая работа выполнена на языке программирования высокого уровня С++ с использованием компилятора Microsoft Visual Studio 2019. Язык программирования С++ достаточно актуален в последнее время. С помощью него можно решать всевозможные задачи, ставящиеся перед современным программистом: написание системных программ, разработка полноценных Windows-приложений, объектное моделирование.
С++ является языком программирования общего назначения. Он известен эффективностью, универсальностью, экономичностью. Использование данного языка в качестве инструментального позволяет получать быстрые и компактные программы в объеме, достаточном для их работы в большом количестве.
Для удобства работы в программе применяются основные средства и объекты языка С++. С целью определения собственных типов данных и простой манипуляции ими в курсовой работе используются классы. Классы позволяют отделить детали, касающиеся реализации нового типа, от определения интерфейса и операций, предоставляемых пользователю. Перечисления (enum): определяет несколько именованных констант. Также используются переменные – изменяемые величины и константы – неизменяемые величины, типы данных – величина, которая определяет внутреннее представление данных в памяти компьютера, множество значений, которые могут принимать величины этого типа, операции и функции, которые можно применять к величинам этого типа.
В программе используются следующие библиотеки:
- Iostream – заголовочный файл с классами, функциями и переменными для организации ввода- вывода в языке программирования С++. Он включен в стандартную библиотеку С++;
- StartForm.h – заголовочный файл, в котором находится форма для работы с главным меню;
- GameForm.h – заголовочный файл, в котором находится форма для работы с игровым полем;
- GameMap.h – заголовочный файл с классами, методами и переменными для работы с игровой картой;
- Gamer.h – заголовочный файл с классами, методами и переменными для работы с игроком;
- Windows.h — подключает использования функционала для предоставляемой операционной системой.
Цель курсового проекта – закрепление и углубление знаний, полученных при изучении курса «Алгоритмические языки» посредством разработки игры «Крестики — нолики» на Windows Forms C++.
1. Постановка задачи
Для успешного выполнения курсового проекта и результативной реализации программы были поставлены следующие задачи:
- Построить схему алгоритма решения задачи;
- Изучить необходимые средства языка для выполнения программы;
- Разработать программу:
3.1 Реализовать графический интерфейс;
3.2 Реализовать возможность выбора игрового режима;
3.3 Реализовать возможность выбора размера игрового поля;
3.4 Реализовать функцию, которая не позволяет пользователю сделать ход на ячейку, которая занята;
- Провести всевозможные испытания и отладку;
- Проанализировать результаты;
- Оформить пояснительную записку.
2. Описание программы
2.1. Правила игры
Правила игры состоят в том, что необходимо выстроить линию из 3-х (или более) крестиков (ноликов) в любом направлении и каким угодно способом (по горизонтали, вертикали или диагонали). Соответственно, ваш противник должен выстроить подобную линию быстрее вас или не давать вам построить такую линию. Игра начинается с того, что в любом месте игрового поля ставится крестик, ваш противник ставит нолик, и так до тех пор, пока не будет выстроена линия. В данной программе чтобы выиграть, необходимо первым составить линию.
2.2. Интерфейс программы
Для запуска программы сделайте двойной щелчок по ярлыку TicTacToeGame.exe. На экране. появится основное окно программы, имеющее следующий вид (Рис. 2.2.1.):
Рис. 2.2.1. Главное меню
На окне размещено несколько кнопок – Начать игру и Выход, а также кнопки выбора режима игры, размерность игрового поля и длина комбинации для победы. Для начала новой игры необходимо выбрать игровой режим, а затем кликнуть по кнопке «Начать игру». Для выхода из программы нужно нажать кнопку «Выход» или нажать на крестик.
После всех действий появится игровое поле. Игровое поле представлено на Рис. 2.2.2. В данной программе первый ход всегда предоставляется Player1. Это и означает начало игры.
Рис. 2.2.2. Игровое поле
При победе одного из игроков появляется сообщение (Рис. 2.2.3.) «Launch a new game?». Если нажать кнопку «Yes», то начнется новая игра, а если «No», то сообщение исчезает.
Рис. 2.2.3. Сообщение о перезапуске игры
В игре возможны несколько исходов:
- победа Player1, проигрыш Player2;
- победа Player2, проигрыш Player1;
- победа Player, проигрыш Computer;
- победа Computer, проигрыш Player;
- ничья.
Каждый исход оповещается соответствующим сообщением. Сообщение об одном из исходов представлено на Рис. 2.2.4.
Рис. 2.2.4. Исход игры «ничья»
2.3. Общий принцип работы программы
В С++ при написании программы на Windows Forms есть четкое разделение файлов: заголовочные файлы (с разрешением «.h») и исполняемые (с разрешением «.cpp»). В файлах с разрешением «.h» инициализированы обработчики нажатия на кнопки. В файлах с разрешением «.cpp» реализованы эти обработчики.
В программе созданы 2 класса: Gamer.h и GameMap.h. Это так называемые сущности для игрока и для карты соответственно. В файле Gamer.h инициализированы всевозможные статусы игрока и класс Gamer, в котором находятся поля и методы класса. Поля имеют закрытый спецификатор доступа, и там описаны статус игрока (status) и то, чем игрок ходит в данный момент (mark). Методы класса, такие как: SetField – задать поля, GetMark – вернуть текущую метку и GetStatus – вернуть статус, имеют открытый спецификатор доступа. В GameMap.h подключены необходимые библиотеки, такие как:
– контейнерный класс, который предоставляет информацию как обычный одномерный массив, и . В классе GameMap находятся поля, которые также имеют закрытый спецификатор доступа, и в них расположены: int** map – матрица, для предоставления карты в виде двумерного массива, Vector2 size – вектор для того, чтобы указать размерность этой матрицы и int length – необходимая длина для выигрышной последовательности. В методах, имеющих открытый спецификатор доступа, в первую очередь описаны GameMap() – конструктор, создающий объект класса, и GameMap() – деструктор, удаляющий объект класса, SetPosition – позволяет задать позицию, IsEmpty – проверить свободную ячейку, SetMap — получить некоторые значения текущей карты, GetValue – получить значения, GetLenght – получить некоторую длину выигрышной последовательности, GetSize – получить размер текущей карты, CheckingWin – проверить, выиграл кто-то из игроков или нет, CanMove – проверить наличие свободных ходов. Решения всех методов реализованы в файлах Gamer.cpp и GameMap.cpp соответственно.
Для реализации интерфейсной части программы были выбраны основные компоненты:
- Label – предназначен для отображения текстовой информации;
- MenuStrip – этот компонент создает главное меню приложения, с помощью которого управляют всей работой приложения и его частей;
- ListBox – представляет собой список, в котором можно выбрать нужный элемент;
- NumericUpDown – представляет пользователю выбор числа из определенного диапазона;
- Button – один из элементов интерфейса пользователя компьютерной программы, «нажатие (клик)» на которую приводит к некоторому действию, заложенному в программе;
- StatusStrip – используется для отображения какого-нибудь информационного текста, отражающего состояние приложения или результат действий пользователя;
- DataGridView – стандартный GUI компонент для отображения и редактирования таблиц.
2.4. Блок-схема алгоритма игры
Блок-схема – совокупность символов, соответствующих этапам работы алгоритма и соединяющих их линий. В данной схеме используются основные блоки: блок начало/конец, блок вычислительных операций, логический блок, ввод/вывод данных, вывод информации на печать.
На Рис. 2.4.1. представлена блок-схема алгоритма работы игры «Крестики-нолики».
Рис. 2.4.1. Блок-схема алгоритма игры «Крестики-нолики»
3. Контрольный пример
Для начала запустим файл TicTacToeGame.exe. Появится окно меню (Рис. 3.1.), в котором будет предложено выбрать режим игры, размерность поля и длину комбинации для выигрыша
Рис. 3.1. Главное меню
Проведем несколько тестовых запусков:
Режим игры PvE: результат победы Player представлен на рисунке 3.2., результат победы Computer представлен на рисунке 3.3., результат исхода «ничья» представлен на рисунке 3.4.
Рис. 3.2. Победа Player
Рис. 3.3. Победа Computer
.
Рис. 3.4. Ничья
Режим игры PvP: результат победы Player1 представлен на Рис. 3.5., результат победы Player2 представлен на Рис. 3.6.
Рис. 3.5. Победа Player1
Рис. 3.6. Победа Player2
Режим игры на увеличенном размере поля (6х6) с длиной комбинации в 4 клетки представлен на Рис. 3.7.
Рис. 3.7. Игра 6х6
Ситуация, когда игрок пытается занять позицию, на которой уже сделан ход представлена на Рис. 3.8.
Рис. 3.8. Попытка сделать ход на позицию, занятую другим игроком
ЗАКЛЮЧЕНИЕ
В курсовом проекта была разработана игра «Крестики — нолики».
В процессе выполнения были изучены новые возможности языка С++. В том числе была изучена графическая подсистема Microsoft Visual Studio 2019.
Все ранее поставленные задачи были успешно выполнены. В программе реализован базовый алгоритм игры, который имеет простой интерфейс, позволяющий максимально упростить работу с ней для любого пользователя. Тестовые запуски с последующей отладкой помогли добиться того, что программа работает корректно.
На практике удалось применить все знания, полученные в процессе изучения курса «Алгоритмические языки».
СПИСОК ЛИТЕРАТУРЫ
- Ашарина, И.В. Основы программирования на языках С и С++: Курс лекций для высших учебных заведений / И.В. Ашарина. — М.: Гор. линия-Телеком, 2018. — 208 c.
- Баженова, И.Ю. Языки программирования: Учебник для студентов учреждений высш. проф. образования / И.Ю. Баженова; Под ред. В.А. Сухомлин. — М.: ИЦ Академия, 2018. — 368 c.
- Базовый курс C++ (MIPT, ILab). Lecture 1. Scent of C++./ Электронный ресурс / Режим доступа: https://www.youtube.com/watch?v=Bym7UMqpVEY, свободный
- Современный С++: новые возможности и лучшие методологии / Электронный ресурс / Режим доступа: https://www.youtube.com/watch?v=tSANTMp-T9o, свободный
- Страуструп Б. Язык программирования C++: Пер. с англ. – 3-е спец. изд. – М.: Бином, 2003. – 1104 с.
- Троелсен, Э. Язык программирования С# 5.0 и платформа .NET 4.5 / Э. Троелсен; Пер. с англ. Ю.Н. Артеменко. — М.: Вильямс, 2016. — 1312 c.
- Уолтер Сэвитч. С++ в примерах /Пер. с англ. – М.: ЭКОМ, 1997. – 736 с
- Эккель Б., Эллисон Ч. Философия C++. Практическое программирование: Пер. с англ. – СПб.: Питер, 2004. – 608 с.
Факультет информационных технологий
Кафедра «Системы информационной безопасности»
Дисциплина «Алгоритмические языки»
КУРСОВАЯ РАБОТА
на тему:
«Разработка программы на языке С++»
Выполнил
студент гр. О-21-ИБ-2-ози-Б,
зач. кн. № 21.0402
_______________ Короткова К.В.
«__»_________ 2022 г.
Проверил
_______________ к.т.н., доц. А.П. Горлов
«__»_________ 2022 г.
Брянск
2022 г.
СОДЕРЖАНИЕ
ВВЕДЕНИЕ………………………………………………………………………………………………….. 3
1. Постановка задачи…………………………………………………………………………………….. 5
2. Описание программы………………………………………………………………………………… 6
2.1. Правила игры……………………………………………………………………………………. 6
2.2. Интерфейс программы………………………………………………………………………. 6
2.3. Общий принцип работы программы…………………………………………………… 8
2.4. Блок-схема алгоритма игры………………………………………………………………. 10
3. Контрольный пример……………………………………………………………………………….. 11
ЗАКЛЮЧЕНИЕ…………………………………………………………………………………………… 14
СПИСОК ЛИТЕРАТУРЫ……………………………………………………………………………. 15
ПРИЛОЖЕНИЕ А – листинг программы………………………………………………………. 16
ВВЕДЕНИЕ
Данная курсовая работа выполнена на языке программирования высокого уровня С++ с использованием компилятора Microsoft Visual Studio 2019. Язык программирования С++ достаточно актуален в последнее время. С помощью него можно решать всевозможные задачи, ставящиеся перед современным программистом: написание системных программ, разработка полноценных Windows-приложений, объектное моделирование.
С++ является языком программирования общего назначения. Он известен эффективностью, универсальностью, экономичностью. Использование данного языка в качестве инструментального позволяет получать быстрые и компактные программы в объеме, достаточном для их работы в большом количестве.
Для удобства работы в программе применяются основные средства и объекты языка С++. С целью определения собственных типов данных и простой манипуляции ими в курсовой работе используются классы. Классы позволяют отделить детали, касающиеся реализации нового типа, от определения интерфейса и операций, предоставляемых пользователю. Перечисления (enum): определяет несколько именованных констант. Также используются переменные – изменяемые величины и константы – неизменяемые величины, типы данных – величина, которая определяет внутреннее представление данных в памяти компьютера, множество значений, которые могут принимать величины этого типа, операции и функции, которые можно применять к величинам этого типа.
В программе используются следующие библиотеки:
- Iostream – заголовочный файл с классами, функциями и переменными для организации ввода- вывода в языке программирования С++. Он включен в стандартную библиотеку С++;
- StartForm.h – заголовочный файл, в котором находится форма для работы с главным меню;
- GameForm.h – заголовочный файл, в котором находится форма для работы с игровым полем;
- GameMap.h – заголовочный файл с классами, методами и переменными для работы с игровой картой;
- Gamer.h – заголовочный файл с классами, методами и переменными для работы с игроком;
- Windows.h — подключает использования функционала для предоставляемой операционной системой.
Цель курсового проекта – закрепление и углубление знаний, полученных при изучении курса «Алгоритмические языки» посредством разработки игры «Крестики — нолики» на Windows Forms C++.
1. Постановка задачи
Для успешного выполнения курсового проекта и результативной реализации программы были поставлены следующие задачи:
- Построить схему алгоритма решения задачи;
- Изучить необходимые средства языка для выполнения программы;
- Разработать программу:
3.1 Реализовать графический интерфейс;
3.2 Реализовать возможность выбора игрового режима;
3.3 Реализовать возможность выбора размера игрового поля;
3.4 Реализовать функцию, которая не позволяет пользователю сделать ход на ячейку, которая занята;
- Провести всевозможные испытания и отладку;
- Проанализировать результаты;
- Оформить пояснительную записку.
2. Описание программы
2.1. Правила игры
Правила игры состоят в том, что необходимо выстроить линию из 3-х (или более) крестиков (ноликов) в любом направлении и каким угодно способом (по горизонтали, вертикали или диагонали). Соответственно, ваш противник должен выстроить подобную линию быстрее вас или не давать вам построить такую линию. Игра начинается с того, что в любом месте игрового поля ставится крестик, ваш противник ставит нолик, и так до тех пор, пока не будет выстроена линия. В данной программе чтобы выиграть, необходимо первым составить линию.
2.2. Интерфейс программы
Для запуска программы сделайте двойной щелчок по ярлыку TicTacToeGame.exe. На экране. появится основное окно программы, имеющее следующий вид (Рис. 2.2.1.):
Рис. 2.2.1. Главное меню
На окне размещено несколько кнопок – Начать игру и Выход, а также кнопки выбора режима игры, размерность игрового поля и длина комбинации для победы. Для начала новой игры необходимо выбрать игровой режим, а затем кликнуть по кнопке «Начать игру». Для выхода из программы нужно нажать кнопку «Выход» или нажать на крестик.
После всех действий появится игровое поле. Игровое поле представлено на Рис. 2.2.2. В данной программе первый ход всегда предоставляется Player1. Это и означает начало игры.
Рис. 2.2.2. Игровое поле
При победе одного из игроков появляется сообщение (Рис. 2.2.3.) «Launch a new game?». Если нажать кнопку «Yes», то начнется новая игра, а если «No», то сообщение исчезает.
Рис. 2.2.3. Сообщение о перезапуске игры
В игре возможны несколько исходов:
- победа Player1, проигрыш Player2;
- победа Player2, проигрыш Player1;
- победа Player, проигрыш Computer;
- победа Computer, проигрыш Player;
- ничья.
Каждый исход оповещается соответствующим сообщением. Сообщение об одном из исходов представлено на Рис. 2.2.4.
Рис. 2.2.4. Исход игры «ничья»
2.3. Общий принцип работы программы
В С++ при написании программы на Windows Forms есть четкое разделение файлов: заголовочные файлы (с разрешением «.h») и исполняемые (с разрешением «.cpp»). В файлах с разрешением «.h» инициализированы обработчики нажатия на кнопки. В файлах с разрешением «.cpp» реализованы эти обработчики.
В программе созданы 2 класса: Gamer.h и GameMap.h. Это так называемые сущности для игрока и для карты соответственно. В файле Gamer.h инициализированы всевозможные статусы игрока и класс Gamer, в котором находятся поля и методы класса. Поля имеют закрытый спецификатор доступа, и там описаны статус игрока (status) и то, чем игрок ходит в данный момент (mark). Методы класса, такие как: SetField – задать поля, GetMark – вернуть текущую метку и GetStatus – вернуть статус, имеют открытый спецификатор доступа. В GameMap.h подключены необходимые библиотеки, такие как:
– контейнерный класс, который предоставляет информацию как обычный одномерный массив, и . В классе GameMap находятся поля, которые также имеют закрытый спецификатор доступа, и в них расположены: int** map – матрица, для предоставления карты в виде двумерного массива, Vector2 size – вектор для того, чтобы указать размерность этой матрицы и int length – необходимая длина для выигрышной последовательности. В методах, имеющих открытый спецификатор доступа, в первую очередь описаны GameMap() – конструктор, создающий объект класса, и GameMap() – деструктор, удаляющий объект класса, SetPosition – позволяет задать позицию, IsEmpty – проверить свободную ячейку, SetMap — получить некоторые значения текущей карты, GetValue – получить значения, GetLenght – получить некоторую длину выигрышной последовательности, GetSize – получить размер текущей карты, CheckingWin – проверить, выиграл кто-то из игроков или нет, CanMove – проверить наличие свободных ходов. Решения всех методов реализованы в файлах Gamer.cpp и GameMap.cpp соответственно.
Для реализации интерфейсной части программы были выбраны основные компоненты:
- Label – предназначен для отображения текстовой информации;
- MenuStrip – этот компонент создает главное меню приложения, с помощью которого управляют всей работой приложения и его частей;
- ListBox – представляет собой список, в котором можно выбрать нужный элемент;
- NumericUpDown – представляет пользователю выбор числа из определенного диапазона;
- Button – один из элементов интерфейса пользователя компьютерной программы, «нажатие (клик)» на которую приводит к некоторому действию, заложенному в программе;
- StatusStrip – используется для отображения какого-нибудь информационного текста, отражающего состояние приложения или результат действий пользователя;
- DataGridView – стандартный GUI компонент для отображения и редактирования таблиц.
2.4. Блок-схема алгоритма игры
Блок-схема – совокупность символов, соответствующих этапам работы алгоритма и соединяющих их линий. В данной схеме используются основные блоки: блок начало/конец, блок вычислительных операций, логический блок, ввод/вывод данных, вывод информации на печать.
На Рис. 2.4.1. представлена блок-схема алгоритма работы игры «Крестики-нолики».
Рис. 2.4.1. Блок-схема алгоритма игры «Крестики-нолики»
3. Контрольный пример
Для начала запустим файл TicTacToeGame.exe. Появится окно меню (Рис. 3.1.), в котором будет предложено выбрать режим игры, размерность поля и длину комбинации для выигрыша
Рис. 3.1. Главное меню
Проведем несколько тестовых запусков:
Режим игры PvE: результат победы Player представлен на рисунке 3.2., результат победы Computer представлен на рисунке 3.3., результат исхода «ничья» представлен на рисунке 3.4.
Рис. 3.2. Победа Player
Рис. 3.3. Победа Computer
.
Рис. 3.4. Ничья
Режим игры PvP: результат победы Player1 представлен на Рис. 3.5., результат победы Player2 представлен на Рис. 3.6.
Рис. 3.5. Победа Player1
Рис. 3.6. Победа Player2
Режим игры на увеличенном размере поля (6х6) с длиной комбинации в 4 клетки представлен на Рис. 3.7.
Рис. 3.7. Игра 6х6
Ситуация, когда игрок пытается занять позицию, на которой уже сделан ход представлена на Рис. 3.8.
Рис. 3.8. Попытка сделать ход на позицию, занятую другим игроком
ЗАКЛЮЧЕНИЕ
В курсовом проекта была разработана игра «Крестики — нолики».
В процессе выполнения были изучены новые возможности языка С++. В том числе была изучена графическая подсистема Microsoft Visual Studio 2019.
Все ранее поставленные задачи были успешно выполнены. В программе реализован базовый алгоритм игры, который имеет простой интерфейс, позволяющий максимально упростить работу с ней для любого пользователя. Тестовые запуски с последующей отладкой помогли добиться того, что программа работает корректно.
На практике удалось применить все знания, полученные в процессе изучения курса «Алгоритмические языки».
СПИСОК ЛИТЕРАТУРЫ
- Ашарина, И.В. Основы программирования на языках С и С++: Курс лекций для высших учебных заведений / И.В. Ашарина. — М.: Гор. линия-Телеком, 2018. — 208 c.
- Баженова, И.Ю. Языки программирования: Учебник для студентов учреждений высш. проф. образования / И.Ю. Баженова; Под ред. В.А. Сухомлин. — М.: ИЦ Академия, 2018. — 368 c.
- Базовый курс C++ (MIPT, ILab). Lecture 1. Scent of C++./ Электронный ресурс / Режим доступа: https://www.youtube.com/watch?v=Bym7UMqpVEY, свободный
- Современный С++: новые возможности и лучшие методологии / Электронный ресурс / Режим доступа: https://www.youtube.com/watch?v=tSANTMp-T9o, свободный
- Страуструп Б. Язык программирования C++: Пер. с англ. – 3-е спец. изд. – М.: Бином, 2003. – 1104 с.
- Троелсен, Э. Язык программирования С# 5.0 и платформа .NET 4.5 / Э. Троелсен; Пер. с англ. Ю.Н. Артеменко. — М.: Вильямс, 2016. — 1312 c.
- Уолтер Сэвитч. С++ в примерах /Пер. с англ. – М.: ЭКОМ, 1997. – 736 с
- Эккель Б., Эллисон Ч. Философия C++. Практическое программирование: Пер. с англ. – СПб.: Питер, 2004. – 608 с.
ЛИСТИНГ ПРОГРАММЫ
GameForm.cpp:
#include «GameForm.h»
#include «StartForm.h»
enum GameMode {
PvP = 0, //игрок против игрока
PvE //игрок против AI
} gameMode;
//общие данные
GameMap map;
Gamer player1;
Gamer player2;
GamerStatus currentPlayer;
Vector2 selectedCellPlayer;
//флажки
bool canPlay;
bool endGame;
bool sound;
//данные для AI
std::vector
allMoves;//все возможные ходы
int currentMoves;//текущий ход, с которого начинать
System::Void TicTacToegame::GameForm::GameForm_Load(System::Object^ sender, System::EventArgs^ e)
{
//инициализируем параметры
if (selectedGameMode == 0) {
gameMode = PvP;
}
else {
gameMode = PvE;
}
NewGame();
}
System::Void TicTacToegame::GameForm::новаяИграToolStripMenuItem_Click(System::Object^ sender, System::EventArgs^ e)
{
if (MessageBox::Show(«Continue?», «Attention!», MessageBoxButtons::YesNo) == Windows::Forms::DialogResult::Yes) {
NewGame();
}
}
System::Void TicTacToegame::GameForm::вернутьсяToolStripMenuItem_Click(System::Object^ sender, System::EventArgs^ e)
{
if (MessageBox::Show(«Continue?», «Attention!», MessageBoxButtons::YesNo) == Windows::Forms::DialogResult::Yes) {
StartForm^ form = gcnew StartForm();
form->Show();
this->Hide();
}
}
System::Void TicTacToegame::GameForm::оПрограммеToolStripMenuItem_Click(System::Object^ sender, System::EventArgs^ e)
{
MessageBox::Show(«The program implements a Tic-Tac-Toe game of the standard type for two players and against the computer. The game was implemented in order to complete the course work. Teacher: Gorlov Alexey Petrovich. Performed by: art. gr. O-21-IB-2-ozi-B Korotkova K.V.»);
}
System::Void TicTacToegame::GameForm::выходToolStripMenuItem_Click(System::Object^ sender, System::EventArgs^ e)
{
if (MessageBox::Show(«Continue?», «Attention», MessageBoxButtons::YesNo) == Windows::Forms::DialogResult::Yes) {
Application::Exit();
}
}
System::Void TicTacToegame::GameForm::dataGridView_CellContentClick(System::Object^ sender, System::Windows::Forms::DataGridViewCellEventArgs^ e)
{
auto senderGrid = (DataGridView^)sender;//преобразуем полученный объект в таблицу
//запоминаем индексы выбранной ячейки
selectedCellPlayer.x = e->RowIndex;
selectedCellPlayer.y = e->ColumnIndex;
//делаем ход
SetPositionPlayer(selectedCellPlayer);
}
void TicTacToegame::GameForm::GameLogic()
{
//проверяем режим игры
if (gameMode == PvE) {
//если очередь AI
if (currentPlayer == Computer) {
//делаем ход
StepAI();
}
Update();
}
}
void TicTacToegame::GameForm::Update()
{
if (endGame)
return;
//проверяем состояние игрового поля
int state_game = map.CheckingWin();
if (state_game == 1) {
if (gameMode == PvP) {
MessageBox::Show(«Congratulations to Player1 on his victory!!!», «Win!»);
}
else {
MessageBox::Show(«Congratulations to Player on his victory!!!», «Win!»);
}
UpdateGameGrid();
endGame = true;
}
else if (state_game == 2) {
if (gameMode == PvP) {
MessageBox::Show(«Congratulations to Player2 on his victory!!!», «Win!»);
}
else {
MessageBox::Show(«Congratulations to Computer on his victory!!!», «Win!»);
}
UpdateGameGrid();
endGame = true;
}
else if (state_game == 3) {
MessageBox::Show(«Friendship won!!!», «Draw!»);
UpdateGameGrid();
endGame = true;
}
if (endGame) {
if (MessageBox::Show(«Launch a new game???», «Attention!», MessageBoxButtons::YesNo) == Windows::Forms::DialogResult::Yes) {
NewGame();
}
return;
}
if (gameMode == PvE) {
if (currentPlayer == Computer) {
status->Text = «Player’s move!»;
currentPlayer = Player;
return;
}
else {
status->Text = «Computer move!»;
currentPlayer = Computer;
GameLogic();
UpdateGameGrid();
return;
}
}
else {
if (currentPlayer == Player1) {
status->Text = «Motion: 0!»;
currentPlayer = Player2;
}
else {
status->Text = «Motion: X!»;
currentPlayer = Player1;
}
}
GameLogic();
UpdateGameGrid();
}
void TicTacToegame::GameForm::NewGame()
{
//инициализация данных
map.SetMap(rows, columns, length);
rand = gcnew Random();
endGame = false;
CreateGameGrid(map.GetSize()); //создаем игровое поле
//инициализируем все возможные ходы для AI
currentMoves = 0;
allMoves.clear();
Vector2 buf;
for (int i = 0; i < map.GetSize().x; i++) {
for (int j = 0; j < map.GetSize().x; j++) {
buf.x = i;
buf.y = j;
allMoves.push_back(buf);
}
}
//перемешиваем все возможные ходы
//несколько раз
int num_mixing = rand->Next(1, 10);
for (int i = 0; i < num_mixing; i++)
std::random_shuffle(allMoves.begin(), allMoves.end());
//проверяем очередность и режимы игры
int state_gamer = rand->Next(1, 3);
if (state_gamer == 1) {
if (gameMode == PvE) {
player1.SetField(Player, 1);
player2.SetField(Computer, 2);
status->Text = «Motion: Player!»;
currentPlayer = Player;
}
else {
player1.SetField(Player1, 1);
player2.SetField(Player2, 2);
status->Text = «Motion: X!»;
currentPlayer = Player1;
}
}
else if (state_gamer == 2) {
if (gameMode == PvE) {
player1.SetField(Player, 1);
player2.SetField(Computer, 2);
status->Text = «Motion: Computer!»;
currentPlayer = Computer;
GameLogic();
UpdateGameGrid();
}
else {
player1.SetField(Player1, 1);
player2.SetField(Player2, 2);
status->Text = «Motion: 0!»;
currentPlayer = Player2;
}
}
else {
MessageBox::Show(«Initial Player generation error!», «Error!»);
return;
}
}
void TicTacToegame::GameForm::StepAI()
{
//проверяем есть ли еще ходы
if (currentMoves < allMoves.size()) {
//проверяем текущий код
if (map.IsEmpty(allMoves[currentMoves])) {
//если свободно, делаем ход
map.SetPosition(allMoves[currentMoves], player2.GetMark());
currentMoves++;
}
else {
//если занята, перемещаемся на следующий ход
//и вызываем рекурсивно данную функцию
currentMoves++;
StepAI();
}
}
}
void TicTacToegame::GameForm::UpdateGameGrid()
{
for (int i = 0; i < map.GetSize().x; i++) {
for (int j = 0; j < map.GetSize().y; j++) {
if (!map.IsEmpty(i, j)) {
if (map.GetValue(i, j) == 1) {
dataGridView->Rows[i]->Cells[j]->Value = «X»;
}
else {
dataGridView->Rows[i]->Cells[j]->Value = «0»;
}
}
}
}
}
void TicTacToegame::GameForm::CreateGameGrid(Vector2 size)
{
//очищаем таблицу
dataGridView->Rows->Clear();
dataGridView->Columns->Clear();
//задаем стиль
System::Drawing::Font^ font = gcnew System::Drawing::Font(«Microsoft Sans Serif», 14);
dataGridView->DefaultCellStyle->Font = font;
dataGridView->ColumnHeadersDefaultCellStyle->Font = font;
dataGridView->RowHeadersDefaultCellStyle->Font = font;
//создаем столбцы
for (int i = 0; i < size.x; i++) {
DataGridViewButtonColumn^ column = gcnew DataGridViewButtonColumn();
column->HeaderCell->Value = Convert::ToString(i + 1);
column->Name = «column» + i;
column->Width = 50;
dataGridView->Columns->Add(column);
}
//создаем строки
for (int i = 0; i < size.y; i++) {
dataGridView->Rows->Add();
dataGridView->Rows[i]->HeaderCell->Value = Convert::ToString(i + 1);
dataGridView->Rows[i]->Height = 50;
}
}
System::Void TicTacToegame::GameForm::SetPositionPlayer(Vector2 cell)
{
//проверяем режим игры
if (gameMode == PvE) {
//проверяем очередность игрока
if (currentPlayer == Player) {
//делаем ход
if (!map.SetPosition(cell, player1.GetMark())) {
MessageBox::Show(«This position is occupied!», «Attention!»);
return;
}
}
else {
MessageBox::Show(«It’s not your turn yet!», «Attention!»);
return;
}
}
else {
//проверяем очередность игрока
if (currentPlayer == Player1) {
//делаем ход
if (!map.SetPosition(cell, player1.GetMark())) {
MessageBox::Show(«This position is occupied!», «Attention!»);
return;
}
}
else {
//делаем ход
if (!map.SetPosition(cell, player2.GetMark())) {
MessageBox::Show(«This position is occupied!», «Attention!»);
return;
}
}
}
Update();
}
StartForm.cpp:
#include «StartForm.h»
#include «GameForm.h»
using namespace System;
using namespace System::Windows::Forms;
[STAThreadAttribute]
void main(array
^ args)
{
Application::EnableVisualStyles();
Application::SetCompatibleTextRenderingDefault(false);
TicTacToegame::StartForm form;
Application::Run(% form);
}
System::Void TicTacToegame::StartForm::оПрограммеToolStripMenuItem_Click(System::Object^ sender, System::EventArgs^ e)
{
MessageBox::Show(«The program implements a Tic — Tac — Toe game of the standard type for two playersand against the computer.The game was implemented in order to complete the course work.Teacher: Gorlov Alexey Petrovich.Performed by : art.gr.O — 21 — IB — 2 — ozi — B Korotkova K.V.»);
}
System::Void TicTacToegame::StartForm::выходToolStripMenuItem_Click(System::Object^ sender, System::EventArgs^ e)
{
Application::Exit();
}
System::Void TicTacToegame::StartForm::buttonStartGame_Click(System::Object^ sender, System::EventArgs^ e)
{
int indexGameMode = listBoxGameMode->SelectedIndex;
if (indexGameMode == -1) {
MessageBox::Show(«Play!»);
return;
}
int size;
size = Convert::ToInt32(numericUpDownSizeMap->Value);
numericUpDownLenght->Maximum = size;
int length = Convert::ToInt32(numericUpDownLenght->Value);
GameForm^ form = gcnew GameForm();
form->rows = size;
form->columns = size;
form->length = length;
form->selectedGameMode = indexGameMode;
form->Show();
this->Hide();
}
GameMap.h:
#pragma once
#include
#include
struct Vector2 {
int x,
y;
};
class GameMap
{
//поля класса
private:
int** map;
Vector2 size;
int length;//необходимая длина выигрышной последовательности
//методы класса
public:
GameMap();//конструктор (создает пустой объект класса)
GameMap(Vector2, int);//конструктор (создает объект класса с заданными параметрами)
GameMap(int, int, int);//конструткор (создает объект класса с заданными параметрами)
GameMap();//деструктор (удаляет необходимые данные относительно текущего класса)
bool SetPosition(Vector2, int); //задание позиции
bool IsEmpty(Vector2); //проверка на свободную ячейку
bool IsEmpty(int, int); //проверка на свободную ячейку
void SetMap(Vector2, int); //получить некоторые значения текущей карты
void SetMap(int, int, int); //получить некоторые значения текущей карты
int GetValue(Vector2); //получить значения
int GetValue(int, int); //получить значения
int GetLength() { return length; }; //получить длину для выигрышной последовательности
Vector2 GetSize() { return size; }; //получить размер текущей карты
int CheckList(std::vector
a);
int CheckingWin(); //проверить выигрыш
bool CanMove(); //проверить свободные ходы
Vector2 GetCenter(); //проверка для центра
};
GameMap.cpp:
#include «GameMap.h»
GameMap::GameMap()
{
size.x = 0;
size.y = 0;
length = 0;
map = new int* [size.x];
for (int i = 0; i < size.x; i++) {
map[i] = new int[size.y];
}
}
GameMap::GameMap(Vector2 _size, int l)
{
//инициализируем игровую карту
size = _size;
length = l;
//выделяем память для нашей карты
map = new int* [size.x];
for (int i = 0; i < size.x; i++) {
map[i] = new int[size.y];
}
//Обнуляем
for (int i = 0; i < size.x; i++) {
for (int j = 0; j < size.y; j++) {
map[i][j] = 0;
}
}
}
GameMap::GameMap(int i, int j, int l)
{
//инициализируме игровую карту
size.x = i;
size.y = j;
length = l;
map = new int* [size.x];
for (i = 0; i < size.x; i++) {
map[i] = new int[size.y];
}
//обнуляем
for (i = 0; i < size.x; i++) {
for (j = 0; j < size.y; j++) {
map[i][j] = 0;
}
}
}
GameMap::GameMap()
{
//Очищаем матрицу
for (int i = 0; i < size.x; i++) {
delete[] map[i];
}
delete[] map;
}
bool GameMap::SetPosition(Vector2 cell, int c)
{
if (IsEmpty(cell)) {
map[cell.x][cell.y] = c;
return true;
}
else {
return false;
}
}
bool GameMap::IsEmpty(Vector2 cell)
{
if (map[cell.x][cell.y] == 0)
return true;
else
return false;
}
bool GameMap::IsEmpty(int i, int j)
{
if (map[i][j] == 0)
return true;
else
return false;
}
void GameMap::SetMap(Vector2 _size, int l)
{
//инициализируем игровую карту
size = _size;
length = l;
map = new int* [size.x];
for (int i = 0; i < size.x; i++) {
map[i] = new int[size.y];
}
//обнуляем
for (int i = 0; i < size.x; i++) {
for (int j = 0; j < size.y; j++) {
map[i][j] = 0;
}
}
}
void GameMap::SetMap(int i, int j, int l)
{
//инициализируем
size.x = i;
size.y = j;
length = l;
map = new int* [size.x];
for (int i = 0; i < size.x; i++) {
map[i] = new int[size.y];
}
//обнуляем
for (int i = 0; i < size.x; i++) {
for (int j = 0; j < size.y; j++) {
map[i][j] = 0;
}
}
}
int GameMap::GetValue(Vector2 cell)
{
return map[cell.x][cell.y];
}
int GameMap::GetValue(int i, int j)
{
return map[i][j];
}
int GameMap::CheckList(std::vector
a)
{
if (a.size() < length)
return 0;
//флажки победы игроков
bool winPl1 = false;
bool winPl2 = false;
int count1 = 0, count2 = 0;
//проверяем набранные очки
for (int i = 0; i < a.size(); i++) {
for (int j = i; j < i + length; j++) {
if (j < a.size()) {
if (a[j] == 1) {
count1++;
}
}
else
break;
}
if (count1 >= length) {
winPl1 = true;
break;
}
count1 = 0;
}
for (int i = 0; i < a.size(); i++) {
for (int j = i; j < i + length; j++) {
if (j < a.size()) {
if (a[j] == 2) {
count2++;
}
}
else
break;
}
if (count2 >= length) {
winPl2 = true;
break;
}
count2 = 0;
}
//выводим результат
if (winPl1 && winPl2) {
return 3;//ничья
}
else if (!winPl1 && winPl2) {
return 2;//победа 2
}
else if (winPl1 && !winPl2) {
return 1;//победа 1
}
else
return 0;
}
int GameMap::CheckingWin()
{
//проходим по карте в поиске последовательностей заданной длины
int stateWin = 0;
std::vector
check;
/*
[][][]
[][][]
[][][]
*/
//1. Проверяем все горизонтали
for (int i = 0; i < size.x; i++) {
for (int j = 0; j < size.y; j++) {
check.push_back(map[i][j]);
}
//Проверяем
stateWin = CheckList(check);
check.clear();
if (stateWin == 3) {
return 3;//ничья
}
else if (stateWin == 2) {
return 2;//победа 2
}
else if (stateWin == 1) {
return 1;//победа 1
}
}
//2. Проверяем все вертикали
for (int i = 0; i < size.x; i++) {
for (int j = 0; j < size.y; j++) {
check.push_back(map[j][i]);
}
//проверяем
stateWin = CheckList(check);
check.clear();
if (stateWin == 3) {
return 3;//ничья
}
else if (stateWin == 2) {
return 2;//победа 2
}
else if (stateWin == 1) {
return 1;//победа 1
}
}
//3. Проверяем все левые диагонали
//Главная диагональ и над ней
for (int i = 0; i < size.x; ++i)
{
for (int j = 0; i + j < size.y; ++j) {
check.push_back(map[i + j][j]);
}
//проверяем
stateWin = CheckList(check);
check.clear();
if (stateWin == 3) {
return 3;//ничья
}
else if (stateWin == 2) {
return 2;//победа 2
}
else if (stateWin == 1) {
return 1;//победа 1
}
}
//Под главной диагональю
for (int i = 1; i < size.x; ++i)
{
for (int j = 0; i + j < size.y; ++j) {
check.push_back(map[i + j][j]);
}
//проверяем
stateWin = CheckList(check);
check.clear();
if (stateWin == 3) {
return 3;//ничья
}
else if (stateWin == 2) {
return 2;//победа 2
}
else if (stateWin == 1) {
return 1;//победа 1
}
}
//4. Проверяем все правые диагонали
//Побочная диагональ и над ней
for (int j = size.y; j > 0; —j) {
for (int i = 0; i < size.x; ++i)
{
if (size.x — j — i >= 0) {
check.push_back(map[i][size.x — j — i]);
}
}
//проверяем
stateWin = CheckList(check);
check.clear();
if (stateWin == 3) {
return 3;//ничья
}
else if (stateWin == 2) {
return 2;//победа 2
}
else if (stateWin == 1) {
return 1;//победа 1
}
}
//Под побочной диагональю
for (int j = 0; j < size.y; ++j) {
for (int i = 0; i < size.x; ++i)
{
if (size.x + j — i < size.x) {
check.push_back(map[i][size.x + j — i]);
}
}
//проверяем
stateWin = CheckList(check);
check.clear();
if (stateWin == 3) {
return 3;//ничья
}
else if (stateWin == 2) {
return 2;//победа 2
}
else if (stateWin == 1) {
return 1;//победа 1
}
}
if (CanMove())
return 0; //нет победителя
else
return 3;
}
bool GameMap::CanMove()
{
for (int i = 0; i < size.x; i++) {
for (int j = 0; j < size.y; j++) {
if (map[i][j] == 0) {
return true;
}
}
}
return false;
}
Vector2 GameMap::GetCenter()
{
Vector2 center;
int x;
if (size.x % 2 != 0) {
x = (size.x — 1) / 2;
}
else {
x = (size.x / 2) — 1;
}
center.x = x;
center.y = x;
return center;
}
Gamer.h:
#pragma once
enum GamerStatus { //перечисляем все возможные статусы игрока
Computer = 0,
Player,
Player1,
Player2
};
class Gamer
{
//поля класса
private:
GamerStatus status;
int mark;
//методы класса
public:
Gamer() {} //конструктор
Gamer() {} //деструктор
void SetField(GamerStatus mode, int m); //задаем поля
int GetMark() { return mark; }; //вернуть то, кто сейчас ходит
GamerStatus GetStatus() { return status; } //вернуть статус
};
Gamer.cpp:
#include «Gamer.h»
void Gamer::SetField(GamerStatus mode, int m)
{
if (mode == 0)
status = Computer;
else if (mode == 1)
status = Player;
if (mode == 2)
status = Player1;
else
status = Player2;
mark = m;
}