Java TicTacToe AI使用Minimax算法抛出ArrayOutOfBoundsException

时间:2015-09-03 11:25:40

标签: java javafx artificial-intelligence tic-tac-toe minimax

我正在尝试使用minimax算法编写AI来播放TicTacToe。我理解算法并研究了如何在java和TicTacToe方面使用它。我有一个对手(AI),在玩家点击一个牌之后计算并播放最佳动作。我遇到了一个问题,即如果玩家点击中心牌,那么所有剩下的8个牌都会被对手的牌子填满。接下来是一个ArrayOutOfBounds异常,其中应返回最佳列。

这是我第一次尝试在Java应用程序中实现算法或任何类型的AI。

到目前为止,这是我的代码:

TicTacToe.java

package com.cmarshall10450.TicTacToe;

import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Duration;

import java.util.ArrayList;
import java.util.List;


/**
 * Created by Chris on 02/08/2015.
 */
public class TicTacToe extends Application {

    public static final int ROWS = 3;
    public static final int COLS = 3;

    enum Seed{
        COMPUTER("O"), PLAYER("X"), EMPTY("");

        String token;

        Seed(String token){
            this.token = token;
        }
    }

    private Pane root = new Pane();

    private boolean playable = true;
    private Seed playerSeed = Seed.PLAYER;
    private Tile[][] board = new Tile[3][3];
    private List<Combo> combos = new ArrayList<>();

    private AIPlayer opponent = new AIPlayerMinimax(board);

    private Parent createContent(){

        root.setPrefSize(600, 600);

        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                Tile tile = new Tile(j*200, i*200);

                root.getChildren().add(tile);

                board[j][i] = tile;
            }
        }

        //Horizontal
        for (int y = 0; y < 3; y++) {
            combos.add(new Combo(board[0][y], board[1][y], board[2][y]));
        }

        //Vertical
        for (int x = 0; x < 3; x++) {
            combos.add(new Combo(board[x][0], board[x][1], board[x][2]));
        }

        //Diagonal
        combos.add(new Combo(board[0][0], board[1][1], board[2][2]));
        combos.add(new Combo(board[2][0], board[1][1], board[0][2]));

        return root;
    }

    private void checkState(){
        for (Combo combo : combos){
            if(combo.isComplete()){
                playable = false;
                playWinAnimation(combo);
                break;
            }
        }
    }

    private void playComputerMove(){
        opponent.setBoard(board);
        playerSeed = Seed.PLAYER;

        int bestRow = opponent.move()[1];
        int bestCol = opponent.move()[2];
        board[bestRow][bestCol].drawPlayerToken(Seed.COMPUTER);

    }

    private void playWinAnimation(Combo combo){
        Line line = new Line();

        line.setStartX(combo.tiles[0].getCenterX());
        line.setStartY(combo.tiles[0].getCenterY());
        line.setEndX(combo.tiles[0].getCenterX());
        line.setEndY(combo.tiles[0].getCenterY());

        root.getChildren().add(line);

        Timeline timeline = new Timeline();
        timeline.getKeyFrames().addAll(new KeyFrame(Duration.seconds(1),
                new KeyValue(line.endXProperty(), combo.tiles[2].getCenterX()),
                new KeyValue(line.endYProperty(), combo.tiles[2].getCenterY())
        ));
        timeline.play();
    }

    private class Combo {
        private Tile[] tiles;
        public Combo(Tile... tiles){
            this.tiles = tiles;
        }

        public boolean isComplete(){
            if(tiles[0].getValue().isEmpty()){
                return false;
            }

            return tiles[0].getValue().equals(tiles[1].getValue())
                    && tiles[0].getValue().equals(tiles[2].getValue());

        }
    }

    public class Tile extends StackPane{
        private Text text = new Text(Seed.EMPTY.token);

        public Tile(int translateX, int translateY){
            Rectangle border = new Rectangle(200, 200);
            border.setFill(null);
            border.setStroke(Color.BLACK);

            text.setFont(Font.font(72));

            setTranslateX(translateX);
            setTranslateY(translateY);

            setAlignment(Pos.CENTER);
            getChildren().addAll(border, text);

            setOnMouseClicked(event -> {

                if(!playable){
                    return;
                }

                if(event.getButton() == MouseButton.PRIMARY && playerSeed == Seed.PLAYER){
                    drawPlayerToken(Seed.PLAYER);
                    playerSeed = Seed.COMPUTER;

                    if(isHintModeEnabled()){
                        //TODO: compare move made by player to best move possible for player
                    }

                    checkState();
                    playComputerMove();
                }
            });
        }

        public String getValue(){
            return text.getText();
        }

        public void drawPlayerToken(Seed playerSeed){
            if(playerSeed == Seed.PLAYER){
                text.setText(Seed.PLAYER.token);
            }
            else{
                text.setText(Seed.COMPUTER.token);
            }
        }

        public Double getCenterX(){
            return getTranslateX() + 100;
        }

        public Double getCenterY(){
            return getTranslateY() + 100;
        }
    }

    private boolean isHintModeEnabled(){
        return true;
    }

    @Override
    public void start(Stage window) throws Exception {
        window.setScene(new Scene(createContent()));
        window.show();
    }

    public static void main(String[] args){
        launch(args);
    }


}

AIPlayer.java

package com.cmarshall10450.TicTacToe;

import com.cmarshall10450.TicTacToe.TicTacToe.Tile;
import com.cmarshall10450.TicTacToe.TicTacToe.Seed;

/**
 * Abstract superclass for all AI players with different strategies.
 * To construct an AI player:
 * 1. Construct an instance (of its subclass) with the game Board
 * 2. Call setSeed() to set the computer's seed
 * 3. Call move() which returns the next move in an int[2] array of {row, col}.
 *
 * The implementation subclasses need to override abstract method move().
 * They shall not modify Tile[][], i.e., no side effect expected.
 * Assume that next move is available, i.e., not game-over yet.
 */
public abstract class AIPlayer {
    protected int rows = TicTacToe.ROWS;  // number of rows
    protected int cols = TicTacToe.COLS;  // number of columns

    protected Tile[][] board; // the board's ROWS-by-COLS array of Cells
    protected Seed aiSeed = Seed.COMPUTER;    // computer's seed
    protected Seed playerSeed = Seed.PLAYER;   // opponent's seed

    /** Constructor with reference to game board */
    public AIPlayer(Tile[][] board) {
        this.board = board;
    }

    /** Set/change the seed used by computer and opponent */
    public void setSeed(Seed seed) {
        this.aiSeed = seed;
        playerSeed = (aiSeed == Seed.COMPUTER) ? Seed.COMPUTER : Seed.PLAYER;
    }

    public void setBoard(Tile[][] board) {
        this.board = board;
    }

    /** Abstract method to get next move. Return int[2] of {row, col} */
    abstract int[] move();  // to be implemented by subclasses
}

AIPlayerMinimax.java

package com.cmarshall10450.TicTacToe;

import java.util.ArrayList;
import java.util.List;

import com.cmarshall10450.TicTacToe.TicTacToe.Tile;
import com.cmarshall10450.TicTacToe.TicTacToe.Seed;

/** AIPlayer using Minimax algorithm */
public class AIPlayerMinimax extends AIPlayer {

    private int[] winningPatterns = {
            0b111000000, 0b000111000, 0b000000111, // rows
            0b100100100, 0b010010010, 0b001001001, // cols
            0b100010001, 0b001010100               // diagonals
    };

    /** Constructor with the given game board */
    public AIPlayerMinimax(Tile[][] board) {
        super(board);
    }

    /** Get next best move for computer. Return int[2] of {row, col} */
    @Override
    int[] move() {
        int[] result = minimax(2, aiSeed); // depth, max turn
        return new int[] {result[1], result[2]};   // row, col
    }

    /** Recursive minimax at level of depth for either maximizing or minimizing player.
     Return int[3] of {score, row, col}  */
    private int[] minimax(int depth, Seed player) {
        // Generate possible next moves in a List of int[2] of {row, col}.
        List<int[]> nextMoves = generateMoves();

        // aiSeed is maximizing; while playerSeed is minimizing
        int bestScore = (player == aiSeed) ? Integer.MIN_VALUE : Integer.MAX_VALUE;
        int currentScore;
        int bestRow = -1;
        int bestCol = -1;

        if (nextMoves.isEmpty() || depth == 0) {
            // Gameover or depth reached, evaluate score
            bestScore = evaluate();
        }
        else {
            for (int[] move : nextMoves) {
                // Try this move for the current "player"
                board[move[0]][move[1]].drawPlayerToken(player);
                if (player == aiSeed) {  // aiSeed (computer) is maximizing player
                    currentScore = minimax(depth - 1, playerSeed)[0];
                    if (currentScore > bestScore) {
                        bestScore = currentScore;
                        bestRow = move[0];
                        bestCol = move[1];
                    }
                } else {  // playerSeed is minimizing player
                    currentScore = minimax(depth - 1, aiSeed)[0];
                    if (currentScore < bestScore) {
                        bestScore = currentScore;
                        bestRow = move[0];
                        bestCol = move[1];
                    }
                }
                // Undo move
                board[move[0]][move[1]].drawPlayerToken(Seed.EMPTY);
            }
        }
        return new int[] {bestScore, bestRow, bestCol};
    }

    /** Find all valid next moves.
     Return List of moves in int[2] of {row, col} or empty list if gameover */
    private List<int[]> generateMoves() {
        List<int[]> nextMoves = new ArrayList<>(); // allocate List

        // If gameover, i.e., no next move
       if (hasWon(aiSeed) || hasWon(playerSeed)) {
            return nextMoves;   // return empty list
        }

        // Search for empty board and add to the List
        for (int row = 0; row < rows; ++row) {
            for (int col = 0; col < cols; ++col) {
                if (board[row][col].getValue() == Seed.EMPTY.token) {
                    nextMoves.add(new int[] {row, col});
                }
            }
        }
        return nextMoves;
    }

    /** The heuristic evaluation function for the current board
     @Return +100, +10, +1 for EACH 3-, 2-, 1-in-a-line for computer.
     -100, -10, -1 for EACH 3-, 2-, 1-in-a-line for opponent.
     0 otherwise   */
    private int evaluate() {
        int score = 0;
        // Evaluate score for each of the 8 lines (3 rows, 3 columns, 2 diagonals)
        score += evaluateLine(0, 0, 0, 1, 0, 2);  // row 0
        score += evaluateLine(1, 0, 1, 1, 1, 2);  // row 1
        score += evaluateLine(2, 0, 2, 1, 2, 2);  // row 2
        score += evaluateLine(0, 0, 1, 0, 2, 0);  // col 0
        score += evaluateLine(0, 1, 1, 1, 2, 1);  // col 1
        score += evaluateLine(0, 2, 1, 2, 2, 2);  // col 2
        score += evaluateLine(0, 0, 1, 1, 2, 2);  // diagonal
        score += evaluateLine(0, 2, 1, 1, 2, 0);  // alternate diagonal
        return score;
    }

    /** The heuristic evaluation function for the given line of 3 board
     @Return +100, +10, +1 for 3-, 2-, 1-in-a-line for computer.
     -100, -10, -1 for 3-, 2-, 1-in-a-line for opponent.
     0 otherwise */
    private int evaluateLine(int row1, int col1, int row2, int col2, int row3, int col3) {
        int score = 0;

        // First cell
        if (board[row1][col1].getValue() == aiSeed.token) {
            score = 1;
        } else if (board[row1][col1].getValue() == playerSeed.token) {
            score = -1;
        }

        // Second cell
        if (board[row2][col2].getValue() == aiSeed.token) {
            if (score == 1) {   // cell1 is aiSeed
                score = 10;
            } else if (score == -1) {  // cell1 is playerSeed
                return 0;
            } else {  // cell1 is empty
                score = 1;
            }
        } else if (board[row2][col2].getValue() == playerSeed.token) {
            if (score == -1) { // cell1 is playerSeed
                score = -10;
            } else if (score == 1) { // cell1 is aiSeed
                return 0;
            } else {  // cell1 is empty
                score = -1;
            }
        }

        // Third cell
        if (board[row3][col3].getValue() == aiSeed.token) {
            if (score > 0) {  // cell1 and/or cell2 is aiSeed
                score *= 10;
            } else if (score < 0) {  // cell1 and/or cell2 is playerSeed
                return 0;
            } else {  // cell1 and cell2 are empty
                score = 1;
            }
        } else if (board[row3][col3].getValue() == playerSeed.token) {
            if (score < 0) {  // cell1 and/or cell2 is playerSeed
                score *= -10;
            } else if (score > 1) {  // cell1 and/or cell2 is aiSeed
                return 0;
            } else {  // cell1 and cell2 are empty
                score = -1;
            }
        }
        return score;
    }

    /** Returns true if thePlayer wins */
    private boolean hasWon(Seed player) {
        int pattern = 0b000000000;  // 9-bit pattern for the 9 board
        for (int row = 0; row < rows; ++row) {
            for (int col = 0; col < cols; ++col) {
                if (board[row][col].getValue() == player.token) {
                    pattern |= (1 << (row * cols + col));
                }
            }
        }
        for (int winningPattern : winningPatterns) {
            if ((pattern & winningPattern) == winningPattern) return true;
        }
        return false;
    }
}

NullPointerException堆栈跟踪

Exception in thread "JavaFX Application Thread"      java.lang.ArrayIndexOutOfBoundsException: -1
    at com.cmarshall10450.TicTacToe.TicTacToe.playComputerMove(TicTacToe.java:100)
    at com.cmarshall10450.TicTacToe.TicTacToe.access$500(TicTacToe.java:28)
    at com.cmarshall10450.TicTacToe.TicTacToe$Tile.lambda$new$0(TicTacToe.java:170)
    at com.cmarshall10450.TicTacToe.TicTacToe$Tile$$Lambda$70/1842446135.handle(Unknown Source)
    at com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:86)
    at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238)
    at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
    at com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
    at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)
    at javafx.event.Event.fireEvent(Event.java:198)
    at javafx.scene.Scene$ClickGenerator.postProcess(Scene.java:3471)
    at javafx.scene.Scene$ClickGenerator.access$8100(Scene.java:3399)
    at javafx.scene.Scene$MouseHandler.process(Scene.java:3767)
    at javafx.scene.Scene$MouseHandler.access$1500(Scene.java:3486)
    at javafx.scene.Scene.impl_processMouseEvent(Scene.java:1762)
    at javafx.scene.Scene$ScenePeerListener.mouseEvent(Scene.java:2495)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:350)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:275)
    at java.security.AccessController.doPrivileged(Native Method)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleMouseEvent$350(GlassViewEventHandler.java:385)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler$$Lambda$98/1651003963.get(Unknown Source)
    at com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:404)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler.handleMouseEvent(GlassViewEventHandler.java:384)
    at com.sun.glass.ui.View.handleMouseEvent(View.java:555)
    at com.sun.glass.ui.View.notifyMouse(View.java:927)

非常感谢您给我的任何帮助或建议。

0 个答案:

没有答案