36. Реализовать консольную игру «Крестики-нолики» произвольного размера
Условие задачи:
Необходимо реализовать консольную игру «Крестики-нолики» с произвольным размером квадратного поля.
Структура программы:
Main— запрашивает размер поля и запускает игру;Game— управляет игровым циклом и определяет победу или ничью;Desk— хранит состояние игрового поля;AI— интерфейс участника игры;Player— получает координаты хода от пользователя;IndianAI— делает случайный допустимый ход.
Игрок X и компьютер O ходят по очереди.
Победой считается заполнение символами игрока целой строки, столбца или одной из двух главных диагоналей.
Спойлеры к решению
Подсказки
Desk должен самостоятельно знать размер поля — получать его через reflection не нужно.💡 Лучше предоставить методы
getSize(), place(), hasWon() и isFull().💡 Участник игры должен уметь сделать ход и сообщить свой символ.
💡
Game отвечает только за последовательность ходов и завершение игры.💡 Для случайного хода компьютера можно сначала определить количество свободных клеток, а затем выбрать одну из них случайно.
Решение
AI задаёт общий контракт для участника игры:
public interface AI {
void makeMove(Desk desk);
char getSymbol();
}
Desk хранит состояние игрового поля:
public class Desk {
private static final char EMPTY = '.';
private final char[][] field;
private final int size;
private int occupiedCells;
public Desk(int size) {
if (size <= 0) {
throw new IllegalArgumentException(
"Размер поля должен быть больше 0"
);
}
this.size = size;
this.field = new char[size][size];
for (int row = 0; row < size; row++) {
Arrays.fill(field[row], EMPTY);
}
}
public int getSize() {
return size;
}
public boolean isCellEmpty(int row, int col) {
return isInside(row, col)
&& field[row][col] == EMPTY;
}
public boolean place(int row, int col, char symbol) {
if (!isCellEmpty(row, col)) {
return false;
}
field[row][col] = symbol;
occupiedCells++;
return true;
}
public boolean isFull() {
return occupiedCells == size * size;
}
public boolean hasWon(char symbol) {
for (int i = 0; i < size; i++) {
if (checkRow(i, symbol)
|| checkColumn(i, symbol)) {
return true;
}
}
return checkMainDiagonal(symbol)
|| checkSecondaryDiagonal(symbol);
}
private boolean checkRow(int row, char symbol) {
for (int col = 0; col < size; col++) {
if (field[row][col] != symbol) {
return false;
}
}
return true;
}
private boolean checkColumn(int col, char symbol) {
for (int row = 0; row < size; row++) {
if (field[row][col] != symbol) {
return false;
}
}
return true;
}
private boolean checkMainDiagonal(char symbol) {
for (int i = 0; i < size; i++) {
if (field[i][i] != symbol) {
return false;
}
}
return true;
}
private boolean checkSecondaryDiagonal(char symbol) {
for (int i = 0; i < size; i++) {
if (field[i][size - 1 - i] != symbol) {
return false;
}
}
return true;
}
private boolean isInside(int row, int col) {
return row >= 0 && row < size
&& col >= 0 && col < size;
}
public void print() {
for (char[] row : field) {
for (char cell : row) {
System.out.print(cell + " ");
}
System.out.println();
}
System.out.println();
}
}
Живой игрок получает координаты из консоли:
public class Player implements AI {
private final Scanner scanner;
private final char symbol;
public Player(Scanner scanner, char symbol) {
this.scanner = scanner;
this.symbol = symbol;
}
@Override
public void makeMove(Desk desk) {
while (true) {
System.out.printf(
"Ход игрока '%c' (строка и столбец от 1 до %d): ",
symbol,
desk.getSize()
);
int row = scanner.nextInt() - 1;
int col = scanner.nextInt() - 1;
if (desk.place(row, col, symbol)) {
return;
}
System.out.println(
"Некорректный ход: клетка занята или находится за пределами поля."
);
}
}
@Override
public char getSymbol() {
return symbol;
}
}
Компьютер выбирает случайную свободную клетку:
public class IndianAI implements AI {
private final char symbol;
public IndianAI(char symbol) {
this.symbol = symbol;
}
@Override
public void makeMove(Desk desk) {
int emptyCells = desk.getSize() * desk.getSize();
for (int row = 0; row < desk.getSize(); row++) {
for (int col = 0; col < desk.getSize(); col++) {
if (!desk.isCellEmpty(row, col)) {
emptyCells--;
}
}
}
if (emptyCells == 0) {
throw new IllegalStateException("Свободных клеток нет");
}
int target = ThreadLocalRandom.current()
.nextInt(emptyCells);
int current = 0;
for (int row = 0; row < desk.getSize(); row++) {
for (int col = 0; col < desk.getSize(); col++) {
if (!desk.isCellEmpty(row, col)) {
continue;
}
if (current == target) {
desk.place(row, col, symbol);
System.out.printf(
"Ход компьютера '%c': %d %d%n",
symbol,
row + 1,
col + 1
);
return;
}
current++;
}
}
}
@Override
public char getSymbol() {
return symbol;
}
}
Game управляет очередностью ходов:
public class Game {
private final Desk desk;
private final AI playerX;
private final AI playerO;
public Game(int size, AI playerX, AI playerO) {
this.desk = new Desk(size);
this.playerX = playerX;
this.playerO = playerO;
}
public void play() {
desk.print();
while (true) {
if (makeMove(playerX)) {
return;
}
if (makeMove(playerO)) {
return;
}
}
}
private boolean makeMove(AI player) {
player.makeMove(desk);
desk.print();
if (desk.hasWon(player.getSymbol())) {
System.out.println(
"Победил '" + player.getSymbol() + "'!"
);
return true;
}
if (desk.isFull()) {
System.out.println("Ничья!");
return true;
}
return false;
}
}
Точка входа:
public class Main {
public static void main(String[] args) {
try (Scanner scanner = new Scanner(System.in)) {
System.out.print("Введите размер поля: ");
int size = scanner.nextInt();
Game game = new Game(
size,
new Player(scanner, 'X'),
new IndianAI('O')
);
game.play();
}
}
}
Пример запуска:
Введите размер поля: 3
. . .
. . .
. . .
Ход игрока 'X' (строка и столбец от 1 до 3): 1 1
X . .
. . .
. . .
Ход компьютера 'O': 2 2
X . .
. O .
. . .
Desk полностью инкапсулирует состояние поля: другие классы не получают прямого доступа к массиву.
Метод:
public boolean place(int row, int col, char symbol)
одновременно проверяет возможность хода и изменяет поле. Это не позволяет случайно перезаписать уже занятую клетку.
Для проверки победы просматриваются все строки, столбцы и две диагонали. Для поля N × N такая проверка работает за O(N²).
isFull() работает за O(1), поскольку количество занятых клеток хранится отдельно.
Случайный ход компьютера занимает O(N²) и выбирает одну из свободных клеток без риска бесконечно долго генерировать уже занятые координаты.