4
\$\begingroup\$

This is a follow-up question of this one: Tic Tac Toe game in Haskell

I have revised my code. How does it look now?

import System.Random (randomRIO)
import System.IO (hFlush, stdout, getLine)
import Data.List (intercalate)
import Data.Array

data Tile   = EmptyTile | X | O deriving (Eq)
data Player = Player1 | Player2

showTile :: Tile -> String
showTile EmptyTile = " "
showTile X         = "X"
showTile O         = "O"

showBoard :: Board -> String
showBoard b = let bstr = fmap showTile b
                  blist = boardAsList bstr
              in  unlines [intercalate "|" row | row <- blist]
              where
                boardAsList b = [[b!(x,y) | y <- [1,2,3]] | x <- [1,2,3]]

type Board = Array (Int,Int) Tile

emptyBoard :: Board
emptyBoard = array ((1,1),(3,3)) [((x,y), EmptyTile) | x <- [1,2,3], y <- [1,2,3]]

put :: Board -> Tile -> Int -> Int -> Maybe Board
put b t x y = case b!(x,y) of
                EmptyTile -> Just (b // [((x,y), t)])
                _         -> Nothing

p1wins, p2wins :: Board -> Bool
p1wins b = tileWins b X
p2wins b = tileWins b O

tileWins :: Board -> Tile -> Bool
tileWins b t = 
   any (\row -> all (\col -> b!(row,col) == t) [1..3]) [1..3] ||
   any (\col -> all (\row -> b!(row,col) == t) [1..3]) [1..3] ||
   all (\rc -> b!(rc,rc) == t) [1..3] ||
   all (\rc -> b!(rc,4-rc) == t) [1..3]

checkFull :: Board -> Bool
checkFull b = all (\row -> all (\col -> b!(row, col) /= EmptyTile) [1..3]) [1..3]

compMove :: Board -> IO (Board)
compMove b = do
    (row, col) <- getRandomEmpty b
    let (Just b') = put b O row col
    return b'

getRandomEmpty :: Board -> IO (Int, Int)
getRandomEmpty b = do
    col <- randomRIO (1,3)
    row <- randomRIO (1,3)
    case b!(row, col) of
        EmptyTile -> return (row, col)
        _         -> getRandomEmpty b

prompt :: String -> IO String
prompt s = do
    putStr s
    hFlush stdout
    getLine

showTileNumbers :: String
showTileNumbers  = (unlines
                   [intercalate "|" ["(" ++ show x ++ "," ++ show y ++ ")" |
                   y <- [1,2,3]] | x <- [1,2,3]])

main :: IO ()
main = do
    putStrLn "This is classic tic tac toe game."
    putStrLn "In order to play, you need to put a number between 0 and 8"
    putStrLn "This table shows tile numbers"
    putStrLn showTileNumbers
    putStrLn $ showBoard emptyBoard
    playTurn emptyBoard Player1
    where
        playTurn b Player1 = do
            row <- prompt "Row: "
            col <- prompt "Col: "
            let newboard = put b X (read row) (read col)
            case newboard of
                Nothing -> do
                            putStrLn "Invalid move."
                            playTurn b Player1
                Just b' -> if p1wins b' then
                                putStrLn "You win"
                            else
                                playTurnIfNotTie b' Player2
        playTurn b Player2 = do
            b' <- compMove b
            putStrLn $ showBoard b'
            if p2wins b' then
                putStrLn "You Lose!"
            else
                playTurnIfNotTie b' Player1

        playTurnIfNotTie b p = if checkFull b then
                                  putStrLn "Tie."
                               else
                                  playTurn b p
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

With the new board representation, the brute-force pattern matching is gone, which is good. Still, I personally prefer the 1–9 numbering for simpler user interface — perhaps a translation function could be implemented. Note that you forgot to adjust the introductory message for the new coordinate system.

Instead of the showTile function, you should use the standard Show typeclass:

instance Show Tile where
    show EmptyTile = " "
    show X         = "X"
    show O         = "O"

In showBoard you used both let … in and where. Pick one or the other (I prefer where).

As mentioned in my previous answer, all of your functions that accept a Board should place the board parameter last, since the board is what they all operate on. With tileWins :: Tile -> Board, for example, you can define p1wins = tileWins X using the point-free style.

data Player = Player1 | Player2 isn't doing anything useful for you. You are hard-coding the Player1 ↔︎ X and the Player2 ↔︎ O correspondence all over the place, in main, p1wins, p2wins, and compMove.

With this second version, then, I would say that the greatest improvement can be made in handling the two players. Here is my suggestion:

humanTurn :: Tile -> Board -> IO Board
humanTurn t b = do
    putStrLn $ showBoard b
    move
    where
        move = do
            row <- prompt "Row: "
            col <- prompt "Col: "
            let newboard = put b t (read row) (read col)
            case newboard of
                Nothing -> do
                            putStrLn "Invalid move."
                            move
                Just b' -> return b'

computerTurn :: Tile -> Board -> IO Board
computerTurn t b = do
    (row, col) <- getRandomEmpty b 
    let (Just b') = put b t row col
    return b'

main :: IO ()
main = do
    putStrLn "This is the classic tic tac toe game."
    putStrLn "Enter the coordinates for each move according to this scheme:"
    putStrLn showTileNumbers
    play player1 player2 emptyBoard
    where
        player1 = (X, humanTurn, "You win!")
        player2 = (O, computerTurn, "You lose.")
        play p@(tile, turn, winMsg) p' b = do
            b' <- turn tile b
            if tileWins tile b' then do
                putStrLn $ showBoard b'
                putStrLn winMsg
            else if checkFull b' then do
                putStrLn $ showBoard b'
                putStrLn "Tie."
            else
                play p' p b'

This turns main into a leaner state machine. As evidence of the superiority of this design, observe that you can make a human-vs.-human game simply by changing two lines to…

       player1 = (X, humanTurn, "X wins.")
       player2 = (O, humanTurn, "O wins.")
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.