Rock Paper Scissors in Elm. A beginner friendly implementation.

This is the classic Rock Paper Scissors game on steroids. There are two more choices that I’ve never heard before – Lizard and Spock – and the implementation is a bit more spicy because we’re going to grab random numbers from random.org so that we get to explore the Http module as well.

This is based off the pux-rock-paper-scissors game made by @spicydonuts in Purescript. If you’re interested in that language you should definitely give Pux a go because it has an almost 1:1 reimplementation of the Elm architecture and I find it pretty awesome.

What we are going to build

Here’s a working demo of the finished project:

Rock paper scissors deathmatch

Just kidding, here’s the finished project. Please enjoy this game responsibly and reach out if you become a world champion.

It’s very simple, in about 300 LOC it should be pretty easy to follow along.

Disclaimer: If you’re looking for a more in depth tutorial, check out Building a memory game in Elm from scratch that covers some basic ground that will be assumed here.

Types first

All right let’s get down to it. First of all we’ll want to encode all the possible choices, so we’ll create a new type Choice for this purpose.

type Choice
    = Rock
    | Paper
    | Scissors
    | Lizard
    | Spock

Next up, we want to define the different outcomes of a match. That is, whether the player or the computer won, or it’s a tie.

type GameResult
    = PlayerWins
    | ComputerWins
    | Tie

We’re going to keep the player score that it’s going to be updated after every match. This is just an Int but to make our code more explicit, let’s add a type alias.

type alias Score =
    Int

Our Model defines 3 states:

  • The player has to make a choice, so only the score will be stored in the model.
  • We have a player choice and we’re waiting for the computer to generate a random one.
  • The game is over, we have both choices, the game result and the updated score.

Let’s write this down

type Model
    = PlayerTurn Score
    | ComputerTurn Score Choice
    | GameOver Score Choice Choice GameResult

Now to the Msg! It’s pretty straightforward.

type Msg
    = NoOp
    | ChoiceClicked Choice
    | RandomNumberReceived (Result Http.Error Int)
    | Reset

The reason for the Result Http.Error Int type is pretty straightforward . Given that there’s an http request involved (remember, we’re grabbing random numbers from an online api) we have to account for errors. If all it’s fine we’ll get back an Int.

A little tour of HTTP requests and JSON decoding

Next up, a function that creates a Task describing the http request we want the Elm runtime to make. Just a reminder, in Elm there’s no way to perform side effects inside our code. We can only describe what needs to happen by returning a Command (Cmd) in our update function.

randomNumberUrl =
    "https://www.random.org/integers/?num=1&min=1&max=5&col=1&base=10&format=plain&rnd=new"


grabRandomNumber : Cmd Msg
grabRandomNumber =
    Http.get randomNumberUrl Json.Decode.int
        |> Http.send RandomNumberReceived

This might not seem like a lot, but it’s helpful to go over what’s happening. randomNumberUrl is easy, it’s just the api endpoint that will return a integer between 1 and 5. Http.get is a function provided by the Http module that given a URL and a Decoder, will return a Request.

Now, JSON encoding and decoding is an hot topic among the Elm community and for a reason. It’s not always easy to wrap your head around code that performs these kind of operations.

But let’s take a step back. Why do we need decoding in the first place? What does it even mean? When we make http requests, we are talking to the external world (meaning we’re leaving the peaceful and pure land of Elm) and we are getting back some kind of data. Being Elm a strongly typed language, we have to know at compile time what kind of data we’re dealing with. This might seem like a burden at first, because every little detail of the (possibly complicated) JSON data we’re getting back from our api, needs to be explicitely described in Elm, otherwhise that data cannot be consumed.

If you think this sucks and it’s just a waste of time, take a deep breath and appreciate how awesome this really is.

You can be absolutely sure at compile time that whatever data you’re getting back from the server, will have the correct shape that your code is expecting it to. Or you’ll find yourself with an error that, by the way, you’ll be forced to handle by the compiler. So there’s no way your program is going to blow up at runtime because you were expecting the description field to be a String but the api is returning null! How cool is that?

In order to have this peace of mind we need to spend a little bit of time to tell the compiler how to transform the JSON data that we receive from the outside into a valid Elm type. You can go pretty crazy when decoding, by using nice stuff like Maybe or adding field conditionally, based on the presence of another field or the value of some other.

So, this is why we need a Decoder.

Decoders are provided in the Json module that comes shipped in core. You can find pretty well written tutorials on decoders elsewhere, but I just want to briefly point out what our simple decoder is doing. The Json.Decode.int decoder essentially takes a JSON number and converts it into an Int so that we can work with it. There’s no black magic, that’s all is happening here.

Moving on, we’re piping our Request into a call to Http.send that will effectively create a Cmd describing the http request we want to make and what Msg will be fed back into the update function once the request completes. Note that the message will have to accept a Result as an argument, because we have to take failure into account and the Result type allows us to do just that (either we get an error or the data we wanted).

Here are a few type signatures in case you’re wondering.

Http.get : String -> Decoder a -> Request a
Http.send : (Result Error a -> msg) -> Request a -> Cmd msg

type Decoder a
Json.Decode.int : Decoder Int

The Update function

Let’s begin with a simple function that returns the initial state of the game. We simply say that it’s the player’s turn and that the score is zero.

createModel : ( Model, Cmd Msg )
createModel =
    let
        model =
            PlayerTurn 0
    in
        ( model, Cmd.none )

We also need a List (Choice, Choice) that holds all the possible winning pairs that we will use to determine the winner.

winConditions : List ( Choice, Choice )
winConditions =
    [ ( Rock, Scissors )
    , ( Rock, Lizard )
    , ( Paper, Rock )
    , ( Paper, Spock )
    , ( Scissors, Paper )
    , ( Scissors, Lizard )
    , ( Lizard, Paper )
    , ( Lizard, Spock )
    , ( Spock, Rock )
    , ( Spock, Scissors )
    ]

Here’s how we do it. Given two values of type Choice, check if the first one wins the game.

wins : Choice -> Choice -> Bool
wins a b =
    let
        match =
            (==) ( a, b )
    in
        List.any match winConditions

List.any is pretty cool because it will stop at the first match, so it won’t iterate over the full list. We are creating a simple helper function match that uses the == function in prefix position (note the () brackets) and partially applies it with the two choices we got as input (a and b).

We also want to know the result of a game.

determineResult : Choice -> Choice -> GameResult
determineResult player computer =
    if player == computer then
        Tie
    else if wins player computer then
        PlayerWins
    else
        ComputerWins

Pretty straightforward stuff.

Now we’ll need a way to convert the Int we’re getting back from our random number generator into a Choice.

intToChoice : Int -> Choice
intToChoice n =
    case n of
        1 ->
            Rock

        2 ->
            Paper

        3 ->
            Scissors

        4 ->
            Lizard

        5 ->
            Spock

        _ ->
            intToChoice <| (rem n 5) + 1

Note the _ branch. This is needed because the compiler tells us that there are many more numbers we should account for besides those in the range of our interest (1-5). A very simple and acceptable solution would be to pick a fixed choice and return that for any other integer greater than 5. But we can be a little bit smarter and feed back the unexpected integer we received by making sure that it’s between 1 and 5, in order to keep some sort of randomness in place.

Let’s create an helper function that updates the current score given a game result.

updateScore : Score -> GameResult -> Score
updateScore score result =
    case result of
        Tie ->
            score

        PlayerWins ->
            score + 1

        ComputerWins ->
            score - 1

Now a function that we will use to update the game state and return the Cmd that generates a random number whenever it’s appropriate (after the player’s turn). Note that we return the model unchanged if we’re in a state where we’re not expecting the player to make their choice. Think for example of a slow http request that would allow the player to click on a different choice button. That would dispatch another ChoiceClicked message inside our update function, but given that we know it’s the computer’s turn, we don’t do anything and wait for the random number to arrive.

Also, we could improve our game further by making sure the buttons in the UI are disabled when we are in the ComputerTurn state.

updatePlayerChoice : Choice -> Model -> ( Model, Cmd Msg )
updatePlayerChoice choice model =
    case model of
        ComputerTurn _ _ ->
            model ! []

        PlayerTurn score ->
            ComputerTurn score choice ! [ grabRandomNumber ]

        GameOver score _ _ _ ->
            ComputerTurn score choice ! [ grabRandomNumber ]

Here’s our helper function to deal with the random number that will be transformed into a valid computer choice.

updateComputerChoice : Int -> Model -> ( Model, Cmd Msg )
updateComputerChoice n model =
    case model of
        ComputerTurn score player ->
            let
                computer =
                    intToChoice n

                result =
                    determineResult player computer

                newScore =
                    updateScore score result
            in
                GameOver newScore player computer result ! []

        _ ->
            model ! []

After that, we’ll transition to the GameOver state. And finally, the update function in all its glory. Simple and to the point thanks to the functions we just wrote.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        NoOp ->
            model ! []

        ChoiceClicked choice ->
            updatePlayerChoice choice model

        RandomNumberReceived (Ok n) ->
            updateComputerChoice n model

        RandomNumberReceived (Err _) ->
            model ! []

        Reset ->
            createModel

Of course this is a bad example in terms of error handling, because we’re just ignoring it! Ideally we’ll have some kind of error message in our Model that we can populate appropriately when things explode.

The View function

Let’s begin with a simple function to convert a Choice into a String.

choiceToString : Choice -> String
choiceToString choice =
    case choice of
        Rock ->
            "Rock"

        Paper ->
            "Paper"

        Scissors ->
            "Scissors"

        Lizard ->
            "Lizard"

        Spock ->
            "Spock"

Same stuff when we have a GameResult.

resultToString : GameResult -> String
resultToString result =
    case result of
        Tie ->
            "It's a tie!"

        PlayerWins ->
            "Yay, you win!"

        ComputerWins ->
            "You lose :("

We’ll need a lower case css class for each Choice, that’s easy enough with our choiceToString helper.

choiceToClass : Choice -> String
choiceToClass =
    String.toLower << choiceToString

Remember: << is function composition. It’s equivalent to choiceToClass c = String.toLower (choiceToString c) but cleaner and shorter.

The next one is purely cosmetic, it just creates an <hr /> tag.

divider : Html a
divider =
    hr [] []

Now, we have a Choice and we want to show a corresponding huge image on the screen.

showChoice : Choice -> Html a
showChoice choice =
    div [ class ("card " ++ choiceToClass choice) ] []

Same with GameResult.

showResult : GameResult -> Html a
showResult result =
    p [] [ text <| resultToString result ]

And Score.

showPlayerScore : Score -> Html a
showPlayerScore score =
    p [] [ text <| "Score: " ++ (toString score) ]

We want a Reset button that will reset the game state.

resetButton : Html Msg
resetButton =
    button [ onClick Reset, class "reset" ] [ text "Reset!" ]

And also a Choice button so that the player can make a choice.

choiceButton : Choice -> Html Msg
choiceButton choice =
    button
        [ class <| "choice " ++ (choiceToClass choice)
        , onClick (ChoiceClicked choice)
        ]
        [ text <| choiceToString choice ]

The next is a cool one. Basically, if you look at our Model at any one time we could have a player choice, a computer choice and a result. But we want our UI code to be written in a simple and straightforward way, so instead of making the showChoice and showResult functions more complicated so that they could accept a Maybe and render something appropriately, we’ll create another function to do the dirty work and call the appropriate view function only when we have a value.

maybeRender : (a -> Html Msg) -> Maybe a -> Html Msg
maybeRender f a =
    Maybe.map f a
        |> Maybe.withDefault (div [] [])

Let’s say we want to render the computer’s choice. We don’t want to check whether the state is PlayerTurn or ComputerTurn or whatever, we just want to call showChoice. To make it safe, we’ll call maybeRender showChoice computerChoice so that an empty div will be rendered if there’s nothing to show. This is very important because it allows us to keep our other functions simple and stupid (again, by not having to deal with Maybe directly).

We are almost done, our last helper is gameView which employs the maybeRender function quite a bit so that our UI is simple to read and reason about.

gameView : Score -> Maybe Choice -> Maybe Choice -> Maybe GameResult -> Html Msg
gameView score playerChoice computerChoice gameResult =
    div
        []
        [ choiceButton Rock
        , choiceButton Paper
        , choiceButton Scissors
        , choiceButton Lizard
        , choiceButton Spock
        , divider
        , maybeRender showChoice playerChoice
        , maybeRender showChoice computerChoice
        , maybeRender showResult gameResult
        , showPlayerScore score
        , maybeRender (\_ -> resetButton) gameResult
        ]

This function will be called by the main view function with different parameters depending on the state. I can’t stress enough how useful it is to restrict the use of Maybe to a couple of functions (maybeRender and gameView). All the other functions are as simple as they can be and this greatly improves the quality of our program in my opinion.

view : Model -> Html Msg
view model =
    case model of
        PlayerTurn score ->
            div []
                [ gameView score Nothing Nothing Nothing
                , h3 [] [ text "Make your move!" ]
                ]

        ComputerTurn score player ->
            gameView score (Just player) Nothing Nothing

        GameOver score player computer result ->
            gameView score (Just player) (Just computer) (Just result)

With gameView, the main view function is extremely nice and I really dig how it is laid out.

This is it! You can play with this code directly in your browser here or on runelm.io. The source code is also available on Github.

If you enjoyed this article, do let me know and share it along! You can follow me on Twitter if this kind of things interest you.