This is the classic Rock Paper Scissors game on steroids. There are two more choices that I’ve never heard before –
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:
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.
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
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
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
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
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 (
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
intToChoice : Int -> Choice intToChoice n = case n of 1 -> Rock 2 -> Paper 3 -> Scissors 4 -> Lizard 5 -> Spock _ -> intToChoice <| (rem n 5) + 1
_ 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
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
choiceToString : Choice -> String choiceToString choice = case choice of Rock -> "Rock" Paper -> "Paper" Scissors -> "Scissors" Lizard -> "Lizard" Spock -> "Spock"
Same stuff when we have a
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
choiceToClass : Choice -> String choiceToClass = String.toLower << choiceToString
<< 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) ] 
showResult : GameResult -> Html a showResult result = p  [ text <| resultToString result ]
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
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
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
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 (
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)
gameView, the main
view function is extremely nice and I really dig how it is laid out.
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.