Arrays, Problem Solving, Software Development

Problem Solving: Xs and Os / Tic-Tac-Toe (Part 2)

Start a new project, I was using IntelliJ for this. The below is my folder structure.

In your GameTest file make 4 tests.

	@Test
	public void completeGameWithWinner_shouldReturnWinnerX() {

	}

	@Test
	public void completeGameWithNoWinner_shouldReturnNoWinner() {
		
	}

	@Test
	public void incompleteGameWithWinner_shouldReturnWinnerX() {
		
	}

	@Test
	public void incompleteGameWithNoWinnerYet_shouldReturnNoWinnerYet() {

	}

The tests will instantiate a new multidimensional array. Create the arrays as below. Remember from part 1 that Empty is 0, X is 1 and O is 2 when you’re creating the array.

	@Test
	public void completeGameWithWinner_shouldReturnWinnerX() {
		//given
		int[][] completeGameWithWinner = {{1, 1, 1},
				{1, 2, 2},
				{2, 2, 1}};
		//when

		//then

	}

	@Test
	public void completeGameWithNoWinner_shouldReturnNoWinner() {
		//given
		int[][] completeGameWithNoWinner = {{2, 1, 1},
				{1, 1, 2},
				{2, 2, 1}};
		//when
		
		//then

	}

	@Test
	public void incompleteGameWithWinner_shouldReturnWinnerX() {
		//given
		int[][] incompleteGameWithWinner = {{1, 2, 0},
				{0, 1, 2},
				{0, 0, 1}};
		//when
		
		//then

	}

	@Test
	public void incompleteGameWithNoWinnerYet_shouldReturnNoWinnerYet() {
		//given
		int[][] incompleteGameWithNoWinnerYet = {{1, 2, 0},
				{0, 1, 0},
				{0, 2, 0}};
		//when
		
		//then

	}

This array will be a 3 X 3 and will represent a game state. In our code we want to verify this game state. By verify we mean we want to be able to find out if X has won in this game state, if O has won, if there were no winners, if the game isn’t finished. We’re going to abstract the details of the game state to a new Game object. A new game will be instantiated for all the tests here:

public class GameTest {
	private Game game;

Assign game in each test using its multidimensional array.

	@Test
	public void completeGameWithWinner_shouldReturnWinnerX() {
		//given
		int[][] completeGameWithWinner = {{1, 1, 1},
				{1, 2, 2},
				{2, 2, 1}};
		//when
		game = new Game(completeGameWithWinner);
		//then
		
	}

	@Test
	public void completeGameWithNoWinner_shouldReturnNoWinner() {
		//given
		int[][] completeGameWithNoWinner = {{2, 1, 1},
				{1, 1, 2},
				{2, 2, 1}};
		//when
		game = new Game(completeGameWithNoWinner);
		//then
		
	}

	@Test
	public void incompleteGameWithWinner_shouldReturnWinnerX() {
		//given
		int[][] incompleteGameWithWinner = {{1, 2, 0},
				{0, 1, 2},
				{0, 0, 1}};
		//when
		game = new Game(incompleteGameWithWinner);
		//then
		
	}

	@Test
	public void incompleteGameWithNoWinnerYet_shouldReturnNoWinnerYet() {
		//given
		int[][] incompleteGameWithNoWinnerYet = {{1, 2, 0},
				{0, 1, 0},
				{0, 2, 0}};
		//when
		game = new Game(incompleteGameWithNoWinnerYet);
		//then		
	}

Lastly add in assertion statements comparing the message we will expect back when a game state is verified vs what is actually returned:

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class GameTest {
	private Game game;

	@Test
	public void completeGameWithWinner_shouldReturnWinnerX() {
		//given
		int[][] completeGameWithWinner = {{1, 1, 1},
				{1, 2, 2},
				{2, 2, 1}};
		//when
		game = new Game(completeGameWithWinner);
		//then
		Assertions.assertEquals("The Winner is X", game.verifyGameState());
	}

	@Test
	public void completeGameWithNoWinner_shouldReturnNoWinner() {
		//given
		int[][] completeGameWithNoWinner = {{2, 1, 1},
				{1, 1, 2},
				{2, 2, 1}};
		//when
		game = new Game(completeGameWithNoWinner);
		//then
		Assertions.assertEquals("No Winner - Game Complete", game.verifyGameState());
	}

	@Test
	public void incompleteGameWithWinner_shouldReturnWinnerX() {
		//given
		int[][] incompleteGameWithWinner = {{1, 2, 0},
				{0, 1, 2},
				{0, 0, 1}};
		//when
		game = new Game(incompleteGameWithWinner);
		//then
		Assertions.assertEquals("The Winner is X", game.verifyGameState());
	}

	@Test
	public void incompleteGameWithNoWinnerYet_shouldReturnNoWinnerYet() {
		//given
		int[][] incompleteGameWithNoWinnerYet = {{1, 2, 0},
				{0, 1, 0},
				{0, 2, 0}};
		//when
		game = new Game(incompleteGameWithNoWinnerYet);
		//then
		Assertions.assertEquals("No Winner Yet - Game Incomplete", game.verifyGameState());
	}
}

Leave GameTest, we’re going to populate Game now:

In Game, start by declaring fields (I have named them according to their multidimensional coordinates). As these don’t change once declared they are marked final.

public class Game {	
	private final int x0y0;
	private final int x0y1;
	private final int x0y2;
	private final int x1y0;
	private final int x1y1;
	private final int x1y2;
	private final int x2y0;
	private final int x2y1;
	private final int x2y2;
}

As mentioned in part 1 we use ints in place of chars. Declare and use these as static finals to avoid ‘magic numbers’ creeping into your code.

public class Game {
	private static final int EMPTY_VALUE = 0;
	private static final int X = 1;
	private static final int O = 2;	

	private final int x0y0;
	private final int x0y1;
	private final int x0y2;
	private final int x1y0;
	private final int x1y1;
	private final int x1y2;
	private final int x2y0;
	private final int x2y1;
	private final int x2y2;
}

Add in a constructor for Game. This constructor takes a multidimensional array (our game state) in order to instantiate a new Game. In the constructor we have assigned fields of the current object (indicated by this keyword) to equal corresponding coordinates of the multidimensional array used.

public class Game {
	private static final int EMPTY_VALUE = 0;
	private static final int X = 1;
	private static final int O = 2;

	private final int x0y0;
	private final int x0y1;
	private final int x0y2;
	private final int x1y0;
	private final int x1y1;
	private final int x1y2;
	private final int x2y0;
	private final int x2y1;
	private final int x2y2;

	public Game(int[][] newGameDetails) {
		this.x0y0 = newGameDetails[0][0];
		this.x0y1 = newGameDetails[0][1];
		this.x0y2 = newGameDetails[0][2];
		this.x1y0 = newGameDetails[1][0];
		this.x1y1 = newGameDetails[1][1];
		this.x1y2 = newGameDetails[1][2];
		this.x2y0 = newGameDetails[2][0];
		this.x2y1 = newGameDetails[2][1];
		this.x2y2 = newGameDetails[2][2];
	}
}

Below the constructor we’ve just added, we’re now going to add in our main method of this class – verifyGameState(). All of our other Game methods will be private as they do their work inside the Game class, but verifyGameState() will be public so we can call it from outside.

To start off I’m just going to add in a call to setup(), a new method we’ll use to do some work for us.

	public String verifyGameState() {
		setup();
	}

We’ll need to create a new ArrayList that will be accessible to the whole class (and you’ll need the below imports):

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

public class Game {
	private final List<Integer> gameState = new ArrayList<>();

In the new method setup() we are adding to an ArrayList all of the fields of the Game object. This new ArrayList now contains our game state and is therefore appropriately named gameState. I’ve done this because for some of the manipulations later on, an ArrayList is the most flexible way of working with this information.

	private void setup() {
		gameState.add(x0y0);
		gameState.add(x0y1);
		gameState.add(x0y2);
		gameState.add(x1y0);
		gameState.add(x1y1);
		gameState.add(x1y2);
		gameState.add(x2y0);
		gameState.add(x2y1);
		gameState.add(x2y2);
	}

So next in verifyGameState(), before we start looking for Xs or Os, we’re going to have a fail fast check, so if we are passed a board of the wrong size for example, we find this quickly and don’t waste time on it. Add in the If Else statement below:

	public String verifyGameState() {
		setup();
		if(validateGameState()) {
                
		}else return "Invalid Values";
	}	

Now to add our validation. In this example there is only one check by validateBoardSize() and the result is returned by validateGameState(). These are abstracted like this so as to allow you to add several validation checks and then validateGameState() can return an overall result of whether the game state is valid or not. Add the below code after the setup() method:

	private boolean validateGameState() {
		return validateBoardSize();
	}

	private boolean validateBoardSize() {
		return gameState.size() == REQUIRED_BOARD_SIZE;
	}

Add in REQUIRED_BOARD_SIZE as a new static final:

public class Game {
	private final List<Integer> gameState = new ArrayList<>();

	private static final int EMPTY_VALUE = 0;
	private static final int X = 1;
	private static final int O = 2;
	private static final int REQUIRED_BOARD_SIZE = 9;

Now we’re going to add more to verifyGameState() by creating a new method to do some work, called findWinner(). findWinner() is going to check through the gameState list and find if X or O won. If neither won we’ll get findWinner() to return the EMPTY_VALUE and then we can investigate further.

Add the below calls to findWinner() along with the If Else logic:

	public String verifyGameState() {
		setup();
		if(validateGameState()) {
			if(findWinner() != EMPTY_VALUE) {
				if(findWinner() == X) {
					return "The Winner is X";
				}else if(findWinner() == O) {
					return "The Winner is O";
				}
			}
		}else return "Invalid Values";
	}	

Below validateBoardSize() add in findWinner() below.

	private int findWinner() {

	}
}

Now because there are three different directions a player can win in we’re going to call new methods to do that searching for us. Add in the below new methods under findWinner():

	private boolean findWinnerHorizontal(int xOrO) {

	}

	private boolean findWinnerVertical(int xOrO) {

	}

	private boolean findWinnerDiagonal(int xOrO) {

	}

As above in the 3 new methods we are passing in an int xOrO (as in X or O).

First we’ll look at how it will work with just findWinnerHorizontal() and X. In findWinner() we are going to call findWinnerHorizontal() and pass in X (remember, because these correspond to our static final ints declared at the top the IDE recognizes them as ints). findWinnerHorizontal() will take in its xOrO value (X in this case). It will compare X to the Game fields (the idea is to fail fast again, start by comparing X/O to the fields and then following that see if the fields are equal, as per part 1 if a field is Empty then the horizontal/vertical/diagonal line being checked would never win) . findWinnerHorizontal() will return a boolean as to whether there was a horizontal line found containing all Xs.

	private int findWinner() {
		if(findWinnerHorizontal(X)) {
			return X;
		} else if //....
	}

	private boolean findWinnerHorizontal(int xOrO) {
		return ((xOrO == x0y0 && x0y0 == x0y1 && x0y1  == x0y2)||
				(xOrO == x1y0 && x1y0 == x1y1 && x1y1  == x1y2)||
				(xOrO == x2y0 && x2y0 == x2y1 && x2y1  == x2y2));
	}

Now I’ve filled in the rest of findWinner(), findWinnerHorizontal(), findWinnerVertical() and findWinnerDiagonal(). As mentioned above if findWinner() can’t find a winner it will return EMPTY_VALUE.

	private int findWinner() {
		if(findWinnerHorizontal(X)) {
			return X;
		} else if(findWinnerHorizontal(O)) {
			return O;
		} else if(findWinnerVertical(X)) {
			return X;
		} else if(findWinnerVertical(O)) {
			return O;
		} else if(findWinnerDiagonal(X)) {
			return X;
		} else if(findWinnerDiagonal(O)) {
			return O;
		} else return EMPTY_VALUE;
	}

	private boolean findWinnerHorizontal(int xOrO) {
		return ((xOrO == x0y0 && x0y0 == x0y1 && x0y1  == x0y2)||
				(xOrO == x1y0 && x1y0 == x1y1 && x1y1  == x1y2)||
				(xOrO == x2y0 && x2y0 == x2y1 && x2y1  == x2y2));
	}

	private boolean findWinnerVertical(int xOrO) {
		return ((xOrO == x0y0 && x0y0 == x1y0 && x1y0  == x2y0)||
				(xOrO == x0y1 && x0y1 == x1y1 && x1y1  == x2y1)||
				(xOrO == x0y2 && x0y2 == x1y2 && x1y2  == x2y2));
	}

	private boolean findWinnerDiagonal(int xOrO) {
		return ((xOrO == x0y0 && x0y0 == x1y1 && x1y1  == x2y2)||
				(xOrO == x2y0 && x2y0 == x1y1 && x1y1  == x0y2));
	}

Now, almost complete. Back to verifyGameState(). We need to handle the scenario where there is no winner. Add in a call to checkGameComplete() and the If Else logic:

	public String verifyGameState() {
		setup();
		if(validateGameState()) {
			if(findWinner() != EMPTY_VALUE) {
				if(findWinner() == X) {
					return "The Winner is X";
				}else if(findWinner() == O) {
					return "The Winner is O";
				}
			}
			if (checkGameComplete()) {
				return "No Winner - Game Complete";
			}else {
				return "No Winner Yet - Game Incomplete";
			}
		}else return "Invalid Values";
	}

Add new method checkGameComplete() below findWinnerDiagonal(). This will utilise Streams to go through our gameState list and return a boolean based on whether any EMPTY_VALUE was discovered in the list. If there are none (returns true) – then the game was completed with no winner. If it finds empty values (returns false) – then the game is incomplete.

	private boolean checkGameComplete() {
		return gameState.stream()
				.noneMatch(currentGameMark -> currentGameMark.equals(EMPTY_VALUE));
	}

That’s it! I hope this is helpful, my GitHub is linked below with the problem we’ve just discussed.

Standard