Hari's Corner

Humour, comics, tech, law, software, reviews, essays, articles and HOWTOs intermingled with random philosophy now and then

Tic Tac Toe (a.k.a Noughts and Crosses) in Haskell

Filed under: Tutorials and HOWTOs by Hari
Posted on Fri, Aug 30, 2013 at 10:21 IST (last updated: Mon, Sep 16, 2013 @ 14:33 IST)

In this series < Previous Next >

A couple of years ago, I had written a C++ version of an AI playable Tic Tac Toe program. I have now written a similar version in Haskell, which uses the same kind of logic as the C++ one. I thought it would be kind of challenging to try a program like this in Haskell, but it turned out to be quite easy, as a matter of fact. I wrote this as a way to revisit Haskell while trying a non-trivial exercise.

I am sure there are better Tic Tac Toe programs/algorithms implemented in Haskell that illustrate more advanced techniques than the ones I have used, but still, I think mine would be a bit easier to understand for a newbie and is perfectly adequate for a simple game like this.

Simply the algorithm I used for the AI is this:

  1. Check for any winning move and make it.
  2. Check for opponent winning move and block it.
  3. Check if the center of the board is free and occupy it.
  4. Check if any one of the corners are free and occupy it.
  5. Occupy any other free cell.

That's about it. This makes the game unwinnable from a human player point of view (which is what most TicTacToe AIs implement). The best result that can be achieved is a draw. Pretty boring game, but still... In fact, the game can be won by the human player as pointed out in the comment by Jonathan below.

Without further ado, here's the code:

-- Tic tac toe in Haskell
-- By V.Harishankar (C) 2013
-- board is a list containing the elements
-- e0, e1, e2
-- e3, e4, e5
-- e6, e7, e8
-- and each can store either 0 (blank) 1 (cross) or 2 (nought)
-- the computer move - return the new board after
-- playing the move
computermove :: Int -> [ Int ] -> [ Int ]
-- the player move - return the new board after 
-- playing the move
playermove :: Int -> Int -> [ Int ] -> [ Int ]
-- format the board for terminal display
printboard :: [ Int ] -> String
-- representation of the nought or cross for 
-- terminal display
rep :: Int -> String
-- statement to display on winning/draw
winstatement :: Int -> String
-- check if the player (nought or cross) has won
-- and return a boolean
checkifwon :: Int -> [ Int ] -> Bool
-- check if the game is over (i.e. all the moves)
-- are completed
checkgameover :: [ Int ] -> Bool
-- function for the gameplay
gameplay :: Int -> [ Int ] -> IO ()
-- representation of nought and cross in the list
rep 1 = " X "
rep (-1) = " O "
rep n = "   "
-- winning/game draw statements 
winstatement 1 = "Cross has won!"
winstatement (-1) = "Noughts has won!"
winstatement n = "Game drawn..."
-- draw the board in its state
printboard board = rep (head board) ++ "|" ++ rep (board !! 1) ++ "|" ++ rep (board !! 2) 
        ++ "\n" ++ "---+---+---" ++ "\n" ++  rep (board !! 3) ++ "|" ++ rep (board !! 4) 
        ++ "|" ++ rep (board !! 5) ++ "\n" ++ "---+---+---" ++ "\n" ++
        rep (board !! 6) ++ "|" ++ rep (board !! 7) ++ "|" ++ rep (board !! 8) ++ "\n"
-- check if the game is over
checkgameover board -- if zero is present in any position, then board is not fully played yet
        | 0 `elem` board = False
        | otherwise = True                                        
-- check if the game is won
checkifwon ch board 
        -- check the rows
        | head board == ch && board !! 1 == ch && board !! 2 == ch = True
        | board !! 3 == ch && board !! 4 == ch && board !! 5 == ch = True
        | board !! 6 == ch && board !! 7 == ch && board !! 8 == ch = True
        -- check the cols
        | head board == ch && board !! 3 == ch && board !! 6 == ch = True
        | board !! 1 == ch && board !! 4 == ch && board !! 7 == ch = True
        | board !! 2 == ch && board !! 5 == ch && board !! 8 == ch = True
        -- check cross \
        | head board == ch && board !! 4 == ch && board !! 8 == ch = True
        -- check cross /
        | board !! 2 == ch && board !! 4 == ch && board !! 6 == ch = True
        -- not won
        | otherwise = False
-- player move
playermove 0 ch board -- if the position is free then play 
        | head board == 0 = ch:tail board
        -- else allow the computer to play on behalf of the player
        | otherwise = computermove ch board                                          
playermove 8 ch board -- if the position is free then play 
        | board !! 8 == 0 = take 8 board ++ [ ch ]
        -- else allow the computer to play on behalf of the player
        | otherwise = computermove ch board
playermove n ch board -- if the move is invalid then allow the computer to play
        | n < 0 || n > 8 = computermove ch board
        -- if the position is free then play 
        | board !! n == 0 = take n board ++ [ ch ] ++ drop (n+1) board
        -- else allow the computer to play on behalf of the player                                        
        | otherwise = computermove ch board
-- computer moves
computermove ch board
        -- if in a winning position then win
        -- examine first row
        | head board == ch && board !! 1 == ch && board !! 2 == 0 = take 2 board ++ [ ch ] ++ drop 3 board
        | head board == ch && board !! 1 == 0 && board !! 2 == ch = [head board] ++ [ ch ] ++ drop 2 board
        | head board == 0 && board !! 1 == ch && board !! 2 == ch = ch:tail board
        -- examine second row 
        | board !! 3 == ch && board !! 4 == ch && board !! 5 == 0 = take 5 board ++ [ ch ] ++ drop 6 board
        | board !! 3 == ch && board !! 4 == 0 && board !! 5 == ch = take 4 board ++ [ ch ] ++ drop 5 board
        | board !! 3 == 0 && board !! 4 == ch && board !! 5 == ch = take 3 board ++ [ ch ] ++ drop 4 board
        -- examine third row
        | board !! 6 == ch && board !! 7 == ch && board !! 8 == 0 = take 8 board ++ [ ch ]
        | board !! 6 == ch && board !! 7 == 0 && board !! 8 == ch = take 7 board ++ [ ch ] ++ drop 8 board
        | board !! 6 == 0 && board !! 7 == ch && board !! 8 == ch = take 6 board ++ [ ch ] ++ drop 7 board
        -- examine first col
        | head board == ch && board !! 3 == ch && board !! 6 == 0 = take 6 board ++ [ ch ] ++ drop 7 board
        | head board == ch && board !! 3 == 0 && board !! 6 == ch = take 3 board ++ [ ch ] ++ drop 4 board
        | head board == 0 && board !! 3 == ch && board !! 6 == ch = ch:tail board
        -- examine second col
        | board !! 1 == ch && board !! 4 == ch && board !! 7 == 0 = take 7 board ++ [ ch ] ++ drop 8 board
        | board !! 1 == ch && board !! 4 == 0 && board !! 7 == ch = take 4 board ++ [ ch ] ++ drop 5 board
        | board !! 1 == 0 && board !! 4 == ch && board !! 7 == ch = [head board] ++ [ ch ] ++ drop 2 board
        -- examine third col
        | board !! 2 == ch && board !! 5 == ch && board !! 8 == 0 = take 8 board ++ [ ch ]
        | board !! 2 == ch && board !! 5 == 0 && board !! 8 == ch = take 5 board ++ [ ch ] ++ drop 6 board
        | board !! 2 == 0 && board !! 5 == ch && board !! 8 == ch = take 2 board ++ [ ch ] ++ drop 3 board
        -- examine the cross \
        | head board == ch && board !! 4 == ch && board !! 8 == 0 = take 8 board ++ [ ch ]
        | head board == ch && board !! 4 == 0 && board !! 8 == ch = take 4 board ++ [ ch ] ++ drop 5 board
        | head board == 0 && board !! 4 == ch && board !! 8 == ch = ch:tail board
        -- examine the cross /
        | board !! 2 == ch && board !! 4 == ch && board !! 6 == 0 = take 6 board ++ [ ch ] ++ drop 7 board
        | board !! 2 == ch && board !! 4 == 0 && board !! 6 == ch = take 4 board ++ [ ch ] ++ drop 5 board
        | board !! 2 == 0 && board !! 4 == ch && board !! 6 == ch = take 2 board ++ [ ch ] ++ drop 3 board
        -- block opponent winning positions
        -- examine first row
        | head board == -ch && board !! 1 == -ch && board !! 2 == 0 = take 2 board ++ [ ch ] ++ drop 3 board
        | head board == -ch && board !! 1 == 0 && board !! 2 == -ch = [head board] ++ [ ch ] ++ drop 2 board
        | head board == 0 && board !! 1 == -ch && board !! 2 == -ch = ch:tail board
        -- examine second row 
        | board !! 3 == -ch && board !! 4 == -ch && board !! 5 == 0 = take 5 board ++ [ ch ] ++ drop 6 board
        | board !! 3 == -ch && board !! 4 == 0 && board !! 5 == -ch = take 4 board ++ [ ch ] ++ drop 5 board
        | board !! 3 == 0 && board !! 4 == -ch && board !! 5 == -ch = take 3 board ++ [ ch ] ++ drop 4 board
        -- examine third row
        | board !! 6 == -ch && board !! 7 == -ch && board !! 8 == 0 = take 8 board ++ [ ch ]
        | board !! 6 == -ch && board !! 7 == 0 && board !! 8 == -ch = take 7 board ++ [ ch ] ++ drop 8 board
        | board !! 6 == 0 && board !! 7 == -ch && board !! 8 == -ch = take 6 board ++ [ ch ] ++ drop 7 board
        -- examine first col
        | head board == -ch && board !! 3 == -ch && board !! 6 == 0 = take 6 board ++ [ ch ] ++ drop 7 board
        | head board == -ch && board !! 3 == 0 && board !! 6 == -ch = take 3 board ++ [ ch ] ++ drop 4 board
        | head board == 0 && board !! 3 == -ch && board !! 6 == -ch = ch:tail board
        -- examine second col
        | board !! 1 == -ch && board !! 4 == -ch && board !! 7 == 0 = take 7 board ++ [ ch ] ++ drop 8 board
        | board !! 1 == -ch && board !! 4 == 0 && board !! 7 == -ch = take 4 board ++ [ ch ] ++ drop 5 board
        | board !! 1 == 0 && board !! 4 == -ch && board !! 7 == -ch = [head board] ++ [ ch ] ++ drop 2 board
        -- examine third col
        | board !! 2 == -ch && board !! 5 == -ch && board !! 8 == 0 = take 8 board ++ [ ch ]
        | board !! 2 == -ch && board !! 5 == 0 && board !! 8 == -ch = take 5 board ++ [ ch ] ++ drop 6 board
        | board !! 2 == 0 && board !! 5 == -ch && board !! 8 == -ch = take 2 board ++ [ ch ] ++ drop 3 board
        -- examine the cross \
        | head board == -ch && board !! 4 == -ch && board !! 8 == 0 = take 8 board ++ [ ch ]
        | head board == -ch && board !! 4 == 0 && board !! 8 == -ch = take 4 board ++ [ ch ] ++ drop 5 board
        | head board == 0 && board !! 4 == -ch && board !! 8 == -ch = ch:tail board
        -- examine the cross /
        | board !! 2 == -ch && board !! 4 == -ch && board !! 6 == 0 = take 6 board ++ [ ch ] ++ drop 7 board
        | board !! 2 == -ch && board !! 4 == 0 && board !! 6 == -ch = take 4 board ++ [ ch ] ++ drop 5 board
        | board !! 2 == 0 && board !! 4 == -ch && board !! 6 == -ch = take 2 board ++ [ ch ] ++ drop 3 board                                    
        -- if  center is blank - choose center of the board
        | board !! 4 == 0 = take 4 board ++ [ ch ] ++ drop 5 board
        -- otherwise choose one of the corners of the board
        | head board == 0 = ch:tail board
        | board !! 2 == 0 = take 2 board ++ [ ch ] ++ drop 3 board
        | board !! 6 == 0 = take 6 board ++ [ ch ] ++ drop 7 board
        | board !! 8 == 0 = take 8 board ++ [ ch ]
        -- otherwise choose one of the sides of the board
        | board !! 1 == 0 = [head board] ++ [ ch ] ++ drop 2 board
        | board !! 3 == 0 = take 3 board ++ [ ch ] ++ drop 4 board
        | board !! 5 == 0 = take 5 board ++ [ ch ] ++ drop 6 board
        | board !! 7 == 0 = take 7 board ++ [ ch ] ++ drop 8 board
        -- if nothing is possible return the board as is
        | otherwise = board
-- main gameplay function
gameplay ch board =     -- if it is the cross player then the human player should
        -- make a move
        if ch == 1 then do
                putStrLn "Your move. Make an invalid move to allow the computer to choose a cell."
                putStrLn "Enter the cell# to play (1..9)"
                cellno <- getLine
                -- make the player move
                let newboard = playermove (read cellno - 1) ch board                                                    
                -- display the board
                putStrLn $ printboard newboard
                -- if game is over 
                if checkgameover newboard
                        then 
                        -- if the player has won display the win statement or
                        -- the drawn statement
                                putStrLn $ winstatement (if checkifwon ch newboard then ch else 0)
                        -- game is not over
                        else 
                        -- if the player has won display the win statement
                        -- else continue gameplay
                                if checkifwon ch newboard
                                        then 
                                                putStrLn $ winstatement ch
                                        else                                                            
                                                gameplay (-ch) newboard
        -- if it is the noughts player let the computer make the move                           
        else do
                let newboard = computermove ch board    
                putStrLn "Computer Move: "
                putStrLn $ printboard newboard
                -- check if game is over
                if checkgameover newboard
                        then 
                        -- if the player has won display the won statement
                        -- else display the draw statement
                                putStrLn $ winstatement (if checkifwon ch newboard then ch else 0)
                        -- game is not over 
                        else 
                        -- if the player has won display the won statement
                        -- else continue gameplay
                                if checkifwon ch newboard
                                        then  
                                                putStrLn $ winstatement ch
                                        else                                                            
                                                gameplay (-ch) newboard
main = do
-- initialize the board
        let board = [0, 0, 0, 0, 0, 0, 0, 0, 0]
        putStrLn "Welcome to Noughts and Crosses"
        putStrLn ""
        putStrLn $ printboard board
        putStrLn "Do you wish to start first (cross) [Y/N]? "
        choice <- getLine
        if choice == "Y" || choice == "y"
                then gameplay 1 board
                else gameplay (-1) board

In this series

2 comment(s)

  1. Unfortunately, your algorithm isn't unbeatable:

    Welcome to Noughts and Crosses

    | |
    ---+---+---
    | |
    ---+---+---
    | |

    Do you wish to start first (cross) [Y/N]?
    y
    Your move. Make an invalid move to allow the computer to choose a cell.
    Enter the cell# to play (1..9)
    1
    X | |
    ---+---+---
    | |
    ---+---+---
    | |

    Computer Move:
    X | |
    ---+---+---
    | O |
    ---+---+---
    | |

    Your move. Make an invalid move to allow the computer to choose a cell.
    Enter the cell# to play (1..9)
    8
    X | |
    ---+---+---
    | O |
    ---+---+---
    | X |

    Computer Move:
    X | | O
    ---+---+---
    | O |
    ---+---+---
    | X |

    Your move. Make an invalid move to allow the computer to choose a cell.
    Enter the cell# to play (1..9)
    7
    X | | O
    ---+---+---
    | O |
    ---+---+---
    X | X |

    Computer Move:
    X | | O
    ---+---+---
    | O |
    ---+---+---
    X | X | O

    Your move. Make an invalid move to allow the computer to choose a cell.
    Enter the cell# to play (1..9)
    4
    X | | O
    ---+---+---
    X | O |
    ---+---+---
    X | X | O

    Cross has won!


    Comment by Jonathan S (visitor) on Sun, Sep 8, 2013 @ 21:12 IST #
  2. Jonathan S. thanks for pointing that out. Yes, that is a case which is not handled by the "AI". I guess it makes it slightly more interesting, though.

    Comment by Hari (blog owner) on Sun, Sep 8, 2013 @ 22:21 IST #

Comments closed

The blog owner has closed further commenting on this entry.