Как написать крестики нолики на java

Немного о Java

Java — это не только язык, это целая экосистема, включающая в себя средства разработки, платформу для запуска готовых приложений, огромный свод документации и активное сообщество. Одним из преимуществ Java на начальном этапе была кросс-платформенность (принцип — «написано один раз — запускается везде»).

Дело в том, что программа на Java исполняется не на прямую процессором компьютера, а виртуальной машиной Java (JVM). Это позволяет абстрагироваться от многих нюансов конкретных платформ. Программу, написанную на Java, можно без изменений кода запускать на Windows, Linux, MacOS и других операционных системах (если, конечно, программа не использует специфичные для ОС функции).

Кто застал начало 2000х, наверное помнит огромное количество мобильных телефонов (тогда еще они не были смартфонами), на каждом телефоне была по сути своя маленькая ОС, но при этом почти на каждом можно было запустить Java игру или приложение.

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

Создание проекта, первые шаги

Сегодня мы начнем изучать Java, причем сразу с примера игры «Крестики-нолики».

Итак, поехали. Надеюсь как установить java SDK ты уже разобрался. Мы будем писать код в IDE IntelliJ IDEA, но если у вас какая-то другая, например Eclipse, то разницы большой не будет.

Итак, создаем новый проект: нажимаем «create new project», выбираем java и щелкаем «next» до окна, где требуется ввести имя проекта, вводим TicTacToe (крестики-нолики). В некоторых случаях на этапе создания потребуется выбрать шаблон проекта, тогда смело выбирай что-либо похожее на JavaConsoleApplication.

После этого нажимаем «Finish». Idea немного подумает и сгенерирует нам проект с классом Main, в котором определена функция main().

Давайте разберемся, что здесь что. Слева открыто окно структуры проекта «Project», как мы видим в папке src в пакете com.company находится единственный java-файл нашей программы с именем Main. Справа показано его содержимое. Тут надо сделать небольшое отступление, дело в том, что в Java почти все представлено классами. В том числе и файлы программы описывают классы, причем имя файла должно совпадать с классом, который описывается в этом файле (например, у нас файл Main.java описывает класс Main). Пусть слово «класс» не смущает на первом этапе. Пока лишь отмечу, что для глубокого изучения Java так или иначе придется познакомиться с объектно-ориентированным подходом. В двух словах, класс можно воспринимать как шаблон, идею, а экземпляры класса — как реализацию этой идеи. Экземпляры класса называются его объектами. Например, вы можете представить идею стола (нечто, на что можно класть предметы), однако конкретных экземпляров такого стола огромное множество (на одной ножке, на четырех, круглые, квадратные, из разных материалов). Примерно так соотносятся классы и объекты в объектно-ориентированном программировании.

Внутри нашего класса Main описана функция main(), в Java с этой функции начинается исполнение программы, это точка входа в наше приложение. Сейчас там написан только автоматический комментарий (комментарии в Java начинаются с двух символов //). Попробуем кое-что добавить в наш код и проверить работоспособность приложения. Внутри функции main() допишем две строки:

Встроенная функция println() просто выводит на экран текстовую информацию. Запустим наше приложение (нажимаем shift-F10 или зеленый треугольник). Внизу, во вкладке run появится вывод нашей программы:

Функция main() отработала и закончилась, вместе с ней закончилась наша программа.

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

Смысл большинства строк понятен из комментариев к ним, отдельно отмечу строку window.setLayout() — здесь устанавливается менеджер расположения, который будет применяется к компонентам, добавляемым в наше окно. Менеджер BorderLayout может располагать новые компоненты относительно сторон света (North(верх), West(слева), East(справа), South(низ)), Center (центр)). По умолчанию он располагает компоненты по центру. Подробнее с менеджерами расположения можно познакомиться в документации.

Теперь, если запустить нашу программу, мы увидим окно:

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

Свой компонент для рисования

Очевидно, что рисовать в консоли у нас не получится, нужен какой-то компонент для более продвинутого взаимодействия с пользователем. Для этой цели создадим еще один класс, назовем его TicTacToe. Щелкаем правой клавишей мыши на имени пакета приложения (в данном случае это com.company)

И в появившемся меню выбираем пункт «New» → «Java Class». В окне создания класса набираем его имя «TicTacToe» и нажимаем «Enter».

У нас в проекте появился еще один класс. В главное окно можно добавлять только объекты класса JComponent, кроме того, нам нужна область для рисования. Поэтому наследуем наш класс TicTacToe от JComponent. Ой сколько непонятных слов! Сейчас постараюсь пояснить.

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

С JComponent то же самое — данный класс реализует идею некоторого графического компонента пользовательского интерфейса. Такой компонент можно добавить в окно и он умеет как-то себя отрисовывать. Например, класс JButton — наследник класса JComponent, это компонент, который выглядит, как кнопка и умеет показывать анимацию клика мышкой.

Здесь же, наследуя класс JComponent, мы создадим свой компонент, в котором сможем рисовать то, что нам нужно.

Итак дописываем extends JComponent в строку описания класса:

Слово extends говорит о том, что наш класс TicTacToe расширяет (наследует) класс JComponent.

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

Переопределим метод paintComponent() в классе TicTacToe следующим образом:

метод setColor() объекта graphics, как очевидно из названия, устанавливает цвет, которым мы будем рисовать, а метод drawOval(x, y, w, h) — в общем случае рисует овал с координатами центра x, y, шириной — w и высотой h. В данном случае рисуется окружность, так как ширина и высота заданы одинаковые — 100. Замечу, что экранные координаты отсчитываются от левого верхнего угла. То есть 0 по вертикали находится вверху.

Чтобы проверить, как выглядит наш объект класса TicTacToe надо создать его экземпляр и добавить в главное окно в качестве дочернего компонента. Создание новых объектов в Java осуществляется с помощью ключевого слова new.

Например, если у нас есть класс Стол и мы хотим создать объект этого класса (настоящий конкретный стол), то мы должны написать что-то такое: стол = new Стол(). Здесь «стол» имя, по которому мы будем обращаться к нашему объекту (взаимодействовать с ним), а Стол — имя класса, объект которого мы создаем.

Замечу сразу, что вместо «стол» мы могли написать любое имя, например «fdgdgdgfd», но программисты обычно стараются давать «говорящие» имена объектам, чтобы код было легче читать. Чтобы создать экземпляр класса TicTacToe мы можем также написать game = new TicTacToe(), а потом добавить его в окно методом add().

Теперь код класса Main выглядит вот так:

Если теперь запустить нашу программу, то мы увидим окно с окружностью:

Ну что ж. Рисовать в базе мы научились. Время приступать к созданию игры.

Создание игрового поля

Вернемся к классу TicTacToe. Для начала необходимо нарисовать игровое поле, состоящее из девяти клеточек. Для этого давайте нарисуем две горизонтальные и две вертикальные линии на нашем поле. Чтобы это сделать, воспользуемся методом drawLine(x1,y1,x2,y2) объекта Graphics, который приходит к нам в метод paintComponent() в качестве параметра. Метод drawLine() рисует линию от точки с координатами x1,y1 до точки x2,y2. Давайте подумаем как нарисовать игровое поле.

Если мы разобьем высоту поля на три (у нас же три клетки в ряду), то получим высоту одной клетки (назовем ее dh). Узнать высоту всего компонента можно методом getHeight(). Значит, мы должны нарисовать первую горизонтальную линию от точки 0,dh до точки w, dh, где w — ширина поля. Но это только одна горизонтальная линия, вторую рисуем также, но координаты будут уже: начало — 0, 2*dh, конец w, 2*dh. По аналогии, если высота поля равна h, а ширина одной клетки равна dw, то вертикальные линии рисуются в координатах d, 0 — d, h и dw*2, 0 — dw*2, h.

Теперь давайте немного поговорим о переменных. Если помните — в алгебре за буквой могло скрываться какое-то значение, например выражение x = 2*a, подразумевало, что на место буквы а можно подставить любое значение и вычислить x.

Примерно то же самое происходит с переменными в программировании. Имя переменной (идентификатор) сопоставлен с некоторым значением и «хранит» его «в себе» (на самом деле, с объектами классов все несколько сложнее, там мы имеем дело со ссылками, но это пока за рамками данного материала).

Помимо этого, в программах есть разные типы данных. Наверное, вы согласитесь, что строку и целое число надо хранить в памяти как-то по-разному? Даже целые и дробные числа требуют разного подхода, поэтому в программах данные соответствуют определенным типам. В нашем примере нам уже понадобились значения ширины и высоты ячейки игрового поля dw и dh. Чтобы вычислить и сохранить их значения в памяти, воспользуемся следующими выражениями:

Здесь int — означает тип данных «целое число». Выражение int a = 10 объявляет переменную с именем a и задает ей сразу значение 10. В нашем примере создаются четыре переменных, значения w и h получаются из методов самого компонента TicTacToe, а dw и dh вычисляются. Обратите внимание, что при делении w / 3 получается целый тип данных. В Java, как и во многих других языках, деление целого на целое дает в результате целое. При этом дробная часть просто отбрасывается (округления нет). Заметьте, что здесь не используется слово «new», так как создаются не объекты, а переменные простых (скалярных) типов данных, в данном случае типа int.

Мы могли бы уже написать код для рисования всех линий, но мы же программисты, а программисты любят все упрощать, правда для этого они пишут много дополнительного кода. Представим, что у нас было бы поле не 3 на 3 клетки а, например, 15х15. Как бы мы его разлиновали? Вручную набирать код для рисования 28 линий это уж слишком.

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

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

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

Попробуем рисовать линии с помощью цикла, в классе TicTacToe создадим свой метод с названием drawGrid(), он будет у нас отвечать за рисование линий сетки игрового поля:

Еще раз пробежимся по коду. Первые четыре строки метода — необходимые нам значения ширины, высоты игрового поля и ширины, высоты одной ячейки. Цикл начинается с ключевого слова for, в скобках после него указывается переменная, которая будет счетчиком (у нас она еще и объявляется сразу int i = 1), условие при ложности которого цикл прервется и выражение изменяющее переменную-счетчик (i++ увеличивает i каждую итерацию цикла на единицу).

Внутри цикла каждую итерацию рисуются очередные горизонтальная и вертикальная линии поля.

Добавим вызов нашего метода drawGrid() в метод отрисовки всего компонента paintComponent():

Запускаем программу и видим разрисованное поле:

Скажи мне, куда ты кликнул?

Итак, наше игровое поле выглядит готовым к игре, но теперь нам надо узнать в какой из квадратов кликнул пользователь. Для этого давайте немного настроим наш компонент TicTacToe, чтобы он смог принимать события от мыши. Во-первых, нам необходимо включить получение таких событий. Делается это с помощью метода enableEvents(), но где его вызвать?

Конечно можно было бы добавить его вызов в наш метод drawGrid() или даже в paintComponent(), но эти методы по логике работы игры будут вызываться каждый раз, когда мы захотим что-то нарисовать. А включить события надо лишь один раз. Где бы найти метод, который вызывается у компонента единожды, например при его создании?

На самом деле такой метод есть у каждого класса и называется он конструктором. Именно конструктор вызывается при попытке создания нового объекта. Конструкторов может быть несколько, он так же как обычный метод может принимать параметры, но вот возвращаемого значения у него нет. Конструктор класса имеет то же имя, что и класс. Создадим конструктор в классе TicTacToe:

Как видим — ничего сложного, просто еще один метод. А как же наш компонент создавался до этого? Ведь в классе Main мы его уже создавали. Помните, game = new TicTacToe()? Тут тоже никакой магии — если конструктор не задан явно, используется конструктор по умолчанию.

Именно здесь мы включим получение событий от мыши:

Хорошо! Получение событий мы включили, а где же мы их будем получать? В методе processMouseEvent() конечно, именно он будет срабатывать каждый раз, когда указатель мыши каким-либо образом взаимодействует с нашим игровым полем.

Приведу на всякий случай полный код класса TicTacToe на текущий момент:

Мозг игры

Ну не то чтобы уж мозг, но некоторую начинку нам создать придется. Итак, давайте подумаем, как хранить состояние игры? Неплохо бы было хранить состояние каждой клетки игрового поля, для этой цели хорошо подойдет двумерный массив целых чисел размером 3х3. Создается он просто int[][] field = new int[3][3].

Массив это уже целый объект, на который выделяется память в отдельной области (куче), поэтому тут мы используем слово new. Создадим в классе TicTacToe новый метод под названием initGame(), он будет отвечать у нас за сброс игры к начальному состоянию, именно здесь мы будем «очищать» игровое поле.

Для хранения состояния ячейки поля создадим три константы со значениями 0, 10 и 200. Ноль будет соответствовать пустой ячейке, 10 — крестику, а 200 — нолику. Первоначально заполним массив нулями.

Теперь добавим немного кода в метод processMouseEvent(), который отвечает за клики мышью на игровом поле. Также заведем логическую переменную isXturn, которая будет показывать чей сейчас ход.

Пробежимся по коду. Переход от координат к индексам довольно прост: мы делим текущую координату на размер одной ячейки и получаем сколько целых ячеек укладывается до текущей (если совсем непонятно, то поясню: разделить на w/3 это то же самое, что умножить на 3/w).

В 42й строке кода стоит условный оператор (также называемый ветвлением), пора с ним познакомиться. Если условие в скобках истинно (в нашем случае если поле пустое), то мы заходим «внутрь» условного оператора (строки 43-46), если же условие ложно (ячейка уже занята), то мы пройдем дальше. Что же происходит если кликнутая ячейка пуста?

В 44й строке после «=» стоит еще один интересный оператор — тернарный, он дает возможность записать в строку ветвление, если в результате него присваивается значение. Записывают его так: isXturn? — это проверка, чей сейчас ход (ходит крестик, если значение «истина»), далее следует определенная нами константа FIELD_X, именно она будет результатом выражения, если isXturn — true.

После FIELD_X стоит двоеточии и константа FIELD_O — ее значение станет результатом выражения, если ход «нолика». После изменения значения в ячейке массива, меняем очередность хода: isXturn =! isXturn изменит значение переменной на противоположное. В конце всех действий — вызываем перерисовку компонента, так как теперь нужно нарисовать крестик или нолик, там где его не было раньше.

Теперь осталось научиться рисовать крестики или нолики. Создадим два метода: drawX() и draw0(). На данном этапе мы уже умеем рисовать линии и круги, поэтому обойдусь комментариями в коде:

Комментарии в коде достаточно очевидны. Коротко поясню, что крестик мы рисуем как пересечение двух линий из угла в угол ячейки, а нолик — как овал чуть вытянутый по вертикали. Теперь у нас есть методы, рисующие крестик и нолик по заданным индексам ячейки поля. Как же мы будем рисовать процесс игры? Пробежимся еще раз по коду. Игроки кликают мышкой на наш компонент, при этом срабатывает метод processMouseEvent(), в котором мы определяем, какое событие произошло, пуста ли ячейка, в которую кликнули и вызываем перерисовку компонента (repaint()). На момент перерисовки в массиве field содержатся актуальные данные о поставленных крестиках и ноликах, остается пробежаться циклами по всему массиву и если встречается нолик — рисовать нолик, а если крестик — рисовать крестик. Поехали, создаем метод drawXO(). Именно в нем будем «просматривать» массив:

Осталось вызвать данный метод в методе painComponent():

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

Определяем победителя

Все бы хорошо, но сейчас игра никак не отслеживает свое состояние, то есть крестики и нолики успешно ставятся, но выигрыш никак не определяется. Придется еще немного потрудиться! Как же нам определить, что игра закончилась? Давайте присмотримся к массиву field. Если в ряду будут стоять одни крестики, значит, там значения 10, 10, 10.

Если нолики — 200, 200, 200. Эврика! Давайте проверять сумму всех ячеек в ряду по горизонтали и вертикали, а также по двум диагоналям. Создаем еще один метод checkState(), он будет каждый ход проверять сложившуюся на поле ситуацию и возвращать -1, если ходов не осталось, 3*FIELD_X если выиграли крестики и 3*FIELD_O, если выиграли нолики, в случае продолжения игры — метод пусть вернет 0.

Основные комментарии даны в коде. Элементы, стоящие на главной диагонали вычисляются просто — их индексы равны ([0][0], [1][1] и т. д.). Побочная диагональ содержит элементы с индексами [0][N-1], [1][N-2] и так далее (N — длина массива).

Часть кода с 142й по 160ю строку отвечает за подсчет суммы значений в ячейках по вертикальным и горизонтальным рядам: каждую «большую» итерацию по i фиксируется вертикальный (горизонтальный) ряд с индексом i и запускается малый цикл с перебором всех ячеек в данном ряду (цикл по j).

Кроме того, проверяется наличие на поле хотя бы одной не занятой ячейки (hasEmpty=true), это нужно чтобы определить ситуацию, когда все ячейки заняты, но никто не выиграл (ничья). Наконец, если нигде ранее не произошел выход из метода мы проверяем значение hasEmpty, если пустые ячейки есть — возвращаем 0, а если нет, то -1 (ничья).

Осталось использовать данный метод. Немного подправим обработку нажатий.

Здесь добавилось получение результата из метода checkState() и его обработка. Метод showMessageDialog() показывает сообщение с заданным текстом и заголовком. В ветвлении проверяем, какое значение вернул метод checkState() и показываем соответствующие сообщения (или продолжаем игру, если результат 0).

На этом данный туториал подходит к концу. За не столь долгое время нам удалось создать игру с графическим интерфейсом и захватывающим геймплеем: ).

Задание на дом: наша игра все-таки ленивая, и почему-то не зачеркивает выигрышный ряд. Подумайте, как можно его зачеркнуть, ведь рисовать линии вы уже умеете (подсказка, обратите внимание на метод checkState() — там уже сделана почти вся работа по нахождению выигрышного ряда).

С помощью нашего шестимесячного курса «Профессия: Разработчик» вы научитесь писать в Java не только это! 👉 Узнать подробности!

import java.util.*;

public class GFG {

    static String[] board;

    static String turn;

    static String checkWinner()

    {

        for (int a = 0; a < 8; a++) {

            String line = null;

            switch (a) {

            case 0:

                line = board[0] + board[1] + board[2];

                break;

            case 1:

                line = board[3] + board[4] + board[5];

                break;

            case 2:

                line = board[6] + board[7] + board[8];

                break;

            case 3:

                line = board[0] + board[3] + board[6];

                break;

            case 4:

                line = board[1] + board[4] + board[7];

                break;

            case 5:

                line = board[2] + board[5] + board[8];

                break;

            case 6:

                line = board[0] + board[4] + board[8];

                break;

            case 7:

                line = board[2] + board[4] + board[6];

                break;

            }

            if (line.equals("XXX")) {

                return "X";

            }

            else if (line.equals("OOO")) {

                return "O";

            }

        }

        for (int a = 0; a < 9; a++) {

            if (Arrays.asList(board).contains(

                    String.valueOf(a + 1))) {

                break;

            }

            else if (a == 8) {

                return "draw";

            }

        }

        System.out.println(

            turn + "'s turn; enter a slot number to place "

            + turn + " in:");

        return null;

    }

    static void printBoard()

    {

        System.out.println("|---|---|---|");

        System.out.println("| " + board[0] + " | "

                           + board[1] + " | " + board[2]

                           + " |");

        System.out.println("|-----------|");

        System.out.println("| " + board[3] + " | "

                           + board[4] + " | " + board[5]

                           + " |");

        System.out.println("|-----------|");

        System.out.println("| " + board[6] + " | "

                           + board[7] + " | " + board[8]

                           + " |");

        System.out.println("|---|---|---|");

    }

    public static void main(String[] args)

    {

        Scanner in = new Scanner(System.in);

        board = new String[9];

        turn = "X";

        String winner = null;

        for (int a = 0; a < 9; a++) {

            board[a] = String.valueOf(a + 1);

        }

        System.out.println("Welcome to 3x3 Tic Tac Toe.");

        printBoard();

        System.out.println(

            "X will play first. Enter a slot number to place X in:");

        while (winner == null) {

            int numInput;

            try {

                numInput = in.nextInt();

                if (!(numInput > 0 && numInput <= 9)) {

                    System.out.println(

                        "Invalid input; re-enter slot number:");

                    continue;

                }

            }

            catch (InputMismatchException e) {

                System.out.println(

                    "Invalid input; re-enter slot number:");

                continue;

            }

            if (board[numInput - 1].equals(

                    String.valueOf(numInput))) {

                board[numInput - 1] = turn;

                if (turn.equals("X")) {

                    turn = "O";

                }

                else {

                    turn = "X";

                }

                printBoard();

                winner = checkWinner();

            }

            else {

                System.out.println(

                    "Slot already taken; re-enter slot number:");

            }

        }

        if (winner.equalsIgnoreCase("draw")) {

            System.out.println(

                "It's a draw! Thanks for playing.");

        }

        else {

            System.out.println(

                "Congratulations! " + winner

                + "'s have won! Thanks for playing.");

        }

      in.close();

    }

}

import java.util.*;

public class GFG {

    static String[] board;

    static String turn;

    static String checkWinner()

    {

        for (int a = 0; a < 8; a++) {

            String line = null;

            switch (a) {

            case 0:

                line = board[0] + board[1] + board[2];

                break;

            case 1:

                line = board[3] + board[4] + board[5];

                break;

            case 2:

                line = board[6] + board[7] + board[8];

                break;

            case 3:

                line = board[0] + board[3] + board[6];

                break;

            case 4:

                line = board[1] + board[4] + board[7];

                break;

            case 5:

                line = board[2] + board[5] + board[8];

                break;

            case 6:

                line = board[0] + board[4] + board[8];

                break;

            case 7:

                line = board[2] + board[4] + board[6];

                break;

            }

            if (line.equals("XXX")) {

                return "X";

            }

            else if (line.equals("OOO")) {

                return "O";

            }

        }

        for (int a = 0; a < 9; a++) {

            if (Arrays.asList(board).contains(

                    String.valueOf(a + 1))) {

                break;

            }

            else if (a == 8) {

                return "draw";

            }

        }

        System.out.println(

            turn + "'s turn; enter a slot number to place "

            + turn + " in:");

        return null;

    }

    static void printBoard()

    {

        System.out.println("|---|---|---|");

        System.out.println("| " + board[0] + " | "

                           + board[1] + " | " + board[2]

                           + " |");

        System.out.println("|-----------|");

        System.out.println("| " + board[3] + " | "

                           + board[4] + " | " + board[5]

                           + " |");

        System.out.println("|-----------|");

        System.out.println("| " + board[6] + " | "

                           + board[7] + " | " + board[8]

                           + " |");

        System.out.println("|---|---|---|");

    }

    public static void main(String[] args)

    {

        Scanner in = new Scanner(System.in);

        board = new String[9];

        turn = "X";

        String winner = null;

        for (int a = 0; a < 9; a++) {

            board[a] = String.valueOf(a + 1);

        }

        System.out.println("Welcome to 3x3 Tic Tac Toe.");

        printBoard();

        System.out.println(

            "X will play first. Enter a slot number to place X in:");

        while (winner == null) {

            int numInput;

            try {

                numInput = in.nextInt();

                if (!(numInput > 0 && numInput <= 9)) {

                    System.out.println(

                        "Invalid input; re-enter slot number:");

                    continue;

                }

            }

            catch (InputMismatchException e) {

                System.out.println(

                    "Invalid input; re-enter slot number:");

                continue;

            }

            if (board[numInput - 1].equals(

                    String.valueOf(numInput))) {

                board[numInput - 1] = turn;

                if (turn.equals("X")) {

                    turn = "O";

                }

                else {

                    turn = "X";

                }

                printBoard();

                winner = checkWinner();

            }

            else {

                System.out.println(

                    "Slot already taken; re-enter slot number:");

            }

        }

        if (winner.equalsIgnoreCase("draw")) {

            System.out.println(

                "It's a draw! Thanks for playing.");

        }

        else {

            System.out.println(

                "Congratulations! " + winner

                + "'s have won! Thanks for playing.");

        }

      in.close();

    }

}

Всем доброго времени суток. На связи Алексей Гулынин. В прошлых статьях, посвященных Java, мы написали игру в процедурном стиле. В данной статье я бы хотел начать писать игру «Крестики-нолики» в объектном стиле с графическим интерфейсом, используя библиотеку Swing. Данная задача поможет разобраться в объектно-ориентированном программировании на Java.

Скачать уже готовый проект полностью можно по ссылке.

Для начала создадим абстрактный класс AGamer, от которого будут наследоваться 2 других наших класса — Player и AI:

public abstract class AGamer {
  // метка
  protected String sign;

  // выстрел в координаты x и y
  abstract boolean shot(int x, int y);

  // проверка победы
  abstract boolean win();
}

Реализуем класс игрока — Player:

public class Player extends AGamer{
  // экземпляр нашего поля
  MainGameField gameField;
  // готовность к стрельбе, если = 0
  // то ходит другой игрок
  int isShotReady = 1;

  // Конструктор
  public Player(String sign)
  {
    this.sign = sign;
  }

  // Выстрел игрока
  boolean shot(int x, int y)
  {
    gameField = MainGameField.getInstance();
    if (!gameField.isCellBusy(x, y))
    {
      gameField.cell[x][y] = sign;
      return true;
    }
    return false;
  }
  // Проверка победы
  boolean win()
  {
    gameField = MainGameField.getInstance();
    return gameField.checkWin(this.sign);
  }
}

Реализуем класс компьютера — AI. Здесь логика точно такая же, как и с примером в процедурном стиле. Скопирую описание данного метода из прошлой статьи:

В случае «aiLevel = 0» используется объект класса Random. В данном случае компьютер просто ходит случайным образом на любую незанятую ячейку.

В случае «aiLevel = 1» компьютер анализирует ситуацию, при которой игрок может выиграть на следующем ходу. Если такой выигрышный ход существует, то компьютер ходит на эту ячейку. Как это реализовано: в двойном цикле проверяются все ячейки, и компьютер в каждую незанятую ячейку ставит метку игрока («field[i][j] = USER_SIGN»). Далее с помощью метода «checkWin()» проверяется является ли данный ход выигрышным, если да, то эти координаты запоминаются, и компьютер ставит на них свою метку (AI_SIGN). После каждого анализа ячейка обнуляется ((«field[i][j] = NOT_SIGN»).

В случае «aiLevel = 2» логика работы такая же, как и на первом уровне. Только в данном случае компьютер анализирует свой ход и, если он является выигрышным, ходит.

import java.util.Random;

public class AI extends AGamer {
  // Экземпляр игрового поля
  MainGameField gameField;
  String playerSign = "";
  // Уровень интеллекта
  static int aiLevel = 0;

  // Конструктор
  public AI(String sign, int aiLevel, String playerSign)
  {
    this.sign = sign;
    this.playerSign = playerSign;
    this.aiLevel = aiLevel;
  }

  // Выстрел компьютера
  boolean shot(int x, int y)
  {
    gameField = MainGameField.getInstance();
    x = -1;
    y = -1;
    boolean ai_win = false;
    boolean user_win = false;
    // Находим выигрышный ход
    if (aiLevel == 2)
    {
      for (int i = 0; i < gameField.linesCount; i++)
      {
        for (int j = 0; j < gameField.linesCount; j++)
        {
          if (!gameField.isCellBusy(i, j))
          {
            gameField.cell[i][j] = sign;
            if (gameField.checkWin(sign))
            {
              x = i;
              y = j;
              ai_win = true;
            }
            gameField.cell[i][j] = gameField.NOT_SIGN;
          }
        }
      }
    }
    // Блокировка хода пользователя, если он побеждает на следующем ходу
    if (aiLevel > 0)
    {
      if (!ai_win)
      {
        for (int i = 0; i < gameField.linesCount; i++)
        {
          for (int j = 0; j < gameField.linesCount; j++)
          {
            if (!gameField.isCellBusy(i, j))
            {
              gameField.cell[i][j] = this.playerSign;
              if (gameField.checkWin(this.playerSign))
              {
                x = i;
                y = j;
                user_win = true;
              }
              gameField.cell[i][j] = gameField.NOT_SIGN;
            }
          }
        }
      }
    }
    if (!ai_win && !user_win)
    {
      do
      {
        Random rnd = new Random();
        x = rnd.nextInt(gameField.linesCount);
        y = rnd.nextInt(gameField.linesCount);
      }
      while (gameField.isCellBusy(x, y));
    }
    gameField.cell[x][y] = sign;
    return true;
  }

  boolean win()
  {
    gameField = MainGameField.getInstance();
    return gameField.checkWin(this.sign);
  }
}

Создадим класс MainForm, который будет отрисовывать основную форму:

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class MainForm extends JFrame {
  public MainForm()
  {
    // Заголовок формы
    setTitle("XO game GUI");
    // Границы формы
    setBounds(300, 300, 455, 525);
    // Можно ли изменять размер формы?
    // в нашем случае - нет
    setResizable(false);
    // При закрытии - форма и программа закрываются
    setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    // Создаём экземпляр нашего игрового поля
    MainGameField gameField = MainGameField.getInstance();
    // Создаём панель для кнопок табличного стиля
    JPanel buttonPanel = new JPanel(new GridLayout());
    // Добавляем игровок поле в центр нашей формы
    add(gameField, BorderLayout.CENTER);
    // Панель для кнопок добавляем вниз формы
    add(buttonPanel, BorderLayout.SOUTH);
    JButton btnStart = new JButton("Начать новую игру");
    JButton btnEnd = new JButton("Закончить игру");
    buttonPanel.add(btnStart);
    buttonPanel.add(btnEnd);
    // Добавляем обработчик событий для закрытия формы
    btnEnd.addActionListener(new ActionListener() {
            @Override

            public void actionPerformed(ActionEvent e)
  {
    System.exit(0);
  }
			  });
			  // Добавляем обработчик событий для создания новой игры
			  btnStart.addActionListener(new ActionListener()
  {
    @Override

            public void actionPerformed(ActionEvent e)
  {
    System.out.println(btnStart.getText());
    // Загружаем новую форму (с настройками игры)
    GameSettingsForm gameSettingsForm = new GameSettingsForm();
  }
			  });
        // Показываем форму
        setVisible(true);
	  }
}

Напоследок создадим основной класс MainClass, из которого мы будем запускать нашу игру:

public class MainClass {
  public static void main(String[] args) {
    MainForm gameForm = new MainForm();
  }
}

В следующей статье мы напишем 2 класса GameSettingsForm и MainGameField.

В данной статье мы начали писать игру «Крестики-нолики» с графическим интерфейсом.

На связи был Алексей Гулынин, оставляйте свои комментарии, увидимся в следующих статьях.

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

Коты против собак

Вместо предисловия

Статья за 2011 год. Интересно, работает ли код сейчас? Спустя 9 лет я решил заново пройтись по проекту. Заодно кое-что поправил в коде.

Однажды я написал статью на Хабре. Один из читателей сказал, что тоже написал одну статью про создание игры «Крестики-нолики», но боится её выкладывать на всеобщее обозрение. Я и несколько других комментаторов сумели убедить его опубликовать свой материал, который вы можете прочитать здесь. В статье речь шла о создании игры в IntelliJ IDEA Community Edition. Если кому-то интересно, то ознакомьтесь с оригиналом статьи, а также почитайте комментарии. Я в свою очередь немного отредактировал статью и сам код игры.


Статья затронет весь цикл разработки приложения. Вместе мы напишем простенькую игру “Крестики-Нолики” с одним экраном.

Для нашего приложения идеально подойдёт макет TableLayout с идентификатором android:id=»@+id/tableLayout».


<?xml version="1.0" encoding="utf-8"?>
<TableLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/tableLayout"
    android:gravity="center">
</TableLayout>

Программный доступ к TableLayout:


package ru.alexanderklimov.tictactoe;

import android.os.Bundle;
import android.widget.TableLayout;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    private TableLayout tableLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        tableLayout = findViewById(R.id.tableLayout);
        buildGameField(); // создание игрового поля
    }
}

Теперь необходимо реализовать метод buildGameField(). Для этого требуется создать поле в виде матрицы. Этим будет заниматься класс Game. Сначала нужно создать класс Player, объекты которого будут заполнять ячейки игрового поля и класс Square для самих ячеек.

Player.java

package ru.alexanderklimov.tictactoe;

public class Player
{
    private String name;

    public Player(String name)
    {
        this.name = name;
    }

    public CharSequence getName()
    {
        return name;
    }
}

Square.java


package ru.alexanderklimov.tictactoe;

public class Square
{
    private Player player = null;

    public void fill(Player player)
    {
        this.player = player;
    }

    public boolean isFilled()
    {
        return player != null;
    }

    public Player getPlayer()
    {
        return player;
    }
}

Game.java


package ru.alexanderklimov.tictactoe;

public class Game
{
    private Square[][] field;
    private int squareCount;

    public Game()
    {
        field = new Square[3][3];
        squareCount = 0;
        // заполнение поля
        for (int i = 0, l = field.length; i < l; i++)
        {
            for (int j = 0, l2 = field[i].length; j < l2; j++)
            {
                field[i][j] = new Square();
                squareCount++;
            }
        }
    }

    public Square[][] getField()
    {
        return field;
    }
}

Метод buildGameField() динамически добавляет строки и колонки в таблицу (игровое поле):


private Button[][] buttons = new Button[3][3];

private void buildGameField()
{
    Square[][] field = game.getField();
    for (int i = 0, lenI = field.length; i < lenI; i++ ) {
        TableRow row = new TableRow(this); // создание строки таблицы
        for (int j = 0, lenJ = field[i].length; j < lenJ; j++)
        {
            Button button = new Button(this);
            buttons[i][j] = button;
            button.setOnClickListener(new Listener(i, j)); // установка слушателя, реагирующего на клик по кнопке
            row.addView(button, new TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT,
                    TableRow.LayoutParams.WRAP_CONTENT)); // добавление кнопки в строку таблицы
            button.setWidth(160);
            button.setHeight(160);
        }
        tableLayout.addView(row,
                new TableLayout.LayoutParams(TableLayout.LayoutParams.WRAP_CONTENT,
                TableLayout.LayoutParams.WRAP_CONTENT)); // добавление строки в таблицу
    }
}

В коде создаётся объект, реализующий интерфейс View.OnClickListener. Создадим вложенный класс Listener. Он будет виден только из активности.


public class Listener implements View.OnClickListener
{
    private int x = 0;
    private int y = 0;

    Listener(int x, int y)
    {
        this.x = x;
        this.y = y;
    }

    public void onClick(View view)
    {
        Button button = (Button) view;
    }
}

Осталось реализовать логику игры. Возвращаемся к Game.java:


package ru.alexanderklimov.tictactoe;

public class Game {
    // игроки
    private Player[] players;

    // поле
    private Square[][] field;

    // начата ли игра?
    private boolean started;

    // текущий игрок
    private Player activePlayer;

    // Считает колличество заполненных ячеек
    private int filled;

    // Всего ячеек
    private int squareCount;

    public Game() {
        field = new Square[3][3];
        squareCount = 0;
        // заполнение поля
        for (int i = 0, l = field.length; i < l; i++) {
            for (int j = 0, l2 = field[i].length; j < l2; j++) {
                field[i][j] = new Square();
                squareCount++;
            }
        }
        players = new Player[2];
        started = false;
        activePlayer = null;
        filled = 0;
    }

    public void start() {
        resetPlayers();
        started = true;
    }

    private void resetPlayers() {
        players[0] = new Player("X");
        players[1] = new Player("O");
        setCurrentActivePlayer(players[0]);
    }

    private void setCurrentActivePlayer(Player player) {
        activePlayer = player;
    }

    public Square[][] getField() {
        return field;
    }

    public boolean makeTurn(int x, int y) {
        if (field[x][y].isFilled()) {
            return false;
        }
        field[x][y].fill(getCurrentActivePlayer());
        filled++;
        switchPlayers();
        return true;
    }

    private void switchPlayers() {
        activePlayer = (activePlayer == players[0]) ? players[1] : players[0];
    }

    public Player getCurrentActivePlayer() {
        return activePlayer;
    }

    public boolean isFieldFilled() {
        return squareCount == filled;
    }

    public void reset() {
        resetField();
        resetPlayers();
    }

    private void resetField() {
        for (int i = 0, l = field.length; i < l; i++) {
            for (int j = 0, l2 = field[i].length; j < l2; j++) {
                field[i][j].fill(null);
            }
        }
        filled = 0;
    }
}

Определение победителя

Очевидно, что в крестики-нолики выигрывает тот, кто построит X или O в линию длиной, равной длине поля по вертикали, по горизонтали или по диагонали. Первая мысль, которая приходит в голову — это написать методы для каждого случая. Думаю, в этом случае хорошо подойдёт паттерн Chain of Responsobility. Определим интерфейс:


package ru.alexanderklimov.tictactoe;

public interface WinnerCheckerInterface 
{
	public Player checkWinner();
}

Так как Game наделён обязанностью выявлять победителя, он реализует этот интерфейс. Настало время создать виртуальных «лайнсменов», каждый из которых будет проверять свою сторону. Все они реализует интерфейс WinnerCheckerInterface.

WinnerCheckerHorizontal.java


package ru.alexanderklimov.tictactoe;

public class WinnerCheckerHorizontal implements WinnerCheckerInterface {
    private Game game;

    public WinnerCheckerHorizontal(Game game) {
        this.game = game;
    }

    public Player checkWinner() {
        Square[][] field = game.getField();
        Player currPlayer;
        Player lastPlayer = null;
        for (int i = 0, len = field.length; i < len; i++) {
            lastPlayer = null;
            int successCounter = 1;
            for (int j = 0, len2 = field[i].length; j < len2; j++) {
                currPlayer = field[i][j].getPlayer();
                if (currPlayer == lastPlayer && (currPlayer != null && lastPlayer != null)) {
                    successCounter++;
                    if (successCounter == len2) {
                        return currPlayer;
                    }
                }
                lastPlayer = currPlayer;
            }
        }
        return null;
    }
}

WinnerCheckerVertical.java


package ru.alexanderklimov.tictactoe;

public class WinnerCheckerVertical implements WinnerCheckerInterface {
    private Game game;

    public WinnerCheckerVertical(Game game) {
        this.game = game;
    }

    public Player checkWinner() {
        Square[][] field = game.getField();
        Player currPlayer;
        Player lastPlayer = null;
        for (int i = 0, len = field.length; i < len; i++) {
            lastPlayer = null;
            int successCounter = 1;
            for (int j = 0, len2 = field[i].length; j < len2; j++) {
                currPlayer = field[j][i].getPlayer();
                if (currPlayer == lastPlayer && (currPlayer != null && lastPlayer != null)) {
                    successCounter++;
                    if (successCounter == len2) {
                        return currPlayer;
                    }
                }
                lastPlayer = currPlayer;
            }
        }
        return null;
    }
}

WinnerCheckerDiagonalLeft.java


package ru.alexanderklimov.tictactoe;

public class WinnerCheckerDiagonalLeft implements WinnerCheckerInterface {
    private Game game;

    public WinnerCheckerDiagonalLeft(Game game) {
        this.game = game;
    }

    public Player checkWinner() {
        Square[][] field = game.getField();
        Player currPlayer;
        Player lastPlayer = null;
        int successCounter = 1;
        for (int i = 0, len = field.length; i < len; i++) {
            currPlayer = field[i][i].getPlayer();
            if (currPlayer != null) {
                if (lastPlayer == currPlayer) {
                    successCounter++;
                    if (successCounter == len) {
                        return currPlayer;
                    }
                }
            }
            lastPlayer = currPlayer;
        }
        return null;
    }
}

WinnerCheckerDiagonalRight.java


package ru.alexanderklimov.tictactoe;

public class WinnerCheckerDiagonalRight implements WinnerCheckerInterface {
    private Game game;

    public WinnerCheckerDiagonalRight(Game game) {
        this.game = game;
    }

    public Player checkWinner() {
        Square[][] field = game.getField();
        Player currPlayer;
        Player lastPlayer = null;
        int successCounter = 1;
        for (int i = 0, len = field.length; i < len; i++) {
            currPlayer = field[i][len - (i + 1)].getPlayer();
            if (currPlayer != null) {
                if (lastPlayer == currPlayer) {
                    successCounter++;
                    if (successCounter == len) {
                        return currPlayer;
                    }
                }
            }
            lastPlayer = currPlayer;
        }
        return null;
    }
}

Проинициализируем их в конструкторе Game:


// "Судьи". После каждого хода они будут проверять,
// нет ли победителя
private WinnerCheckerInterface[] winnerCheckers;

// Инициализация "судей"
winnerCheckers = new WinnerCheckerInterface[4];
winnerCheckers[0] = new WinnerCheckerHorizontal(this);
winnerCheckers[1] = new WinnerCheckerVertical(this);
winnerCheckers[2] = new WinnerCheckerDiagonalLeft(this);
winnerCheckers[3] = new WinnerCheckerDiagonalRight(this);

Реализация checkWinner():


public Player checkWinner() {
    for (WinnerCheckerInterface winChecker : winnerCheckers) {
        Player winner = winChecker.checkWinner();
        if (winner != null) {
            return winner;
        }
    }
    return null;
}

Возвращаемся в класс активности. Победителя проверяем после каждого хода. Добавим код в метод onClick() класса Listener


public void onClick(View view) 
{
    Button button = (Button) view;
    Game g = game;
    Player player = g.getCurrentActivePlayer();
    if (makeTurn(x, y)) 
    {
        button.setText(player.getName());
    }
    Player winner = g.checkWinner();
    if (winner != null) 
    {
        gameOver(winner);
    }
    if (g.isFieldFilled()) 
    {  // в случае, если поле заполнено
        gameOver();
    }
}

Метод gameOver() реализован в 2-х вариантах.

Метод makeTurn() проверяет очерёдность хода игрока, а метод refresh() обновляет состояние поля:

Весь код активности.


// Если этот код работает, его написал Александр Климов,
// а если нет, то не знаю, кто его писал.
package ru.alexanderklimov.as34;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    private TableLayout tableLayout;

    private Button[][] buttons = new Button[3][3];

    private Game game;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        tableLayout = findViewById(R.id.tableLayout);


        game = new Game();
        buildGameField(); // создание игрового поля
        game.start(); // будет реализован позже
    }

    private void buildGameField() {
        Square[][] field = game.getField();
        for (int i = 0, lenI = field.length; i < lenI; i++) {
            TableRow row = new TableRow(this); // создание строки таблицы
            for (int j = 0, lenJ = field[i].length; j < lenJ; j++) {
                Button button = new Button(this);
                buttons[i][j] = button;
                button.setOnClickListener(new Listener(i, j)); // установка слушателя, реагирующего на клик по кнопке
                row.addView(button, new TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT,
                        TableRow.LayoutParams.WRAP_CONTENT)); // добавление кнопки в строку таблицы
                button.setWidth(160);
                button.setHeight(160);
            }
            tableLayout.addView(row,
                    new TableLayout.LayoutParams(TableLayout.LayoutParams.WRAP_CONTENT,
                            TableLayout.LayoutParams.WRAP_CONTENT)); // добавление строки в таблицу
        }
    }

    public class Listener implements View.OnClickListener {
        private int x;
        private int y;

        Listener(int x, int y) {
            this.x = x;
            this.y = y;
        }

        public void onClick(View view) {
            Button button = (Button) view;
            Game g = game;
            Player player = g.getCurrentActivePlayer();
            if (makeTurn(x, y)) {
                button.setText(player.getName());
            }
            Player winner = g.checkWinner();
            if (winner != null) {
                gameOver(winner);
            }
            if (g.isFieldFilled()) {  // в случае, если поле заполнено
                gameOver();
            }
        }
    }

    private void gameOver(Player player) {
        CharSequence text = "Player "" + player.getName() + "" won!";
        Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
        game.reset();
        refresh();
    }

    private void gameOver() {
        CharSequence text = "Draw";
        Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
        game.reset();
        refresh();
    }

    private boolean makeTurn(int x, int y) {
        return game.makeTurn(x, y);
    }

    private void refresh() {
        Square[][] field = game.getField();

        for (int i = 0, len = field.length; i < len; i++) {
            for (int j = 0, len2 = field[i].length; j < len2; j++) {
                if (field[i][j].getPlayer() == null) {
                    buttons[i][j].setText("");
                } else {
                    buttons[i][j].setText(field[i][j].getPlayer().getName());
                }
            }
        }
    }
}

Видео готового приложения

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

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

Дополнительное чтение

Ещё один вариант игры

SavvaVyatkin/CatDogToe — правильный вариант игры с участием котов. Картинка для статьи взята из этого проекта.

Крестики-нолики (Custom View)

Реклама

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

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

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

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<Player> 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 {
    System.out.print(" Ничья! ");
}

Introduction: How to Write a Tic-Tac-Toe Program in Java

Introduction:

Tic-Tac-Toe is a very common game that is fairly easy to play. The rules of the game are simple and well-known. Because of these things, Tic-Tac-Toe is fairly easy to code up. In this tutorial, we will be looking at how to code a working game of Tic-Tac-Toe in Java. This tutorial assumes that you have knowledge of the basic syntax of Java, and access to a working Java compiler. This tutorial will use the Eclipse IDE.

General Outline:
There are many ways to implement a game of Tic-Tac-Toe in Java, so before we begin coding, we must think about how we will implement the game specifically. For this tutorial, we will be coding a text-based version of Tic-Tac-Toe. Our Tic-Tac-Toe will start out by printing the board, and then asking for input from the first player that will specify where on the board to place that player’s mark. After placing the mark, we will print the board state again and then ask the other player for their move. That process will be continued until one player wins or the board is filled up (indicating that a tie occurred). The input that will be taken in to specify where to place a mark will be in the format of two integers, which specify the row and column where the mark is to be placed. Below is a sample of what a game will play like.

Step 1: Creating Your Project

The first step in coding anything is to make a new project! In your IDE, create a new Java project named TicTacToe. In this Instructable, the default package is what will be used. Inside of your project package, create two classes: Main.java, and TTT.java. Main.java will host the main method and will be used to run the code in TTT.java. TTT.java will contain a Tic-Tac-Toe object that contains the state of the board and methods to manipulate the game.

Step 2: Starting the TTT Class

Before we can make code to run our TTT object, we need to create a working TTT object. Since a TTT object is representative of one game of Tic-Tac-Toe, it must contain two member variables. Below are the member variables that should be placed into the TTT class followed by descriptions of why they are needed.

private char[][] board;

This variable is a 2D array of characters that will be representative of the three-by-three board of a game of Tic-Tac-Toe. It will hold the state of the game inside of the TTT object at any given time.

private char currentPlayerMark;

This variable will hold either an ‘x’ or an ‘o’, representing which player’s turn it is at any given point of time. The methods of the TTT class will use this when marking the board to determine which type of mark will be placed.

Step 3: Initializing Method Prototypes in the TTT Class

The following is a general setup of the program. Below are all of the method headers for the methods that belong inside of the TTT class. These methods will act upon the member variables of the TTT class to make the game of Tic-Tac-Toe mutable. Each one has a short description of what the method will do under it. These behaviours are necessary for playing a full game of Tic-Tac-Toe.

public TTT()

This is the constructor. It will be responsible for ensuring the board gets initialized properly, and for setting who the first player will be.

public void initializeBoard()

This method will initialize the board variable such that all slots are empty.

public void printBoard()

This method will print the Tic-Tac-Toe board to standard output.

public boolean isBoardFull()

This method will check whether or not the board is full. It will return true if the board is full and a false otherwise.

public boolean checkForWin()

This method will check to see if a player has won, and if so, it will return true.

private boolean checkRowsForWin()

This method will specifically check the rows for a win.

private boolean checkColumnsForWin()

This method will specifically check the columns for a win.

private boolean checkDiagonalsForWin()

This method will specifically check the diagonals for a win.

private boolean checkRowCol(char c1, char c2, char c3)

This method will check the three specified characters taken in to see if all three are the same ‘x’ or ‘o’ letter. If so, it will return true.

Note: If you code method stubs for all of the method headers into your TTT class, your compiler will likely inform you that your code has errors. This is normal. The compiler is simply expecting for a value to be returned for all non-void methods.

Step 4: Initialize the Board

public void initializeBoard()

This will set the board to all empty values. Inside this method, you must create 2 for loops inside of each other that will loop through all the rows and columns setting each space to ‘-’. To loop through the rows, create a for loop and an integer, in this case named i, to represent which row we are currently observing.

for (int i = 0; i < 3; i++) { }

Inside of this for loop, we will create a second for loop with an integer j to represent which column we are currently observing.

for (int j = 0; j < 3; j++) { }

Inside of the second for loop, we set the board location to ‘-’. With both loops completed and nested properly, we can iterate through every place inside of the board 2D array.

board[i][j] = ‘-‘;

Attached to this step is an image showing one possible implementation of the initializeBoard() method.

Step 5: Printing the Board

The initial printed board will look like the first image .

It will be handled in the method public void printBoard(), which is located in the TTT class. To print the board we must access every place in the 2D array named board in our TTT class. Because we are dealing with a 2D array, this will be handled with nested for loops.

First, we just need to print a line of dashes (13 of them in this case) designating the top of the board. Below that, we need a for loop that will loop through each of the three rows. This loop will contain a call to print a ‘|’ character, another for loop to loop through the columns, and a call to the System.out.println() function to print a new line and the next 13 dashes to the screen.

Our inner for loop will also only loop through three columns. Since our outer for loop already printed the first | character of each row of the board, we can go ahead to print the character that belongs in the box. To do this, we’ll print the character at that row and column using board[i][j] (i being the variable used for the outer for loop, which was the row, and j being the variable used for the inner loop, which is the column.) This print statement will also contain a concatenated | character, to separate the boxes.

The only thing left is to print the last call to print the new line to separate each row, followed by the 13 dashes. The second attached image shows an example of what the described print function might look like.

Step 6: Checking for a Winner (Part 1)

There are three different functions to check for a win: rows, columns, and diagonals. Computers have to separate these into different conditions because they are all different in terms of arrays. checkForWin() will be our main function to test all 3 of these functions for each scenario that the user input has affected.

For checkForWin() method: You simply need a return statement that calls upon the three different functions. If checking the rows for win does not return true then check the columns for win, etc. Inside of the return statement should look like: checkRowsForWin() || checkColumnsForWin() || checkDiagonalsForWin()

For checkRowsForWin() method: We are looping through the rows to see if there are any winners. This will require one for loop with an if statement inside of it. The for loop will be incrementing through integer i so as to check each row. for (int i = 0; i < 3; i++) The if statement compares each space in the row to each other and gives a ‘true’ value if they are all equal. For instance if the row had three x’s in a row, the method would return true. if (checkRowCol(board[i][0], board[i][1], board[i][2]) == true) So, inside of this if statement, there should be a: return true; And after the for loop, if the method never stopped, then the method needs return that this row did not have three consecutive matching symbols. Therefore, right before closing off the method with its final ‘}’, we will write: return false; Satisfying the need to return a boolean.

For checkColumnsForWin() method: Copy and paste the contents of the checkRowsForWin() method. The only change will be inside of the if statement. Instead of incrementing through the rows, we will be incrementing through the columns. So while in checkRowsForWin has an if statement that says: if (checkRowCol(board[i][0], board[i][1], board[i][2]) == true) checkColumnsForWin() will have an if statement that says: if (checkRowCol(board[0][i], board[1][i], board[2][i]) == true) Other than that, everything else in the method remains the same.

For checkDiagonalsForWin() method: Everything written can be contained inside the parentheses of a return() statement. The first check we will perform is on the diagonal from the top left corner to the bottom right corner. To do this, we check all the spaces that would be included in this section. checkRowCol(board[0][0], board[1][1], board[2][2]) == true) Then we will have one more statement, but we will separate the two by an OR symbol: ‘||’ The second statement will check from the top right corner to the bottom left corner. checkRowCol(board[0][2], board[1][1], board[2][0]) == true) So your final product of the checkDiagonalsForWin() method should be a return(), and inside it should contain the first statement OR the second statement.

Step 7: Check for a Winner (Part 2)

Now we have to make sure if a player gets three in a row, he or she wins. checkRowCol() will be a function that will compare all three letters to each other, and if they do match, then return true.

For checkRowCol() method: This method is used by the other methods to send down three values. We first check to make sure the first value is not an empty one such as ‘-’. Then we compare the first value to the second, and the second to the third, and if and only if all three values are the same AND they are not empty statements, then this method will return true. So inside of one return() statement, our first statement will check this is not a ‘-’. (c1 != ‘-‘) Separate the first and second statements with an ‘&&’ The second statement will see if the first value is equal to the second value. (c1 == c2) Separate the second and third statements with an ‘&&’ The third statement will see if the second value is equal to the third. (c2 == c3) So your final checkRowCol() method will be one return() containing the first statement && the second statement && the third statement.

Step 8: Changing Between Players (x and O)

public void changePlayer()

The changePlayer() method will swap the variable currentPlayerMark between x and o. To do this, just check what the variable currently holds. If the variable is holding an ‘x’, then change it to an ‘o’. Otherwise, change it to an ‘x’.

public boolean placeMark(int row, int col)

The placeMark() method will place the correct letter onto the specified row and col in the board variable (taken in as parameters). It will return true if it was a valid placement. Otherwise, no modification to the board variable will be made, and the player will have to try and place their letter on a different place, as an invalid spot was selected or a spot where a player already placed their letter was selected. To accomplish this behavior, a few things must be checked. First, ensure (using an if statement) that the row argument was between 0 and 2. Next, check to ensure that the col argument was between 0 and 2. Finally, check to make sure that the spot in question currently contains a ‘-‘, signifying that no payer has marked that spot yet. If all three of these conditions check out, then place a mark (the type of which is specified by the class variable currentPlayerMark) at the location specified by row and col and then return true. If any of the three conditions are not met, then noting should happen and false should be returned.

Attached to this step are images showing possible implementations of the methods mentioned above.

Step 9: Player Input and Playing the Game

Now that the TTT class and all of its methods are completed, a main method that runs through an entire game of Tic-Tac-Toe using out TTT object must be created. The main method must do quite a few things in order to run a full game of Tic-Tac-Toe.

First, it must create a Scanner object to take input from System.in. Also, it must instantiate a TTT object to play the Tic-Tac-Toe game with. After these things, it must initialize the board of the TTT object by calling it’s initializeBoard() method.

After these steps are completed, actual game play must be accounted for. To go through turns, a do while loop is required. The loop should break out when the game is over, meaning whenever the TTT object’s board is full or has a winner. Inside the loop, the current board state should be printed before each turn so show the player what spaces are available and what spaces are taken. Then, two inputs should be taken in signifying the row and column to place a mark for the turn. After this input is taken in, the mark should be placed using the TTT object’s method, and the player should be changed as well using the TTT object’s method.

Below the while loop that handles all turns until the end of the game, it will be necessary to declare who the winner of the game is (or if the game was a tie). To do this, check if the game was a tie first by checking if both the board was full and there was no winner. If these things are the case, then print out that the game was a tie. Otherwise, print out who won by printing out the opposite of the current state of the TTT object’s currentPlayerMark variable. This can be accomplished by first calling the TTT object’s changePlayer() method and then using the TTT object’s getCurrentPlayerMark() method to get the state of the currentPlayerMark variable. It may also be nice to print the board out once more to show the final state of the board.

An example main method has been attached as an image.

Step 10: Use Class, Compile, and Run

Once you have gotten to this point, your project should be complete enough to run. If there are errors or missing parentheses or semicolons, now would be the time to find these and fix them. If you are at all lost as to how your project should look, we have included a downloadable file for you to compare your code to a working final product. Go ahead and compile your project. In Eclipse there is a play button that looks like the image attached to this step.

Be the First to Share

Recommendations

Понравилась статья? Поделить с друзьями:
  • Как написать крепкого прекрепкого здоровья
  • Как написать кредитный калькулятор на java
  • Как написать крашу что ты его любишь
  • Как написать крашу что он тебе нравится
  • Как написать крашу если боишься