I am starting to learn Haskell, and following Dr. Erik Meijer lectures online, he suggests as an exercise to implement the game of Nim in Haskell, so I implemented a really basic game for two players (no computer player).
I can imagine that this code is far from being good Haskell, and I accept all possible criticism and suggestions about it (if you can link me to other lectures/talks/guidelines, I appreciate it), but there are some aspects that I would love to ask you:
The comments: I am not really sure how am I supposed to document the code in Haskell, so I just tried to explain each of the methods before its declaration. If there is another convention, please let me know.
The parenthesis: Should I use more the
.operators than parenthesis? I kind of struggle with trying to use composition with functions when there is more than one argument.
doblocks: I fixed an error doing it, but it feels incredibly hacky and I don't really understand why the compiler is not able to understand the code when there is more than one line inside an if clause (doesn't matter which one, both were problematic). If I don't put the
dokeyword, the compiler was interpreting that
putStrLinewas receiving 4 arguments: its real argument and the next function call (
turn board player, for instance). Why did it happen?
This is the code itself:
import qualified Data.Sequence as Seq import qualified Data.Foldable as Fol import qualified Data.List as List import Data.Maybe -- Player type is used to take control of the turns and who wins data Player = One | Two deriving (Show, Eq) -- Change player returns the player that is NOT the one passed as argument change :: Player -> Player change One = Two change Two = One -- Board is an alias for a Sequence of Ints, as they allow us to update single -- elements in easily. type Board = Seq.Seq Int -- This is the initial board structure initialBoard :: Board initialBoard = Seq.fromList [5, 4, 3, 2, 1] -- The move method checks if the a movement can be executed and returns the -- updated board in case it is possible move :: Board -> (Int, Int) -> Maybe Board move board (row, stars) | and [(Seq.index board row) >= stars, row < 5] = Just (Seq.adjust (\x -> x - stars) row board) | otherwise = Nothing -- The display methods transforms a Board into a nice, enumerated String of -- asterisks display :: Board -> String display board = List.intercalate "\n" (zipWith (++) numbers (stars board)) where numbers = ["1. ", "2. ", "3. ", "4. ", "5. "] stars board = [(concat . take n) (repeat "* ") | n <- Fol.toList board] -- The next methods are the ones that control IO main :: IO () main = nim -- Main method welcomes the player, displays the initial board and calls the -- first turn nim :: IO () nim = do putStrLn "Welcome to nim!" putStrLn (display initialBoard) turn initialBoard One -- The turn method displays the player and asks for a movement, then checks if -- there was a problem performing that movement and continues the game. This is -- the main game loop turn :: Board -> Player -> IO () turn board player = do putStrLn ("\nIt's your turn, Player " ++ (show player) ++ "!") putStrLn "Choose a row to remove stars!" row <- getLine putStrLn "How many stars do you want to remove?" stars <- getLine let newBoard = move board ((read row) - 1, read stars) if newBoard == Nothing then do putStrLn "Not valid movement" turn board player else isOver (fromJust newBoard) (change player) -- isOver checks if the Board is empty, and checks whether the game is over or -- the next turn must be called isOver :: Board -> Player -> IO() isOver board player = do if board == Seq.fromList [0, 0, 0, 0, 0] then putStrLn ("Congratulations, Player " ++ (show player) ++ ", you win!") else do putStrLn "" putStrLn (display board) turn board player