com.github.vatbub.tictactoe.Board.java Source code

Java tutorial

Introduction

Here is the source code for com.github.vatbub.tictactoe.Board.java

Source

package com.github.vatbub.tictactoe;

/*-
 * #%L
 * tictactoe
 * %%
 * Copyright (C) 2016 - 2017 Frederik Kammel
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import com.github.vatbub.common.core.logging.FOKLogger;
import com.github.vatbub.tictactoe.common.Move;
import com.github.vatbub.tictactoe.kryo.KryoGameConnections;
import com.github.vatbub.tictactoe.view.AILevel;
import com.github.vatbub.tictactoe.view.Main;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import org.apache.commons.io.output.NullOutputStream;
import org.jetbrains.annotations.Nullable;

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;

/**
 * A classic tic tac toe board.
 */
@SuppressWarnings("WeakerAccess")
public class Board {
    private static final int gemsInARowToWin = 3;

    private final Cell[][] board;
    private final int dimRows;
    private final int dimCols;
    private final ObjectProperty<Player> currentPlayer = new SimpleObjectProperty<>();
    private final ObjectProperty<GameEndRunnable> gameEndCallback = new SimpleObjectProperty<>();
    private AILevel aiLevel;
    private Player player1;
    private Player player2;
    private Move lastMove;

    /**
     * Initializes a new 3*3 game board
     */
    @SuppressWarnings("unused")
    public Board() {
        this(3);
    }

    /**
     * Initializes a new quadratic game board with the given dimension
     *
     * @param quadraticDim The edge length of the board
     */
    public Board(@SuppressWarnings("SameParameterValue") int quadraticDim) {
        this(quadraticDim, quadraticDim);
    }

    /**
     * Initializes a new board with the given dimensions
     *
     * @param dimRows The desired width of the board
     * @param dimCols The desired height of the board.
     */
    public Board(int dimRows, int dimCols) {
        this(dimRows, dimCols, null, null);
    }

    /**
     * Initializes a new board with the given dimensions
     *
     * @param dimRows The desired width of the board
     * @param dimCols The desired height of the board.
     * @param player1 The first player on the board
     * @param player2 The second player on the board
     */
    public Board(int dimRows, int dimCols, @SuppressWarnings("SameParameterValue") Player player1,
            @SuppressWarnings("SameParameterValue") Player player2) {
        this.dimCols = dimCols;
        this.dimRows = dimRows;
        setPlayer1(player1);
        setPlayer2(player2);
        board = new Cell[dimRows][dimCols];
        for (int r = 0; r < dimRows; r++) {
            for (int c = 0; c < dimCols; c++) {
                board[r][c] = new Cell();
            }
        }
    }

    public void setPlayerAt(int row, int col, Player player) {
        board[row][col].setCurrentPlayer(player);
    }

    /**
     * Returns the {@link Player} that currently occupies the specified cell or {@code null} if the cell is free.
     *
     * @param row The row of the cell to get the player for
     * @param col The column of the cell to get the player for
     * @return The {@link Player} that currently occupies the specified cell or {@code null} if the cell is free.
     */
    public Player getPlayerAt(int row, int col) {
        return board[row][col].getCurrentPlayer();
    }

    public int getRowCount() {
        return this.dimRows;
    }

    public int getColumnCount() {
        return this.dimCols;
    }

    public Player getPlayer1() {
        return player1;
    }

    public void setPlayer1(Player player1) {
        this.player1 = player1;
    }

    public Player getPlayer2() {
        return player2;
    }

    public void setPlayer2(Player player2) {
        this.player2 = player2;
    }

    public Player getCurrentPlayer() {
        if (currentPlayerProperty().get() == null) {
            currentPlayerProperty().set(getPlayer1());
        }

        return currentPlayer.get();
    }

    public ObjectProperty<Player> currentPlayerProperty() {
        return currentPlayer;
    }

    public void doTurn(Move move) {
        doTurn(move, false);
    }

    public void doTurn(Move move, boolean ignoreAI) {
        lastMove = move;

        if (getPlayerAt(move.getRow(), move.getColumn()) != null) {
            throw new IllegalStateException("Cell is already taken by a player");
        }

        this.setPlayerAt(move.getRow(), move.getColumn(), getCurrentPlayer());

        if (getOpponent(getCurrentPlayer()).getPlayerMode().equals(PlayerMode.internetHuman) && !ignoreAI) {
            KryoGameConnections.sendMove(move);
        }

        WinnerInfo winnerInfo = getWinner(move.getRow(), move.getColumn());
        if (winnerInfo.winningPlayer != null) {
            if (getGameEndCallback() != null) {
                getGameEndCallback().run(winnerInfo);
            }
            return;
        }

        currentPlayerProperty().set(getOpponent(getCurrentPlayer()));

        if (getCurrentPlayer().getPlayerMode().equals(PlayerMode.ai) && !ignoreAI) {
            final Board thisCopy = this;
            Thread aiWaitThread = new Thread(() -> {
                PrintStream nullPrintStream = new PrintStream(new NullOutputStream());
                while (Main.currentMainWindowInstance.isBlockedForInput()) {
                    // wait
                    nullPrintStream.println("Waiting...");
                }

                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    FOKLogger.log(Board.class.getName(), Level.SEVERE, "An error occurred", e);
                }

                getCurrentPlayer().doAiTurn(thisCopy);
                Main.currentMainWindowInstance.updateCurrentPlayerLabel();
                Main.currentMainWindowInstance.renderRows();
            });
            aiWaitThread.setName("aiWaitThread");
            aiWaitThread.start();
        }
    }

    /**
     * Returns the opponent of the specified player
     *
     * @param player The player whose opponent needs to be determined. {@code player} must be either Player1 or Player2 on this board, otherwise a {@code IllegalArgumentException} is thrown
     * @return The opponent of the specified player
     */
    public Player getOpponent(Player player) {
        if (player == getPlayer1()) {
            return getPlayer2();
        } else if (player == getPlayer2()) {
            return getPlayer1();
        } else {
            throw new IllegalArgumentException(
                    player.getName() + " is not part of the game and thus cannot have an opponent");
        }
    }

    /**
     * Determines the winner of the game
     *
     * @param startRow The row of the cell where the winner search shall start. This should be the row of the last turn.
     * @param startCol The column of the cell where the winner search shall start. This should be the column of the last turn.
     * @return The winner of the game, {@code Player.TIE_PLAYER} if the game is finished but there is no winner or {@code null} if the game is not finished yet.
     */
    public WinnerInfo getWinner(int startRow, int startCol) {
        int gemsFound = 1;
        int r = startRow;
        int c = startCol;
        WinnerInfo res = new WinnerInfo();
        res.winningPlayer = getPlayerAt(r, c);
        res.winLineStartRow = r;
        res.winLineStartColumn = c;
        res.winLineEndRow = r;
        res.winLineEndColumn = c;

        // go up
        while (gemsFound < gemsInARowToWin && r - 1 >= 0 && r - 1 < getRowCount()) {
            r = r - 1;
            if (getPlayerAt(r, c) == res.winningPlayer) {
                gemsFound = gemsFound + 1;
                res.winLineStartRow = r;
            }
        }
        if (gemsFound >= gemsInARowToWin) {
            return res;
        }

        // go down
        r = startRow;
        while (gemsFound < gemsInARowToWin && r + 1 >= 0 && r + 1 < getRowCount()) {
            r = r + 1;
            if (getPlayerAt(r, c) == res.winningPlayer) {
                res.winLineEndRow = r;
                gemsFound = gemsFound + 1;
            }
        }
        if (gemsFound >= gemsInARowToWin) {
            return res;
        }

        // go left
        r = startRow;
        res.winLineStartRow = r;
        res.winLineEndRow = r;
        gemsFound = 1;
        while (gemsFound < gemsInARowToWin && c - 1 >= 0 && c - 1 < getColumnCount()) {
            c = c - 1;
            if (getPlayerAt(r, c) == res.winningPlayer) {
                res.winLineStartColumn = c;
                gemsFound = gemsFound + 1;
            }
        }
        if (gemsFound >= gemsInARowToWin) {
            return res;
        }

        // go right
        c = startCol;
        while (gemsFound < gemsInARowToWin && c + 1 >= 0 && c + 1 < getColumnCount()) {
            c = c + 1;
            if (getPlayerAt(r, c) == res.winningPlayer) {
                res.winLineEndColumn = c;
                gemsFound = gemsFound + 1;
            }
        }
        if (gemsFound >= gemsInARowToWin) {
            return res;
        }

        // go up-left
        c = startCol;
        res.winLineStartColumn = c;
        res.winLineEndColumn = c;
        gemsFound = 1;
        while (gemsFound < gemsInARowToWin && c - 1 >= 0 && c - 1 < getColumnCount() && r - 1 >= 0
                && r - 1 < getRowCount()) {
            c = c - 1;
            r = r - 1;
            if (getPlayerAt(r, c) == res.winningPlayer) {
                res.winLineStartRow = r;
                res.winLineStartColumn = c;
                gemsFound = gemsFound + 1;
            }
        }
        if (gemsFound >= gemsInARowToWin) {
            return res;
        }

        // go down-right
        c = startCol;
        r = startRow;
        while (gemsFound < gemsInARowToWin && c + 1 >= 0 && c + 1 < getColumnCount() && r + 1 >= 0
                && r + 1 < getRowCount()) {
            c = c + 1;
            r = r + 1;
            if (getPlayerAt(r, c) == res.winningPlayer) {
                res.winLineEndRow = r;
                res.winLineEndColumn = c;
                gemsFound = gemsFound + 1;
            }
        }
        if (gemsFound >= gemsInARowToWin) {
            return res;
        }

        // go up-right
        c = startCol;
        r = startRow;
        res.winLineStartColumn = c;
        res.winLineEndColumn = c;
        res.winLineStartRow = r;
        res.winLineEndRow = r;
        gemsFound = 1;
        while (gemsFound < gemsInARowToWin && c + 1 >= 0 && c + 1 < getColumnCount() && r - 1 >= 0
                && r - 1 < getRowCount()) {
            c = c + 1;
            r = r - 1;
            if (getPlayerAt(r, c) == res.winningPlayer) {
                res.winLineStartRow = r;
                res.winLineStartColumn = c;
                gemsFound = gemsFound + 1;
            }
        }
        if (gemsFound >= gemsInARowToWin) {
            return res;
        }

        // go down-left
        c = startCol;
        r = startRow;
        while (gemsFound < gemsInARowToWin && c - 1 >= 0 && c - 1 < getColumnCount() && r + 1 >= 0
                && r + 1 < getRowCount()) {
            c = c - 1;
            r = r + 1;
            if (getPlayerAt(r, c) == res.winningPlayer) {
                res.winLineEndRow = r;
                res.winLineEndColumn = c;
                gemsFound = gemsFound + 1;
            }
        }
        if (gemsFound >= gemsInARowToWin) {
            return res;
        }

        // either tie or not finished

        // check if there is any space left
        for (int row = 0; row < getRowCount(); row++) {
            for (int col = 0; col < getColumnCount(); col++) {
                if (getPlayerAt(row, col) == null) {
                    // we've found an empty space
                    res.winningPlayer = null;
                    return res;
                }
            }
        }

        // it's a tie
        res.winningPlayer = Player.TIE_PLAYER;
        return res;
    }

    public List<Move> getAvailableMoves() {
        List<Move> res = new ArrayList<>();
        for (int row = 0; row < getRowCount(); row++) {
            for (int column = 0; column < getColumnCount(); column++) {
                if (getPlayerAt(row, column) == null) {
                    // cell is empty so it is a valid move
                    res.add(new Move(row, column));
                }
            }
        }

        Collections.shuffle(res);
        return res;
    }

    @Override
    public String toString() {
        StringBuilder res = new StringBuilder();

        for (int r = 0; r < this.getRowCount(); r++) {
            for (int c = 0; c < this.getColumnCount(); c++) {
                if (getPlayerAt(r, c) == null) {
                    res.append("-");
                } else if (getPlayerAt(r, c) == getPlayer1()) {
                    res.append("1");
                } else if (getPlayerAt(r, c) == getPlayer2()) {
                    res.append("2");
                }

                if (c < getColumnCount() - 1) {
                    res.append(", ");
                }
            }

            if (r < getRowCount() - 1) {
                res.append("; ");
            }
        }

        return "[" + res + "]";
    }

    public GameEndRunnable getGameEndCallback() {
        return gameEndCallback.get();
    }

    public void setGameEndCallback(GameEndRunnable gameEndCallback) {
        this.gameEndCallback.set(gameEndCallback);
    }

    @SuppressWarnings("unused")
    public ObjectProperty<GameEndRunnable> gameEndCallbackProperty() {
        return gameEndCallback;
    }

    /**
     * Returns the last {@link Move} that was done on this board.
     *
     * @return The last {@link Move} that was done on this board or {@code null} if no move was performed on this board so far.
     */
    @Nullable
    public Move getLastMove() {
        return lastMove;
    }

    @SuppressWarnings("MethodDoesntCallSuperMethod")
    @Override
    public Board clone() {
        Board res = new Board(getRowCount(), getColumnCount(), getPlayer1(), getPlayer2());
        res.currentPlayerProperty().set(this.getCurrentPlayer());
        res.lastMove = this.lastMove;
        res.aiLevel = this.aiLevel;

        for (int row = 0; row < getRowCount(); row++) {
            for (int column = 0; column < getColumnCount(); column++) {
                res.setPlayerAt(row, column, this.getPlayerAt(row, column));
            }
        }

        return res;
    }

    public AILevel getAiLevel() {
        return aiLevel;
    }

    public void setAiLevel(AILevel aiLevel) {
        this.aiLevel = aiLevel;
    }

    public class WinnerInfo {
        /**
         * The player who won the game, {@code null} if the game is not finished yet or {@link Player#TIE_PLAYER} if the game ended in a tie.
         */
        public Player winningPlayer;

        /**
         * The row coordinate of the starting point of the line of gems that caused the {@link #winningPlayer} to win
         */
        public int winLineStartRow;

        /**
         * The column coordinate of the starting point of the line of gems that caused the {@link #winningPlayer} to win
         */
        public int winLineStartColumn;

        /**
         * The row coordinate of the end point of the line of gems that caused the {@link #winningPlayer} to win
         */
        public int winLineEndRow;

        /**
         * The column coordinate of the end point of the line of gems that caused the {@link #winningPlayer} to win
         */
        public int winLineEndColumn;

        public boolean isFinished() {
            return winningPlayer != null;
        }

        public boolean isTie() {
            return winningPlayer == Player.TIE_PLAYER;
        }

        public double getHeuristicValue(Player maximizingPlayer) {
            if (winningPlayer == maximizingPlayer) {
                return 15;
            } else if (winningPlayer == getOpponent(maximizingPlayer)) {
                return -15;
            } else {
                // tie
                return 0;
            }
        }
    }
}