I'm trying to write a connect four game with AI using the minimax algorithm and alpha-beta pruning. I know that the heuristic works OK (at the very least it scores 4 in the row very highly), but for some reason my minimax isn't working.
It seems like the problem is that the current score and the minimax score are always equal, when this obviously should not be the case.
The computer frequently makes stupid decisions like going to a random spot when the human is about to get 4 in a row.
Any and all help would be greatly appreciate.
public class AI {
public Move minimax(Board b, int depth, int alpha, int beta, String player, Move move) {
if(depth == 0 || b.boardScore >= 10000) {
return move;
}
else if(player.equals("computer")) {
Move maxMove = new Move(100,100);
maxMove.moveScore = -10000000;
//Do this for every possible column
for(int x = 0; x < 7; x++) {
//If column is filled, move on.
while(b.findYSpot(x) == -1) {
x++;
if(x > 6) {
x = 0;
}
}
Move m = new Move(b.findYSpot(x), x); // Create the move
b.putPiece(player, m.xMove); // Put the piece down
m.moveScore = b.boardScore;//Set the score the move
Move miniMaxMove = minimax(b, depth-1, alpha, beta, "human", m);
// Maximize the score
if (m.moveScore >= miniMaxMove.moveScore) {
if(m.moveScore >= maxMove.moveScore) {
maxMove = m;
}
}
else {
if(miniMaxMove.moveScore >= maxMove.moveScore) {
maxMove = miniMaxMove;
}
}
b.removePiece(m); //Remove the piece
//Adjust for alpha-beta pruning
alpha = Math.max(alpha, maxMove.moveScore);
if (alpha >= beta) {
break;
}
}
return maxMove;
}
else {
Move minMove = new Move(100, 100);
minMove.moveScore = 10000000;
//Do this for every possible column
for(int x = 0; x < 7; x++) {
// If column is filled, move on
while(b.findYSpot(x) == -1) {
x++;
if(x>6) {
x = 0;
}
}
Move m = new Move(b.findYSpot(x), x); // Make the move
b.putPiece(player, m.xMove); // Put the piece down
m.moveScore = b.boardScore; //Set the score the move
Move miniMaxMove = minimax(b, depth-1, alpha, beta, "computer", m);
//Minimize the score
if (m.moveScore <= miniMaxMove.moveScore) {
if(m.moveScore <= minMove.moveScore) {
minMove = m;
}
}
else {
if(miniMaxMove.moveScore <= minMove.moveScore) {
minMove = miniMaxMove;
}
}
b.removePiece(m);
//Adjust for alpha-beta pruning
beta = Math.min(beta, minMove.moveScore);
if(alpha >= beta) {
break;
}
}
return minMove;
}
}
}
Related
It exists a 2 Dimensional Array for a field of (x,y) length, here for instance 9x6. What I need to do here is to check how many free fields are around the Orange and Red Star. The black (filled) fields represent the occupied fields. In this example for instance I have 7 free fields for Orange, 1 for Red. I know that I can loop through each field and see whether one field is occupied or not, but how could I loop through so that I know that these non-occupied fields are next to the Star or in the Radius of the Star of non-occupied fields? I hope I could elaborate my question well.
Field[][] fields = new Field[9][6];
private void checkEmptyFields(Star star) {
for (int i = 0; i < 9; i++){ // Hardcoded size as an example
for (int j = 0; i < 6; i++) {
if(fields[i][j].isOccupied())
{
//It is occupied, but what now?
}
}
}
}
isOccupied Function:
public boolean isOccupied(){
return occupied;
}
I expect the output to be in this example Orange: 7, Red: 1 (because Red is blocked by the Orange Star and the occupied boxes)
This seems like a problem where breadth-first-search is the appropriate algorithm to use here. Breadth-first-search, or BFS, is when you visit all of a node's, or in this case fields', neighbors first. In your case, "visiting", will just mean checking if it's occupied or not. If it the neighboring field is not occupied and hasn't been visited before, then you can search that field and it's neighbors. The order in which you search is determined by using a Queue-like data structure like so,
private void checkEmptyFields(Star star) {
boolean visited[9][6] = new visited[9][6];
//get the star's coordinates somehow, you may have to change this
int i = star.row;
int j = star.col;
visited[i][j] = true;
int freeFieldCount = 0;
Queue<Field> q = new LinkedList<Field>();
q.add(fields[i][j]);
while(!q.isEmpty()) {
Field current = q.poll();
//get the coordinates from the field, you may have to change this
i = current.row;
j = current.col;
int rowUpperLimit = i + 1;
int rowLowerLimit = i - 1;
int colUpperLimit = j + 1;
int colLowerLimit = j - 1;
if(rowUpperLimit >= 9) {
rowUpperLimit = 8;
}
if(rowLowerLimit < 0) {
rowLowerLimit = 0;
}
if(colUpperLimit >= 6) {
colUpperLimit = 5;
}
if(colLowerLimit < 0) {
colUpperLimit = 0;
}
//check immediate neighbors
for(int m = rowLowerLimit; m <= rowUpperLimit; m++) {
for(int n = colLowerLimit; n <= colUpperLimit; n++) {
if((m != i && n != j) && !visited[m][n] && !fields[m][n].isOccupied()) {
freeFieldCount++;
visited[m][n] = true;
q.add(fields[m][n]);
}
}
}
}
return freeFieldCount;
}
As user #juvian mentioned, this is an 8-neighbor approach. If you want to do a 4-neighbor approach, simply visit only the neighbors immediately to the left, right, above, or below the current field. You can modify the while loop like so,
while(!q.isEmpty()) {
Field current = q.poll();
//get the coordinates from the field, you may have to change this
i = current.row;
j = current.col;
int rowUpperLimit = i + 1;
int rowLowerLimit = i - 1;
int colUpperLimit = j + 1;
int colLowerLimit = j - 1;
if(colLowerLimit > -1) {
//check neighbor to the left
if(!visited[i][colLowerLimit] && !fields[i][colLowerLimit].isOccupied()) {
freeFieldCount++;
visited[i][colLowerLimit] = true;
q.add(fields[i][colLowerLimit]);
}
}
if(colUpperLimit < 6) {
//check neighbor to the right
if(!visited[i][colUpperLimit] && !fields[i][colUpperLimit].isOccupied()) {
freeFieldCount++;
visited[i][colUpperLimit] = true;
q.add(fields[i][colUpperLimit]);
}
}
if(rowLowerLimit > -1) {
//check neighbor below
if(!visited[rowLowerLimit][j] && !fields[rowLowerLimit][j].isOccupied()) {
freeFieldCount++;
visited[rowLowerLimit][j] = true;
q.add(fields[rowLowerLimit][j]);
}
}
if(rowUpperLimit < 9) {
//check neighbor above
if(!visited[rowUpperLimit][j] && !fields[rowUpperLimit][j].isOccupied()) {
freeFieldCount++;
visited[rowUpperLimit][j] = true;
q.add(fields[rowUpperLimit][j]);
}
}
}
}
I'm facing a problem and I'd like to know some ideas on how to solve it. I have a matrix of two dimensions and given a point on that matrix I need to look for ten cells up, down right, left and with diagonal movements (see the following image).
What I'm doing is selecting the values of i and j as the values to be multiplied by the position coordinates and give the direction that I want in each case. For example:
Diagonal A : i=1 j=-1 since i increases and j decreases
Diagonal B : i=1 j=1 since both increase
And for the vertical and horizontal movements i and j will take values 1 or 0.
Having these two values I can take the center and add a value to it starting this value to 1 and increasing it until I get to the limit (10). If the center is (2,4) and I'm in diagonal A I will add the value that is starting in one multiplied by i and j for each coordinate having the following results:
(3,3) (4,2) (5,1) (6,0)
Right now I am interested in computing i and j so that they take all the needed values for the diagonals and the axis. My java code that goes inside a loop is the following:
GS.r++;
if (GS.r > 10) {
GS.r = 0;
if (GS.iteration) {
int i = 0, j = 0;
if (GS.i == 1) {
i = -1;
} else if (GS.i == -1) {
i = 0;
j = 1;
}
if (GS.j == 1) {
j = -1;
} else if (GS.j == -1) {
j = 0;
i = 1;
}
GS.i = i;
GS.j = j;
if (GS.i == 1 && GS.j == 0) {
GS.iteration = false;
GS.i = -1;
GS.j = -1;
}
} else {
if (GS.i == 1 && GS.j == 1) {
GS.iteration = true;
GS.i = 1;
GS.j = 0;
} else {
if (GS.i == 1)
GS.j *= -1;
GS.i *= -1;
}
}
}
GS.i and GS.j are initialised as -1. And with this code I get first the diagonals since the values for GS.i and GS.j would be (-1,-1) (1, -1) (-1, 1) (1, 1) and then the axis having these values: (-1, 0) (1, 0) (0, -1) (0, 1).
I was wondering if there is a better way of generating i and j since my code is not that clean.
Thank you!
What I would do is create an enum for all the four directions you can move in that is Diagonal right up,Diagonal right down,Diagonal left up and Diagonal left down.
Sample code :
public enum Direction {
DIAGONAL_RIGHT_UP(1,1),
DIAGONAL_RIGHT_DOWN(1,-1),
DIAGONAL_LEFT_UP(-1,1),
DIAGONAL_LEFT_DOWN(-1,-1);
public int x;
public int y;
private Direction(int xCoordinateChange,int yCoordinateChange) {
x=xCoordinateChange;
y=yCoordinateChange;
}
}
Then use this to traverse.
public class CartesianCoordinate {
private long xCoordinate;
private long yCoordinate;
public CartesianCoordinate(long xCoordinate,long yCoordinate) {
this.xCoordinate=xCoordinate;
this.yCoordinate=yCoordinate;
}
public long getXCoordinate() {
return xCoordinate;
}
public long getYCoordinate() {
return yCoordinate;
}
public void moveCoordinateByStepSize(Direction direction,long stepSize) {
xCoordinate+=direction.x*stepSize;
yCoordinate+=direction.y*stepSize;
}
#Override
public int hashCode() {
int hashCode=0;
hashCode += (int)(xCoordinate-yCoordinate)*31;
hashCode += (int)(yCoordinate+xCoordinate)*17;
return hashCode;
}
#Override
public boolean equals(Object object) {
if(object == null || !(object instanceof CartesianCoordinate)) {
return false;
}
if( this == object) {
return true;
}
CartesianCoordinate cartesianCoordinateObject = (CartesianCoordinate)object;
if(xCoordinate == cartesianCoordinateObject.getXCoordinate() && yCoordinate == cartesianCoordinateObject.getYCoordinate()) {
return true;
}
return false;
}
#Override
public String toString() {
return "["+xCoordinate+","+yCoordinate+"]";
}
public CartesianCoordinate getAClone() {
return new CartesianCoordinate(xCoordinate,yCoordinate);
}
}
Now say you have a point (1,3) as a starting point. what you can do is for 100 iterations in a particular line.
CartesianCoordinate startingPoint = new CartesianCoordinate(1,3);
CartesianCoordinate rightUpDiag = startingPoint.getAClone(),leftUpDiag = startingPoint.getAClone(),rightDownDiag = startingPoint.getAClone(),leftDownDiag = startingPoint.getAClone();
for(int counter = 0 ;counter < 100; counter ++) {
System.out.println(rightUpDiag.moveCoordinateByStepSize(Direction.DIAGONAL_RIGHT_UP,1));
System.out.println(leftUpDiag.moveCoordinateByStepSize(Direction.DIAGONAL_LEFT_UP,1));
System.out.println(rightDownDiag.moveCoordinateByStepSize(Direction.DIAGONAL_RIGHT_DOWN,1));
System.out.println(leftDownDiag.moveCoordinateByStepSize(Direction.DIAGONAL_LEFT_DOWN,1));
}
So, I have been making this Tic Tac Toe program for a while now.
It's a basic Tic Tac Toe game but the game board is scalable. The program is almost finished but one little feature is missing.
I have to make the game end if player gets five or more marks in a row when the game board is larger than 4x4.
F.E. If the game board is 9x9 the game has to end when the player or the computer gets five marks in a row.
(Mark = "O" or "X").
The game now ends when someone gets marks in a row equals to the size of the board (if 9x9 you need 9 marks in a row to win).
I have to implement a feature in the playerHasWon and I've been having a lot of trouble finding out how. I think it's a simple to implement thing but I have not found out how to do it.
Hope my explanation is easy enough to understand. Here's the code:
package tictac;
import java.util.Scanner;
import java.util.Random;
public class Tictac {
public static final int DRAW = 0; // game ends as a draw
public static final int COMPUTER = 1; // computer wins
public static final int PLAYER = 2; // player wins
public static final char PLAYER_MARK = 'X'; // The "X"
public static final char COMPUTER_MARK = 'O'; // The "O"
public static int size; // size of the board
public static String[][] board; // the board itself
public static int score = 0; // game win score
public static Scanner scan = new Scanner(System.in); // scanner
/**
* Builds the board with the integer size and user input.
*
* Displays game win message and switches play turns.
*
* #param args the command line parameters. Not used.
*/
public static void main(String[] args) {
while (true) {
System.out.println("Select board size");
System.out.print("[int]: ");
try {
size = Integer.parseInt(scan.nextLine());
} catch (Exception e) {
System.out.println("You can't do that.");
continue; // after message, give player new try
}
break;
}
int[] move = {};
board = new String[size][size];
setupBoard();
int i = 1;
loop: // creates the loop
while (true) {
if (i % 2 == 1) {
displayBoard();
move = getMove();
} else {
computerTurn();
}
switch (isGameFinished(move)) {
case PLAYER:
System.err.println("YOU WIN!");
displayBoard();
break loop;
case COMPUTER:
System.err.println("COMPUTER WINS!");
displayBoard();
break loop;
case DRAW:
System.err.println("IT'S A DRAW");
displayBoard();
break loop;
}
i++;
}
}
/**
* Checks for game finish.
*
* #param args command line parameters. Not used.
*
* #return DRAW the game ends as draw.
* #return COMPUTER the game ends as computer win.
* #return PLAYERE the game ends as player win.
*/
private static int isGameFinished(int[] move) {
if (isDraw()) {
return DRAW;
} else if (playerHasWon(board, move,
Character.toString(COMPUTER_MARK))) {
return COMPUTER;
} else if (playerHasWon(board, move,
Character.toString(PLAYER_MARK))) {
return PLAYER;
}
return -1; // can't be 0 || 1 || 2
}
/**
* Checks for win for every direction on the board.
*
* #param board the game board.
* #param move move on the board.
* #param playerMark mark on the board "X" or "O".
* #return the game is won.
*/
public static boolean playerHasWon(String[][] board, int[] move,
String playerMark) { //playermark x || o
// horizontal check
for (int i = 0; i < size; i++) {
if (board[i][0].equals(playerMark)) {
int j;
for (j = 1; j < size; j++) {
if (!board[i][j].equals(playerMark)) {
break;
}
}
if (j == size) {
return true;
}
}
}
// vertical check
for (int i = 0; i < size; i++) {
if (board[0][i].equals(playerMark)) {
int j;
for (j = 1; j < size; j++) {
if (!board[j][i].equals(playerMark)) {
break;
}
}
if (j == size) {
return true;
}
}
}
// diagonals check
int i;
for (i = 0; i < size; i++) {
if (!board[i][i].equals(playerMark)) {
break;
}
}
if (i == size) {
return true;
}
for (i = 0; i < size; i++) {
if (!board[i][(size - 1) - i].equals(playerMark)) {
break;
}
}
return i == size;
}
/**
* Checks for draws.
*
* #return if this game is a draw.
*/
public static boolean isDraw() {
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
if (board[i][j] == " ") {
return false;
}
}
}
return true;
}
/**
* Displays the board.
*
*
*/
public static void displayBoard() {
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
System.out.printf("[%s]", board[i][j]);
}
System.out.println();
}
}
/**
* Displays the board.
*
*
*/
public static void setupBoard() {
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
board[i][j] = " ";
}
}
}
/**
* Takes in user input and sends it to isValidPlay.
*
* #return null.
*/
public static int[] getMove() {
Scanner sc = new Scanner(System.in);
System.out.println("Your turn:");
while (true) {
try {
System.out.printf("ROW: [0-%d]: ", size - 1);
int x = Integer.parseInt(sc.nextLine());
System.out.printf("COL: [0-%d]: ", size - 1);
int y = Integer.parseInt(sc.nextLine());
if (isValidPlay(x, y)) {
board[x][y] = "" + PLAYER_MARK;
return new int[]{x, y};
} else { // if input is unallowed
System.out.println("You can't do that");
continue; // after message, give player new try
}
} catch (Exception e) {
System.out.println("You can't do that.");
}
return null;
}
}
/*
* Randomizes computer's turn, where it inputs the mark 'O'.
*
*
*/
public static void computerTurn() {
Random rgen = new Random(); // Random number generator
while (true) {
int x = (int) (Math.random() * size);
int y = (int) (Math.random() * size);
if (isValidPlay(x, y)) {
board[x][y] = "" + COMPUTER_MARK;
break;
}
}
}
/**
* Checks if a move is possible.
*
* #param inX x-move is out of bounds.
* #param inY y-move is out of bounds.
* #return false
*/
public static boolean isValidPlay(int inX, int inY) {
// Play is out of bounds and thus not valid.
if ((inX >= size) || (inY >= size)) {
return false;
}
// Checks if a play have already been made at the location,
// and the location is thus invalid.
return (board[inX][inY] == " ");
}
}
// End of file
Took a quick look, detected the problem and came up with a quick fix:
public static boolean checkDiagonal(String markToLook) {
// how many marks are we looking for in row?
int sizeToWin = Math.min(size, 5);
// running down and right
// don't need to iterate rows that can't be the starting point
// of a winning diagonal formation, thus can exlude some with
// row < (size - (sizeToWin - 1))
for (int row = 0; row < (size - (sizeToWin - 1)); row++) {
for (int col = 0; col < size; col++) {
int countOfMarks = 0;
// down and right
for (int i = row; i < size; i++) {
if (board[i][i] == null ? markToLook == null :
board[i][i].equals(markToLook)) {
countOfMarks++;
if (countOfMarks >= sizeToWin) {
return true;
}
}
}
countOfMarks = 0;
// down and left
for (int i = row; i < size; i++) {
if (board[i][size - 1 - i] == null ? markToLook == null :
board[i][size - 1 - i].equals(markToLook)) {
countOfMarks++;
if (countOfMarks >= sizeToWin) {
return true;
}
}
}
}
}
return false;
}
And call it from your PlayerHasWon method instead of performign the checks there. Basically we iterate each possible starting square on the board for a diagonal winning formation, and run check down+left and down+right for each of the squares.
I am in awful hurry and did not test it much, but will return in couple of hours to improve this solution. Seems to work.
Edit: My previous solution I found lacking in further tests, I've updated the above code to function as desired.
First, I think playerMark should be a char and not a String. That said, let's go for the answer. The "horizontal" case would be:
// This is the number of marks in a row required to win
// Adjust formula if necessary
final int required = size > 4 ? 5 : 3;
for (int i = 0; i < size; i++) {
int currentScore = 0;
for (j = 0; j < size; j++) {
if (board[i][j].equals(playerMark)) {
currentScore++;
if (currentScore >= required)
return true;
}
else {
currentScore = 0;
}
}
}
}
The vertical case would be analogous. The diagonal one is a bit trickier as now it would require board[i][i+k] for the main diagonal and board[i][k-i] for the secondary; and it may not be obvious which values k and i must traverse. Here's my attempt (variable required as in horizontal case):
Note: everything from here down has been completely rewritten on 2015-12-16. The previous versions didn't work and the algorithm was not explained.
After two failed attempts I decided to do my homework and actually sort things out instead of doing everything in my head thinking I can keep track of all variables. The result is this picture:
Main diagonals are painted in blue, secondary diagonals in green.
Each diagonal is identified by a value of k, with k=0 being always being the longest diagonal of each set. Values of k grow as diagonals move down, so diagonals above the longest one have negative k while diagonals below the longest one have positive k.
Things that hold for both diagonals:
Diagonal contains size-abs(k) elements. Diagonals in which size-abs(k) is less than required need not be searched. This means that, for board size size and required length required, we'll search values of k from required-size to size-required. Notice that these have the same absolute value, with the first being <=0 and the second >=0. These values are both zero only when required==size, i.e. when we need the full diagonal to claim a win, i.e. when we only need to search k=0.
For k<=0, possible values of i (row) go from 0 to size+k. Values greater than or equal to size+k cross the right edge of the board and are thus outside the board.
For k>=0, possible values of i (row) go from k to size. Values below k cross the left edge of the board and are thus outside the board.
Only for main (blue) diagonals:
Value of j (column) is k+i.
Only for secondary (green) diagonals:
Value of j (column) is size-1+k-i. In case this is not obvious, just pick the top right corner (k=0,i=0) and notice j=size-1. Then notice that adding 1 to k (keeping i constant) always moves j by 1 right (it would go out of the board if done from k=0,i=0, just think about the intersection of horizontal line i=0 with diagonal k=1), and adding 1 to i (keeping k constant) always moves j by 1 to the left.
The ressulting code would be:
// Main diagonal
for (int k = required - size; k < size - required; k++)
{
int currentScore = 0;
startI = Math.max (0, k);
endI = Math.min (size, size+k);
for (int i = startI, i < endI; i++)
{
if (board[i][k+i].equals (playerMark))
{
currentScore++;
if (currentScore >= required)
return true;
}
else
currentScore = 0;
}
}
// Secondary diagonal
for (int k = required - size; k < size - required; k++)
{
int currentScore = 0;
startI = Math.max (0, k);
endI = Math.min (size, size+k);
for (int i = startI, i < endI; i++)
{
if (board[i][size-1+k-i].equals (playerMark))
{
currentScore++;
if (currentScore >= required)
return true;
}
else
currentScore = 0;
}
}
At this point, the code is nearly identical in both cases, changing only the j index in board[i][j]. In fact, both loops could be merged, taking care only of keeping two currentScore variables, one for the main (blue) diagonal and the other for the secondary (green) diagonal.
Ok so I've been following this tutorial on Connect4 in java and I've tried to modify it to fit what I already had and to fit into libgdx. After implementing it, i have a few odd problems.
Problem 1: After i make the first move, the computer fills the entire bottom row with his chips and then makes his first move.
Problem 2: The computer isn't displaying any A.I. and simply starts at the first column and first row available and places a chip there. The computer will keep following this pattern.
Problem 3: My winning checker no longer realizes if I have won the game but does realize when the computer has won. When I first designed the game I started by having the computer place chips at random (for testing) and my winning checker worked for the computer and for myself.
The article I followed was here: Connect4 in Java
Here is my code.
ConnectFour.java
package com.comp452.tme31;
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
public class ConnectFour extends ApplicationAdapter implements InputProcessor {
// Create final ints for number of columns and rows in game.
protected final static int COLUMNS = 7;
protected final static int ROWS = 6;
protected final static int TILESIZE = 64;
// Create boolean to determine if player can take a turn.
protected boolean playersTurn = true;
// Create boolean for gameover.
protected boolean gameOver = false;
// Create boolean for winners.
private boolean winner = false;
// Sprite batch for texture drawing.
SpriteBatch batch;
// Create textures to represent board and player pieces.
Texture drawingTile, empty, player, computer;
// Create 2D array to hold game board.
private final static int[][] gameBoard = new int[COLUMNS][ROWS];
public static int[][] getGameBoard() {
return gameBoard;
}
// Create variables to display status message.
BitmapFont mainStatusDisplay;
public static String mainStatusString;
public static String winningString;
// Create and set max depth for tree search
private final int MAX_DEPTH = 4;
// Create win, loss and nothing for zero sum game.
private final float WIN = 1f;
private final float LOSE = -1f;
private final float TIE = 0f;
#Override
public void create () {
batch = new SpriteBatch();
empty = new Texture("empty.jpg");
player = new Texture("player.jpg");
computer = new Texture("computer.jpg");
Gdx.input.setInputProcessor(this);
// Initialize display for status messages.
mainStatusDisplay = new BitmapFont();
mainStatusString = "Player's Turn";
winningString = "";
}
#Override
public void render () {
update();
Gdx.gl.glClearColor(0, 0, 0, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
batch.begin();
drawBoard();
mainStatusDisplay.setColor(Color.YELLOW);
mainStatusDisplay.draw(batch, mainStatusString, 32, 416);
batch.end();
}
public void drawBoard() {
for (int i = 0; i < COLUMNS; i ++) {
for (int j = 0; j < ROWS; j++) {
if (gameBoard[i][j] == 0) {
drawingTile = empty;
}
else if (gameBoard[i][j] == 1) {
drawingTile = player;
}
else if (gameBoard[i][j] == 2) {
drawingTile = computer;
}
batch.draw(drawingTile, i * 64, j * 64);
}
}
}
// Method update handles updates to game logic.
public void update() {
// If it's gameover, end the game.
if (gameOver) {
// Set players turn to true to prevent computer from taking another turn.
playersTurn = true;
// Set status message to winning message;
mainStatusString = winningString;
// Disable input processor to prevent player from taking another turn.
Gdx.input.setInputProcessor(null);
}
// If it's not players turn, call computersTurn.
else if (!playersTurn) {
mainStatusString = "Computer's Turn";
computersTurn();
}
if (checkForWin(1) && checkForWin(2)) {
gameOver = true;
}
}
public void computersTurn() {
double maxScore = 2. * Integer.MIN_VALUE;
int xValue = 0;
// Search the gameboard and find the best move.
for (int x = 0; x < COLUMNS; x++) {
// If x column is a value move...
if (canMove(x)) {
// Set score of move from function.
double score = moveScore(x);
// If score is greater than max score...
if (score > maxScore) {
// Set score to max score and xValue to column.
maxScore = score;
xValue = x;
// If the score is a win, break from loop.
if (score == WIN) {
break;
}
}
}
}
// Set the piece for player at column as x.
setPiece(2, xValue);
// Set players turn and string status.
playersTurn = true;
mainStatusString = "Player's Turn";
}
// Method moveScore determines the value of a move and returns it.
public double moveScore(int xValue) {
// Set the piece in place.
setPiece(2, xValue);
// Get the score and check it's value with alpha beta pruning.
double score = alphaBetaPrune(MAX_DEPTH, Integer.MIN_VALUE, Integer.MAX_VALUE, 1);
// Remove the piece.
takeAwayPiece(xValue);
return score;
}
public double alphaBetaPrune(int depth, double alpha, double beta, int whoPlayed) {
winner = checkForWin(1) || checkForWin(2);
// If we've reached the max depth of the tree or there is a winner...
if (depth == 0 || winner) {
double score;
// If there is a winner...
if (winner) {
// If player is the winner...
if (checkForWin(1)) {
// Set a losing score (Computer does not want player to win).
score = LOSE;
}
// Else this is a win for the computer...
else {
// Set score to a win.
score = WIN;
}
}
// Otherwise there is no winner...
else {
// Set score to TIE (0).
score = TIE;
}
// Return score and remove depth level.
return score / (MAX_DEPTH - depth +1);
}
// If computer is making the move...
if (whoPlayed == 2) {
// Iterate through gameboard.
for (int x = 0; x < COLUMNS; x++) {
// Check and see if next move can be made.
if (canMove(x)) {
// Make move for computer to x.
setPiece(2, x);
// Set alpha equal to return from recursion step minus one depth level.
alpha = Math.max(alpha, alphaBetaPrune(depth - 1, alpha, beta, 1));
// Remove piece.
takeAwayPiece(x);
// Check returned alpha against beta and break from loop if beta is less than alpha.
if (beta <= alpha) {
break;
}
}
}
// We're here if alpha is larger and we didn't break from loop.
return alpha;
}
// Else if player is making move...
else {
// Iterate through gameboard.
for (int x = 0; x < COLUMNS; x++) {
// Check and see if next move can be made.
if (canMove(x)) {
// Make move for player to x.
setPiece(1, x);
// Set beta equal to return from recursion step minus one depth level for beta.
beta = Math.min(beta, alphaBetaPrune(depth - 1, alpha, beta, 2));
// Remove piece.
takeAwayPiece(x);
// Check returned alpha against beta and break from loop if beta is less than alpha.
if (beta <= alpha) {
break;
}
}
}
// We're here if alpha is larger than beta and we didn't break from loop.
return beta;
}
}
// Method setPiece takes two int values as parameters and places a piece on the game board.
public void setPiece(int whoPlayed, int xValue) {
// For loop to iterate through each row.
for (int i = 0; i < ROWS; i++) {
// If row is empty...
if (gameBoard[xValue][i] == 0) {
// Place piece on the board.
gameBoard[xValue][i] = whoPlayed;
break;
}
}
}
// Method takeAwayPiece takes two int values as parameters and removes a piece from the board.
public void takeAwayPiece(int xValue) {
// For loop to iterate through each row.
for (int i = ROWS - 1; i > 0; i--) {
// If row contains a piece..
if (gameBoard[xValue][i] != 0) {
// Remove piece.
gameBoard[xValue][i] = 0;
break;
}
}
}
// Method to determine if a move is valid
public boolean canMove(int xValue) {
// If the top spot in the given column is 0, return true.
return (gameBoard[xValue][ROWS-1] == 0);
}
// Method checkForWin takes a flag and checks to see if that player has won the game.
public boolean checkForWin(int whoPlayed) {
// Create counter to check for 4 in a row.
int win = 0;
// Iterate through gameboard and count pieces in a row.
for (int y = 0; y < ROWS; y++) {
for (int x = 0; x < COLUMNS; x++) {
// If piece is player who is checking, increment counter.
if (gameBoard[x][y] == whoPlayed) {
win++;
}
// Not in a row, set counter to 0.
else {
win = 0;
}
if (win == 4) {
break;
}
}
// If win counter is 4, winner.
if (win == 4) {
winningString = "Horizontal Win for Player " + whoPlayed;
return true;
}
// Else, reset win counter and check next column.
else {
win = 0;
}
}
// Iterate through gameboard and count pieces in a column.
for (int x = 0; x < COLUMNS; x++) {
for (int y = 0; y < ROWS; y++) {
// If piece is player who is checking, increment counter.
if (gameBoard[x][y] == whoPlayed) {
win++;
}
// Not in a row, set counter to 0.
else {
win = 0;
}
if (win == 4) {
break;
}
}
// If win counter is 4, player won.
if (win == 4) {
winningString = "Vertical Win for Player " + whoPlayed;
return true;
}
// Else, reset win counter and check next column.
else {
win = 0;
}
}
// Iterate through gameboard and count pieces in a diagonal row, left to right.
for (int x = 0; x < 3; x++) {
for (int y = 0; y < 2; y++) {
// If piece is player who is checking, check next piece diagonally.
if (gameBoard[x][y] == whoPlayed) {
// Then check next diagonal piece.
if (gameBoard[x+1][y+1] == whoPlayed) {
// Then check next diagonal piece.
if (gameBoard[x+2][y+2] == whoPlayed) {
// Then check last diagonal piece.
if (gameBoard[x+3][y+3] == whoPlayed) {
// Set winning message to player won and set gameover flag.
winningString = "Diagonal Win (LR) for Player " + whoPlayed;
// Exit function.
return true;
}
}
}
}
}
}
// Iterate through gameboard and count pieces in a diagonal row, right to left.
for (int x = 3; x < COLUMNS; x++) {
for (int y = 0; y < 2; y++) {
// If piece is player who is checking, check next piece diagonally.
if (gameBoard[x][y] == whoPlayed) {
// Then check next diagonal piece.
if (gameBoard[x-1][y+1] == whoPlayed) {
// Then check next diagonal piece.
if (gameBoard[x-2][y+2] == whoPlayed) {
// Then check last diagonal piece.
if (gameBoard[x-3][y+3] == whoPlayed) {
// Set winning message to player won and set gameover flag.
winningString = "Diagonal Win (RL) for Player " + whoPlayed;
// Exit function.
return true;
}
}
}
}
}
}
// Iterate through gameboard and if no 0 slots remain, game is a tie.
for (int x = 0; x < COLUMNS; x++) {
for (int y = 0; y < ROWS; y++) {
// If a 0 slot remains, return.
if (gameBoard[x][y] == 0) {
return false;
}
}
}
// If we're here, then there was no winners and no slots left.
winningString = "Tie Game";
return false;
}
#Override
public boolean touchDown(int x, int y, int pointer, int button) {
if (playersTurn) {
if (button == Input.Buttons.LEFT) {
if (canMove(x / TILESIZE)) {
setPiece(1, x / TILESIZE);
playersTurn = false;
return true;
}
}
}
return false;
}
#Override
public boolean touchUp(int i, int i1, int i2, int i3) {
return false;
}
#Override
public boolean touchDragged(int i, int i1, int i2) {
return false;
}
#Override
public boolean mouseMoved(int i, int i1) {
return false;
}
#Override
public boolean scrolled(int i) {
return false;
}
#Override
public boolean keyDown(int i) {
return false;
}
#Override
public boolean keyUp(int i) {
return false;
}
#Override
public boolean keyTyped(char c) {
return false;
}
}
Thanks!
So after the first fix, it was fairly easy to narrow down what the problem was for the other problem. In my takeAwayPiece method, it wasn't removing anything on the bottom row so when the A.I calculated it's next move, its testpieces it was placing at the bottom didn't work.
So in the takeAwayPiece method, i changed it to the following
// Method takeAwayPiece takes two int values as parameters and removes a piece from the board.
public void takeAwayPiece(int xValue) {
// For loop to iterate through each row.
for (int i = ROWS; i > 0; i--) {
System.out.println(i);
// If row contains a piece..
if (gameBoard[xValue][i-1] != 0) {
// Remove piece.
//System.out.println("Piece at column " + xValue + " " + i);
gameBoard[xValue][i-1] = 0;
break;
}
}
}
You might notice that the difference is that i starts = to ROWS instead of ROWS -1 and then i was checking against x-1 instead of x (Previously i just tried changing the condition from i > 0 to i = 0 but that had some crazy strange results).
I'm trying to learn about artificial intelligence and how to implement it in a program. The easiest place to start is probably with simple games (in this case Tic-Tac-Toe) and Game Search Trees (recursive calls; not an actual data structure). I found this very useful video on a lecture about the topic.
The problem I'm having is that the first call to the algorithm is taking an extremely long amount of time (about 15 seconds) to execute. I've placed debugging log outputs throughout the code and it seems like it is calling parts of the algorithm an excessive amount of times.
Here's the method for choosing the best move for the computer:
public Best chooseMove(boolean side, int prevScore, int alpha, int beta){
Best myBest = new Best();
Best reply;
if (prevScore == COMPUTER_WIN || prevScore == HUMAN_WIN || prevScore == DRAW){
myBest.score = prevScore;
return myBest;
}
if (side == COMPUTER){
myBest.score = alpha;
}else{
myBest.score = beta;
}
Log.d(TAG, "Alpha: " + alpha + " Beta: " + beta + " prevScore: " + prevScore);
Move[] moveList = myBest.move.getAllLegalMoves(board);
for (Move m : moveList){
String choice;
if (side == HUMAN){
choice = playerChoice;
}else if (side == COMPUTER && playerChoice.equals("X")){
choice = "O";
}else{
choice = "X";
}
Log.d(TAG, "Current Move: column- " + m.getColumn() + " row- " + m.getRow());
int p = makeMove(m, choice, side);
reply = chooseMove(!side, p, alpha, beta);
undoMove(m);
if ((side == COMPUTER) && (reply.score > myBest.score)){
myBest.move = m;
myBest.score = reply.score;
alpha = reply.score;
}else if((side == HUMAN) && (reply.score < myBest.score)){
myBest.move = m;
myBest.score = reply.score;
beta = reply.score;
}//end of if-else statement
if (alpha >= beta) return myBest;
}//end of for loop
return myBest;
}
Where the makeMove method makes the move if the spot is empty and returns a value (-1 - human win, 0 - draw, 1 - computer win, -2 or 2 - otherwise). Though I believe the error may be in the getAllLegalMoves method:
public Move[] getAllLegalMoves(String[][] grid){
//I'm unsure whether this method really belongs in this class or in the grid class, though, either way it shouldn't matter.
items = 0;
moveList = null;
Move move = new Move();
for (int i = 0; i < 3; i++){
for(int j = 0; j < 3; j++){
Log.d(TAG, "At Column: " + i + " At Row: " + j);
if(grid[i][j] == null || grid[i][j].equals("")){
Log.d(TAG, "Is Empty");
items++;
if(moveList == null || moveList.length < items){
resize();
}//end of second if statement
move.setRow(j);
move.setColumn(i);
moveList[items - 1] = move;
}//end of first if statement
}//end of second loop
}//end of first loop
for (int k = 0; k < moveList.length; k++){
Log.d(TAG, "Count: " + k + " Column: " + moveList[k].getColumn() + " Row: " + moveList[k].getRow());
}
return moveList;
}
private void resize(){
Move[] b = new Move[items];
for (int i = 0; i < items - 1; i++){
b[i] = moveList[i];
}
moveList = b;
}
To sum it all up: What's causing my call, to choose the best move, to take so long? What am I missing? Is there an easier way to implement this algorithm? Any help or suggestions will be greatly appreciated, thanks!
A minimax tree with alpha beta pruning should be visualized as a tree, each node of the tree being a possible move that many turns into the future, and its children being all the moves that can be taken from it.
To be as fast as possible and guarantee you'll only need space linear on number of moves you're looking ahead, you do a depth first search and 'sweep' from one side to another. As in, if you imagine the whole tree being constructed, your program would actually only construct a single strand from lead to root one at a time, and discard any parts of it it is done with.
I'm just going to copy the wikipedia pseudo code at this point because it's really, really succinct and clear:
function alphabeta(node, depth, α, β, Player)
if depth = 0 or node is a terminal node
return score
if Player = MaxPlayer
for each child of node
α := max(α, alphabeta(child, depth-1, α, β, not(Player) ))
if β ≤ α
break (* Beta cut-off *)
return α
else
for each child of node
β := min(β, alphabeta(child, depth-1, α, β, not(Player) ))
if β ≤ α
break (* Alpha cut-off *)
return β
Notes:
-'for each child of node' - Rather than editing the state of the current board, create an entirely new board that is the result of applying the move. By using immutable objects, your code will be less prone to bugs and quicker to reason about in general.
-To use this method, call it for every possible move you can make from the current state, giving it depth -1, -Infinity for alpha and +Infinity for beta, and it should start by being the non-moving player's turn in each of these calls - the one that returns the highest value is the best one to take.
It's very very conceptually simple. If you code it right then you never instantiate more than (depth) boards at once, you never consider pointless branches and so on.
I am not going to profile your code for you, but since this is such a nice coding kata I wrote a small ai for tic tac toe:
import java.math.BigDecimal;
public class Board {
/**
* -1: opponent
* 0: empty
* 1: player
*/
int[][] cells = new int[3][3];
/**
* the best move calculated by eval(), or -1 if no more moves are possible
*/
int bestX, bestY;
int winner() {
// row
for (int y = 0; y < 3; y++) {
if (cells[0][y] == cells[1][y] && cells[1][y] == cells[2][y]) {
if (cells[0][y] != 0) {
return cells[0][y];
}
}
}
// column
for (int x = 0; x < 3; x++) {
if (cells[x][0] == cells[x][1] && cells[x][1] == cells[x][2]) {
if (cells[x][0] != 0) {
return cells[x][0];
}
}
}
// 1st diagonal
if (cells[0][0] == cells[1][1] && cells[1][1] == cells[2][2]) {
if (cells[0][0] != 0) {
return cells[0][0];
}
}
// 2nd diagonal
if (cells[2][0] == cells[1][1] && cells[1][1] == cells[0][2]) {
if (cells[2][0] != 0) {
return cells[2][0];
}
}
return 0; // nobody has won
}
/**
* #return 1 if side wins, 0 for a draw, -1 if opponent wins
*/
int eval(int side) {
int winner = winner();
if (winner != 0) {
return side * winner;
} else {
int bestX = -1;
int bestY = -1;
int bestValue = Integer.MIN_VALUE;
loop:
for (int y = 0; y < 3; y++) {
for (int x = 0; x < 3; x++) {
if (cells[x][y] == 0) {
cells[x][y] = side;
int value = -eval(-side);
cells[x][y] = 0;
if (value > bestValue) {
bestValue = value;
bestX = x;
bestY = y;
if (bestValue == 1) {
// it won't get any better, we might as well stop thinking
break loop;
}
}
}
}
}
this.bestX = bestX;
this.bestY = bestY;
if (bestValue == Integer.MIN_VALUE) {
// there were no moves left, it must be a draw!
return 0;
} else {
return bestValue;
}
}
}
void move(int side) {
eval(side);
if (bestX == -1) {
return;
}
cells[bestX][bestY] = side;
System.out.println(this);
int w = winner();
if (w != 0) {
System.out.println("Game over!");
} else {
move(-side);
}
}
#Override
public String toString() {
StringBuilder sb = new StringBuilder();
char[] c = {'O', ' ', 'X'};
for (int y = 0; y < 3; y++) {
for (int x = 0; x < 3; x++) {
sb.append(c[cells[x][y] + 1]);
}
sb.append('\n');
}
return sb.toString();
}
public static void main(String[] args) {
long start = System.nanoTime();
Board b = new Board();
b.move(1);
long end = System.nanoTime();
System.out.println(new BigDecimal(end - start).movePointLeft(9));
}
}
The astute reader will have noticed I don't use alpha/beta cut-off. Still, on my somewhat dated notebook, this plays through a game in 0.015 seconds ...
Not having profiled your code, I can't say for certain what the problem is. However, you logging each possible move at every node in the search tree might have something to do with it.