DEV Community

Nikhil Thomas
Nikhil Thomas

Posted on

Writing a Tictactoe game in Haskell


Code can be found here


While I struggle with some misunderstandings of the OCaml module system, I decided to put my interpreter on temporary hold to try some Haskell. It's a language that's been on my radar and been a goal to learn. A combination of Haskell's terse notation ( it turns out sum . take 10 $ (*) <$> [2,4,6] <*> [1,2,3,4,5] is a completely legitimate, if daunting line of code ) combined with the new terminology (cue monad tutorial) caused some delay. But after deciding to commit, and OCaml's slightly more friendly entry to FP, I felt capable of getting my hands dirty. I felt particularly inspired after seeing code written for Github's Semantic project. At some point, I'd like to work on some meaty Haskell productive code and there's no place to start like starting.

After reading a lot of Learn You A Haskell For Great Good and watching some Youtube videos, I felt reasonably comfortable in being able to write a small Tictactoe CLI game. I went with Stack as my build tool of choice.

Like OCaml, Haskell's type system encourages domain modeling early. I decided to make my board a list of 9 elements, where each element could be either an X, an O, or empty. Since Haskell doesn't really have a null type, I decided to create both a Move as well as a Cell

import System.Environment
import Data.List

data Move = X | O
data Cell = Occupied Move | Empty

We're taking System.Environment because we'll need some IO behavior, and Data.List for some future functions.

I could have made Cell a Maybe type, but chose a more descriptive way to express a cell. This way, I can keep track of the move to play as well as see what was in the cell. I also needed a way to render this board out. Since I'm using custom types, I needed to create instances of the Show typeclass.

instance Show Move where
  show X = "X"
  show O = "O"

instance Show Cell where
  show (Occupied X)     = "X"
  show (Occupied O)    = "O"
  show Empty            = " "

I'm semi-positive I could have used deriving (Show) on my Move type but that'll be for a later refactor. Today's primary goal was just writing code. My next plan was to get some board-rendering code up. I needed a function that simply took my board, my [Cell] and output something pretty.

renderRow :: [Cell] -> String
renderRow row = intercalate " | " $ fmap show row

dividingLine :: String
dividingLine = "----------"

renderBoard :: [Cell] -> IO ()
renderBoard board = do
  putStrLn $ renderRow firstRow
  putStrLn dividingLine
  putStrLn $ renderRow secondRow
  putStrLn dividingLine
  putStrLn $ renderRow thirdRow
  where firstRow  = take 3 board
        secondRow = drop 3 . take 6 $ board
        thirdRow  = drop 6 board

renderRow takes a list of cells and returns the readable version joined by pipes. renderBoard just some some list-slicing to render no more than 3 rows of 3 elements. Since I'm writing to console, I'll need to return an IO (), the IO monad. Without getting too in the weeds, I/O is considered to be a side-effect and therefore Haskell forces you to wrap it in a monad.

If I were to call renderBoard with a list of empty elements [Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty, Empty] I would get a very pretty

  |   |
----------
  |   |
----------
  |   |

My next goal was some idea of assignment. I needed to be able to take a Move and a [Cell] and return an updated board. There are a couple of rules to this

1) The selected cell must be within bounds.
2) The selected cell must be free.

Given this, I decided to simply create a map of input strings to List indices. Is it pretty? Nope. But it works fine for this case.

getBoardIndex :: String -> Maybe Int
getBoardIndex "A1" = Just 0
getBoardIndex "A2" = Just 1
getBoardIndex "A3" = Just 2
getBoardIndex "B1" = Just 3
getBoardIndex "B2" = Just 4
getBoardIndex "B3" = Just 5
getBoardIndex "C1" = Just 6
getBoardIndex "C2" = Just 7
getBoardIndex "C3" = Just 8
getBoardIndex _    = Nothing

Pattern matching in Haskell is a little more terse than OCaml, in that I don't need a match statement. I simply create functions for every possiblity, similar to Elixir's matching. You'll see also I'm returning a Maybe Int - I chose this because not just do I care if a board index is real, but also if it's free. Two if statements, so I can use monadic binding, or the >>= operator. For reference:

  (>>=)            :: m a -> (a -> m b) -> m b

What this says is "Give me a monad of some a, and give me a function that turns some a into a monad of b and I'll return a monad of b. If I have a Maybe Int from getBoardIndex and my function for "is that cell free to assign" takes an Int and returns a Maybe then I can use this binding.

data CellTransform = Success [Cell] | Fail String [Cell]


verifyIsFree ::  [Cell] -> Int -> Maybe Int
verifyIsFree board ix = if board !! ix == Empty then Just ix else Nothing

assignCell :: String -> Move -> [Cell] -> CellTransform
assignCell location move board =
  case getBoardIndex location >>= verifyIsFree board of
    Nothing -> Fail "Invalid move" board
    Just i -> Success ((take i board) ++ [Occupied move] ++ (drop (i+1) board))

You'll see this new CellTransform type. I added a new type just to carry along error messages and an unmodified board if the board is taken. So my verifyIsFree takes a board and index, and if the board is free at that element returns the index in a Maybe, else Nothing. Since I'm doing some equality checks of a custom data type, I'll need to make sure that Cell is also an instance of the Eq typeclass

instance Eq Cell where
  Occupied X == Occupied X = True
  Occupied O == Occupied O = True
  Empty == Empty           = True
  _ == _                   = False

This just sets my equality operator for all possible states of a Cell.

Lastly, my actual game. I need my game to

1) Ask for input
2) Try to assign the cell
3a) If the cell is invalid, tell the user and let them pick again
3b) If the cell is valid, check for a winner
4a) If there's a winner, alert them and end the game
4b) If there's no winner, hand over to the next player

Let's get this coded out

playRound :: Move  -> [Cell] -> IO ()
playRound move board = do
  putStrLn $ (show move) ++ " 's turn."
  putStrLn $ "Pick a cell from A1 to C3."
  renderBoard board
  putStr "\nInput: "
  cell <- getLine
  case assignCell cell move board of
    Fail err board -> do
      putStrLn err
      playRound move board
    Success newBoard -> do
      if isThereAWinner move newBoard then do
        putStrLn $ ("Winner! " ++ (show move) ++ " has won!")
        renderBoard newBoard
        return ()
      else playRound (nextMove move) newBoard

Since we're using I/O again, we need to return an IO Monad. That also gives us the do notation benefits of some slightly more imperative-reading code.

You'll see some fake functions - isThereAWinner and nextMove move. We can code these out.

nextMove :: Move -> Move
nextMove X = O
nextMove O = X

isThereAWinner :: Move -> [Cell] -> Bool
isThereAWinner move board =
  or [
    -- check top row
    board !! 0 == (Occupied move) && board !! 1 == (Occupied move) && board !! 2 == (Occupied move),
    -- check middle row
    board !! 3 == (Occupied move) && board !! 4 == (Occupied move) && board !! 5 == (Occupied move),
    -- check bottom row
    board !! 6 == (Occupied move) && board !! 7 == (Occupied move) && board !! 8 == (Occupied move),
    -- check left column
    board !! 0 == (Occupied move) && board !! 3 == (Occupied move) && board !! 6 == (Occupied move),
    -- check middle column
    board !! 1 == (Occupied move) && board !! 4 == (Occupied move) && board !! 7 == (Occupied move),
    -- check right column
    board !! 2 == (Occupied move) && board !! 5 == (Occupied move) && board !! 8 == (Occupied move),
    -- check top left -> bottom right
    board !! 0 == (Occupied move) && board !! 4 == (Occupied move) && board !! 8 == (Occupied move),
    -- check bottom left -> top right
    board !! 6 == (Occupied move) && board !! 4 == (Occupied move) && board !! 2 == (Occupied move)
  ]

This is my least-favorite function here. It's readable with comments, but definitely isn't pleasant. I can't imagine changing this into a 5x5 tic-tac-toe board or something. But once we have this, we can create a main function.

main :: IO ()
main = do
  putStrLn $ "The game is beginning."
  let newBoard = replicate 9 Empty
  playRound X newBoard

And we can build our tictactoe.hs with stack ghc tictactoe.hs and run ./tictactoe to play!

This was a fun experiment. I tried to avoid having to dig too into monadic operators, the State monad, or advanced Haskell techniques. My primary focus was to just type code and try to get comfortable with the syntax. The compiler is pretty helpful, but not as explicit as Elm's compiler. Since my career goals are to get a job writing backend ML-family code (Scala, OCaml, Haskell, etc) I'll keep on practicing. I'd love any project ideas. I might try to write a Lisp interpreter for a bigger, meatier project.

Discussion (3)

Collapse
jvanbruegge profile image
Jan van Brügge • Edited on

Very nice article, just two small tips:

  1. There is not really any reason to write a custom Eq instance ever, just derive it
  2. You could shorten your last function a bit with list comprehensions:
isThereAWinner move board = or . fmap (all check) $ idxs
  where check x = board !! x == Occupied move
        idxs = rows <> colums <> diagonals
        rows = [ [row * n + i | i <- [0..n - 1] ] | row <- [0..n - 1] ]
        columns = [ [col + i * n | i <- [0..n - 1] ] | col <- [0..n - 1] ]
        diagonals = [ [ ((i * j) `mod` n) + i * n | i <- [0..n - 1] ] | j <- [-1, 1] ]
        n = 3

this additionally scales easily to bigger boards by changing n

I am not completely sure if the code is correct as I hadnt had the chance to test it yet

Collapse
nt591 profile image
Nikhil Thomas Author

Thanks Jan! That list comprehension is really readable. I’ll be sure to play with that and practice. Coming from a largely-JS/Ruby background, list comprehensions are definitely something I have to further internalize.

Thanks for noting the Eq derivation as well. Actually after writing this, I re-read that chapter of Learn You A Haskell and realized the same. I’ll update my source and leave an edit in this article.

Collapse
stemvork profile image
stemvork

Thanks for the addition. There is a small typo in line 3, 'colums' should be 'columns'.