I want to learn Haskell and figured the best way to do it is to write a simple TicTacToe implementation. However the result feels very inelegant and I think it might be the best playground for your rants and give me an opportunity to learn some lessons.
{-|
TODO: -filter symmetric states from game tree
|-}
module Tictactoe where
import Data.List
initialBoardStr = " "
data Tree a = Node a [Tree a] deriving (Show)
type Board = String
type Points = Int
type Pos = Int
expandGameTree :: Board -> Tree Board
expandGameTree state = Node state $ succesors state
where succesors cur_state = [Node s (succesors s)
| s <- indicateAllowedMoves cur_state, s /= []]
evalBoard :: Board -> Points
evalBoard board
| win && nextSymb board == "X" = 100
| win && nextSymb board == "O" = (-100)
| otherwise = 0
where win = winingBoard board
evalState :: Tree Board -> Points
evalState (Node a []) = evalBoard a
evalState (Node a (x:xs)) = (minimax a) [evalState b | b <- (x:xs)]
minimax board
| nextSymb board == "X" = minimum
| nextSymb board == "O" = maximum
nextMove = (makeNextMove . expandGameTree)
numerate :: Board -> Board
numerate board
| length (findIndices (==' ') board) == 0 = board
| otherwise = numerate $ replace board (idx board) (show (idx board))
where idx board = (findIndices (==' ') board) !! 0
makeNextMove (Node a (x:xs)) = getBoardFromTree ((x:xs) !! (maxIndex (rates (x:xs))))
where maxIndex xs = head $ filter ((== maximum xs) . (xs !!)) [0..]
rates (x:xs) = [evalState c | c <- (x:xs)]
getBoardFromTree (Node a (x:xs)) = a
getBoardFromTree (Node a []) = a
showBoard :: Board -> String
showBoard board = "|" ++ (row 0) ++ "|\n|" ++ (row 1) ++ "|\n|" ++ (row 2) ++ "|\n"
where numeratedBoard = numerate board
row i = takeRange numeratedBoard [3*i..(3*i)+2]
indicateAllowedMoves :: Board -> [Board]
indicateAllowedMoves board
| winingBoard board = []
| otherwise = [replace board i symb | i <- [0..8], board !! i == ' ']
where symb = nextSymb board
replace :: String -> Pos -> String -> String
replace str idx char = take idx str ++ char ++ drop (idx+1) str
isAllowedMove :: Board -> Pos -> Bool
isAllowedMove board pos = elem next allowed
where next = insertInBoardStr board pos
allowed = indicateAllowedMoves board
winingRow :: String -> Bool
winingRow row
| row == "XXX" || row == "OOO" = True
| otherwise = False
rows :: Board -> [String]
rows board = row ++ cols ++ diag
where row = [takeRange board [i..(i+2)] | i <- [0,3,6]]
cols = [takeRange board $ map (+i) [0,3,6] | i <- [0..2]]
diag = [takeRange board [0,4,8]] ++ [takeRange board [2,4,6]]
winingBoard :: Board -> Bool
winingBoard board = any winingRow $ rows board
--takeRange :: String -> [Int] -> String
takeRange str range = [str !! i | i <- range]
insertInBoardStr :: Board -> Pos -> Board
insertInBoardStr board pos = replace board pos symb
where symb = nextSymb board
nextSymb :: Board -> String
nextSymb board
| countLetters board 'X' == countLetters board 'O' = "X"
| otherwise = "O"
where countLetters str c = length $ filter (== c) str
curSymb :: Board -> String
curSymb board
| nextSymb board == "X" = "O"
| otherwise = "X"
isEquivivalent :: String -> String -> Bool
isEquivivalent row1 row2
| row1 == row2 = True
| row1 == reverse row2 = True
| otherwise = False
play board = do
putStr "\ESC[2J"
putStrLn $ showBoard board
if (winingBoard board) == True then
putStrLn ("Player: " ++ (curSymb board) ++ " wins")
else
putStrLn "Make your move: "
input <- getLine
let pos = (read input :: Int)
if (isAllowedMove board pos) == True then
play (nextMove (insertInBoardStr board pos))
else
play board
main = play initialBoardStr