Консоль Java Tic Tac Toe

Я создал немного некрасивую консольную игру Tic Tac Toe, используя Java. Я учусь кодировать и хотел бы услышать несколько комментариев о моем коде. Поскольку это довольно сложно, я не использую классы и объекты (мое задание не было использовать их). Заранее спасибо!

package com.company;

import java.util.ArrayList;
import java.util.Scanner;

public class Main {
    public static Scanner scan = new Scanner(System.in);

    public static char[][] table = {
        {'1', '2', '3'},
        {'4', '5', '6'},
        {'7', '8', '9'}
    };

    public static ArrayList<Character> picksTillNow = new ArrayList<Character>();
    public static char playerOne=" ";
    public static char playerTwo = ' ';

    public static void main(String[] args) {

    for(char i = '1'; i <= '9'; i++){
        picksTillNow.add(i);
    }

    printTable();

    while (true){
        playerOneChoice();
        endGame();
        playerTwoChoice();
        endGame();
    }
}

/**
 * Prints the table with no players choices made ( 1 - 9 )
 */
public static void printTable() {

    for (int i = 0; i < table.length; i++) {
        for (int j = 0; j < table[i].length; j++) {
            System.out.print(table[i][j] + " ");
        }
        if (table[i][2] != 9) {
            System.out.println();
        }
    }
}


/**
 * Prints the table with the players choice included
 * @param player - player 1 or player 2
 * @param choice - char from 1 - 9
 */
public static void printTable(int player, char choice){

    for (int i = 0; i < table.length ; i++) {
        for (int j = 0; j < table[i].length; j++) {
            if(choice == table[i][j] && player == 1){
                //X
                table[i][j] = 'X';
            }else if(choice == table[i][j] && player == 2){
                //O
                table[i][j] = 'O';
            }
            System.out.print(table[i][j] + " ");
        }
        if(table[i][2] != 9){
            System.out.println();
        }
    }
}


/**
 * Asks for player one choice method and prints the table, checks if that position has been chosen
 */
public static void playerOneChoice(){
    System.out.print("Играч 1: ");
    playerOne = scan.next().charAt(0);
    if(picksTillNow.contains(playerOne)){
        picksTillNow.remove(picksTillNow.indexOf(playerOne));
        printTable(1, playerOne);
    }else{
        playerOneChoice();
    }
}


/**
 * Asks for player one choice method and prints the table, checks if that position has been chosen
 */
public static void playerTwoChoice(){
    System.out.print("Играч 2: ");
    playerTwo = scan.next().charAt(0);
    if(picksTillNow.contains(playerTwo)){
        picksTillNow.remove(picksTillNow.indexOf(playerTwo));
        printTable(2, playerTwo);
    }else{
        playerTwoChoice();
    }
}

/**
 * Checks for a winner. If yes - exits program
 */
public static void endGame(){
    for(int i = 0; i < table.length; i++){
        if(table[i][0] == table[i][1] && table[i][1] == table[i][2] && table[0][i] != 'О'){
            System.out.println("Победа!");
            System.exit(0);
        }
    }

    for(int j = 0; j < table.length; j++){
        if(table[0][j] == table[1][j] && table[1][j] == table[2][j] && table[j][0] != 'O'){
            System.out.println("Победа!");
            System.exit(0);
        }
    }

    if(table[0][0] == table[1][1] && table[1][1] == table[2][2] && table[0][0] != 'O'){
        System.out.println("Победа!");
        System.exit(0);
    }

    if(table[0][2] == table[1][1] && table[1][1] == table[2][0] && table[0][2] != 'O'){
        System.out.println("Победа!");
        System.exit(0);
    }

    if(picksTillNow.isEmpty()) System.exit(0);
 }
}

2 ответа
2

Ваше форматирование кажется непоследовательным, используйте автоматическое форматирование кода (скорее всего, в вашей IDE он есть).

public static Scanner scan = new Scanner(System.in);

Скорее всего, следует ограничить использование сканера (или потоков в целом). Потоки довольно легко ассоциируются с собственными ресурсами, которые для освобождения должны быть явно уничтожены.


    public static char[][] table = {
        {'1', '2', '3'},
        {'4', '5', '6'},
        {'7', '8', '9'}
    };

Почему таблица предварительно заполнена значениями?


    public static ArrayList<Character> picksTillNow = new ArrayList<Character>();

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


    public static char playerOne=" ";
    public static char playerTwo = ' ';

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


    for(char i = '1'; i <= '9'; i++){
        picksTillNow.add(i);
    }

Автобокс.


    while (true){

Вы также могли бы иметь boolean любить running или playing или gameRunning и установите его в зависимости от возвращаемого значения `endGame ().

Или, поскольку количество оборотов фиксировано, вы можете сделать сочетание обоих:

int turn = 0;

while (turn++ < 9 && nobodyHasWonYet) {
    // Logic.
}

        endGame();

Это плохое название для метода, поскольку он не всегда заканчивает игру.


    for (int i = 0; i < table.length; i++) {
        for (int j = 0; j < table[i].length; j++) {

Я настойчивый сторонник того, что вам разрешено использовать только однобуквенные имена переменных при работе с размерами («x», «y», «z»), и это исключает использование i и j). В этом случае на самом деле, используя x и y поскольку имена переменных улучшат читаемость кода. Еще лучше было бы использовать row и column.


        if (table[i][2] != 9) {
            System.out.println();
        }

Но значение этого поля по умолчанию в какой-то момент перезаписывается, не так ли?


    }else{
        playerOneChoice();
    }

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


public static void endGame(){
    for(int i = 0; i < table.length; i++){
        if(table[i][0] == table[i][1] && table[i][1] == table[i][2] && table[0][i] != 'О'){
            System.out.println("Победа!");
            System.exit(0);
        }
    }

    for(int j = 0; j < table.length; j++){
        if(table[0][j] == table[1][j] && table[1][j] == table[2][j] && table[j][0] != 'O'){
            System.out.println("Победа!");
            System.exit(0);
        }
    }

    if(table[0][0] == table[1][1] && table[1][1] == table[2][2] && table[0][0] != 'O'){
        System.out.println("Победа!");
        System.exit(0);
    }

    if(table[0][2] == table[1][1] && table[1][1] == table[2][0] && table[0][2] != 'O'){
        System.out.println("Победа!");
        System.exit(0);
    }

    if(picksTillNow.isEmpty()) System.exit(0);
 }

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


            System.exit(0);

Следует иметь в виду, что System.exit не «выйти из приложения», а «убить процесс JVM». При вызове этого даже не finally блок может работать. В данном случае это не имеет значения, но нужно иметь в виду.


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

int fieldChoice = getChoice();
int row = fieldChoice % 3;
int column = fieldChoice / 3; // Might be other way around...never can remember.

char selectedField = playingField[row][column];

if (selectedField != 'X' && selectedField != 'O') {
    // Set it.
} else {
    // No dice.
}

playerOneChoice и playerTwoChoice можно объединить в один, передав желаемого полевого игрока в качестве параметра. Используя мой пример выше:

public static void playerChoice(char playerCharacter) {
    int fieldChoice = getChoice();
    int row = fieldChoice % 3;
    int column = fieldChoice / 3; // Might be other way around...never can remember.
    
    char selectedField = playingField[row][column];
    
    if (selectedField != 'X' && selectedField != 'O') {
        playingField[row][column] = playerCharacter;
    } else {
        // No dice.
    }
}

Сказав это, ваша функция печати тогда будет сводиться к следующему:

for (int row = 0; row < 3; row++) {
    for (int column = 0; column < 3; column++) {
        System.out.println(playingField[row][column] + " ");
    }
    
    System.out.println();
}

Жесткое программирование размера здесь – неплохая вещь, если вы предполагаете игру в крестики-нолики с игровым полем 3×3. Если вы хотите поддерживать большие игровые поля, вам все равно придется изменить остальную логику.

Приступая к проверке, выиграл ли кто-нибудь, условия выигрыша сводятся к следующему:

// Rows
playingField[0][0] == playingField[0][1] && playingField[0][1] == playingField[0][2];
playingField[1][0] == playingField[1][1] && playingField[1][1] == playingField[1][2];
playingField[2][0] == playingField[2][1] && playingField[2][1] == playingField[2][2];

// Columns
playingField[0][0] == playingField[1][0] && playingField[1][0] == playingField[2][0];
playingField[0][1] == playingField[1][1] && playingField[1][1] == playingField[2][1];
playingField[0][2] == playingField[1][2] && playingField[1][2] == playingField[2][2];

// Diagonal
playingField[0][0] == playingField[1][1] && playingField[1][1] == playingField[2][2];
playingField[0][2] == playingField[1][1] && playingField[1][1] == playingField[2][0];

Вы заметите, что это так же быстро, как и ваши петли, и мы можем связать их с || непосредственно для возврата. Однако это не скажет нам, кто именно. Для этого нужна дополнительная логика, а именно if на каждой строке. Но мы также можем предположить, что победителем становится игрок, который играл последним, что на самом деле является справедливым предположением. Чтобы сделать его более читабельным, мы можем добавить нам три вспомогательные функции:

public static boolean isWinningRow(int rowIndex) {
    return playingField[row][0] == playingField[row][1] && playingField[0][1] == playingField[row][2]; 
}

public static boolean isWinningColumn(int column) {
    return playingField[0][column] == playingField[1][column] && playingField[1][column] == playingField[2][column]; 
}

public static boolean isLeftRightDiagonalWin() {
    return playingField[0][0] == playingField[1][1] && playingField[1][1] == playingField[2][2]; 
}

public static boolean isRightLeftDiagonalWin() {
    return playingField[0][2] == playingField[1][1] && playingField[1][1] == playingField[2][0]; 
}

Это не упрощает общую сложность, но улучшает читаемость:

return isWinningRow(0)
        || isWinningRow(1)
        || isWinningRow(2)
        || isWinningColumn(0)
        || isWinningColumn(1)
        || isWinningColumn(2)
        || isLeftRightDiagonalWin()
        || isRightLeftDiagonalWin();

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

int turn = 0;
boolean running = true;

while (turn < 9 && running) {
    int currentPlayer = (turn % 2) + 1;
    
    playerChoice(currentPlayer);
    printPlayingField();
    
    if (hasWon()) {
        System.out.println("Winner: " + Integer.toString(currentPlayer);
        
        running = false;
    }
    
    turn++;
}

Меня немного не устраивает код для выбора плеера, но он должен работать. И в целом должно дать вам хорошее представление, с чего начать.

  • Лол, спасибо, Бобби! Это была моя первая попытка написать код, отличный от решения простых задач. Очень полезные заметки, я попробую переписать свой код! Спасибо еще раз! 🙂

    – Ценко Алексиев

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

Вы можете думать о крестиках как о единицах и нулях (истинных и ложных):

boolean[][] board = new boolean[][]{
     {false, false, false},
     {false, true, false},
     {false, false, true}
};

и поэтому преобразуем доску в целое число:

int intboard = 0;
int exponent = 0;
for (int i = 0; i < board.length; i++) {
    for (int j = 0; j < board[0].length; j++) {
        intboard += board[i][j] ? Math.pow(2, exponent) : 0;
        exponent++;
    }
}

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

List<Integer> winningBoards = List.of(
        7, // Horizontal, row-0 --> 000000111
        56, // Horizontal, row-1 --> 7 << 3
        448, // Horizontal, row-2 --> 7 << 6
        73, // Vertical, column-0 --> 001001001
        146, // Vertical, column-1 --> 73 << 1
        292, // Vertical, column-2 --> 73 << 2
        273, // Diagonal-0 --> 100010001
        84 // Diagonal-1 --> (273 >> 2) | 16
);

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

Наконец, проверка выигрышных случаев – это вопрос применения масок WinBoards на intboard:

final int boardToCheck = intboard;
boolean youHaveWon = winningBoards.stream()
        .anyMatch(winningboard -> (boardToCheck & winningboard) == winningboard);

System.out.println("Nought has won: " + youHaveWon);

Если вы хотите проверить crosses вам просто нужно перевернуть биты выигрышных досок с помощью оператора дополнения ~:

youHaveWon = winningBoards.stream()
        .anyMatch(winningboard -> (~boardToCheck & winningboard) == winningboard);

System.out.println("Cross has won: " + youHaveWon);

Преимущество этого метода неочевидно для такой простой игры, как крестики-нолики, но если вы работаете над более сложными играми и вам нужно проверять множество досок (даже миллионы или миллиарды!) При выполнении Minimax или его эквивалента, вы можете получить очень хорошее ускорение за счет изменения представления вашей игры с использования объектов на биты.

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

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