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)]]
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.