Migrated repos from Github
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
/ComputerPlayer.class
|
||||
/Connect4Model.class
|
||||
/Connect4MoveMessage.class
|
||||
/IllegalArgumentException.class
|
||||
/Minimax_AlphaBeta$GameState.class
|
||||
/Minimax_AlphaBeta.class
|
||||
/RandomAI.class
|
||||
/PatternMatchingAI.class
|
||||
@@ -0,0 +1,10 @@
|
||||
package model;
|
||||
|
||||
/**
|
||||
* An interface for generating strategies.
|
||||
*/
|
||||
|
||||
public interface ComputerPlayer {
|
||||
|
||||
public int getMove(Connect4Model gameModel, boolean max);
|
||||
}
|
||||
@@ -0,0 +1,481 @@
|
||||
package model;
|
||||
|
||||
/**
|
||||
* This class represents the Connect4 game model.
|
||||
*
|
||||
*/
|
||||
|
||||
public class Connect4Model extends java.util.Observable implements java.io.Serializable {
|
||||
|
||||
/**
|
||||
* Java generated serialization ID.
|
||||
*/
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* The maximum size for each rows.
|
||||
*/
|
||||
private final int ROWS_NUM = 7;
|
||||
|
||||
/**
|
||||
* The maximum size of each column.
|
||||
*/
|
||||
private final int COLS_NUM = 6;
|
||||
|
||||
/**
|
||||
* The number of consecutive pieces either horizontally, vertically, or
|
||||
* diagonally to win the game.
|
||||
*/
|
||||
private final int CONNECT_SIZE = 4;
|
||||
|
||||
/**
|
||||
* The default character representation of the player.
|
||||
*/
|
||||
private final char humanChar = 'X';
|
||||
|
||||
/**
|
||||
* The default character representation of the computer player.
|
||||
*/
|
||||
private final char computerChar = 'O';
|
||||
|
||||
/**
|
||||
* The default character representation of an empty cell.
|
||||
*/
|
||||
private final char blankChar = '_';
|
||||
|
||||
/**
|
||||
* The object which represents the computer's strategy for playing the game.
|
||||
*/
|
||||
private transient ComputerPlayer computerPlayer;
|
||||
|
||||
/**
|
||||
* Encapsulate move information to send over to the views.
|
||||
*/
|
||||
private transient Connect4MoveMessage moveMsg;
|
||||
|
||||
/**
|
||||
* Indicates if it's currently the human turn.
|
||||
*/
|
||||
private boolean isHumanMove;
|
||||
|
||||
/**
|
||||
* Counts the total number of moves that have been made.
|
||||
*/
|
||||
private int moveMaked;
|
||||
|
||||
/**
|
||||
* Representation of the board.
|
||||
*/
|
||||
private char[][] board;
|
||||
|
||||
/**
|
||||
* Ctor for Connect4Model.
|
||||
*/
|
||||
public Connect4Model() {
|
||||
|
||||
board = new char[ROWS_NUM][COLS_NUM];
|
||||
for (int i = 0; i < ROWS_NUM; i++) {
|
||||
for (int j = 0; j < COLS_NUM; j++) {
|
||||
board[i][j] = blankChar;
|
||||
}
|
||||
}
|
||||
|
||||
// Human moves first by default.
|
||||
isHumanMove = true;
|
||||
computerPlayer = new Minimax_AlphaBeta();
|
||||
moveMaked = 0;
|
||||
moveMsg = null;
|
||||
|
||||
setChanged();
|
||||
notifyObservers(moveMsg);
|
||||
}
|
||||
|
||||
public void setComputerPlayer(ComputerPlayer another) {
|
||||
this.computerPlayer = another;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a character on the board at row x and column y.
|
||||
*
|
||||
* @param x The row number.
|
||||
* @param y The column number.
|
||||
* @return A character on the board at row x and column y.
|
||||
* @throws IllegalArgumentException when x is not between 0 and ROWS_NUM or y is
|
||||
* not between 0 and COLS_NUM.
|
||||
*/
|
||||
public char getObjectAt(final int x, final int y) throws model.IllegalArgumentException {
|
||||
|
||||
if ( (x >= this.ROWS_NUM || x < 0) ||
|
||||
(y >= this.COLS_NUM || y < 0)) {
|
||||
|
||||
throw new model.IllegalArgumentException("Invalid arguments.");
|
||||
}
|
||||
|
||||
return this.board[x][y];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an object at cell (x, y) to a character.
|
||||
*
|
||||
* @param x stands for row
|
||||
* @param y stands for column
|
||||
* @param obj The character representation of any player (can be blank).
|
||||
* @throws IllegalArgumentException when x is not between 0 and ROWS_NUM or y is
|
||||
* not between 0 and COLS_NUM.
|
||||
*/
|
||||
public void setObjectAt(final int x, final int y, final char obj) throws model.IllegalArgumentException {
|
||||
if ((x >= this.ROWS_NUM || x < 0) || (y >= this.COLS_NUM || y < 0)) {
|
||||
|
||||
throw new model.IllegalArgumentException("Invalid arguments.");
|
||||
}
|
||||
|
||||
this.board[x][y] = obj;
|
||||
|
||||
if (obj == this.getComputerChar()) {
|
||||
|
||||
moveMsg = new Connect4MoveMessage(this.board[0].length - y - 1, x , 2);
|
||||
} else {
|
||||
|
||||
moveMsg = new Connect4MoveMessage(this.board[0].length - y - 1, x , 1);
|
||||
}
|
||||
|
||||
setChanged();
|
||||
notifyObservers(moveMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a cell is empty or not.
|
||||
*
|
||||
* @param x stands for row
|
||||
* @param y stand for column
|
||||
* @return true if the cell (x,y) is empty.
|
||||
*/
|
||||
public boolean isBlank(final int x, final int y) {
|
||||
return this.board[x][y] == blankChar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the maximum row of the board.
|
||||
*
|
||||
* @return The maximum row of the board.
|
||||
*/
|
||||
public int getMaxRow() {
|
||||
return this.ROWS_NUM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the maximum column of the board.
|
||||
*
|
||||
* @return The maximum column of the board.
|
||||
*/
|
||||
public int getMaxCol() {
|
||||
return this.COLS_NUM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor to the model's character representation of the player.
|
||||
*
|
||||
* @return A character representing the player.
|
||||
*/
|
||||
public char getHumanChar() {
|
||||
return humanChar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor to the model's character representation of the computer player.
|
||||
*
|
||||
* @return A character representing the computer.
|
||||
*/
|
||||
public char getComputerChar() {
|
||||
return computerChar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor to the model's character representation of an empty slot.
|
||||
*
|
||||
* @return A blank character.
|
||||
*/
|
||||
public char getBlankChar() {
|
||||
return blankChar;
|
||||
}
|
||||
|
||||
/**
|
||||
* An accessor to isHumanMove.
|
||||
*
|
||||
* @return Return true if it's currently the human's move.
|
||||
*/
|
||||
public boolean isHumanMove() {
|
||||
return this.isHumanMove;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the flag for human turn.
|
||||
*
|
||||
* @param isHumanTurn Set isHumanMove to true if this is true and false otherwise.
|
||||
*/
|
||||
public void setHumanTurn(final boolean isHumanTurn) {
|
||||
this.isHumanMove = isHumanTurn;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper method to switch turn in a single game.
|
||||
*/
|
||||
public void switchTurn() {
|
||||
|
||||
this.moveMaked++;
|
||||
isHumanMove = !isHumanMove;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the winning condition by column.
|
||||
*
|
||||
* @param playerChar A character that represents the player.
|
||||
* @return True if playerChar wins the game by column and false otherwise.
|
||||
*/
|
||||
public boolean wonByCol(final char playerChar) {
|
||||
|
||||
boolean won = false;
|
||||
|
||||
for (int i = 0; i < ROWS_NUM; i++) {
|
||||
|
||||
int colSum = 0;
|
||||
for (int j = 0; j < COLS_NUM; j++) {
|
||||
|
||||
if (board[i][j] == playerChar) {
|
||||
|
||||
colSum++;
|
||||
if (colSum == CONNECT_SIZE) {
|
||||
|
||||
won = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
colSum = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return won;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the winning condition by row.
|
||||
*
|
||||
* @param playerChar A character that represents the player.
|
||||
* @return True if playerChar wins the game by row and false otherwise.
|
||||
*/
|
||||
public boolean wonByRow(final char playerChar) {
|
||||
|
||||
boolean won = false;
|
||||
|
||||
for (int i = 0; i < COLS_NUM; i++) {
|
||||
|
||||
int rowSum = 0;
|
||||
for (int j = 0; j < ROWS_NUM; j++) {
|
||||
|
||||
if (board[j][i] == playerChar) {
|
||||
|
||||
rowSum++;
|
||||
if (rowSum == CONNECT_SIZE) {
|
||||
|
||||
won = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
|
||||
rowSum = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return won;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the winning condition diagonally, both left and right.
|
||||
*
|
||||
* @param playerChar A character that represents the player.
|
||||
* @return True if playerChar wins the game diagonally and false otherwise.
|
||||
*/
|
||||
public boolean wonByDiagonal(final char playerChar) {
|
||||
int sum = 0;
|
||||
boolean won = false;
|
||||
|
||||
// Covering from left to right.
|
||||
for (int i = 3; i < ROWS_NUM; i++) {
|
||||
sum = 0;
|
||||
for (int j = 0; j < COLS_NUM && j <= i; j++) {
|
||||
if (board[i - j][j] == playerChar) {
|
||||
|
||||
sum++;
|
||||
if (sum == CONNECT_SIZE) {
|
||||
won = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
sum = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int j = 1; j < 3; j++) {
|
||||
|
||||
sum = 0;
|
||||
for (int k = 0; j + k < COLS_NUM; k++) {
|
||||
|
||||
if (board[ROWS_NUM - 1 - k][j + k] == playerChar) {
|
||||
sum++;
|
||||
if (sum == CONNECT_SIZE) {
|
||||
won = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
sum = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Covering from right to left.
|
||||
for (int i = 3; i >= 0; i--) {
|
||||
sum = 0;
|
||||
for (int j = 0; j < COLS_NUM && (i + j) < ROWS_NUM; j++) {
|
||||
if (board[i + j][j] == playerChar) {
|
||||
sum++;
|
||||
if (sum == CONNECT_SIZE) {
|
||||
won = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
sum = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int j = 1; j < 3; j++) {
|
||||
sum = 0;
|
||||
for (int k = 0; j + k < COLS_NUM; k++) {
|
||||
if (board[0 + k][j + k] == playerChar) {
|
||||
sum++;
|
||||
if (sum == CONNECT_SIZE) {
|
||||
won = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
sum = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return won;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a player win the game either by row, column, or diagonal.
|
||||
*
|
||||
* @param playerChar A character representing the player who won.
|
||||
* @return True if playerChar win the game in some ways.
|
||||
*/
|
||||
public boolean didWin(char playerChar) {
|
||||
return wonByRow(playerChar) || wonByCol(playerChar) || wonByDiagonal(playerChar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the tie condition of the game. Tie happens when nobody wins and there's
|
||||
* no move left.
|
||||
*
|
||||
* @return True if it's a tie and false otherwise.
|
||||
*/
|
||||
public boolean isTied() {
|
||||
// Tie condition: Out of moves AND nobody wins.
|
||||
return (moveMaked == ROWS_NUM * COLS_NUM && !(didWin(computerChar) || didWin(humanChar)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts the computer to make a move.
|
||||
*
|
||||
* @param c The character token that will belong to the move.
|
||||
*/
|
||||
public void computerMove(char c, boolean max) {
|
||||
boolean foundAMove = false;
|
||||
while (!foundAMove) {
|
||||
int colMove = computerPlayer.getMove(this, max);
|
||||
if (colMove == -1)
|
||||
return;
|
||||
for (int i = 0; i < COLS_NUM; i++) {
|
||||
if (board[colMove][i] == blankChar) {
|
||||
board[colMove][i] = c;
|
||||
if (c == this.computerChar)
|
||||
moveMsg = new Connect4MoveMessage(board[0].length - i - 1, colMove , 2);
|
||||
else
|
||||
moveMsg = new Connect4MoveMessage(board[0].length - i - 1, colMove, 1);
|
||||
foundAMove = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
setChanged();
|
||||
notifyObservers(moveMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return A 2d characters array that represents the board.
|
||||
*/
|
||||
public char[][] getBoard() {
|
||||
return this.board;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return True if the game is already over.
|
||||
*/
|
||||
public boolean isOver() {
|
||||
return this.didWin(this.getComputerChar()) || this.didWin(this.getHumanChar()) || this.isTied();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A move message that is used to update the observers.
|
||||
*/
|
||||
public Connect4MoveMessage getMoveMsg() {
|
||||
return this.moveMsg;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public void printRawBoard() {
|
||||
for (int i = 0; i < board.length; i++) {
|
||||
for (int j = 0; j < board[i].length; j++) {
|
||||
System.out.print(board[i][j] + " ");
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the reference of the character array board to this.board. Its only
|
||||
* purpose is to self assign a given board to model's subclass, in which
|
||||
* this.board is not visible.
|
||||
*
|
||||
* @param board A character array board.
|
||||
*/
|
||||
public void setBoard(char[][] board) {
|
||||
this.board = board;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the move made for this object. Similar to setBoard(), only to do a deep
|
||||
* copy of model's subclass object. It shouldn't be called in other cases.
|
||||
*
|
||||
* @param m The number of moves that should be assigned to this instance's move.
|
||||
*/
|
||||
public void setMoveMaked(int m) {
|
||||
this.moveMaked = m;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The number of moves that have been made in this model.
|
||||
*/
|
||||
public int getMoveMaked() {
|
||||
return moveMaked;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package model;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
|
||||
/**
|
||||
* This class encapsulates the rows, columns, and color information of the move.
|
||||
* An instance of this class is created and sent by model to update the view.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
public class Connect4MoveMessage implements Serializable {
|
||||
public static int YELLOW = 1;
|
||||
public static int RED = 2;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private int row;
|
||||
private int col;
|
||||
private int color;
|
||||
|
||||
/**
|
||||
* Constructs a connect4 message object with the given parameters.
|
||||
*
|
||||
* @param row An integer for the row.
|
||||
* @param col An integer for the column.
|
||||
* @param color an integer 1 or 2 representing colors.
|
||||
*/
|
||||
public Connect4MoveMessage(int row, int col, int color) {
|
||||
this.row = row;
|
||||
this.col = col;
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return The row number of this move message.
|
||||
*/
|
||||
public int getRow() { return row; }
|
||||
|
||||
/**
|
||||
* @return The column number of this move message.
|
||||
*/
|
||||
public int getColumn() { return col; }
|
||||
|
||||
/**
|
||||
* @return The color number either 1 or 2 for this move message.
|
||||
*/
|
||||
public int getColor() { return color; }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package model;
|
||||
|
||||
public class IllegalArgumentException extends Exception {
|
||||
public IllegalArgumentException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
package model;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This class implements the minimax alpha-beta pruning algorithm.
|
||||
*
|
||||
* @author Minh Bui
|
||||
*
|
||||
*/
|
||||
|
||||
public class Minimax_AlphaBeta implements ComputerPlayer {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = -8226404171733596155L;
|
||||
/**
|
||||
* Specify the maximum depth for the search tree.
|
||||
*/
|
||||
private final int init_depth = 5;
|
||||
|
||||
/**
|
||||
* Comment: Since minimax looks at all of the nodes in the search tree, we need
|
||||
* a class that is similar to the game model.
|
||||
*
|
||||
* It's quite inefficient since we have to create many instances of this class
|
||||
* for each tree node.
|
||||
*
|
||||
* @author Minh Bui
|
||||
*/
|
||||
private class GameState extends Connect4Model {
|
||||
|
||||
/**
|
||||
* Java generated serialize UID. Since GameState extends Connect4Model, it's
|
||||
* just here for the sake of surpressing warning.
|
||||
*/
|
||||
private static final long serialVersionUID = 2L;
|
||||
|
||||
/**
|
||||
* Create a GameState object given a game model.
|
||||
*
|
||||
* @param gameModel
|
||||
* The given game model.
|
||||
*/
|
||||
public GameState(Connect4Model gameModel) {
|
||||
// Create a deep copy of the board array.
|
||||
char[][] board = new char[gameModel.getMaxRow()][gameModel.getMaxCol()];
|
||||
char[][] orig_board = gameModel.getBoard();
|
||||
for (int i = 0; i < board.length; i++) {
|
||||
board[i] = Arrays.copyOf(orig_board[i], orig_board[i].length);
|
||||
}
|
||||
|
||||
// Use setObjectAt instead of setBoard because it's a bad design.
|
||||
this.setBoard(board);
|
||||
this.setHumanTurn(gameModel.isHumanMove());
|
||||
this.setMoveMaked(gameModel.getMoveMaked());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a GameState object given the crucial information for a game state.
|
||||
* Used to make a deep copy of another game state.
|
||||
*
|
||||
* @param board
|
||||
* The board that models the game connect4.
|
||||
* @param isHumanTurn
|
||||
* True if it's currently the human's turn in the given state.
|
||||
* @param m
|
||||
* The number of moves made in the given state.
|
||||
*/
|
||||
public GameState(char[][] board, boolean isHumanTurn, int m) {
|
||||
// Create a deep copy of the board array.
|
||||
char[][] copy_board = new char[board.length][board[0].length];
|
||||
for (int i = 0; i < board.length; i++)
|
||||
copy_board[i] = Arrays.copyOf(board[i], board[0].length);
|
||||
this.setBoard(copy_board);
|
||||
this.setHumanTurn(isHumanTurn);
|
||||
this.setMoveMaked(m);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Returns a list of possible moves for this game state.
|
||||
*/
|
||||
private List<Integer> getLegalMoves() {
|
||||
|
||||
List<Integer> availableMoves = new ArrayList<>();
|
||||
if (this.isOver())
|
||||
return availableMoves;
|
||||
for (int i = 0; i < this.getBoard().length; i++) {
|
||||
if (this.getBoard()[i][this.getBoard()[0].length - 1] == this.getBlankChar()) {
|
||||
availableMoves.add(i);
|
||||
}
|
||||
}
|
||||
return availableMoves;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a game model object, use alpha-beta pruning minimax algorithm to
|
||||
* calculate and return the next optimal move.
|
||||
*
|
||||
* @param gameModel
|
||||
* The given game model object.
|
||||
* @param max
|
||||
* True if the agent is playing to maximize its utility and false in
|
||||
* the case of minimizing utility.
|
||||
*/
|
||||
@Override
|
||||
public int getMove(Connect4Model gameModel, boolean max) {
|
||||
GameState currentState = new GameState(gameModel);
|
||||
|
||||
List<Integer> legalMoves = currentState.getLegalMoves();
|
||||
|
||||
if (legalMoves.isEmpty())
|
||||
return -1;
|
||||
int bestMove = 0;
|
||||
double bestUtil = 0;
|
||||
if (max) {
|
||||
// Need to get the first available's move utility to have an initial value to
|
||||
// compare to.
|
||||
bestMove = legalMoves.get(0);
|
||||
GameState nextState1 = getNextState(currentState, bestMove);
|
||||
bestUtil = minValue(init_depth, nextState1, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
|
||||
|
||||
// Get the move with the minimum utility value.
|
||||
for (int i = 1; i < legalMoves.size(); i++) {
|
||||
double thisMoveUtil = minValue(init_depth, getNextState(currentState, legalMoves.get(i)),
|
||||
Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
|
||||
if (bestUtil < thisMoveUtil) {
|
||||
bestUtil = thisMoveUtil;
|
||||
bestMove = legalMoves.get(i);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Need to get the first available's move utility to have an initial value to
|
||||
// compare to.
|
||||
bestMove = legalMoves.get(0);
|
||||
GameState nextState1 = getNextState(currentState, bestMove);
|
||||
bestUtil = maxValue(init_depth, nextState1, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
|
||||
|
||||
// Get the move with the maximum utility value.
|
||||
for (int i = 1; i < legalMoves.size(); i++) {
|
||||
double thisMoveUtil = maxValue(init_depth, getNextState(currentState, legalMoves.get(i)),
|
||||
Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
|
||||
if (bestUtil > thisMoveUtil) {
|
||||
bestUtil = thisMoveUtil;
|
||||
bestMove = legalMoves.get(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
System.out.println("Computer bestmove: " + bestMove);
|
||||
System.out.println("Utility: " + bestUtil);
|
||||
return bestMove;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the max value for the current state. (State or node for the human
|
||||
* player)
|
||||
*
|
||||
* @param depth
|
||||
* The current depth of the search tree.
|
||||
* @param state
|
||||
* The current game state that needs to a minimax value.
|
||||
* @param alpha
|
||||
* The value of the best choice we have found so far in the MAX path.
|
||||
* @param beta
|
||||
* The value of the best choice we have found so far in the MIN path.
|
||||
* @return The max value for the current state.
|
||||
*/
|
||||
private double maxValue(int depth, GameState state, double alpha, double beta) {
|
||||
if (depth == 0 || state.isOver())
|
||||
return utilityOf(state);
|
||||
double v = Double.NEGATIVE_INFINITY;
|
||||
for (int move : state.getLegalMoves()) {
|
||||
v = Math.max(v, minValue(depth - 1, getNextState(state, move), alpha, beta));
|
||||
if (v >= beta)
|
||||
return v;
|
||||
alpha = Math.max(alpha, v);
|
||||
}
|
||||
/*
|
||||
* state.printRawBoard(); System.out.println(v);
|
||||
*/
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the min value for the current state. (State or node for the
|
||||
* computer player)
|
||||
*
|
||||
* @param depth
|
||||
* The current depth of the search tree.
|
||||
* @param state
|
||||
* The current game state that needs to a minimax value.
|
||||
* @param alpha
|
||||
* The value of the best choice we have found so far in the MAX path.
|
||||
* @param beta
|
||||
* The value of the best choice we have found so far in the MIN path.
|
||||
* @return The min value for the current state.
|
||||
*/
|
||||
private double minValue(int depth, GameState state, double alpha, double beta) {
|
||||
if (depth == 0 || state.isOver())
|
||||
return utilityOf(state);
|
||||
double v = Double.POSITIVE_INFINITY;
|
||||
for (int move : state.getLegalMoves()) {
|
||||
v = Math.min(v, maxValue(depth - 1, getNextState(state, move), alpha, beta));
|
||||
if (v <= alpha)
|
||||
return v;
|
||||
beta = Math.min(beta, v);
|
||||
}
|
||||
/*
|
||||
* System.out.println("In min value"); state.printRawBoard();
|
||||
* System.out.println(v);
|
||||
*/
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a move and the current game state, generate the next game state.
|
||||
*
|
||||
* @param curState
|
||||
* the current game state
|
||||
* @param move
|
||||
* The move that will be made in the next state.
|
||||
* @return The next state with the move.
|
||||
*/
|
||||
private GameState getNextState(GameState curState, int move) {
|
||||
GameState nextState = new GameState(curState.getBoard(), curState.isHumanMove(), curState.getMoveMaked());
|
||||
// curState.printRawBoard();
|
||||
try {
|
||||
char c = '\0';
|
||||
if (curState.isHumanMove())
|
||||
c = curState.getHumanChar();
|
||||
else
|
||||
c = curState.getComputerChar();
|
||||
for (int i = 0; i < nextState.getMaxCol(); i++) {
|
||||
if (curState.isBlank(move, i)) {
|
||||
nextState.setObjectAt(move, i, c);
|
||||
nextState.switchTurn();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
/*
|
||||
* System.out.println(""); nextState.printRawBoard(); System.out.println("");
|
||||
*/
|
||||
return nextState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function assigning the utility values to terminal states of the game.
|
||||
* Need a better evaluation function for non-terminal state. Right now we are
|
||||
* relying on randomness.
|
||||
*
|
||||
* @param curState
|
||||
* @return A number indicating the utility (score) for the given state.
|
||||
*/
|
||||
private double utilityOf(GameState curState) {
|
||||
if (curState.isTied())
|
||||
return 0;
|
||||
else if (curState.didWin(curState.getHumanChar()))
|
||||
return 10;
|
||||
else if (curState.didWin(curState.getComputerChar()))
|
||||
return -10;
|
||||
else {
|
||||
// return Math.random() * -10 + Math.random() * 10;
|
||||
double score = countConsecutives(curState, curState.getHumanChar(), 2)
|
||||
+ countConsecutives(curState, curState.getHumanChar(), 3) * 2
|
||||
- countConsecutives(curState, curState.getComputerChar(), 2)
|
||||
- countConsecutives(curState, curState.getComputerChar(), 3) * 2;
|
||||
if (score == 0)
|
||||
return Math.random() * -10 + Math.random() * 10;
|
||||
else
|
||||
return score;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan the board and count for n consecutive character c that is right before a
|
||||
* a blank character. Only does so partially for the sake of improving the evaluation
|
||||
* function.
|
||||
*
|
||||
* @param s the current game state
|
||||
* @param c the character that represent the player
|
||||
* @param n the number of consecutive characters.
|
||||
* @return
|
||||
*/
|
||||
private int countConsecutives(GameState s, char c, int n) {
|
||||
int count = 0;
|
||||
// Count n characters in columns.
|
||||
try {
|
||||
for (int i = 0; i < s.getMaxRow(); i++) {
|
||||
int colSum = 0;
|
||||
for (int j = 0; j < s.getMaxCol(); j++) {
|
||||
if (colSum == n) {
|
||||
if (s.getObjectAt(i, j) == s.getBlankChar())
|
||||
count++;
|
||||
}
|
||||
if (s.getObjectAt(i, j) == c) {
|
||||
colSum++;
|
||||
|
||||
} else
|
||||
colSum = 0;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Count n characters in rows.
|
||||
for (int i = 0; i < s.getMaxCol(); i++) {
|
||||
int rowSum = 0;
|
||||
for (int j = 0; j < s.getMaxRow(); j++) {
|
||||
if (rowSum == n) {
|
||||
|
||||
if (s.getObjectAt(j, i) == s.getBlankChar()) {
|
||||
count++;
|
||||
}
|
||||
if (s.getObjectAt(j, i) == c) {
|
||||
rowSum++;
|
||||
} else
|
||||
rowSum = 0;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
int sum = 0;
|
||||
// Count n characters diagonally.// Covering from left to right.
|
||||
for (int i = 3; i < s.getMaxRow(); i++) {
|
||||
sum = 0;
|
||||
for (int j = 0; j < s.getMaxCol() && j <= i; j++) {
|
||||
if (sum == n) {
|
||||
if (s.getObjectAt(i - j, j) == s.getBlankChar())
|
||||
count++;
|
||||
}
|
||||
if (s.getObjectAt(i - j, j) == c) {
|
||||
sum++;
|
||||
} else {
|
||||
sum = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int j = 1; j < 3; j++) {
|
||||
sum = 0;
|
||||
for (int k = 0; j + k < s.getMaxCol(); k++) {
|
||||
if (sum == n) {
|
||||
if (s.getObjectAt(s.getMaxRow() - 1 - k, j + k) == s.getBlankChar())
|
||||
count++;
|
||||
}
|
||||
if (s.getObjectAt(s.getMaxRow() - 1 - k, j + k) == c) {
|
||||
sum++;
|
||||
|
||||
} else
|
||||
sum = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Covering from right to left.
|
||||
for (int i = 3; i >= 0; i--) {
|
||||
sum = 0;
|
||||
for (int j = 0; j < s.getMaxCol() && (i + j) < s.getMaxRow(); j++) {
|
||||
if (sum == n) {
|
||||
if (s.getObjectAt(i + j, j) == s.getBlankChar())
|
||||
count++;
|
||||
}
|
||||
if (s.getObjectAt(i + j, j) == c) {
|
||||
sum++;
|
||||
} else {
|
||||
sum = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int j = 1; j < 3; j++) {
|
||||
sum = 0;
|
||||
for (int k = 0; j + k < s.getMaxCol(); k++) {
|
||||
if (sum == n) {
|
||||
if (s.getObjectAt(0 + k, j + k) == s.getBlankChar())
|
||||
count++;
|
||||
}
|
||||
if (s.getObjectAt(0 + k, j + k) == c) {
|
||||
sum++;
|
||||
|
||||
} else
|
||||
sum = 0;
|
||||
}
|
||||
}
|
||||
} catch (model.IllegalArgumentException e) {
|
||||
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package model;
|
||||
|
||||
/**
|
||||
* The class implements a random strategy for connect4 game.
|
||||
*/
|
||||
|
||||
import model.Connect4Model;
|
||||
import java.util.Random;
|
||||
|
||||
public class RandomAI implements ComputerPlayer {
|
||||
public int getMove(Connect4Model gameModel, boolean max) {
|
||||
Random randomGenerator = new Random();
|
||||
return randomGenerator.nextInt(gameModel.getMaxRow());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user