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
$
or.
operators than parenthesis? I kind of struggle with trying to use composition with functions when there is more than one argument.The nested
do
blocks: 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 thedo
keyword, the compiler was interpreting thatputStrLine
was 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