我正在尝试使用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)
非常感谢您给我的任何帮助或建议。