In my newminimax499 method, I have a minimax algorithm that uses memoization and alpha beta cropping. The method works fine for 3x3 games, however, when I play 4x4 games, I get strange unexpected choices for the computer. He never loses anyway, but he doesn't seem to play to win. To illustrate the problem, here is a scenario of 2 games in 3x3 and 4x4. First, here is the script from the 3x3 game, where player X makes the first move: 
This is not bad, in fact it is what you would expect from a computer. Now consider the script from the 4x4 game. Again, this is the computer and X starts: 
As you can see, the computer simply places Os in a systematic order one after another and only violates this order in order to block X when it has a potential victory. This is a very defensive game, unlike what was seen in the 3x3 game. So why does the method behave differently for 3x3 and 4x4?
Here is the code:
//This method returns a 2 element int array containing the position of the best possible //next move and the score it yields. Utilizes memoization and alpha beta //pruning to achieve better performance. public int[] newminimax499(int a, int b){ //int bestScore = (turn == 'O') ? +9 : -9; //X is minimizer, O is maximizer int bestPos=-1; int alpha= a; int beta= b; int currentScore; //boardShow(); String stateString = ""; for (int i=0; i<state.length; i++) stateString += state[i]; int[] oldAnswer = oldAnswers.get(stateString); if (oldAnswer != null) return oldAnswer; if(isGameOver()!='N'){ int[] answer = {score(), bestPos}; oldAnswers.put (stateString, answer); return answer; } else{ for(int x:getAvailableMoves()){ if(turn=='X'){ //X is minimizer setX(x); //System.out.println(stateID++); currentScore = newminimax499(alpha, beta)[0]; revert(x); if(currentScore<beta){ beta=currentScore; bestPos=x; } if(alpha>=beta){ break; } } else { //O is maximizer setO(x); //System.out.println(stateID++); currentScore = newminimax499(alpha, beta)[0]; revert(x); if(currentScore>alpha){ alpha=currentScore; bestPos=x; } if(alpha>=beta){ break; } } } } if(turn=='X'){ int[] answer = {beta, bestPos}; oldAnswers.put (stateString, answer); return answer; } else { int[] answer = {alpha, bestPos}; oldAnswers.put (stateString, answer); return answer; } }
Below are other components and additional methods necessary for you to use the code. Fields and constructor used in my State2 class:
private char [] state; //Actual content of the board private char turn; //Whose turn it is private Map<String,int[]> oldAnswers; //Used for memoization. It saves every state along with the score it yielded which allows us to stop exploring the children of a certain node if a similar node score has been previously calculated. The key is the board state(ie OX------X for example), the int array is a 2 element array containing the score and position of last placed seed of the state. private Map<Integer, int []> RowCol; //A mapping of positions from a board represented as a normal array to a board represented as a 2d array. For example: The position 0 maps to 0,0 on a 2d array board, 1 maps to 0,1 and so on. private static int n; //Size of the board private static int stateID; //An simple incrementer used to show number of recursive calls in the newminiax49 method. private static int countX, countO; //Number of placed Xs and Os private static int lastAdded; //Position of last placed seed private char [][] DDState; //A 2d array representing the board. Contains the same values as state[]. Used for simplicity in functions that check the state of the board. public State2(int n){ int a=0; State2.n=n; state=new char[n*n]; RowCol=new HashMap<Integer, int []>(); countX=0; countO=0; //Initializing the board with empty slots for(int i = 0; i<state.length; i++){ state[i]='-'; } //Mapping for(int i=0; i<n; i++){ for(int j=0; j<n; j++){ RowCol.put(a, new int[]{i, j}); a++; } } a=0; DDState=new char[n][n]; //Initializing the 2d array with the values from state[](empty slots) for(int i=0; i<n; i++){ for(int j=0; j<n; j++){ DDState[i][j]=state[a]; a++; } } oldAnswers = new HashMap<String,int[]>(); }
Additional methods:
getAvailableMoves returns an array with empty slots on the board (i.e. possible next steps).
public int[] getAvailableMoves(){ int count=0; int i=0; for(int j=0; j<state.length; j++){ if(state[j]=='-') count++; } int [] availableSlots = new int[count]; for(int j=0; j<state.length; j++){ if(state[j]=='-') availableSlots[i++]=j; } return availableSlots; }
isGameOver2 (), just checks the current state of the board to see if the game is over. returns char 'X', 'O', 'D' and 'N', which mean X won, O won, Draw and Not gameover respectively.
public char isGameOver2(){ char turnOpp; int count; if(turn=='X'){ count=countO; turnOpp='O'; } else { count=countX; turnOpp='X'; } if(count>=n){ for(int i=0; i<n; i++){ if(DDState[i][RowCol.get(lastAdded)[1]]!=turnOpp) break; if(i==(n-1)){ return turnOpp; } } //Check row for win for(int i=0; i<n; i++){ if(DDState[RowCol.get(lastAdded)[0]][i]!=turnOpp) break; if(i==(n-1)){ return turnOpp; } } //Check diagonal for win if(RowCol.get(lastAdded)[0] == RowCol.get(lastAdded)[1]){ //we're on a diagonal for(int i = 0; i < n; i++){ if(DDState[i][i] != turnOpp) break; if(i == n-1){ return turnOpp; } } } //check anti diagonal for(int i = 0; i<n; i++){ if(DDState[i][(n-1)-i] != turnOpp) break; if(i == n-1){ return turnOpp; } } //check for draw if((countX+countO)==(n*n)) return 'D'; } return 'N'; }
boardShow returns a matrix display of the current state of the board:
public void boardShow(){ if(n==3){ System.out.println(stateID); for(int i=0; i<=6;i+=3) System.out.println("["+state[i]+"]"+" ["+state[i+1]+"]"+" ["+state[i+2]+"]"); System.out.println("***********"); } else { System.out.println(stateID); for(int i=0; i<=12;i+=4) System.out.println("["+state[i]+"]"+" ["+state[i+1]+"]"+" ["+state[i+2]+"]"+" ["+state[i+3]+"]"); System.out.println("***********"); } }
this is a simple scoring function that returns +10 for winning O, -10 for winning X and 0 for a tie:
public int score(){ if(isGameOver2()=='X') return -10; else if(isGameOver2()=='O') return +10; else return 0; }
Seeders:
//Sets an X at a certain location and updates the turn, countX and lastAdded variables public void setX(int i){ state[i]='X'; DDState[RowCol.get(i)[0]][RowCol.get(i)[1]]='X'; turn='O'; countX++; lastAdded=i; } //Sets an O at a certain location and updates the turn, countO and lastAdded variables public void setO(int i){ state[i]='O'; DDState[RowCol.get(i)[0]][RowCol.get(i)[1]]='O'; turn='X'; countO++; lastAdded=i; }
Revert, just returns the move. For example, if X was placed at position 0, it returns (0) sets β-β in it and updates the variables changed with setX:
public void revert(int i){ state[i]='-'; DDState[RowCol.get(i)[0]][RowCol.get(i)[1]]='-'; if(turn=='X'){ turn = 'O'; countO--; } else { turn = 'X'; countX--; } }
What the main method might look like:
public static void main(String[] args) { State2 s=new State2(4); int [] results=new int[2]; s.setX(0); long startTime = System.currentTimeMillis(); results=s.newminimax499(Integer.MIN_VALUE,Integer.MAX_VALUE); long endTime = System.currentTimeMillis(); System.out.println("Score: "+results[0]+" Position: "+ results[1]); System.out.println("Run time: " + (endTime-startTime)); s.boardShow(); }