Cogs and Levers A blog full of technical stuff

Tic Tac Toe in Haskell

Introduction

Still trying to flex my skills in the Haskell arena and am still chasing a real project to do. Until I find said project, silly little games it is. A little while ago I watched the Haskell Live videos and in these videos a good start is made on a chess framework. Very interesting stuff to see how something like this is put together and if you haven’t seen them, I suggest you go and check out episode 1 and episode 2. Anyway, the videos don’t yet take you through to a completed implementation and while I’d love to say that I possess the skills to take the chess game through, I’d be punching above my weight. In compromise, I’ve been able to use some of the concepts that Rein has demonstrated in his videos - in my own implementation of Tic Tac Toe. Today’s post will take you through my implementation.

Functional Specification

Well, if you’re really wanting the functional in’s-and-out’s of the game Tic-tac-toe, I suggest you step our of your bubble and read this. This implementation will involve two rather random computer players battling it out, so no user interactivity (yet). Well, thanks to a cheeky link to a wikipedia article, that’s the smallest functional specification I’ve ever seen. On to the code.

Pieces

First up, we’ll deal with pieces. To this program, a piece can be a Naught or a Cross. When the piece is a Naught, we want to represent it with a ‘o’, when the piece is a Cross, we want to represent it with an ‘x’. Here’s the type declaration and functions we’ll use to work with pieces.

-- | A piece is what a player places in a cell
data Piece = Naught | Cross                   
   deriving (Eq, Show, Read)
 
-- | Converts a piece into the board representation     
showPiece :: Piece -> Char                              
showPiece Naught = 'o'                                  
showPiece Cross  = 'x'                                  
                                                        
-- | Converts the board representation back into a piece
readPiece :: Char -> Piece                              
readPiece 'o' = Naught                                  
readPiece 'x' = Cross                                   

A piece can only be either a Naught or a Cross, nothing else. The functions are all very straight forward and meeting the requirements of our specification. Naughts will look like “o’s” and Crosses will look like “x’s”. Just to be sure, we can setup a couple of QuickCheck tests to assure us that it’s ok.

quickCheck (readPiece (showPiece Naught) == Naught) 
quickCheck (readPiece (showPiece Cross) == Cross)   

We’re ready to move on.

Cells

A cell is a representation of one of the nine squares that you find on a tic-tac-toe board. A cell differs from a piece as a cell can have a piece value or it can be empty. In our implementation, empty cells will be represented by a period .. Let’s take a look at the types and the functions we’ll use to interact with cells.

-- | A cell is empty or it contains a piece
type Cell = Maybe Piece                    

-- | Shows the value of a cell        
showCell :: Cell -> Char              
showCell Nothing  = '.'               
showCell (Just p) = showPiece p       
                                      
-- | Reads a board cell and transforms
readCell :: Char -> Cell              
readCell '.' = Nothing                
readCell p   = Just $ readPiece p     

We use Maybe to allow either a Nothing or a Piece value for a cell. With the help of those functions we’ve already written for pieces, you can see that these cell functions are very simple. We’ve only had to cater for one extra case, the case of when there’s no value in a cell. We’ll setup a few more QuickCheck tests just to make sure that our Cell code is working ok.

quickCheck (readCell (showCell Nothing) == Nothing)            
quickCheck (readCell (showCell (Just Cross)) == (Just Cross))  
quickCheck (readCell (showCell (Just Naught)) == (Just Naught))

Great. With this code written we’re ready to wrap up a List of these cells into a field that we will play on.

The Field

A 3x3 grid is normally what’s drawn prior to playing a game of tic-tac-toe. We will define the field as an array of an array of cells. Here’s the type and supporting functions for a field.

-- | A field is a 3x3 array of cells
type Field = [[Cell]]               

-- | Shows a field of cells        
showField :: Field -> [String]     
showField = map showCellRow        
   where showCellRow = map showCell
                                   
-- | Reads a field                 
readField :: [String] -> Field     
readField = map readCellRow        
   where readCellRow = map readCell

Much as before, you can see that we’re leaning on our already-built set of functions to implement all of our field-based functions. At this point, we can define the starting field that all of our games will originate with.

initialField = ["..." 
               ,"..." 
               ,"..."]

You can see that the field is in string format. It’s a little more human to read and it’s all ready for our readField function to use as is demonstrated by the following QuickCheck test.

quickCheck (showField (readField initialField) == initialField)

It’s at this point where any similarities between the Haskell Live videos and my implementation end. It’s probably natural that this is the point as we’re going to start dealing with some game logic.

Winners and Losers

There are a few tests that we have to be constantly running in order to simulate a real-life game of tic-tac-toe. One of which is “do we have a winner yet? Testing if a winner has been established is testing if the same symbol appears on a horizontal, vertical or diagonal line. In each of these configurations, there are only going to be three cells to test, so I made a general purpose array tester. I’ve use pattern matching in this function to be as clear as possible. It’s not as DRY as it could be, but we can improve it later - let’s just get it working. Here’s the function to test three list elements to see if there’s a winner.

-- | Tests a prepared row for a winner                       
testRow :: [Cell] -> Maybe Piece                             
testRow [Just Naught, Just Naught, Just Naught] = Just Naught
testRow [Just Cross, Just Cross, Just Cross]    = Just Cross 
testRow _                                       = Nothing    

Easy. This function looks like it will be very effective in testing winners on the horizontal lines. But what about the vertical lines that occur in different arrays? and the diagonal lines that exist in different arrays and in different indexes across the horizontal axis? My answer here is to create a function that make all of these testable vectors into horizontal rows so that we can use the above function on all of the different win configurations. Here’s the code to build an array of these 3 element arrays.

-- | Flattens the winnable view of a field into testable rows         
makeRows :: Field -> [[Cell]]                                         
makeRows f = horiz ++ vert ++ diag                                    
   where horiz = [r | r <- f]                                         
         col1  = [r | r <- head f]                                    
         col2  = [r | r <- head $ drop 1 f]                           
         col3  = [r | r <- head $ drop 2 f]                           
         vert  = [col1, col2, col3]                                   
         diag  = [[head col1, head $ drop 1 col2, head $ drop 2 col3] 
                 ,[head $ drop 2 col1, head $ drop 1 col2, head col3]]

Walking through this one piece-by-piece, we can see a number of items in the where clause. Sure, these could be consolidated into a big-fat-one-liner, but I wanted to make clear exactly what was being concatenated. So, horiz is a list comprehension giving you each horizontal line, col1, col2 and col3 are the vertical columns. diag is a little less clearer through its use of head and drop, but it does form the diagonals into testable rows.

Playing the game

Weather you’re a computer or human player, you’ll need to know where are the valid spots to move. It’ll be intuitive enough from the user interface presented for a human to realise where their next move will be, but our computer player counterparts will need to be told explicitly where they can move. This next function just finds all of the available cells that you can move into.

-- | Gathers all of the positions that are free                     
getOptions :: Field -> [Int]                                        
getOptions f = [snd x | x <- zip (concat f) [0..], fst x == Nothing]

We assign an index (from 0 to 8) to each cell and only return the indexes that are paired with a Nothing value. This function will help us later when we go to build a user interface for this application. We’ll be able to prompt the user intuitively.

Flattening the array gives us the [0..8] range, which over the 3x3 grid (which is really our tic-tac-toe representation), the range looks like this.

0 1 2
3 4 5
6 7 8

This is timely information as we’re just about to present the function that will place a piece on the field for us. Placing a piece will require us to provide the current game field, the piece we want to apply and the index that we want to apply it at. This function cuts the list on the index that we want to apply and then re-concatenates the broken list around the new piece (obviously dropping the cell that we’re putting a piece into). Here’s the code.

-- | Places a piece on the board                        
placePiece :: Field -> Piece -> Int -> Field            
placePiece f p n = chunksOf 3 $ prior ++ [cell] ++ after
   where flat = concat f                                
         prior = take n $ flat                          
         after = drop (n + 1) $ flat                    
         cell = (Just p) :: Cell                        

Trying to be as clear as possible with my names here. prior being the list before the break, after is what comes after. Now it’s time to introduce our “artificial intelligence” which really - there isn’t any intelligence here. It’s based purely off of a random number generator. Here’s how our computer players will play a round of tic-tac-toe.

-- | Takes a random turn on a field                                      
takeRandomTurn :: StdGen -> Piece -> Field -> (StdGen, Field)            
takeRandomTurn gen p f = (seed, placePiece f p (opts !! idx))            
   where opts        = getOptions f                                      
         (idx, seed) = randomR(0, (length opts) - 1) gen :: (Int, StdGen)

We’re taking in a random number generator, the piece type that the computer will place and the field that it will be placed in. The output to which will be a new random number generator and the changed field. This way, we can repeatedly call this function and not worry about losing our sense of entropy (or field state!). Finally we have the function that will play our game until we have a result (win naught, win cross or tie). I needed to implement a “not” function for my piece type. This is just so I can offer each “player” a go interchangeably. Here’s the code.

-- | Acts like a boolean "not" for tic-tac-toe pieces                         
otherPiece :: Piece -> Piece                                                  
otherPiece Cross = Naught                                                     
otherPiece Naught = Cross                                                     
                                                                              
-- | Plays a game between two random cpu players                              
playGame :: StdGen -> Piece -> Field -> IO ()                                 
playGame gen p f = if spaces == 0                                             
                      then putStrLn "Game was a tie!"                         
                      else if winner /= Nothing                               
                              then do                                         
                                 putStrLn $ (unlines fstr)                    
                                 putStrLn $ (show winner) ++ " won the game!" 
                              else do                                         
                                 putStrLn $ (unlines fstr)                    
                                 playGame ng (otherPiece p) nf                
   where winner   = hasWinner f                                               
         spaces   = length $ getOptions f                                     
         (ng, nf) = takeRandomTurn gen p f                                    
         fstr     = showField f                                               

This reads fairly humanly. We assume that if execution breaks into this function, a result wasn’t established on the execution prior, so we’re comfortable in the first check that if there are no spaces left to put pieces, we must declare the game as a tie. The next test is if the previous call to this function caused the other player to win the game. In the event that this check is confirmed, a message is written out and we do not continue any further. If there still isn’t a winner, we get this player to place a piece into a cell and then recurse on this function as the other player. Here is how a game runs when using 1 into mkStdGen.

*TicTacToe> let f = readField initialField
*TicTacToe> playGame (mkStdGen 1) Cross f
...
...
...

..x
...
...

o.x
...
...

o.x
...
..x

o.x
...
.ox

oxx
...
.ox

oxx
.o.
.ox

oxx
.o.
xox

oxx
.oo
xox

Game was a tie!

That’s tic-tac-toe.

Next time I’ll add some user interactivity so that the computer doesn’t have all the fun!