Tic Tac Toe: Rules and Conditional Logic

The next bit that I am doing in my Tic Tac Toe game is putting in place the game rules and conditional logic around the actual game mechanics. I am saving the UI code for another time because that will be far more intensive than laying out the rules.

One of my goals with my code is to make it completely testable. One way of doing that is to create stand alone functions that can be tested outside of the game scene. Rather than creating a large nest of mutating side effects, I would like to take in parameters and return parameters, thus keeping all of my changes safely within a testable function. This will likely go through some refactoring in the future as I think about the best way of doing things. I know when I tried this before I had a lot of people yelling at me about the way I chose to write my code. It’s my code and my codebase, so I will write it however I please.

Board Properties

Tic Tac Toe is a fairly simple game. It is a board of nine squares. It has two players. On each player’s turn, they choose a square that has not been chosen yet. If there are three squares in a row, that player wins. If there are not, then the next player goes. This continues until either someone wins or all of the squares have been chosen.

This means that we need to have some kind of structure to represent the board state and we need to keep track of the current player:

var board:[TileState] = []
var currentPlayer:GameState = .notPlaying

I’m opting to use an array of tiles to represent the tile state. If this were a more complex board I would opt to do a different data structure and lay out actual rows and columns. However, since there are only nine tiles to keep track of and I am planning to number them when I set up the UI, I am opting for the more simple route and hard coding my logic. I know this is making people pull their hair out and scream, but I want to try and keep things simple. The rules haven’t changed in however many years this has existed, so the likelihood I will need to update requirements is minimal. I may change my mind, but that is for refactoring, not prototyping.

I also need to have a mutating variable to track the state of who is the current player. Since the game begins without a current player, the state is initialized as .notPlaying.

Setting Up the Board

The board is represented by a set of nine tiles. These tiles will be selected by the two different players, but not initially. When the board is initially laid out, it will be completely unselected. This can be represented by a function:

func resetBoard() -> [TileState] {
	var board = [TileState]()
	
	for _ in 0...8 {
		let tile = TileState.notSelected
		board.append(tile)
	}
	
	return board
}

This function returns an array of nine tiles that are all in the unselected state. These can be accessed by their index, which is how I will make moves and how I will check for wins.

Switch Players

When I was going to school for programming, my teacher told us that if you are describing what a function does and you have to use the word “and,” then you need another function. Each function should do one and only one thing. These functions can call other functions that take care of that functionality for you, but a function should be an encapsulated piece of code that does one thing.

In that spirit, I am adding a function to switch the current player:

func switchPlayer(currentPlayer: GameState) -> GameState {
	switch currentPlayer {
	case .playerA:
		return GameState.playerB
	case .playerB:
		return GameState.playerA
	case .notPlaying:
		return GameState.playerA
	}
}

I am making the determination in this game that Player A always goes first. You can do Rock Paper Scissors Lizard Spock to determine which player is Player A, but within my game logic Player A always goes first. In my exhaustive switch statement, I am checking to see what the current game state is. If the current player is Player A, then it is now Player B’s turn. If the current player is Player B, then it’s Player A’s turn. If this is the beginning of the game and the current state is Not Playing, then by default it will be Player A’s turn because Player A always goes first.

Making a Move

Beyond the UI, what goes into making a move? Making a move means selecting one tile within the array of tiles and changing its state to reflect which player you are. To keep this functional, you need to take in a board state, a current player state, and the index of the tile whose state needs to change. You then return a brand new board state based on the changes taken in by the function:

func makeMove(tile: Int, currentPlayer: GameState, board: [TileState]) -> [TileState] {
	var newBoard = board
	
	switch currentPlayer {
	case .playerA:
		newBoard[tile] = .playerA
	case .playerB:
		newBoard[tile] = .playerB
	case .notPlaying:
		newBoard[tile] = .notSelected
	}
	
	return newBoard
}

I do not believe that the final .notPlaying state will ever be called, but to ensure an exhaustive switch statement and to make sure the application doesn’t crash, it seemed logical to just include that as a case. It would not impact the game in any way to just keep the tile unselected in case something goes off the rails.

Checking for a Win

The final stand alone function I want to create is the function that checks for a win. As anyone over the age of six can tell you, most Tic Tac Toe games end in a draw. So when checking for a win, you must also consider the possibility there are simply no more moves to be made and the game ends unceremoniously.

To this end, we need another state enum to check for ending the game:

enum GameEndState {
	case playerAWin
	case playerBWin
	case draw
}

The game can end in one of three states:

  • Player A wins
  • Player B wins
  • Stalemate

To check for a win, you need to know the current player and the current state of the board. Since it’s possible that the game hasn’t entered one of these states yet, you need to make the return type optional:

func checkForWin(currentPlayer: GameState, board: [TileState]) -> GameEndState? {
	var requiredState:TileState
	var gameEnd:GameEndState
	
	if currentPlayer == .playerA {
		requiredState = .playerA
		gameEnd = .playerAWin
	} else if currentPlayer == .playerB {
		requiredState = .playerB
		gameEnd = .playerBWin
	} else {
		return nil
	}

In order to satisfy the compiler’s type safety, you need to create a conversion between the current player being passed in and the states you are checking. You can’t just compare the board’s TileState directly against the current player because they are of different types even though their members have the same name.

There are eight possible win states in a game of Tic Tac Toe:

  • Three across the top
  • Three across the middle
  • Three across the bottom
  • Three down the left
  • Three down the middle
  • Three down the right
  • Three diagonally from the upper left
  • Three diagonally from the upper right

I am checking each of these based on the index of the position in the array of tiles. If you count the tiles starting from the upper left and continuing to the right and down, they have a predictable index. The upper left tile is represented as index 0 and the lower right tile is represented by index 8. We can use the indices to check for the various win conditions. Let’s start with the across win checks.

	// Three across the top
	if board[0] == requiredState && board[1] == requiredState && board[2] == requiredState {
		return gameEnd
	}
	
	// Three across the middle
	if board[3] == requiredState && board[4] == requiredState && board[5] == requiredState {
		return gameEnd
	}
	
	// Three across the bottom
	if board[6] == requiredState && board[7] == requiredState && board[8] == requiredState {
		return gameEnd
	}

These checks are checking for contiguous tiles that all match the current player state. If any of these conditions are satisfied, the current player wins the game and the rest of the logic is never executed as it’s unnecessary. Next, we do something similar with the vertical win conditions.

	// Three down the left
	if board[0] == requiredState && board[3] == requiredState && board[6] == requiredState {
		return gameEnd
	}
	
	// Three down the middle
	if board[1] == requiredState && board[4] == requiredState && board[7] == requiredState {
		return gameEnd
	}
	
	// Three down the right
	if board[2] == requiredState && board[5] == requiredState && board[8] == requiredState {
		return gameEnd
	}

These are slightly more complicated as the indices are not contiguous. I made a diagram of the correlating tiles to ensure that my hard coded values were correct. Yes, I know hard coded values are evil. Bite me.

Finally, we check for the two diagonal win conditions.

	// Three diagonally from the upper left
	if board[0] == requiredState && board[4] == requiredState && board[8] == requiredState {
		return gameEnd
	}
	
	// Three diagonally from the upper right
	if board[2] == requiredState && board[4] == requiredState && board[6] == requiredState {
		return gameEnd
	}

If none of these win conditions have been satisfied, that means that the board is in one of two states. Either we are still playing and the game should continue, or there are no more moves to be made an no one can win. The easiest way to check for a continuing game is to check if there are any unselected tiles still on the board:

	// Draw
	for tile in board {
		if tile == .notSelected {
			return nil
		}
	}
	
	gameEnd = .draw
	return gameEnd
}

If there is even one tile that isn’t selected, the game continues. By making the game end state optional, we are allowing a scenario where you return nil if the game is not in fact at its end state.

If you have exhausted all of these options, where there isn’t a win state and there are no more tiles to be selected, then the game has concluded in a draw. You set the game end state to a draw and return that at the very end if there has not been any scenario where the method can end differently.

By far the largest and most involved function I have in this application is the checking for win function. I am not super happy with how long this turned out, but for the time being I can’t really think of a better way of doing this. There is a lot of conditional logic in here that is specific to the game, so there is going to be a lot of code, I just don’t know if there should be this much. I will ponder in the future if there are ways of cutting down on this.

Conclusion

You can access the current version of the code here.

This isn’t necessarily the way someone else would set up their application, but there isn’t a right or a wrong way to do anything. There are matters of personal preference. There is probably a way to make this code less complex, which I hope to figure out later. One thing that frustrates me about talking to other people about code is this idea that code is written in stone and can never be changed. This isn’t the case at all.

It’s better to just get something done and to refactor it later. Otherwise, you stare at a screen and nothing gets done. When I worked for Brad Larson on our rewrite of his robotics software, he regularly refactored his code. He would notice he was doing something over and over again and that it could be pulled out as a stand alone function. The process of writing the code base informed his refactoring decisions.

You can’t just go into a code base knowing all the challenges you will encounter. You learn that by working with it. My code will probably look significantly different than this when it’s done because I will figure out better ways of doing things and I will refactor the code base. This is how you should be approaching your code.

So this is all well and good to have these disparate stand alone functions, but how can we tell if they actually work or not? In my next blog post, I will set up unit tests to prove that these work as designed.