5
\$\begingroup\$

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 <- getStdGen

    humanchoice <- prompt "Rock, Paper, or Scissors: "

    let (compchoice, _) = randomR (0,2) gen
        compchoice' = case (compchoice :: Int) of
                          0 -> 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?

\$\endgroup\$
0

2 Answers 2

5
\$\begingroup\$

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
\$\endgroup\$
5
  • \$\begingroup\$ I think let (compchoice, _) = randomRIO (0,2) gen should be compchoice <- randomRIO (0,2). According to hackage.haskell.org/package/random-1.0.0.3/docs/… randomRIO doesnt get a generator and doesnt return one \$\endgroup\$
    – yasar
    Apr 30, 2015 at 18:34
  • \$\begingroup\$ Any suggestions to improve the shoot function? It can get very large especially if we play Sheldon's variant: bigbangtheory.wikia.com/wiki/Rock_Paper_Scissors_Lizard_Spock \$\endgroup\$ Apr 30, 2015 at 22:12
  • \$\begingroup\$ @yasar D'oh! You're completely right, I switched out randomIO for randomRIO without adjusting any of the context. \$\endgroup\$
    – bisserlis
    Apr 30, 2015 at 22:53
  • 1
    \$\begingroup\$ @GeneralBecos Interesting question. Provided an Enum RPS instance and a specifically ordered RPS definition, we can reduce the problem to modular arithmetic. E.g., data RPS = Rock | Paper | Scissors | Lizard | Spock deriving Enum shoot left right | result == 0 = Tie; | odd result = PlayerOne; | even result = PlayerTwo; where result = mod ((fromEnum left + 1) - (fromEnum right + 1)) 5. \$\endgroup\$
    – bisserlis
    Apr 30, 2015 at 23:40
  • 1
    \$\begingroup\$ You can use random-derive to automatically create a Random instance defined in terms of Enum and Bounded. \$\endgroup\$ May 3, 2015 at 20:36
5
\$\begingroup\$

Just a footnote to the already excellent answer from bisserlis above. Started with a short discussion arising from the comments. I would implement the shoot function as below. It is simpler to understand (for me) + easier to extend to Rock-Paper-Scissors-Lizard-Spock

beats :: RPS -> RPS -> Bool
beats Paper    Rock     = True
beats Rock     Scissors = True
beats Scissors Paper    = True
beats _        _        = False

shoot :: RPS -> RPS -> Winner
shoot a b  
        | a == b      = Tie
        | a `beats` b = PlayerOne
        | otherwise   = PlayerTwo
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Guard notation would be more idiomatic and tidier than if-then-else if-then-else. \$\endgroup\$ May 4, 2015 at 16:15
  • 1
    \$\begingroup\$ Oh nice, that's very clever. I might even go so far as to define beats infix, e.g., Paper `beats` Rock = True. \$\endgroup\$
    – bisserlis
    May 5, 2015 at 0:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.