Перед прочтением данной статьи рекомендую ознакомиться с предыдущей, «Быстрый старт с Java: начало», поскольку ожидается, что читатель владеет материалом, изложенным в ней — знает о переменных, условиях, циклах и импорте классов. Сегодня мы углублим знания о Java, создавая игру «Крестики-нолики», которая работает в командной строке (консоли). В процессе будет рассмотрена работа с массивами, а также некоторые аспекты объектно-ориентированного программирования (нестатические методы, нестатические поля, конструктор).
Массивы
При написании игры используется массив, поэтому давайте для начала рассмотрим, что это. Массивы хранят набор однотипных переменных. Если переменная похожа на коробочку, с написанным на боку типом, именем и со значением внутри, то массив похож на блок таких коробочек. И тип, и имя у блока одно, а доступ к той или иной коробочке (значению) происходит по номеру (индексу).
В Java массивы являются объектами, их создают с помощью директивы new. При создании указываем количество элементов массива или инициализируем его набором значений. Приведенный код иллюстрирует оба варианта:
class Arrays { public static void main(String[] args) { int[] arr = new int[5]; int[] arrInit = {1, 2, 3, 4, 5}; for (int i = 0; i < arr.length; i++) { arr[i] = i * 2 + arrInit[i]; } for (int a : arr) { System.out.println(a); } } }
С элементами массива можно работать как с обычными переменными, присваивая им результат выражения и читая хранимые значения. При этом в квадратных скобках указывается индекс элемента массива. Индексация в Java идёт с 0 (с нуля). Первый цикл инициализирует элементы массива arr при помощи значений из массива arrInit. Каждый массив имеет поле length, содержащее количество его элементов. Второй цикл выводит элементы массива в консоль, используя второй вариант for — без счётчика цикла.
Методы
Кроме main() класс может содержать и другие методы. Рассмотрим в качестве примера класс с методом add(), который вычисляет и возвращает сумму двух значений, переданных как параметры. Обратите внимание на тип int, который стоит перед именем метода — это тип возвращаемого значения. Две переменные в скобках — параметры. Совокупность имени и параметров называют сигнатурой метода. Вызов метода происходит по имени, в скобках указывают передаваемые значения. В методе они попадают в параметры-переменные. Команда return возвращает результат сложения этих двух переменных и обеспечивает выход из метода.
class MethodStatic { public static void main(String[] args) { int c = add(5, 6); System.out.println("5 + 6 = " + c); } static int add(int a, int b) { return a + b; } }
Слово static означает, что метод статический. Если мы обращается к какому-либо методу из статического метода, то вызываемый тоже должен быть статическим. Вот почему add() статический — он вызывается из статического main(). Использование статических методов — скорее исключение, чем правило, поэтому давайте посмотрим как сделать add() нестатическим.
Решение одно — создать объект на основании класса. И затем вызывать метод через точку после имени объекта. В этом случае метод может быть нестатическим. Представленный ниже код это иллюстрирует.
class MethodNotStatic { public static void main(String[] args) { MethodNotStatic method = new MethodNotStatic(); int c = method.add(5, 6); System.out.println("5 + 6 = " + c); } int add(int a, int b) { return a + b; } }
Поля класса
Переменные существуют в рамках лишь того метода, где они объявлены. А если нужна переменная, доступная во всех методах класса, то пришло время использовать поля. Поля объявляются подобно переменным, с указанием типа и имени. Но располагаются они не в методах, а прямо в теле класса. Подобно методам, они могут быть статическими и нестатическими. Нестатические поля, как и методы, доступны только после создания объекта.
class FieldExample { int a; public static void main(String[] args) { FieldExample field = new FieldExample(); field.a = 12; System.out.println("a = " + field.a); System.out.println(field.getA()); field.printA(); } int getA() { return a; } void printA() { System.out.println(a); } }
Приведённый выше код иллюстрирует работу с нестатическим полем int a. Описание полей принято размещать первыми в коде класса, затем идут описания методов. Возможность обращаться к полю (запись, чтение) мы получаем только после создания объекта. Также видно, что это поле доступно во всех нестатических методах объекта, а в статическом main() — через точку после имени объекта.
Крестики-нолики. Шаблон класса
Приступим к написанию кода игры. Начнём с шаблона класса и определения нужных полей. Именно это содержит приведённый ниже код. Первые две строки — импорт классов. Первыми в теле класса идут описания полей, затем методов. Метод main() используется для создания объекта (так как поля и методы нестатические) и вызова метода game() с игровой логикой.
import java.util.Random; import java.util.Scanner; class TicTacToe { final char SIGN_X = 'x'; final char SIGN_O = 'o'; final char SIGN_EMPTY = '.'; char[][] table; Random random; Scanner scanner; public static void main(String[] args) { new TicTacToe().game(); } TicTacToe() { // конструктор: инициализация полей } void game() { // игровая логика } // дополнительные методы }
В качестве полей используем три символьные константы: SIGN_X, SIGN_O и SIGN_EMPTY. Их значения нельзя изменять, об этом говорит модификатор final. Двумерный символьный массив table будет нашим игровым полем. Потребуется также объект random для генерации ходов компьютера и scanner для ввода данных от пользователя.
Имена методов принято писать с маленькой буквы. Однако в коде мы видим метод TicTacToe() — есть ли тут нарушение? Нет, поскольку этот метод особенный и в объектно-ориентированном программировании называется конструктор. Конструктор вызывается сразу после того, как объект создан. Его имя, как видим, должно совпадать с именем класса. Мы используем конструктор для инициализации полей.
TicTacToe() { random = new Random(); scanner = new Scanner(System.in); table = new char[3][3]; }
Игровая логика
Игровая логика располагается в методе game() и базируется на бесконечном цикле while. Ниже в фрагменте кода последовательность действий описана через комментарии:
// инициализация таблицы while (true) { // ход человека // проверка: если победа человека или ничья: // сообщить и выйти из цикла // ход компьютера // проверка: если победа компьютера или ничья: // сообщить и выйти из цикла }
При написании рабочего кода, каждое действие — например, «ход человека», «ход компьютера», «проверка» — мы заменим на вызов соответствующего метода. При возникновении выигрышной или ничейной ситуации (все клетки таблицы заполнены), выходим из цикла с помощью break, завершая игру.
void game() { initTable(); while (true) { turnHuman(); if (checkWin(SIGN_X)) { System.out.println("YOU WIN!"); break; } if (isTableFull()) { System.out.println("Sorry, DRAW!"); break; } turnAI(); printTable(); if (checkWin(SIGN_O)) { System.out.println("AI WIN!"); break; } if (isTableFull()) { System.out.println("Sorry, DRAW!"); break; } } System.out.println("GAME OVER."); printTable(); }
Реализация вспомогательных методов
Пришло время написать код методов, вызываемых в game(). Самый первый, initTable(), обеспечивает начальную инициализацию игровой таблицы, заполняя её ячейки «пустыми» символами. Внешний цикл, со счетчиком int row, выбирает строки, а внутренний, со счётчиком int col, перебирает ячейки в каждой строке.
void initTable() { for (int row = 0; row < 3; row++) for (int col = 0; col < 3; col++) table[row][col] = SIGN_EMPTY; }
Также потребуется метод, отображающий текущее состояние игровой таблицы printTable().
void printTable() { for (int row = 0; row < 3; row++) { for (int col = 0; col < 3; col++) System.out.print(table[row][col] + " "); System.out.println(); } }
В методе turnHuman(), который позволяет пользователю сделать ход, мы используем метод nextInt() объекта scanner, чтобы прочитать два целых числа (координаты ячейки) с консоли. Обратите внимание как используется цикл do-while: запрос координат повторяется в случае, если пользователь укажет координаты невалидной ячейки (ячейка таблицы занята или не существует). Если с ячейкой всё в порядке, туда заносится символ SIGN_X — «крестик».
void turnHuman() { int x, y; do { System.out.println("Enter X and Y (1..3):"); x = scanner.nextInt() - 1; y = scanner.nextInt() - 1; } while (!isCellValid(x, y)); table[y][x] = SIGN_X; }
Валидность ячейки определяет метод isCellValid(). Он возвращает логическое значение: true — если ячейка свободна и существует, false — если ячейка занята или указаны ошибочные координаты.
boolean isCellValid(int x, int y) { if (x < 0 || y < 0 || x >= 3|| y >= 3) return false; return table[y][x] == SIGN_EMPTY; }
Метод turnAI() похож на метод turnHuman() использованием цикла do-while. Только координат ячейки не считываются с консоли, а генерируются случайно, при помощи метода nextInt(3) объекта random. Число 3, передающееся как параметр, является ограничителем. Таким образом, генерируются случайные целые числа от 0 до 2 (в рамках индексов массива игровой таблицы). И метод isCellValid() снова позволяет нам выбрать только свободные ячейки для занесения в них знака SIGN_O — «нолика».
void turnAI() { int x, y; do { x = random.nextInt(3); y = random.nextInt(3); } while (!isCellValid(x, y)); table[y][x] = SIGN_O; }
Осталось дописать два последних метода — проверка победы и проверка на ничью. Метод checkWin() проверяет игровую таблицу на «победную тройку» — три одинаковых знака подряд, по вертикали или горизонтали (в цикле), а также по двум диагоналям. Проверяемый знак указан как параметр char dot, за счёт чего метод универсален - можно проверять победу и по «крестикам» и по «ноликам». В случае победы возвращается булевское значение true, в противном случае — false.
boolean checkWin(char dot) { for (int i = 0; i < 3; i++) if ((table[i][0] == dot && table[i][1] == dot && table[i][2] == dot) || (table[0][i] == dot && table[1][i] == dot && table[2][i] == dot)) return true; if ((table[0][0] == dot && table[1][1] == dot && table[2][2] == dot) || (table[2][0] == dot && table[1][1] == dot && table[0][2] == dot)) return true; return false; }
Метод isTableFull() во вложенном двойном цикле проходит по всем ячейкам игровой таблицы и, если они все заняты, возвращает true. Если хотя бы одна ячейка ещё свободна, возвращается false.
boolean isTableFull() { for (int row = 0; row < 3; row++) for (int col = 0; col < 3; col++) if (table[row][col] == SIGN_EMPTY) return false; return true; }
Теперь осталось собрать все эти методы внутри TicTacToe. Последовательность их расположения в теле класса не важна. А после этого можно попробовать сыграть с компьютером в крестики-нолики.
Заключение
На всякий случай прилагаю мой telegram — @biblelamp. Если вас заинтересовала тема, рекомендую почитать «Java-программирование для начинающих» Майка МакГрата и «Изучаем Java» Кэти Сьерра и Берт Бейтс. Также напоминаю ссылку на мою предыдущую статью, где мы начали знакомство с Java.
Если язык Java вас заинтересовал — приглашаем на факультет Java-разработки. Если ещё не совсем уверены — посмотрите истории успеха наших Java-выпускников:
- «Иногда за сутки я спал один час в метро перед работой». Из белорусской типографии — в московские тимлиды.
- Первая работа в IT с переездом в Москву: как это бывает. Опыт собеседований, тестовых заданий, учебных проектов и трудоустройства в международную компанию.
- Взгляд изнутри: как работается в «Альфа-Банке». Рассказывает Михаил Степнов, выпускник GeekUniversity и программист банка.