Rock Paper Scissors in Haskell
2014-05-01 18:00
Rock Paper Scissors in Haskell
This is a simple Rock Paper Scissors game in Haskell, it was made as an exercise in learning Haskell, especially the syntax, data constructors, and IO.
Program Design
This is a very simple program. The parts of the code that has to do with the logic is actually very short and simple, because RPS has very simple rules:
-- Player or AI can make any of these moves each turn
data Move = Rock | Paper | Scissors deriving (Show)
-- Player is the current person playing, and AI is our intelligent program!
data Winner = Player | AI | Draw deriving (Show)
-- use a throwaway function getWinner' because
-- we want to be clear that `user' is the first parameter
getWinner :: Move -> Move -> Winner
= getWinner' user ai
getWinner user ai where getWinner' Rock Paper = AI
Rock Scissors = Player
getWinner' Paper Scissors = AI
getWinner' Paper Rock = Player
getWinner' Scissors Rock = AI
getWinner' Scissors Paper = Player
getWinner' = Draw
getWinner' _ _
-- a heuristics
makeAIMove :: Move
= Paper makeAIMove
I think the getWinner
method is quite ugly, but I haven’t thought of a good way to write it. Maybe I can differentiate the Move
to UserMove
or AIMove
, something along those lines, so I can make use of the type checker to ensure what that the arguments are fed correctly.
The AI is currently really smart, reason is that I’m not concerned with learning about random numbers now, I just want this to be an exercise in writing Haskell, doing IO and some other stuff.
Interaction with User
This is a text-based game, so there are quite a few instances where the program prints a line, and gets another line of input from the user, which resulted in me writing this helper:
getResponse :: String -> IO String
= putStrLn s >> getLine getResponse s
The >>
operator basically discards whatever output putStrLn s
gives. This is required because getLine
does not take any arguments.
Initially I wanted to define all the interaction strings else where, thereby removing magic constants, and also stick to the DRY principle:
welcomeMessage :: String
= "Lets play Rock Paper Scissors" welcomeMessage
But I found this to be too verbose and distracting. I decided that instead of doing this, I should refactor it such that a string is only used at a single place. I made good progress with that, and the only String that is repeated is the prompt/insructions.
Handling invalid input
I handled invalid input using Either
and recursion. Either
allows me to determine if the user’s input was valid or not, and this check is done by convertToMove
:
-- handle invalid cases
convertToMove :: String -> Either String Move
= convert $ map toLower input
convertToMove input where convert "r" = Right Rock
"s" = Right Scissors
convert "p" = Right Paper
convert = Left "I don't know that move!" convert _
The above function is called by another function, getValidMove
, whos job is to get a valid move from the user. By pattern matching against the value of convertToMove
, it can either display the error message when it is a Left
, and recursively call itself, or it will return the Move
.
getValidMove :: IO Move
= do
getValidMove <- convertToMove <$> getResponse "What's your move?"
userMove case userMove of
Left msg -> do
putStrLn msg
putStrLn "R for rock, P for paper, S for scissors."
getValidMoveRight m -> return m
This is called from within the game
function. game
runs a single round of RPS, which is marked by an outcome, or a Winner
.
game :: IO ()
= do
game <- getValidMove
userMove
play userMove makeAIMove<- getResponse "Continue? Y/N"
choice
continue choicewhere play m ai = putStrLn $ announceWinner $ getWinner m ai
"y" = game
continue = do putStrLn "Thanks for playing!" continue _
announceWinner
’s role is to give the correct String output given an outcome of a game. This is to showcase how creative I can be with messages :P
-- Gives the correct anouncement String for different outcomes of the game
announceWinner :: Winner -> String
AI = "The AI won :)"
announceWinner Draw = "It was a draw!"
announceWinner Player = "Yay you won!" announceWinner
As explained above, getValidMove
will always give a IO Move
, so the value of userMove
is always a Move
. This delegation of retrieving and validating the user’s input for a move simplifies this function. I should adopt this way of building and composing functions more.
The entire program runs in the main
function:
main :: IO ()
= do
main -- Message to user when the user first runs this program
putStrLn "Lets play Rock Paper Scissors"
-- Instructions on ohw to play this game
putStrLn "R for rock, P for paper, S for scissors."
game
This function is extremely simple, it just prints a welcome message and instructions for new players.
So that’s it, my first haskell program from scratch! Pretty happy with it now, next idea I will try is probably another simple game, like tic tac toe or some.