Как сделать игру крестики нолики
Перейти к содержимому

Как сделать игру крестики нолики

  • автор:

Конспект к Java-марафону: пишем игру «Крестики-нолики»

Конспект к Java-марафону: пишем игру

Чтобы начать работу над проектом, открой раздел “Игры” и нажми на карточку с игрой “Крестики-нолики”: После этого вы увидите описание игры и видео с демонстрацией того, как она должна выглядеть. Чтобы перейти к написанию проекта, нажмите кнопку “Написать свое решение”. После этого снизу появится окно с webIDE от JavaRush (webIDE – онлайн-версия среды разработки): Это наш онлайн-симулятор, который позволяет писать собственный код, корректировать его, отправлять на проверку и получать результат того, правильно ли написано решение. В поле справа содержится описание и условия для выполнения задания. Сначала внимательно прочти условия задания, а затем, в поле слева, начинай работать с кодом!

Основные функции WebIDE от JavaRush

Конспект к Java-марафону: пишем игру

  1. Проверка – проверка валидатором правильности твоего решения. Нажимаешь на эту кнопку – получаешь результат.
  2. Помощь – набор различных функций для помощи с кодом:
    • Помощь сообщества — переход к обсуждению этой задачи в разделе «Помощь»: ты можешь задать свой вопрос или воспользоваться информацией из существующих обсуждений. Впрочем, на Java-стажировке рекомендуем тебе обсуждать свои решения в коммьюнити в Telegram-чате, поскольку игра
    • «Крестики-нолики» разработана специально под стажировку, и ее пишут только его участники
    • Правильное решение – возможность посмотреть, как выглядит правильное решение задачи. Советуем использовать только в исключительном случае. Если внимательно смотреть занятия с ментором, то все получится и без подсказок!
    • С помощью кнопки “Вернуть мой код” ты можешь вернуться к собственному решению, без подсказок.
    • Обнулить решение – сбросить написанный код и начать сначала.
  3. Запуск – запуск кода без проверки решения.
  4. Анализ кода – возможность получить рекомендации по улучшению твоего кода.
После того, как ты напишешь решение, которое пройдет проверку, ты сможешь перейти к следующей подзадаче в проекте по написанию игры.

Как сдавать домашние задания

Конспект к Java-марафону: пишем игру

Все выполненные задачи будет проверять виртуальный помощник – валидатор, встроенный в онлайн-инструмент для кодинга на сайте JavaRush. Он будет давать ответ через 1-2 секунды. Впрочем, нам тоже нужно наблюдать за твоими успехами на стажировке. Чтобы кураторы стажировки засчитали твое задание и у тебя была возможность посоревноваться за подарки от JavaRush, присылай в Телеграмм-бот в дедлайн (17:00 на следующий день после занятия) скриншот, на котором хорошо видно: 1️⃣ Окно WebIDE JavaRush 2️⃣ Номер задания 3️⃣ Успешный результат проверки Все, как на этом скриншоте:

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

1. Основные понятия языка Java, которые тебе полезно знать

ООП – объектно-ориентированное программирование. Java – объектно-ориентированный язык. Это означает, что все, что создается в программе, должно быть создано в виде объекта. Программа – это набор (список) команд. Сначала выполняется первая команда, затем вторая, третья и т.д. Когда все команды выполнены, программа завершается. Программы на языке Java выполняет JVM (Java Virtual Machine – виртуальная машина Java). JVM – это специальная программа, которая умеет выполнять программы, написанные на языке Java. Основные принципы написания команд Принцип первый: в языке Java каждую команду принято писать с новой строки. В конце команды ставится точка с запятой:

Java-университет

System.out.println("Привет, будущий программист");

Принцип второй: программа не может просто состоять из команд. Команды языка Java должны находиться внутри методов, а методы внутри классов. Общую иерархию можно описать так: Java-приложения состоят из классов, классы содержат методы, а методы – команды.

Важные принципы форматирования кода

  • Не забывай ставить точку с запятой ; в конце команды, которую ты пишешь
  • У каждой скобки или открывающей кавычки: (, , “, должна быть скобка или закрывающая кавычка: ), >, ”
  • Если твой код подсвечивается красным, значит, в нем есть ошибка — присмотрись внимательнее
  • Используй автоматические подсказки и автодополнения – это поможет тебе писать код правильно
  • Помни о том, что твои названия переменных, методов и классов должны быть читабельными, то есть из названия должно быть понятно, что они делают и для чего предназначены

Конспект к занятию #1: создаем заготовку игры

В разработке игры Tic Tac Toe мы будем работать с публичным (public) классом TicTacToe, унаследованным от класса Game. Это класс, написанный разработчиками JavaRush, и в нем есть набор методов, которые помогут тебе с написанием собственной игры.

1. Создаем игровую сетку
  • public – означает публичный метод, который может использоваться за пределами нашего класса
  • void – значение, указывающее на то, что метод нам ничего не возвращает
2.1 Создаем игровую модель и ее отображение
  1. private – означает частный метод, который будет использоваться только в пределах нашего класса
  2. [] — квадратные скобки обозначают квадратный массив
  • 0 – пусто (пустая клетка)
  • 1 — х (ход первого игрока, ставящего крестик)
  • 2 — 0 (ход второго игрока, ставящего нолик)
2.2 Фиксируем текущего игрока

После модели нужно создать переменную private int currentPlayer , в которой будем запоминать, какой сейчас игрок ходит (1 – первый игрок, 2 – второй).

3. Инициализируем переменные, в которых игра сохраняет свои состояния
  • чтобы заполнить массив нулями, используем два вложенных цикла for
  • переменной currentPlayer передайте значение 1 (1 — ход первого игрока)
4. Реализуем отображение ячейки
  • Если value равно 0, в ячейке нужно отобразить пустую строку — «» (пробел)
  • Если value равно 1, в ячейке нужно отобразить «X» (Большой латинский икс)
  • Если value равно 2, в ячейке нужно отразить нолик «О» (большая латинская).
5. Реализуем отображение всех ячеек массива model на игровом поле

Создай метод public void updateView() для отображения всех ячеек массива model на игровом поле. Добавь его вызов в конце метода initialize() . Внутри метода updateView() нужно пройтись по всеми ячейками массива model и отразить значение каждой из них на экране: Для этого нужно написать два вложенных цикла, которые проходят по всем ячейкам массива. С помощью вызова метод updateCellView(x, y, value) отображает значение каждой ячейки на экране.

6. Добавляем обработку щелчков мышкой
  1. Записать в ячейку model[x][y] значение currentPlayer .
  2. Отобразить модель на экране, вызвав метод updateView().
  3. Переключение текущего игрока. Сменить 1 на 2, а 2 на 1.

Домашнее задание

  • Произведи все, что сделал ментор на видео занятия — напиши шесть методов, которые помогут создать заготовку игры в подзаданиях 1-6.
  • Самостоятельно выполни подзадачу №7. Раскрась Х в красный, а О – в синий цвет. Используй для этого метод setCellValueEx()

Конспект к занятию #2: добавляем ходы по очереди

Чтобы продолжить работу над проектом, открой раздел «Игры» и нажми на карточку с игрой «Крестики-нолики».

1. Проверяем, есть ли в ячейке определенный знак

Начнем с того, что запретим ставить крестики/нолики к клетке, в которой что-то уже есть. Для этого необходимо добавить проверку в нашем методе onMouseLeftClick() с помощью условного оператора if.

2. Фиксируем конец игры и запрещаем игроку делать ход после этого

Также нужно запретить игроку делать ход, если игра уже кончилась. Для этого нам, конечно, нужен статус завершения игры. Для этого мы используем boolean (логическую) переменную: private boolean isGameStopped . Стартовое значение для нее будет false. Проверку, завершена ли игра мы должны добавить к методу onMouseLeftClick() . Если игра остановлена — перерыв выполнения метода onMouseLeftClick() выполнил оператор return .

3. Делаем проверку выигрыша

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

 public boolean checkWin(int x, int y, int player) 

public boolean checkWin() , принимающий координаты ячейки, в которую поставили знак (Х или О), и номер происходящего игрока. Метод boolean (логический), поскольку он должен проверять, есть ли победитель или нет. В первом случае он должен возвращать true, во втором — false. Его нужно написать после метода для хода текущего игрока перед передачей хода к следующему игроку.

4. Прописываем сообщение о победе в игре
  • Если checkWin() вернул true, то нужно установить переменную isGameStopped значение true
  • Отобразить победителя с помощью вызова метода showMessageDialog()
  • Завершить выполнение метода
  • cellColor – цвет диалогового окна
  • text — текст сообщения
  • textColor – цвет текста
  • textSize – размер текста в процентах от высоты окна

Домашнее задание

  1. Воспроизведи все, что сделал ментор на видео занятия, в подзаданиях 8-11
  2. Самостоятельно выполни подзадачу №12. Дополнительно напиши проверку, которая покажет, заполнены ли клетки по диагонали (одной и другой) тремя крестиками или ноликами, чтобы завершить проверку того, одержал ли какой-нибудь из игроков победу. Это необходимо сделать в способе checkWin() . Метод должен вернуть true, если имеется вертикаль, горизонталь или диагональ, заполненная значением n. В противном случае метод должен вернуть false

Конспект к занятию #3: перезагрузка игры

1. Выносим наш код к методу setSignAndCheck()

Пора провести небольшой рефакторинг нашего кода. Рефакторинг (от англ. refactoring) — изменение внутренней структуры программы, не влияющей на ее внешнее поведение. По сути это упрощение и улучшение уже существующего кода, чтобы придать ему более компактный и читабельный вид, повысить производительность и избежать ошибок в коде в будущем. Нам нужно вынести часть из кода метода onMouseLeftClick() (посвящённому ходу игрока и проверке на победу в игре) в отдельный метод — public void setSignAndCheck(int x, int y) . В видео ментор наглядно показывает, как это сделать. В методе onMouseLeftClick() замените весь код на вызов setSignAndCheck(x, y) .

2. Пишем проверку на ничью

А что делать в случае, когда игра кончилась, доступных ячеек для хода нет, но победителя тоже невозможно определить? Нам нужно прописать в коде обработку ничьи как результата игры. Создай новый метод — public boolean hasEmptyCell() — он будет проверять, есть ли еще на поле пустые клетки. В этом методе нужно пройтись всеми значениями в массиве model и вернуть true, если хотя бы одно значение равно нулю. В противном случае метод должен вернуть false.

3. Добавляем проверку, есть ли на игровом поле пустая клетка
  • показать сообщение про ничью.
  • установить isGameStopped значение true.
4. Добавляем перезагрузку игры

Условием перезагрузки игры и сброса всех предварительных результатов мы сделаем нажатие на пробел. Для этого нам нужно переопределить метод, уже имеющийся у класса Game (нашего игрового движка). Нажми ctrl+O и выбирай метод onKeyPress – именно то, что нам нужно. В методе нужно прописать цикл, содержание которого такое: если наша игра завершена, и пользователь нажал на клавишу пробел (SPACE), то мы должны заново инициализировать модель и обновить view (игровое представление).

5. Добавляем рестарт незавершенной игры

Также добавляем возможность перезапустить незавершенную игру в любое время с помощью Escape. Если пользователь нажал ESC, нужно начать игру заново. Старая логика тоже должна работать: если пользователь нажал пробел, и игра была окончена, то нужно стартовать игру заново. Пример проверки нажатия клавиши пропуска: if (key == Key.SPACE) < . >

Домашнее задание

  • Воспроизведи все, что сделал ментор на видео в подзаданиях 13-17.
  • Самостоятельно выполни подзадачу №18. перепиши текст сообщений: вместо «Победа игрока N» — «You win», вместо «Ничья» — «Game over».
  • «You Win!» нужно отобразить зеленым цветом, если первый игрок выиграл.
  • «Game Over» нужно отображать красным, если первый игрок проиграл — выиграл второй.
  • Изменения нужно производить в методе setSignAndCheck() .

Конспект к занятию #4: прописываем ходы компьютера

1. Добавляем ходы компьютера

Чтобы добавить ход компьютера, используем метод public void computerTurn() и добавляем условие, благодаря которому компьютер всегда будет делать ход в центральную ячейку, если она будет свободна после хода игрока, в противном случае — в соседнее. Метод computerTurn() нужно вызвать в самом конце метода onMouseLeftClick() и после этого снова изменить текущего игрока: currentPlayer = 3 — currentPlayer; .

2. Учим компьютер «ходить»
  • Проверим, свободна ли центральная клетка, и если свободна, то идем на нее
  • В противном случае идем на первую найденную свободную клеточку
3. Предполагаем победу игрока

Чтобы усложнить мышление компьютера, нам понадобится метод checkFutureWin() . Добавь метод public boolean checkFutureWin(int x, int y, int n) . Он должен вернуть true, если игрок n выигрывает игру после хода на ячейку (x, y). В противном случае метод должен вернуть false.

4. Улучшаем логику игры компьютера

Соединяем методы computerTurn() и checkFutureWin() , чтобы улучшить логику метода computerTurn() . После проверки центральной клетки добавь такую логику: перебираем все ячейки массива model и проверяем: если можно выиграть игру, сделав ход в клеточку (x,y), то делаем ход в ячейку (x,y).

Домашнее задание

  • Воспроизведи все, что сделал ментор на видео занятия в подзаданиях 19-22
  • Самостоятельно выполни подзадачу №23. улучши метод ComputerTurn() : напиши метод checkEnemyWin() , который предполагает победу противника и препятствует ему.
  • делаем попытку походить в центральную клеточку
  • делаем ход, если можем выиграть одним ходом
  • мешаем противнику, если он может выиграть
  • делаем ход в первую попавшуюся пустую клетку

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

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

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

Игровое поле:

class GameTable < public GameTable(int row, int col) < . >// Выполнение хода игроком // Параметр Player player - игрок с методом player.getChip() // return есть ли выигрышная комбинация (true), или нет (false) public boolean move(Player player) < // Здесь игрок player выбирает не занятую/свободную ячейку и помещает // на нее свою фишку player.getChip() return this.checkWinCombination(selectedRow, selectedColumn); >// Проверяет заполненность стола. Если заполнен и ходов больше сделать нельзя, // то true, иначе false public boolean isFull() < >// Метод проверки выигрышной комбинации, начиная с заданной ячейки row, col private boolean checkWinCombination(int row, int col) < // Берем ячейку и пробегаем смежные ячейки по горизонтали, // вертикали и диагоналям в поисках выигрышной последовательности // Если нашли, то return true // иначе false >> 

Игрок:

class Player < // name - имя игрока // chip - фишка игрока public Player(String name, String chip) < >public String getName() < >public String getChip() < >> 

Инициализация

//Список игроков: List players = new ArrayList<>(); // Игровое поле GameTable gameTable = new GameTable(3, 3); // Создаем игроков // Первый параметр - имя игрока, второй - тип фишки (chip) Player player1 = new Player("Крестик", "X"); Player player2 = new Player("Нолик", "0"); players.add(player1); players.add(player2); 

Игровой процесс

Ходят игроки последовательно друг за другом до тех пор пока один из них не выставит свои фишки в ряд (горизонтально, вертикально, или по-диагоналям).

Ага, так, последовательно, друг за другом, пока кто-то не будет признан выигравшим после проверки очередного хода на выполненную/выигрышную комбинацию:

// Ссылка на победителя Player winner = null; // Индекс текущего игрока int currentPlayerIndex = 0; // Цикл будет крутиться до тех пор, пока внутри него игроки будут делать ходы и // либо один из них не выиграет, либо пока не заполнится игровое поле while(true) < Player player = players.get(currentPlayerIndex); if ( gameTable.move( player ) ) < // у нас есть победитель winner = player; break; >else if (gameTable.isFull()) < break; >// Если нет победителя и на доске еще есть место для ходов, то // меняем индекс текущего игрока, чтобы передать ход другому. // Здесь мы увеличиваем индекс на единицу и делим по модулю на // количество игроков, тем самым избегая "выскакивания" индекса за пределы массива currentPlayerIndex = ++currentPlayerIndex % players.size(); > if ( winner != null ) < System.out.print(" Победитель: " + player.getName()); >else

Создание игры «Крестики-нолики» при помощи TypeScript, React и Mocha

Представляем вам перевод статьи Josh Kuttler, опубликованной на blog.bitsrc.io. Узнайте, как создать приложение «Крестики-нолики», используя React и TypeScript.

Простая игра в крестики-нолики создана по модульному принципу и загружена на сайт Bit. Вы можете изменять компоненты моей игры и тестировать ее онлайн на Bit PlayGround при помощи NPM, Yarn или Bit. Для этого перейдите к моей коллекции компонентов.

Когда создаешь игры типа «Крестики-нолики» по модульному принципу, трудно найти причину, по которой компоненты UI могут снова когда-либо использоваться. Поэтому я сосредоточился в основном на игровых утилитах.

Для программирования я выбрал язык TypeScript — скомпилировал код при помощи TypeScript на сайте Bit. Затем воспользовался фреймворком Mocha для тестирования.

Чтобы установить компоненты из моего проекта, сначала настройте bit.dev в качестве реестра области (скопируйте и вставьте на своем устройстве). Это следует сделать только один раз! При дальнейшем использовании сайта Bit проводить повторную настройку не понадобится.

npm config set '@bit:registry' https://node.bit.dev

Затем установите компонент при помощи менеджеров пакетов Yarn или NPM:

npm i @bit/joshk.tic-tac-toe-game.game yarn add @bit/joshk.tic-tac-toe-game.game

Компонент «игра»

Компонент «игра» является основным компонентом моего приложения — он создан при помощи одного компонента Board и двух компонентов Prime React.

Я использовал компоненты Button и Input-text для экрана настройки — протестировать и посмотреть их код можно здесь.

Установите компоненты PrimeReact в свой проект:

yarn add @bit/primefaces.primereact.inputtext yarn add @bit/primefaces.primereact.button

После настройки параметров можно кликнуть на «Играть» и… играть!

Компонент Board

Компонент Board создает динамическую таблицу при помощи Props, устанавливает очередь для игроков и определяет победителя. Протестировать и посмотреть код можно здесь.

Компонент Square

Компонент Square — это обычная ячейка, которая получает значение с опциональным цветом и отправляет ивент компоненту Board при изменении значения. Протестировать и посмотреть код можно здесь.

Функция Empty cell

Функция Empty cell — это вспомогательная функция для функции Winner-calc, которая проверяет, есть ли пустые ячейки в таблице игры.

Bit позволяет увидеть документы компонента и результаты тестов:

Код функции

/** * @description * check if 2d array have an empty cell * @param <>> matrix 2d array * @param rowsNum number of rows * @param colsNum number of columns * @returns return true if empty cell was found, and false if not. * @example * import haveEmptyCell from '@bit/joshk.tic-tac-toe-game.utils.have-empty-cell'; * * const matrix = [ * ['X', 'O', 'X'], * ['O', 'X', 'O'], * ['O', 'X', 'O'] * ]; * const result = haveEmptyCell(matrix, 3, 3); * * export default result * @example * import haveEmptyCell from '@bit/joshk.tic-tac-toe-game.utils.have-empty-cell'; * * const matrix = [ * ['X', 'O', 'X'], * ['O', '', 'O'], * ['O', 'X', 'O'] * ]; * const result = haveEmptyCell(matrix, 3, 3); * * export default result * @example * import haveEmptyCell from '@bit/joshk.tic-tac-toe-game.utils.have-empty-cell'; * * const matrix = [ * ['X', 'O', 'X'], * ['O', , 'O'], * ['O', 'X', 'O'] * ]; * const result = haveEmptyCell(matrix, 3, 3); * * export default result * @example * import haveEmptyCell from '@bit/joshk.tic-tac-toe-game.utils.have-empty-cell'; * * const matrix = [ * ['X', 'O', 'X'], * ['O', null, 'O'], * ['O', 'X', 'O'] * ]; * const result = haveEmptyCell(matrix, 3, 3); * * export default result */ function haveEmptyCell(matrix: Array>, rowsNum: number, colsNum: number): boolean < let empty: boolean = false; for (let x = 0; x < rowsNum; x++) < for (let y = 0; y < colsNum; y++) < const element: any = matrix[x][y]; if (!element) < empty = true; break; >> if (empty) break; > return empty; > export default haveEmptyCell

Функция Winner calculation

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

Bit позволяет увидеть документы компонента и результаты тестов:

Код функции

/** * @description * check winner horizontal, vertical and diagonal * @param > matrix 2d array with X and O * @param rowsNum number of rows * @param colsNum number of columns * @param numToWin the number of matching to win * @param lastRow the row number of the square player click * @param lastCol the column number of the square player click * @returns return the winner, X or O or '' if no one win. * @example * import winnerCalc from '@bit/joshk.tic-tac-toe-game.utils.winner-calc'; * * const matrix = [ * ['O', 'O', 'X'], * ['O', 'X', ''], * ['X', '', ''] * ]; * const result = winnerCalc(matrix, 3, 3, 3, 0, 2); * * export default result */ import haveEmptyCell from '../HaveEmptyCell' function winnerCalc(matrix: Array, rowsNum: number, colsNum: number, numToWin: number, lastRow: number, lastCol: number): string < let winner: string = ''; let match: number = 0; const lastValue: string = matrix[lastRow][lastCol]; //check Horizontal for (let c = 0; c < colsNum; c++) < let currentValue = matrix[lastRow][c]; if (currentValue === lastValue) match++; else match = 0; if (match === numToWin) < winner = lastValue; break; >> if (winner !== '') return winner; match = 0; //check Vertical for (let r = 0; r < rowsNum; r++) < let currentValue = matrix[r][lastCol]; if (currentValue === lastValue) match++; else match = 0; if (match === numToWin) < winner = lastValue; break; >> if (winner !== '') return winner; //check diagonal top-left to bottom-right - include middle match = 0; for (let r = 0; r rowPosition++; > if (winner !== '') break; > if (winner !== '') return winner; //check diagonal top-left to bottom-right - after middle match = 0; for (let c = 1; c columnPosition++; > if (winner !== '') break; > if (winner !== '') return winner; //check diagonal bottom-left to top-right - include middle match = 0; for (let r = rowsNum - 1; r >= rowsNum - numToWin - 1; r--) < let rowPosition = r; for (let column = 0; column < colsNum && rowPosition < rowsNum && rowPosition >= 0; column++) < let currentValue = matrix[rowPosition][column]; if (currentValue === lastValue) match++; else match = 0; if (match === numToWin) < winner = lastValue; break; >rowPosition--; > if (winner !== '') break; > if (winner !== '') return winner; //check diagonal bottom-left to top-right - after middle match = 0; for (let c = 1; c < colsNum; c++) < let columnPosition = c; for (let row = rowsNum - 1; row < rowsNum && row >= 0 && columnPosition < colsNum && columnPosition >= 1; row--) < console.log(`[$][$]`); let currentValue = matrix[row][columnPosition]; if (currentValue === lastValue) match++; else match = 0; if (match === numToWin) < winner = lastValue; break; >columnPosition++; > if (winner !== '') break; > if (winner !== '') return winner; if(haveEmptyCell(matrix, rowsNum, colsNum) === false) < winner = '-1'; >return winner; > export default winnerCalc

Проект доступен в моей коллекции на Bit и в моём репозитории GitHub.

Не стесняйтесь комментировать эту статью и подписывайтесь на мой Twitter.

  • разработка игр
  • разработка приложений
  • игры
  • начинающим
  • своими руками
  • крестики-нолики
  • крестики нолики
  • typescript
  • mocha
  • react
  • программирование игр
  • программирование
  • программирование для начинающих

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

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

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

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

Что делаем

Проект будет состоять из трёх частей:

  1. Игровой интерфейс, который работает в браузере, — игровое поле, фигуры, их постановка.
  2. Логика самой игры — правила и проверка выигрыша.
  3. Поведение бота, который будет играть против нас.

Исходная пустая страница

Для игры нам достаточно обычной веб-страницы. Мы разместим на ней название игры, кнопку перезапуска и игровое поле. Игровое поле — таблица 3 на 3, чтобы было проще ориентироваться в клетках. Клеткам сразу дадим id с номером, чтобы потом к ним обращаться. Ещё сразу добавим блок с сообщением о выигрыше или проигрыше, но мы его сначала скроем, а покажем только в самом конце.

    Крестики-нолики 

Крестики-нолики

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

Добавляем стили

Чтобы всё выглядело прилично, накатим стили: создадим файл style.css и добавим туда такой код. Читайте комментарии, если будут непонятные моменты:

/* настройки ячейки */ td < /* граница */ border: 2px solid #333; /* высота и ширина ячейки */ height: 100px; width: 100px; /* выравнивание по центру */ text-align: center; vertical-align: middle; /* настраиваем шрифт */ font-family: "Comic Sans MS", cursive, sans-serif; font-size: 70px; /* меняем вид курсора над ячейкой */ cursor: pointer; >/* настройки всей таблицы */ table < /* общая граница пусть выглядит как одна */ border-collapse: collapse; /* включаем абсолютное позиционирование */ position: absolute; /* размещаем таблицу на странице */ left: 50%; margin-left: -155px; top: 50px; >/* убираем границы снаружи таблицы, чтобы расчертить поле как для крестиков-ноликов */ table tr:first-child td < border-top: 0; >table tr:last-child td < border-bottom: 0; >table tr td:first-child < border-left: 0; >table tr td:last-child < border-right: 0; >/* блок с сообщением о конце игры */ .endgame < /* на старте не показываем */ display: none; /* размеры и положение блока */ width: 200px; top: 120px; position: absolute; left: 50%; /* цвет фона */ background-color: rgba(205,133,63, 0.8); /* настраиваем отступы */ margin-left: -100px; padding-top: 50px; padding-bottom: 50px; text-align: center; /* радиус скругления */ border-radius: 5px; /* цвет и размер шрифта */ color: white; font-size: 2em; >

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

Пишем скрипт

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

Если мы пронумеруем все клетки от 0 до 8, то сможем выписать все выигрышные комбинации. Например, для выигрыша по горизонтали комбинации будут такие:

То же самое можно сделать и для выигрышных комбинаций по вертикали и диагонали:

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

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

Создадим файл script.js и запишем туда всё это:

// переменная для игрового поля var origBoard; // игрок ставит крестики, компьютер — нолики const huPlayer = 'X'; const aiPlayer = 'O'; // выигрышные комбинации const winCombos = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [6, 4, 2] ] // получаем доступ к HTML-клеткам на доске const cells = document.querySelectorAll('.cell'); // запускаем игру startGame(); // запуск игры function startGame() < // скрываем текст про то, что игра закончилась document.querySelector(".endgame").style.display = "none"; // формируем игровое поле origBoard = Array.from(Array(9).keys()); // перебираем все клетки, очищаем их, убираем цвет фона и вешаем обработчик клика на каждую for (var i = 0; i < cells.length; i++) < cells[i].innerText = ''; cells[i].style.removeProperty('background-color'); cells[i].addEventListener('click', turnClick, false); >>

Обрабатываем нажатия на клетки

Логика нажатия будет такая: если в этом месте не число (а крестик или нолик), то ничего не происходит, а если число — ставим там крестик и проверяем, привело это к победе или нет.

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

// обрабатываем клик на клетке function turnClick(square) < // если клетка свободна if (typeof origBoard[square.target.id] == 'number') < // ставим там крестик turn(square.target.id, huPlayer) // если после хода игрок не выиграл и нет ничьей, компьютер находит лучшее место для нолика и ставит его туда if (!checkWin(origBoard, huPlayer) && !checkTie()) turn(bestSpot(), aiPlayer); >> // обработка хода function turn(squareId, player) < // ставим фигуру на выбранное место origBoard[squareId] = player; // рисуем её на игровом поле на странице document.getElementById(squareId).innerText = player; // проверяем, есть ли победа после хода let gameWon = checkWin(origBoard, player) // если есть — выводим сообщение об этом if (gameWon) gameOver(gameWon) >// компьютер находит лучшее поле для хода function bestSpot() < // получаем номер клетки для лучшего хода из минимаксного алгоритма return minimax(origBoard, aiPlayer).index; >// проверяем, выиграл ли кто-то после своего хода function checkWin(board, player) < // проходим по доске и собираем все комбинации, проставленные участником let plays = board.reduce((a, e, i) =>(e === player) ? a.concat(i) : a, []); // на старте считаем, что выигрышной ситуации нет let gameWon = null; // перебираем все выигрышные комбинации и сравниваем их с ситуацией на доске for (let [index, win] of winCombos.entries()) < // если одна из них совпадает с тем, что на доске — формируем информацию о победителе if (win.every(elem =>plays.indexOf(elem) > -1)) < gameWon = ; break; > > // возвращаем информацию о победителе return gameWon; > // конец игры function gameOver(gameWon) < // берём выигрышную комбирацию for (let index of winCombos[gameWon.index]) < // и раскрашиваем её в нужные цвета document.getElementById(index).style.backgroundColor = gameWon.player == huPlayer ? "blue" : "red"; >// убираем обработчик нажатия со всех клеток for (var i = 0; i < cells.length; i++) < cells[i].removeEventListener('click', turnClick, false); >// выводим сообщение о проигрыше или победе игрока declareWinner(gameWon.player == huPlayer ? "Вы выиграли!" : "Вы проиграли."); > // вывод сообщения о победе function declareWinner(who) < // делаем сообщение видимым document.querySelector(".endgame").style.display = "block"; // заполняем его нужным текстом document.querySelector(".endgame .text").innerText = who; >

Применяем стратегию минимакса

Мы уже разбирали этот алгоритм в отдельной статье, вот ключевые моменты:

  1. Первый игрок ставит крестик в любом месте поля. Всего клеток 9, одну он занял, осталось 8.
  2. Мы по очереди виртуально ставим нолики в оставшиеся 8 клеток и оцениваем ситуацию, выиграли ли мы или проиграли. Если непонятно — закидываем уже новую ситуацию в алгоритм и прогоняем её с новыми вводными.
  3. Так делаем до тех пор, пока не заполнятся все клетки — при этом у нас будет очень много вариантов и ветвлений.
  4. С теми ветками, где мы выиграли, каждому своему ходу в определённую клетку добавляем какое-то количество очков, а где проиграли — отнимаем такое же количество.
  5. После просчёта мы получим для каждой из 8 клеток, с которых начали в самом начале, свою оценку хода.
  6. Наконец, мы выбираем для ответного хода ту клетку, которая набрала больше всего очков, и ставим нолик туда.

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

// функция, которая проверяет, пустая ли выбранная клетка на поле или нет function emptySquares() < return origBoard.filter(s =>typeof s == 'number'); > // проверка на ничью function checkTie() < // если пустых клеток не осталось if (emptySquares().length == 0) < // перебираем все клетки и раскрашиваем их зелёным for (var i = 0; i < cells.length; i++) < cells[i].style.backgroundColor = "green"; // отключаем обработчики нажатий cells[i].removeEventListener('click', turnClick, false); >// выводим сообщение про ничью declareWinner("Ничья!") return true; > return false; >

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

// алгоритм поиска лучшего хода с минимаксной стратегией function minimax(newBoard, player) < // получаем все клетки, доступные для ходов var availSpots = emptySquares(); // если при текущем расположении побеждает игрок if (checkWin(newBoard, huPlayer)) < // отнимаем от результата 10 очков return ; // если выиграет компьютер > else if (checkWin(newBoard, aiPlayer)) < // прибавляем 10 очков return ; // если ничья, то не менеяем очки > else if (availSpots.length === 0) < return ; > // тут будем хранить все будущие ходы для их оценки var moves = []; // перебираем доступные клетки for (var i = 0; i < availSpots.length; i++) < // переменная для следующего шага var move = <>; // делаем шаг в очередную пустую клетку и получаем новое положение на доске move.index = newBoard[availSpots[i]]; // заполняем эту клетку символом того, чей ход мы рассчитываем newBoard[availSpots[i]] = player; // если считаем ход для компьютера if (player == aiPlayer) < // рекурсивно вызываем минимаксную функцию для нового положения и указываем, что следующий ход делает человек var result = minimax(newBoard, huPlayer); move.score = result.score; // то же самое, но если считаем ход человека >else < var result = minimax(newBoard, aiPlayer); move.score = result.score; >// запоминаем результат newBoard[availSpots[i]] = move.index; // добавляем ход в список ходов moves.push(move); > // переменная для лучшего хода var bestMove; // если считаем ход компьютера if(player === aiPlayer) < // берём максимально низкое значение var bestScore = -10000; // перебираем все ходы, что у нас получились for(var i = 0; i < moves.length; i++) < // если очки текущего хода больше лучшего значения if (moves[i].score >bestScore) < // запоминаем это как лучшее значение bestScore = moves[i].score; // запоминаем номер хода bestMove = i; >> // то же самое делаем с ходом, если моделируем ход человека > else < var bestScore = 10000; for(var i = 0; i < moves.length; i++) < if (moves[i].score < bestScore) < bestScore = moves[i].score; bestMove = i; >> > // возвращаем лучший ход return moves[bestMove]; >

Обновляем страницу и играем в игру, в которой компьютер всегда или побеждает, или сводит игру вничью:

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

    Крестики-нолики 

Крестики-нолики

/* настройки ячейки */ td < /* граница */ border: 2px solid #333; /* высота и ширина ячейки */ height: 100px; width: 100px; /* выравнивание по центру */ text-align: center; vertical-align: middle; /* настраиваем шрифт */ font-family: "Comic Sans MS", cursive, sans-serif; font-size: 70px; /* меняем вид курсора над ячейкой */ cursor: pointer; >/* настройки всей таблицы */ table < /* общая граница пусть выглядит как одна */ border-collapse: collapse; /* включаем абсолютное позиционирование */ position: absolute; /* размещаем таблицу на странице */ left: 50%; margin-left: -155px; top: 50px; >/* убираем границы снаружи таблицы, чтобы расчертить поле как для крестиков-ноликов */ table tr:first-child td < border-top: 0; >table tr:last-child td < border-bottom: 0; >table tr td:first-child < border-left: 0; >table tr td:last-child < border-right: 0; >/* блок с сообщением о конце игры */ .endgame < /* на старте не показываем */ display: none; /* размеры и положение блока */ width: 200px; top: 120px; position: absolute; left: 50%; /* цвет фона */ background-color: rgba(205,133,63, 0.8); /* настраиваем отступы */ margin-left: -100px; padding-top: 50px; padding-bottom: 50px; text-align: center; /* радиус скругления */ border-radius: 5px; /* цвет и размер шрифта */ color: white; font-size: 2em; >
// переменная для игрового поля var origBoard; // игрок ставит крестики, компьютер — нолики const huPlayer = 'X'; const aiPlayer = 'O'; // выигрышные комбинации const winCombos = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [6, 4, 2] ] // получаем доступ к HTML-клеткам на доске const cells = document.querySelectorAll('.cell'); // запускаем игру startGame(); // запуск игры function startGame() < // скрываем текст про то, что игра закончилась document.querySelector(".endgame").style.display = "none"; // формируем игровое поле origBoard = Array.from(Array(9).keys()); // перебираем все клетки, очищаем их, убираем цвет фона и вешаем обработчик клика на каждую for (var i = 0; i < cells.length; i++) < cells[i].innerText = ''; cells[i].style.removeProperty('background-color'); cells[i].addEventListener('click', turnClick, false); >> // обрабатываем клик на клетке function turnClick(square) < // если клетка свободна if (typeof origBoard[square.target.id] == 'number') < // ставим там крестик turn(square.target.id, huPlayer) // если после хода игрок не выиграл и нет ничьей, компьютер находит лучшее место для нолика и ставит его туда if (!checkWin(origBoard, huPlayer) && !checkTie()) turn(bestSpot(), aiPlayer); >> // обработка хода function turn(squareId, player) < // ставим фигуру на выбранное место origBoard[squareId] = player; // рисуем её на игровом поле на странице document.getElementById(squareId).innerText = player; // проверяем, есть ли победа после хода let gameWon = checkWin(origBoard, player) // если есть — выводим сообщение об этом if (gameWon) gameOver(gameWon) >// проверяем, выиграл ли кто-то после своего хода function checkWin(board, player) < // проходим по доске и собираем все комбинации, проставленные участником let plays = board.reduce((a, e, i) =>(e === player) ? a.concat(i) : a, []); // на старте считаем, что выигрышной ситуации нет let gameWon = null; // перебираем все выигрышные комбинации и сравниваем их с ситуацией на доске for (let [index, win] of winCombos.entries()) < // если одна из них совпадает с тем, что на доске — формируем информацию о победителе if (win.every(elem =>plays.indexOf(elem) > -1)) < gameWon = ; break; > > // возвращаем информацию о победителе return gameWon; > // конец игры function gameOver(gameWon) < // берём выигрышную комбирацию for (let index of winCombos[gameWon.index]) < // и раскрашиваем её в нужные цвета document.getElementById(index).style.backgroundColor = gameWon.player == huPlayer ? "blue" : "red"; >// убираем обработчик нажатия со всех клеток for (var i = 0; i < cells.length; i++) < cells[i].removeEventListener('click', turnClick, false); >// выводим сообщение о проигрыше или победе игрока declareWinner(gameWon.player == huPlayer ? "Вы выиграли!" : "Вы проиграли."); > // вывод сообщения о победе function declareWinner(who) < // делаем сообщение видимым document.querySelector(".endgame").style.display = "block"; // заполняем его нужным текстом document.querySelector(".endgame .text").innerText = who; >// функция, которая проверяеят, пустая ли выбранная клетка на поле или нет function emptySquares() < return origBoard.filter(s =>typeof s == 'number'); > // компьютер находит лучшее поле для хода function bestSpot() < // получаем номер клетки для лучшего хода из минимаксного алгоритма return minimax(origBoard, aiPlayer).index; >// проверка на ничью function checkTie() < // если пустых клеток не осталось if (emptySquares().length == 0) < // перебираем все клетки и раскрашиваем их зелёным for (var i = 0; i < cells.length; i++) < cells[i].style.backgroundColor = "green"; // отключаем обработчики нажатий cells[i].removeEventListener('click', turnClick, false); >// выводим сообщение про ничью declareWinner("Ничья!") return true; > return false; > // алгоритм поиска лучшего хода с минимаксной стратегией function minimax(newBoard, player) < // получаем все клетки, доступные для ходов var availSpots = emptySquares(); // если при текущем расположении побеждает игрок if (checkWin(newBoard, huPlayer)) < // отнимаем от результата 10 очков return ; // если выиграет компьютер > else if (checkWin(newBoard, aiPlayer)) < // прибавляем 10 очков return ; // если ничья, то не менеяем очки > else if (availSpots.length === 0) < return ; > // тут будем хранить все будущие ходы для их оценки var moves = []; // перебираем доступные клетки for (var i = 0; i < availSpots.length; i++) < // переменная для следующего шага var move = <>; // делаем шаг в очередную пустую клетку и получаем новое положение на доске move.index = newBoard[availSpots[i]]; // заполняем эту клетку символом того, чей ход мы рассчитываем newBoard[availSpots[i]] = player; // если считаем ход для компьютера if (player == aiPlayer) < // рекурсивно вызываем минимаксную функцию для нового положения и указываем, что следующий ход делает человек var result = minimax(newBoard, huPlayer); move.score = result.score; // то же самое, но если считаем ход человека >else < var result = minimax(newBoard, aiPlayer); move.score = result.score; >// запоминаем результат newBoard[availSpots[i]] = move.index; // добавляем ход в список ходов moves.push(move); > // переменная для лучшего хода var bestMove; // если считаем ход компьютера if(player === aiPlayer) < // берём максимально низкое значение var bestScore = -10000; // перебираем все ходы, что у нас получились for(var i = 0; i < moves.length; i++) < // если очки текущего хода больше лучшего значения if (moves[i].score >bestScore) < // запоминаем это как лучшее значение bestScore = moves[i].score; // запоминаем номер хода bestMove = i; >> // то же самое делаем с ходом, если моделируем ход человека > else < var bestScore = 10000; for(var i = 0; i < moves.length; i++) < if (moves[i].score < bestScore) < bestScore = moves[i].score; bestMove = i; >> > // возвращаем лучший ход return moves[bestMove]; >

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *