HiveBrain v1.2.0
Get Started
← Back to all entries
patternMinor

Rock Paper Scissors game

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
scissorspapergamerock

Problem

This is a very simple rock paper scissors game implemented in Haskell.

import System.IO (hFlush, stdout)
import System.Random (getStdGen, randomR)

data RPS = Rock | Paper | Scissors deriving (Eq, Show, Read)

instance Ord RPS where
    Rock `compare` Scissors = GT
    Scissors `compare` Rock = LT

    Paper `compare` Rock    = GT
    Rock `compare` Paper    = LT

    Scissors `compare` Paper = GT
    Paper    `compare` Scissors = LT
    _ `compare` _ = EQ

prompt :: String -> IO String
prompt text = do
    putStr text
    hFlush stdout
    getLine

main = do

    gen  Rock
                          1 -> Paper
                          2 -> Scissors
        humanchoice' = read humanchoice :: RPS

    case compchoice' `compare` humanchoice' of
        EQ -> putStrLn "Tie."
        GT -> putStrLn "You Lose :("
        LT -> putStrLn "You Win! :)"


All kinds of reviews are welcome, but I am specifically interested in knowing what kind of beginner mistakes I might have done. For example, are there things that I could simplify, or are things that you wouldn't see in a intermediate level haskell code?

Solution

As @200_success mentions in a comment, this isn't a kosher use of Ord. I believe though that it is the property of antisymmetry that is broken, not transitivity. \$Rock \leq Paper \leq Scissors \leq Rock\$ means that by transitivity \$Paper \leq Rock\$, which violates antisymmetry since \$Rock \neq Paper\$. This may seem academic, but many library functions will break or produce incoherent results given your instance. Try to guess what sort [Rock, Scissors, Paper] will return. Don't get in the habit of writing bad instances just because you like their namespace.

What you should do instead is just write a grab bag of pure functions and domain-specific datatypes. This would look something like—

data Winner = PlayerOne | PlayerTwo | Tie

shoot :: RPS -> RPS -> Winner
shoot Rock Rock     = Tie
shoot Rock Paper    = PlayerTwo
shoot Rock Scissors = PlayerOne
-- ...


All top-level definitions should have a type signature always, full stop. main is always main :: IO ().

Since you're not doing anything fancy with your StdGen, just use the *IO functions.

main = do
          humanchoice <- prompt "Rock, Paper, or Scissors: "

          compchoice <- randomRIO (0,2)
          -- ...


One typeclass it would be appropriate to instance is Random, so that you can have a more semantic computer-player interface. I'll toss in Enum and Bounded instances because they make the Random instance more future-proof.

data RPS = Rock | Paper | Scissors
    deriving (Eq, Show, Read, Enum, Bounded)

instance Random RPS where
    random g = randomR (minBound, maxBound) g

    randomR (lo, hi) g = first toEnum $ randomR (fromEnum lo, fromEnum hi) g
        where first f (a, b) = (f a, b)


The last thing I would do would be to factor out the result message code from main, it's a little repetitive having putStrLn there three times, and you can conceive of various scenarios like making this two player where you'd want to be able to easily swap around result message generators.

humanTwo :: Winner -> String
humanTwo PlayerOne = "You lose :("
humanTwo PlayerTwo = "You win! :)"
humanTwo Tie       = "Tie."


And with all of those changes you end up with a main function like—

main :: IO ()
main = do
          humanchoice <- fmap read $ prompt "Rock, Paper or Scissors: "
          compchoice <- randomIO
          let winner = shoot compchoice humanchoice
          putStrLn $ humanTwo winner

Code Snippets

data Winner = PlayerOne | PlayerTwo | Tie

shoot :: RPS -> RPS -> Winner
shoot Rock Rock     = Tie
shoot Rock Paper    = PlayerTwo
shoot Rock Scissors = PlayerOne
-- ...
main = do
          humanchoice <- prompt "Rock, Paper, or Scissors: "

          compchoice <- randomRIO (0,2)
          -- ...
data RPS = Rock | Paper | Scissors
    deriving (Eq, Show, Read, Enum, Bounded)

instance Random RPS where
    random g = randomR (minBound, maxBound) g

    randomR (lo, hi) g = first toEnum $ randomR (fromEnum lo, fromEnum hi) g
        where first f (a, b) = (f a, b)
humanTwo :: Winner -> String
humanTwo PlayerOne = "You lose :("
humanTwo PlayerTwo = "You win! :)"
humanTwo Tie       = "Tie."
main :: IO ()
main = do
          humanchoice <- fmap read $ prompt "Rock, Paper or Scissors: "
          compchoice <- randomIO
          let winner = shoot compchoice humanchoice
          putStrLn $ humanTwo winner

Context

StackExchange Code Review Q#88395, answer score: 5

Revisions (0)

No revisions yet.