我在Haskell写了一个简单的tic-tac-toe程序.它在命令行上运行,具有一个和两个播放器模式,并在你对抗它时实现一个minimax算法.
我习惯用OO语言编写适当的代码,但Haskell对我来说是新手.这段代码工作得相当好,但似乎很难读(甚至对我来说!).关于如何使这个代码更多的任何建议......哈斯克利安?
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"
编辑:特别感谢@Chi和@Caridorc,我做了以下更改.还将考虑和更新进一步的建议
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 _ -> 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]] displayBoard :: Board -> IO () displayBoard board = do mapM_ print board otherPlayer :: Player -> Player otherPlayer PX = PO otherPlayer PO = PX gameLoop :: Int -> Board -> Player -> IO () gameLoop noOfPlayers board player = do case detectWin board of Just res -> endgame board res Nothing -> case noOfPlayers of 1 -> case player of PX -> enterMove 1 board player PO -> enterBestMove board PO 2 -> enterMove 2 board player enterMove :: Int -> Board -> Player -> IO () enterMove noOfPlayers board player = do displayBoard board case noOfPlayers of 1 -> do putStrLn ("Make your move. (A-I)") 2 -> 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` (concat board) then do gameLoop noOfPlayers (newBoard (read (map toUpper move) :: Square) player board) (otherPlayer player) 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) -- minimax algorithm 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 = case detectWin b of Nothing -> snd (findBestMove (otherPlayer player) b) Just x -> x boards :: Player -> Board -> [(Square, Board)] boards player board = [(sq, newBoard sq player board) | sq <- concat board, sq /= X, sq /=O] 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` (triplets board) = Just XWin | [O,O,O] `elem` (triplets board) = Just OWin | [X,X,X,X,X,O,O,O,O] == (sort $ concat board) = Just Tie | otherwise = Nothing triplets :: Board -> [[Square]] triplets board = board ++ transpose board ++ [diagonal1] ++ [diagonal2] where flat = concat board diagonal1 = [flat !! 0, flat !! 4, flat !! 8] diagonal2 = [flat !! 2, flat !! 4, flat !! 6] endgame :: Board -> Result -> IO () endgame board result = do displayBoard board putStrLn $ endGameMessage result putStrLn "Want to play again? (y/n)" again <- getLine if again `elem` ["y", "Y", "yes", "Yes", "YES"] then gameSelect else do putStrLn "Goodbye" endGameMessage :: Result -> String endGameMessage result | result `elem` [XWin, OWin] = winnerNotice ++ loserNotice | otherwise = "The game is a tie\n" ++ "You are both losers! Ugh!" where winner = case result of XWin -> PX OWin -> PO winnerNotice = "The game is over, and " ++ show winner ++ " wins!\n" loserNotice = (show $ otherPlayer winner) ++ " is a loser lol"
chi.. 14
代码风格通常是个人偏好的问题,在Haskell中可以说比其他语言更具有"标准"风格指南.不过,这里有一些随机的建议.
不要过度缩进case
:只需使用另一行
case gameMode of "1" -> onePlayerMode "2" -> twoPlayerMode gameMode -> gameSelect
VS
case gameMode of "1" -> onePlayerMode "2" -> twoPlayerMode gameMode -> gameSelect
甚至
case gameMode of "1" -> onePlayerMode "2" -> twoPlayerMode _ -> gameSelect
case
通常更喜欢if .. == Constructor
:
if player == PX then enterMove 1 board player else enterBestMove board PO
VS
case player of PX -> enterMove 1 board player PY -> enterBestMove board PO
我强烈建议不要使用部分功能fromJust
,因为如果您忘记Nothing
事先检查,它们可能会导致程序崩溃.存在更安全的替代方案,从不会造成这种崩溃 - 减轻程序员的负担.
if detectWin b == Nothing then snd (findBestMove (if player == PX then PO else PX) b) else fromJust $ detectWin b
VS
case detectWin b of Nothing -> snd $ findBestMove (if player == PX then PO else PX) b Just x -> x
要么
fromMaybe (snd $ findBestMove (if player == PX then PO else PX) b) $ detectWin b
尝试分解常用功能.例如
nextPlayer PX = PO nextPlayer PO = PX
可以代替使用
if player == PX then PO else PX
do
只有一个声明时不需要:
if noOfPlayers == 1 then do putStrLn ("Make your move. (A-I)") -- no need for parentheses here else do putStrLn (show player ++ ", it's your turn. (A-I)")
自从你where
在标题中提到以来,让我说where
一般情况下我对此感到复杂.我知道我经常倾向于避免where
赞成let
,但这种感觉并没有与其他许多Haskeller分享,所以请小心谨慎.
就个人而言,我倾向于将我的where
用途限制为单行:
foo = f x y where x = ... y = ...
特别是在do
可能跨越几行的块中,我更喜欢let
s:
foo = do line line using x -- what is x ??!? line ... line where x = ... -- ah, here it is
VS
foo = do line let x = ... line using x line ... line
但是,请随意采用您觉得更具可读性的风格.
另外不要忘记添加一些评论,正如@mawalker指出的那样.一些定义很明显,不需要任何解释.其他人可以从解释目的的几行中受益.
代码风格通常是个人偏好的问题,在Haskell中可以说比其他语言更具有"标准"风格指南.不过,这里有一些随机的建议.
不要过度缩进case
:只需使用另一行
case gameMode of "1" -> onePlayerMode "2" -> twoPlayerMode gameMode -> gameSelect
VS
case gameMode of "1" -> onePlayerMode "2" -> twoPlayerMode gameMode -> gameSelect
甚至
case gameMode of "1" -> onePlayerMode "2" -> twoPlayerMode _ -> gameSelect
case
通常更喜欢if .. == Constructor
:
if player == PX then enterMove 1 board player else enterBestMove board PO
VS
case player of PX -> enterMove 1 board player PY -> enterBestMove board PO
我强烈建议不要使用部分功能fromJust
,因为如果您忘记Nothing
事先检查,它们可能会导致程序崩溃.存在更安全的替代方案,从不会造成这种崩溃 - 减轻程序员的负担.
if detectWin b == Nothing then snd (findBestMove (if player == PX then PO else PX) b) else fromJust $ detectWin b
VS
case detectWin b of Nothing -> snd $ findBestMove (if player == PX then PO else PX) b Just x -> x
要么
fromMaybe (snd $ findBestMove (if player == PX then PO else PX) b) $ detectWin b
尝试分解常用功能.例如
nextPlayer PX = PO nextPlayer PO = PX
可以代替使用
if player == PX then PO else PX
do
只有一个声明时不需要:
if noOfPlayers == 1 then do putStrLn ("Make your move. (A-I)") -- no need for parentheses here else do putStrLn (show player ++ ", it's your turn. (A-I)")
自从你where
在标题中提到以来,让我说where
一般情况下我对此感到复杂.我知道我经常倾向于避免where
赞成let
,但这种感觉并没有与其他许多Haskeller分享,所以请小心谨慎.
就个人而言,我倾向于将我的where
用途限制为单行:
foo = f x y where x = ... y = ...
特别是在do
可能跨越几行的块中,我更喜欢let
s:
foo = do line line using x -- what is x ??!? line ... line where x = ... -- ah, here it is
VS
foo = do line let x = ... line using x line ... line
但是,请随意采用您觉得更具可读性的风格.
另外不要忘记添加一些评论,正如@mawalker指出的那样.一些定义很明显,不需要任何解释.其他人可以从解释目的的几行中受益.