jeudi 7 janvier 2016

Best Practices for Where clauses

I wrote a simple tic-tac-toe program in Haskell. It runs on the command line, has a one and two player mode, and implements a minimax algorithm when you play against it.

I'm used to writing proper code in OO languages, but Haskell is new to me. This code works reasonably well, but seems hard to read (even to me!). Any suggestions on how to make this code more...Haskellian?

import Data.List
import Data.Char
import Data.Maybe
import Control.Monad

data Square = A | B | C | D | E | F | G | H | I | X | O deriving (Read, Eq, Ord)
instance Show Square where
   show A = "a" 
   show B = "b" 
   show C = "c" 
   show D = "d" 
   show E = "e" 
   show F = "f" 
   show G = "g" 
   show H = "h" 
   show I = "i" 
   show X = "X" 
   show O = "O"
type Row = [Square]
type Board = [Row]
data Player = PX | PO deriving (Read, Eq)
instance Show Player where
   show PX = "Player X"
   show PO = "Player O"
data Result = XWin | Tie | OWin deriving (Read, Show, Eq, Ord) 

main :: IO ()
main = do
    putStrLn "Let's play some tic tac toe!!!"
    putStrLn "Yeeeaaaaaahh!!!"
    gameSelect

gameSelect :: IO ()
gameSelect = do
    putStrLn "Who gonna play, one playa or two??? (Enter 1 or 2)"
    gameMode <- getLine
    case gameMode of "1" -> onePlayerMode
                     "2" -> twoPlayerMode
                     gameMode -> gameSelect
    where onePlayerMode = do
             putStrLn "One playa"
             putStrLn "Cool!  Get ready to play...AGAINST MY INVINCIBLE TIC TAC TOE AI!!!!! HAHAHAHA!!!"
             gameLoop 1 emptyBoard PX
          twoPlayerMode = do
             putStrLn "Two players"
             gameLoop 2 emptyBoard PX
          emptyBoard = [[A,B,C],[D,E,F],[G,H,I]]

gameLoop :: Int -> Board -> Player -> IO ()
gameLoop noOfPlayers board player = do
    case detectWin board of Just XWin -> endgame board XWin
                            Just OWin -> endgame board OWin
                            Just Tie -> endgame board Tie
                            Nothing -> if noOfPlayers == 1
                                       then if player == PX 
                                            then enterMove 1 board player 
                                            else enterBestMove board PO
                                       else enterMove 2 board player

enterMove :: Int -> Board -> Player -> IO () 
enterMove noOfPlayers board player = do
     displayBoard board
     if noOfPlayers == 1
     then do putStrLn ("Make your move. (A-I)")
     else do putStrLn (show player ++ ", it's your turn. (A-I)")
     move <- getLine
     print move
     if not $ move `elem` ["a","b","c","d","e","f","g","h","i"]
         then do
            putStrLn $ move ++ " is not a move, doofus"
            gameLoop noOfPlayers board player
         else if (read (map toUpper move) :: Square) `elem` [ sq | sq <- concat board]
            then do
               gameLoop noOfPlayers (newBoard (read (map toUpper move) :: Square) player board) (if player == PX then PO else PX)
            else do
               putStrLn "That square is already occupied"
               gameLoop noOfPlayers board player

enterBestMove :: Board -> Player -> IO ()
enterBestMove board player = gameLoop 1 (newBoard bestmove player board) PX
    where bestmove = fst $ findBestMove PO board
          findBestMove :: Player -> Board -> (Square, Result)
          findBestMove player board
            | player == PO = findMax results
            | player == PX = findMin results
            where findMin = foldl1 (\ acc x -> if snd x < snd acc then x else acc)
                  findMax = foldl1 (\ acc x -> if snd x > snd acc then x else acc)
                  results = [ (sq, getResult b) | (sq, b) <- boards player board ]
                  getResult b = if detectWin b == Nothing 
                                then snd (findBestMove (if player == PX then PO else PX) b) 
                                else fromJust $ detectWin b
                  boards :: Player -> Board -> [(Square, Board)]
                  boards player board = [(sq, newBoard sq player board) | sq <- concat board, sq /= X, sq /=O]

displayBoard :: Board -> IO ()
displayBoard board = do
    mapM_ print board

newBoard :: Square -> Player -> Board -> Board
newBoard move player board = [ [if sq == move then mark else sq | sq <- row] | row <- board]
    where mark = if player == PX then X else O

detectWin :: Board -> (Maybe Result)
detectWin board
   | [X,X,X] `elem` board ++ transpose board = Just XWin
   | [X,X,X] `elem` [diagonal1 board, diagonal2 board] = Just XWin
   | [O,O,O] `elem` board ++ transpose board = Just OWin
   | [O,O,O] `elem` [diagonal1 board, diagonal2 board] = Just OWin
   | [X,X,X,X,X,O,O,O,O] == (sort $ concat board) = Just Tie
   | otherwise = Nothing
   where
     diagonal1 :: Board -> [Square]
     diagonal1 bs = bs!!0!!0 : bs!!1!!1 : bs!!2!!2 : []
     diagonal2 :: Board -> [Square]
     diagonal2 bs = bs!!0!!2 : bs!!1!!1 : bs!!2!!0 : []

endgame :: Board -> Result -> IO ()
endgame board result = do
    displayBoard board
    if result `elem` [XWin, OWin]
        then 
            let player = if result == XWin then PX else PO
            in do 
                putStrLn ("The game is over, and " ++ show player ++ " wins!")
                putStrLn ((if player == PX then show PO else show PX) ++ " is a loser lol")
        else do
            putStrLn "The game is a tie"
            putStrLn "You are both losers!  Ugh!"
    putStrLn "Want to play again? (y/n)"
    again <- getLine
    if again `elem` ["y", "Y", "yes", "Yes", "YES"] 
        then gameSelect 
        else do
            putStrLn "Goodbye"

Aucun commentaire:

Enregistrer un commentaire