Rock, paper, scissors game in JavaScript
Summary
In this article, I’ll show you how to develop a Rock, Paper, Scissors game in JavaScript. It’ll be a one-player game, i.e., computer vs the user. Our emphasis will be on the game’s logic with JavaScript and not on styling.
By following along, you’ll learn the following:
- Setting up radio buttons and a button in an HTML form
- Manipulating the DOM in JavaScript
- Attaching and defining event listeners to form controls in JavaScript
Rules of the game
Skip ahead if you know the rules of the game. But here they are just in case:
- The player and the computer choose one of rock, paper, scissors
- If both players chose the same item, it is a tie
- If one player chooses rock and the other chooses paper, the second one wins, because paper wraps rock
- If one player chooses rock and the other chooses scissors, the first one wins, because rock breaks scissors
- If one player chooses scissors and the other chooses paper, the first one wins, because scissors cut paper
Problem analysis
Let’s break the problem down into smaller sub-problems, so that we can solve it systematically. Here is what we need to do:
- Create a web page on which the game will be played.
- Allow the learner to select from rock, paper, and scissors. Update the web page with the player’s choice.
- Implement a computer player that randomly picks from rock, paper, scissors. Update the web page with the computer’s choice.
- Decide who won and display the result on the screen.
The Setup
Follow these steps to get started:
- Create a folder for the code
- Inside that folder, create an HTML file
index.html
- Inside the same folder, create a JavaScript file
app.js
- Also inside the same folder, create a CSS file
style.css
The HTML
Here are the contents of theindex.html
file.:
<!DOCTYPE HTML>
<HTML lang="en">
<head>
<meta charset="utf-8">
<title>Rock Paper Scissors</title>
<link rel='stylesheet' href='style.css'>
</head>
<body>
<h2>Computer Choice: <span id="computer-choice"></span></h2>
<h2>Your Choice: <span id="user-choice"></span></h2>
<h2>Result: <span id="result"></span></h2>
<input type="radio" id="rock" name="choice" value="rock">
<label for="rock">Rock</label><br>
<input type="radio" id="paper" name="choice" value="paper">
<label for="paper">Paper</label><br>
<input type="radio" id="scissors" name="choice" value="scissors">
<label for="scissors">Scissors</label>
<button id="play" disabled>Play</button>
<script src="app.js" charset="utf-8"></script>
</body>
</HTML>
We have the DOCTYPE
tag to tell the browser that we’ll use HTML 5. We’ve set lang = en
to declare the language as English, and charset='utf-8'
to declare the UTF 8 character encoding. These are often not necessary, but a good idea for accessibility.
We first created h2
elements to show the computer and player one choices. Inside the h2
elements, we’ve placed span
elements to hold the values picked by both players. In the HTML, these are empty. We’ll assign the text inside these span
elements in JavaScript.
For the player to choose an item, we’ve used radio buttons, created using the input
and the label
tags. The input
tag creates the radio button, with type
attribute set to radio
. The name
attribute for all radio
buttons is set to the same value choice
, so that the browser knows that they belong to the same group, and only one can be selected at a time.
The id
attributes for all radio buttons, and span
elements, are set appropriately, because we’ll want to access these in JavaScript. For the radio button labels, you’ll notice that the label
tags have the for
attribute set equal to the id
attribute of the corresponding radio button.
Note that I have disabled the play button. My thought process, here, is that until the player makes a choice with the radio button, there’s no point pushing the play button and invoking its click event handler.
Note that we’ve placed the script
tag referencing the JavaScript file at the end of the body
tag. This is a best practice, so that the web page rendering does not get blocked by JavaScript execution.
The CSS file
We wouldn’t bother with the styling of the application. However, I’ve set the text-transform
style for the span
elements to capitalize
so that we can use words in lowercase in JavaScript and have the browser worry about rendering “scissors” as “Scissors”, for example. Here are the contents of the style.css
file:
span {
text-transform: capitalize;
}
The preliminary code
Let’s start by getting handles to the necessary HTML elements, and declaring an array that holds the three strings that the computer may randomly choose from. To get programmatic access to an HTML element using its unique id
, we can use the document.getElementById
library function. We’ll pass the uniqueid
values that we set in the index.html
file earlier.
Place this in theapp.js
:
const compChoice = document.getElementById('computer-choice')
const yourChoice = document.getElementById('user-choice')
const resultDisplay = document.getElementById('result')
const playButton = document.getElementById('play')
const possibleChoices = ['rock', 'paper', 'scissors']
The first thing that I want to do is to enable the play button as soon as a radio button is clicked. To do that, I need to add an event listener to all the radio buttons that sets the disabled
attribute of the play button to false
.
Append the following to the JavaScript code we have so far:
const choices = document.getElementsByName('choice')
choices.forEach((c) => {
c.addEventListener('click', () => {
playButton.disabled = false
})
})
We could have picked the radio buttons one by one using their id
attributes, and the getElementyById
function, but it is more convenient and less error prone to access all the radio buttons using a single variable. This is possible, because they all have the same name
attribute. We used the getElementsByName
function, this time. Note the plural “Elements” in the function name. It returns a collection of all HTML elements with the name
value specified as the argument.
To iterate over the choices
button collection, I’ve used the functional programming style of forEach
. The forEach
function invokes a function for each value in the collection on which it is called. Since we are saying choices.forEach
, there’ll be one call to a callback function for each value in the choices
collection, which holds all of our radio buttons. The callback function is for us to specify as an argument to the forEach
function. I used an anonymous function, here. The variable c
will be assigned each radio button, one by one, in each call.
Since we wanted to add an event listener that enables the play button when radio button is clicked, we call the addEventListener
function on c
. We also specified click
as the event that we want to handle. A callback function needs to be passed as the second argument of the addEventListener
function. Again, we used an anonymous function, here. Since we already have access to the play button courtesy of the variable playButton
, all that we needed to do was playButton.disabled = false
.
Handling the play button click
We’ll add an anonymous function as the click event listener on the play button. The first thing that we’d like to know when the play button is clicked, is the player’s choice. This is what we’ll start with:
playButton.addEventListener('click', (e) => {
const buttonsArray = Array.from(choices)
const selected = buttonsArray.filter((b)=> b.checked)
})
I wanted to obtain the particular radio button that was checked
in the choices
collection. I could’ve done if-else
, here. But why not use filter.
Note that choices
, is a collection of HTML elements, not a JavaScript array. That means, that we can’t call filter
on it. So, we first convert the collection of HTML elements into an array with Array.from
. The argument to this function is the collection choices
.
Now, we call filter
on the buttonsArray
using an anonymous callback function. This function will be called for every element in that array. For each element, b
, we specify a boolean condition b.checked
. The filter
function returns all the elements in the buttonsArray
for which this boolean condition evaluates to true. Since the array is based on radio buttons, we are confident that it’ll return an array with one element — the radio button element which was selected.
How do we extract the string associated with the radio button? Because of the way we defined the radio buttons (<input type=”radio” id=”rock” name=”choice” value=”rock”>
, for example), we may use the id
property. Now, let’s set the player choice span
to this string.
Displaying the player choice
Since we’ll have to display the computer choice, too, I’ll define a function that adds a string to a span
element, given both of these as arguments. Here’s the code:
function addTextToSpan(spanControl, text) {
spanControl.textContent = text
}
We replace the textContent
of the span
element with the argument passed to the function. Now, let’s go back to the play button click handler that were working on, earlier, and call this function, to display the player’s choice:
playButton.addEventListener('click', (e) => {
const buttonsArray = Array.from(choices)
const selected = buttonsArray.filter((b)=> b.checked)
addTextToSpan(yourChoice, selected[0].id);
})
By now, the player will see the play button initially disabled. Once they click on any of the radio buttons, the play button will be enabled. If they click on the play button, they’ll see the choice that they made in front of “Your Choice” on the page.
The game AI
I admit it isn’t really an AI, but, anyway. We want the computer to make a random choice. We want it to pick a value at random from the possibleChoices
array. So, we need to generate a random index.
JavaScript provides a Math
library with several useful functions. One of these is the Math.random
function, which returns a number randomly picked between 0 and 1. In fact, the random value can be 0, but it is always less than 1. It is a fractional value, like 0.42245.
A fractional value can’t be used as an array index. It has to be an integer. How do we convert a randomly drawn fractional value between 0 and 1, to an integer value between 0 and 1?
If we multiply this random value with 3 (the size of the array), the results will be in the range 0 to 3. Why? Because the smallest randomly drawn number we had was 0, multiply that by 3, and you still have 0. The largest randomly drawn number was 1, multiply that by 3 and you have 3. Since any other randomly drawn value will be greater than 0, and less than 1, the result after multiplication with 3, will be between 0 and 3.
Great, so now we have a fractional random value, that lies between 0 and 3. We still don’t have an integer. There are two ways that you can convert a fractional number to an integer: ceiling and floor.
Ceiling is when you pick the smallest integer greater than the fractional value. Let’s consider how we might mentally calculate the ceiling of a fractional number. Imagine the value 0.113. Do you replace it with 0 or 1, or 2, or 3, or so on? Which one of these is the smallest integer greater than 0.113? 0 isn’t greater than 0.113, so that’s out of the question. 1 is greater than 0.113. So, are 2, and 3, and so on. But 1 is the smallest of these values. So, the ceiling of 0.113 is 1. The following illustration demonstrates this:
Floor is when you pick the greatest integer smaller than the fractional value. What’s the floor of 0.113? Is it 0, 1, 2, 3, …? Well, 1, 2, 3, … are eliminated because they aren’t smaller than 0.113. That leaves us with 0 (and the negative numbers, which don’t satisfy the “greatest” part of the definition). So, the floor of 0.113 is 0. The following illustration demonstrates this:
So, if we take the ceiling of the random number times 3, then we get a positive integer that is either 0, 1, 2, or 3. On the other hand, if we take the floor, then we get a positive integer that is either 0, 1, or 2. Since we have to pick from one of three values (rock, paper, scissors), we’ll take the floor.
Here’s the function that makes a random choice, sets the HTML element to show the player what the computer chose, and returns the chosen value so that the calling function may decide who won.
function generateComputerChoice() {
const randomNumber = Math.floor(Math.random() * possibleChoices.length)
const computerChoice = possibleChoices[randomNumber]
addTextToSpan(compChoice, possibleChoices[randomNumber])
return computerChoice
}
Now, let’s call this function in the play button click handler that we were building earlier:
playButton.addEventListener('click', (e) => {
const buttonsArray = Array.from(choices)
const selected = buttonsArray.filter((b)=> b.checked)
addTextToSpan(yourChoice, selected[0].id);
const randChoice = generateComputerChoice()
})
Deciding the result
We’ll extend the play button handler to decide who won. we’ll define a function that takes two arguments: the player’s choice, and the computer’s choice, and decides who won. Here’s the code:
function showResult(userChoice, computerChoice) {
if (userChoice === computerChoice) {
addTextToSpan(resultDisplay, 'tied')
}
else if (userChoice === 'rock') {
if (computerChoice === 'paper') {
addTextToSpan(resultDisplay, 'you lost')
}
else if (computerChoice == 'scissors') {
addTextToSpan(resultDisplay, 'you won')
}
}
else if (userChoice === 'paper') {
if (computerChoice === 'scissors') {
addTextToSpan(resultDisplay, 'you lost')
}
else if (computerChoice == 'rock') {
addTextToSpan(resultDisplay, 'you won')
}
}
else if (userChoice === 'scissors') {
if (computerChoice === 'rock') {
addTextToSpan(resultDisplay, 'you lost')
}
else if (computerChoice == 'paper') {
addTextToSpan(resultDisplay, 'you won')
}
}
}
We first checked if there’s a tie, because that’s easy to check. We used the ===
JavaScript operator. Note that we didn’t use the ==
operator, because it isn’t safe — it may do type coercion to deduce equality among different types of data.
Further, we’ve encoded the game’s rules into the else if
parts. Each of the player’s choices becomes one else if
. If the player chooses rock
, at this point, the computer’s choice was either paper
, or scissors
. Why could it not be rock
, too? Because we are in the else if
part already, and that possibility was exhausted in the if
part at the beginning. That’s why each else if
block has two possibilities.
Notice that we’re calling our addTextToSpan
function to display the result on the page. Let’s now add a call to this function in the play button click handler that we were working on:
playButton.addEventListener('click', (e) => {
const buttonsArray = Array.from(choices)
const selected = buttonsArray.filter((b)=> b.checked)
addTextToSpan(yourChoice, selected[0].id);
const randChoice = generateComputerChoice()
showResult(selected[0].id, randChoice)
})
With this addition, our game is nearly complete. There’s only one improvement I’d like to make to it. Once the result of a game is decided, I’d like the radio buttons to be reset, again, and the play button disabled, so that the learner can start another round of the game. Here’s the modified event handler:
playButton.addEventListener('click', (e) => {
const buttonsArray = Array.from(choices)
const selected = buttonsArray.filter((b)=> b.checked)
addTextToSpan(yourChoice, selected[0].id);
const randChoice = generateComputerChoice()
showResult(selected[0].id, randChoice)
e.target.disabled = true;
choices.forEach((b)=> {
b.checked = false;
})
})
Since this is the click handler for the play button, and the event source is specified in the anonymous callback function argument (the e
parameter), the play button itself is available through the e.target
property. So, we needed to do e.target.disabled = true
. Also, we iterated over the choices
collection to reset all the radio buttons, using the forEach
function.
The complete code
You may download the complete code from this github repository. But, here’s the JavaScript app.js file:
const compChoice = document.getElementById('computer-choice')
const yourChoice = document.getElementById('user-choice')
const resultDisplay = document.getElementById('result')
const playButton = document.getElementById('play')
const choices = document.getElementsByName('choice')
const possibleChoices = ['rock', 'paper', 'scissors']
choices.forEach((c) => {
c.addEventListener('click', () => {
playButton.disabled = false
})
})
playButton.addEventListener('click', (e) => {
const buttonsArray = Array.from(choices)
const selected = buttonsArray.filter((b)=> b.checked)
addTextToSpan(yourChoice, selected[0].id);
const randChoice = generateComputerChoice()
showResult(selected[0].id, randChoice)
e.target.disabled = true;
choices.forEach((b)=> {
b.checked = false;
})
})
function generateComputerChoice() {
const randomNumber = Math.floor(Math.random() * possibleChoices.length)
const computerChoice = possibleChoices[randomNumber]
addTextToSpan(compChoice, possibleChoices[randomNumber])
return computerChoice
}
function addTextToSpan(spanControl, text) {
while(spanControl.firstChild) {
spanControl.removeChild(spanControl.firstChild)
}
spanControl.appendChild(document.createTextNode(text))
}
function showResult(userChoice, computerChoice) {
if (userChoice === computerChoice) {
addTextToSpan(resultDisplay, 'tied')
}
else if (userChoice === 'rock') {
if (computerChoice === 'paper') {
addTextToSpan(resultDisplay, 'you lost')
}
else if (computerChoice == 'scissors') {
addTextToSpan(resultDisplay, 'you won')
}
}
else if (userChoice === 'paper') {
if (computerChoice === 'scissors') {
addTextToSpan(resultDisplay, 'you lost')
}
else if (computerChoice == 'rock') {
addTextToSpan(resultDisplay, 'you won')
}
}
else if (userChoice === 'scissors') {
if (computerChoice === 'rock') {
addTextToSpan(resultDisplay, 'you lost')
}
else if (computerChoice == 'paper') {
addTextToSpan(resultDisplay, 'you won')
}
}
}
The corresponding HTML file index.html
:
<!DOCTYPE HTML>
<HTML lang="en">
<head>
<meta charset="utf-8">
<title>Rock Paper Scissors</title>
<link rel='stylesheet' href='style.css'>
</head>
<body>
<h2>Computer Choice: <span id="computer-choice"></span></h2>
<h2>Your Choice: <span id="user-choice"></span></h2>
<h2>Result: <span id="result"></span></h2>
<input type="radio" id="rock" name="choice" value="rock">
<label for="rock">Rock</label><br>
<input type="radio" id="paper" name="choice" value="paper">
<label for="paper">Paper</label><br>
<input type="radio" id="scissors" name="choice" value="scissors">
<label for="scissors">Scissors</label>
<button id="play" disabled>Play</button>
<script src="app.js" charset="utf-8"></script>
</body>
</HTML>
The super-simple CSS file style.css
:
span {
text-transform: capitalize;
}