DEV Community

loading...

My first implementation of Tic Tac Toe in Haskell!

yujiri8 profile image Ryan Westlund ・3 min read

I'm still kind of a newb at Haskell, but want to get better, so I wrote Tic Tac Toe. (No AI yet, the computer makes random moves.)

import Data.List
import Data.Maybe
import System.Random

data State = X | O | Blank deriving (Eq, Read)
type Board = [[State]]
type MoveSource = Board -> IO (Int, Int)

instance Show State where
  show X = "X"
  show O = "O"
  show Blank = " "

showBoard :: Board -> String
showBoard board =
    let printedRows = map (\row -> (show $ row !! 0) ++ "|" ++ (show $ row !! 1) ++ "|" ++ (show $ row !! 2)) board
    in (intercalate "\n-----\n" printedRows) ++ "\n"

board = [[Blank, Blank, Blank], [Blank, Blank, Blank], [Blank, Blank, Blank]]

main = do
  (winner, finalState) <- playGame humanMove aiMove board X
  putStrLn "\nGame over!"
  putStr $ showBoard finalState
  putStrLn "\nWinner:"
  print winner

humanMove :: MoveSource
humanMove board = do
  putStr $ showBoard board
  putStr "Enter row/col (1-3):"
  line <- getLine
  let row = (read (line !! 0 :"") :: Int) - 1
      col = (read (line !! 1 :"") :: Int) - 1
  if (row < 0) || (row > 2) || (col < 0) || (col > 2)
    then do
      putStrLn "Bad coords. Try again."
      humanMove board
    else do
      if not $ checkState board Blank (row, col)
        then do
          putStrLn "Spot taken. Try again."
          humanMove board
        else pure (row, col)

aiMove :: MoveSource
aiMove board = do
  row <- randomRIO (0,2)
  col <- randomRIO (0,2)
  if not $ checkState board Blank (row, col)
    then aiMove board
    else pure (row, col)

playGame :: MoveSource -> MoveSource -> Board -> State -> IO (State, Board)
playGame xMoveSource oMoveSource board currentTurn = do
  move <- if currentTurn == X
    then xMoveSource board
    else oMoveSource board
  let nextTurn = if currentTurn == X then O else X
      newBoard = makeMove move currentTurn board
  if gameOver newBoard /= Nothing
    then pure $ (fromJust $ gameOver newBoard, newBoard)
    else playGame xMoveSource oMoveSource newBoard nextTurn

makeMove :: (Int, Int) -> State -> Board -> Board
makeMove (row, col) state board =
  let oldrow = board !! row
      newrow = editList oldrow col state
  in editList board row newrow

gameOver :: Board -> Maybe State
gameOver board =
  let xWon = any id $ fmap (checkPattern board X) waysToWin
      oWon = any id $ fmap (checkPattern board O) waysToWin
      draw = all id $ fmap (\row -> all id $ fmap (\slot -> slot /= Blank) row) board
  in
  if xWon
    then Just X
    else if oWon
      then Just O
      else if draw
        then Just Blank
        else Nothing

-- a generic helper to update a list item in-place
editList :: [a] -> Int -> a -> [a]
editList list i new = (take i list) ++ new : (drop (i+1) list)

checkPattern :: Board -> State -> [(Int, Int)] -> Bool
checkPattern board state pattern =
  if all id $ fmap (checkState board state) pattern
    then True
    else False

checkState :: Board -> State -> (Int, Int) -> Bool
checkState board state coord =
  let row = board !! (fst coord)
      slot = row !! (snd coord)
  in slot == state

waysToWin = [
  [(0,0), (0,1), (0,2)],
  [(1,0), (1,1), (1,2)],
  [(2,0), (2,1), (2,2)],
  [(0,0), (1,0), (2,0)],
  [(0,1), (1,1), (2,1)],
  [(0,2), (1,2), (2,2)],
  [(0,0), (1,1), (2,2)],
  [(0,2), (1,1), (2,0)]]
Enter fullscreen mode Exit fullscreen mode

I'm sure it can be done a lot better than this, so I'd much appreciate feedback from some Haskell vets.

I originally thought of having playGame be pure and take lists of moves from lazy IO, but I had enough trouble with that I decided to go without.

One thing I'm starting to really despise about Haskell's error handling is that if it crashes with an exception, I don't get a stack trace or a line number. When this was close to the state it's in now, I had a Prelude.!!: index too large or whatever it said, and it took me quite a while to track it down with literally nothing else to go on.

Also, can I not do multiline lambdas inside let? I read that multiline lambdas work, but when I tried to put the one in showBoard on one line I got persistent parse errors and couldn't figure out how to make it work.

Discussion

pic
Editor guide
Collapse
deciduously profile image
Ben Lovy

Nice! We ended up with some pretty similar implementations in spots but I think this is cleaner overall. I think I like your playGame approach better.

Collapse
rodiongork profile image
Rodion Gorkovenko

I'm sure it can be done a lot better than this

Honestly, I'm not sure. Or rather I'm sure it couldn't be done well in functional style. The task in whole is quite contrary to "functional" idea: see, you are entering moves one after another and update some state (board) - however:

  • in "very pure functional world" there is no "before" or "after" - all parameters are fed to function at the same moment, right
  • and surely we don't have some "updatable" state

E.g. I mean you can safely leave this part (of making moves and updating state) as is - it is anyway not good for Haskell. The part which can be good is the AI which is given the current state of the board and tells where to put next mark - it really looks like pure function (though implementation for more complicated games may require optimization which will make it not very pure inside).

Also I wonder

then True
else False

could this be written simpler? :)

Collapse
yujiri8 profile image
Ryan Westlund Author

Thanks! I just implemented error-checking on the input. I'll post again when I've got the AI.

Collapse
deciduously profile image
Ben Lovy

When it comes to Haskell, I safely assume everyone else's is better than mine :D

Collapse
lautarolobo profile image
Lautaro Lobo

Hey man good work. I love it!