8
\$\begingroup\$

I want to get better at functional programming so I started with this simple 2-player TicTacToe game. Next I want to add a simple min-max algorithm to make it a 1-player game, but before that I'd like to get some feedback on this program. I tried to follow the FP ideology, but I may have done some things the imperative way, or just made some functions inefficient. I would greatly appreciate any comments.

import Data.Char
import Data.Either
import Data.List
import Data.Maybe
data Player = O | X deriving (Eq, Show)
data Move = Move Int Player deriving (Eq, Show)
instance Ord Move where
 (Move i1 _) `compare` (Move i2 _) = i1 `compare` i2
type Board = [Move]
showBoard :: Board -> String
showBoard board = intercalate "\n" $ intersperse "--+---+--" $ map showRow (segment 3 $ fillBoard board [1..9])
 where showRow b = intercalate " | " (map toChar b)
 toChar (Left i) = show i
 toChar (Right (Move _ p)) = show p
 
segment :: Int -> [a] -> [[a]]
segment n [] = []
segment n xs = take n xs : segment n (drop n xs)
fillBoard :: Board -> [Int] -> [Either Int Move]
fillBoard [] xs = map Left xs
fillBoard b@(m@(Move i p):ms) (x:xs) | i > x = Left x : fillBoard b xs
 | i == x = Right m : fillBoard ms xs
 | otherwise = error "Impossible state"
moveLegal :: Board -> Int -> Bool
moveLegal [] _ = True
moveLegal ((Move i' _ ):ms) i = (i /= i') && moveLegal ms i
makeMove :: Board -> Int -> Board
makeMove board pos = sort $ Move pos (nextPlayer board) : board
gameOver :: Board -> Bool
gameOver board = length board == 9 || isJust (hasWinner board)
nextPlayer :: Board -> Player
nextPlayer [] = O
nextPlayer (m:ms) = if nextPlayer ms == O then X else O
hasWinner :: Board -> Maybe Player
hasWinner board | winnerO board = Just O
 | winnerX board = Just X
 | otherwise = Nothing
getPositions :: Board -> Player -> [Int]
getPositions board p = map (\(Move i _) -> i) $ filter (filterByPlayer p) board
 where filterByPlayer player (Move _ p) = p == player
winnerO board = containsLine $ getPositions board O
winnerX board = containsLine $ getPositions board X
containsLine :: [Int] -> Bool
containsLine ps = any (containsAll ps) Main.lines
{- HLINT ignore -}
containsAll :: (Eq a) => [a] -> [a] -> Bool
containsAll as [] = True
containsAll as (b:bs) = b `elem` as && containsAll as bs
-- Every possible winning line
lines :: [[Int]]
lines = [[1,2,3],
 [4,5,6],
 [7,8,9],
 [1,4,7],
 [2,5,8],
 [3,6,9],
 [1,5,9],
 [3,5,7]]
doTurn :: Board -> IO ()
doTurn board = do
 putStrLn $ showBoard board
 if gameOver board then
 case hasWinner board of
 Nothing -> putStrLn "Draw"
 Just w -> putStrLn $ show w ++ " wins"
 else do
 inp <- getLine
 let pos = read inp
 if not (moveLegal board pos) then do
 putStrLn "Illegal move, please try again"
 doTurn board
 else
 doTurn (makeMove board pos)
main :: IO ()
main = doTurn []
toolic
15.1k5 gold badges29 silver badges210 bronze badges
asked Jun 7, 2024 at 15:24
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

This is a pretty neat program overall!

One thing you could experiment with is to use a different representation of the board. As a list of moves, adding a new move is easy, but all of the other operations (showing the board, is a move legal, whose turn is it, is there a winner) are slightly tedious to implement. Different representations have different trade offs in that respect.

The Ord Move instance is not lawful: compare x y == EQ should be equivalent to x == y. Here it is only used by sort. Instead, use sortBy or sortOn to specify a custom ordering independently of Ord.

moveLegal and containsAll are special cases of all :: (a -> Bool) -> [a] -> Bool.

read is a partial function. Use readMaybe instead.

answered Jun 20, 2024 at 8:05
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Thanks for the feedback, I appreciate it! Regarding the board representation, I chose to store the moves so that I never have to modify the state (apart from sorting it), and can still derive all necessary information from the state. Storing a board is probably more developer-friendly though, I might try a version with that instead. \$\endgroup\$ Commented Jun 21, 2024 at 12:41
  • \$\begingroup\$ Does your code need some comments, possibly why you've done the things you've done ? This is a genuine question as I'm a Haskell novice (though not a coding one). Perhaps all your functions are completely obvious in what they do and why and so comments are not necessary ? \$\endgroup\$ Commented Dec 20, 2024 at 0:07

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.