Rock paper scissors in JavaScript, in one HTML file

Three buttons, one random number and one line of arithmetic make a complete game. This guide builds it in a single file, explains each step, and shows the mistakes that break it.

A rock paper scissors game needs three buttons, a random pick for the computer and a way to decide who won. In JavaScript that fits in about ten lines of script inside one .html file, with no server, library or repository.

Try it first. Click a button and the computer answers.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Rock Paper Scissors</title>
<style>
  body {
    margin: 0; padding: 22px 16px; text-align: center;
    font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330;
  }
  h1 { font-size: 20px; margin: 0 0 14px; }
  button {
    font: inherit; font-size: 16px; padding: 10px 16px; margin: 4px;
    border: 1px solid #c9ced8; border-radius: 10px; background: #fff; cursor: pointer;
  }
  button:hover { background: #eef2ff; }
  #out { min-height: 3em; margin-top: 18px; font-size: 17px; line-height: 1.5; }
</style>
</head>
<body>
<h1>Rock Paper Scissors</h1>
<div>
  <button type="button">Rock</button>
  <button type="button">Paper</button>
  <button type="button">Scissors</button>
</div>
<p id="out" aria-live="polite">Pick one.</p>

<script>
  const moves = ['rock', 'paper', 'scissors'];   // index 0, 1, 2 - same order as the buttons
  const out = document.getElementById('out');

  document.querySelectorAll('button').forEach((btn, player) => {
    btn.addEventListener('click', () => {
      const computer = Math.floor(Math.random() * 3);  // 0, 1 or 2
      const diff = (player - computer + 3) % 3;        // 0 = draw, 1 = you win, 2 = you lose
      const verdict = ['Draw.', 'You win!', 'You lose.'][diff];
      out.textContent = 'You: ' + moves[player] + '. Computer: ' + moves[computer] + '. ' + verdict;
    });
  });
</script>
</body>
</html>
The smallest version: three buttons, one random pick, one line that decides the winner.

Save that code as rps.html and open it in a browser, and it works the same way. The rest of this page explains each step, then fixes the mistakes people usually hit.

How the code works, step by step

Every round is the same four moments. The click starts it, and each step hands one number to the next.

One round: the click gives your move, the computer picks, the judge compares, the page shows the text.
One round: the click gives your move, the computer picks, the judge compares, the page shows the text.
  1. Number the moves. The array ['rock', 'paper', 'scissors'] gives each move an index: 0, 1 and 2. Keep the buttons in the same order as the array.
  2. Listen for clicks. Looping over querySelectorAll('button') with forEach passes each button and its position to the callback. That position is the player's move.
  3. Pick for the computer. Math.floor(Math.random() * 3) gives 0, 1 or 2.
  4. Judge. One expression compares the two numbers, explained next.
  5. Show the result. Setting textContent writes plain text into the paragraph. The paragraph has aria-live="polite", so a screen reader announces the new result without interrupting.

Why the judge is one line of arithmetic

Put the moves in this order: rock, paper, scissors. Each move then beats the one before it, and rock beats scissors by wrapping around the end of the list.

The three moves in a loop, and the three possible values of the judging expression.
The three moves in a loop, and the three possible values of the judging expression.

So the difference between your number and the computer's tells the whole story:

const diff = (player - computer + 3) % 3;
// 0 = draw, 1 = you win, 2 = the computer wins

The + 3 matters. In JavaScript the % operator keeps the sign of the number on its left, so without it a difference of -1 would stay -1 and match none of your cases.

Make the computer's pick fair

Math.random() returns a number from 0 up to, but not including, 1. Multiplying by 3 and rounding down splits that range into three equal slices. Rounding to the nearest whole number does not.

Rounding to nearest gives paper twice the chance of rock. Rounding down gives each move an equal share.
Rounding to nearest gives paper twice the chance of rock. Rounding down gives each move an equal share.

The next example checks both claims by running the code. It builds the nine-pair result table from the judging expression, then picks 30,000 times with each formula and counts the results.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Check the rules and the random pick</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #fff; color: #1d2330; }
  h2 { font-size: 16px; margin: 0 0 6px; }
  p { margin: 0 0 10px; font-size: 14px; line-height: 1.45; color: #374151; }
  section { border: 1px solid #e1e4ea; border-radius: 12px; padding: 14px; margin-bottom: 14px; }
  table { border-collapse: collapse; width: 100%; max-width: 420px; font-size: 14px; }
  th, td { border: 1px solid #e1e4ea; padding: 6px 8px; text-align: center; }
  th { background: #f4f5f7; font-weight: 600; }
  td.win { background: #e8f6ec; color: #0f5132; }
  td.lose { background: #fdeee9; color: #9a3412; }
  td.draw { background: #f4f5f7; color: #4b5563; }
  #agree { margin-top: 10px; font-weight: 600; }
  button { font: inherit; font-size: 14px; padding: 8px 14px; border: 1px solid #c9ced8; border-radius: 9px; background: #fff; cursor: pointer; }
  .bars { margin-top: 12px; }
  .row { display: grid; grid-template-columns: 112px 1fr; gap: 8px; align-items: center; margin: 4px 0; font-size: 13px; }
  .bar { height: 20px; border-radius: 5px; background: #eef1f5; position: relative; overflow: hidden; }
  .fill { height: 100%; width: 0; background: #16a34a; }
  .bad .fill { background: #ea580c; }
  .bar span { position: absolute; left: 8px; top: 2px; font-size: 12px; color: #111827; }
  h3 { font-size: 14px; margin: 14px 0 4px; }
</style>
</head>
<body>

<section>
  <h2>1. All nine pairs</h2>
  <p>Rows are your move, columns are the computer's. Each cell comes from the same one-line formula.</p>
  <table id="grid"></table>
  <div id="agree"></div>
</section>

<section>
  <h2>2. Is the random pick fair?</h2>
  <p>Pick 30,000 times with each formula and count the three results.</p>
  <button type="button" id="run">Pick 30,000 times</button>
  <div class="bars" id="bars"></div>
</section>

<script>
  const moves = ['rock', 'paper', 'scissors'];

  // Way 1: one line of arithmetic. 0 = draw, 1 = first move wins, 2 = second move wins.
  function judge(a, b) { return (a - b + 3) % 3; }

  // Way 2: spell out every winning pair.
  function judgeLong(a, b) {
    if (a === b) return 0;
    if ((a === 1 && b === 0) || (a === 2 && b === 1) || (a === 0 && b === 2)) return 1;
    return 2;
  }

  // 1. Build the 3 x 3 table and compare both ways.
  const grid = document.getElementById('grid');
  const labels = ['draw', 'win', 'lose'];
  let same = 0;
  let html = '<tr><th></th>' + moves.map(m => '<th>' + m + '</th>').join('') + '</tr>';
  for (let p = 0; p < 3; p++) {
    html += '<tr><th>' + moves[p] + '</th>';
    for (let c = 0; c < 3; c++) {
      const r = judge(p, c);
      if (r === judgeLong(p, c)) same++;
      html += '<td class="' + labels[r] + '">' + labels[r] + '</td>';
    }
    html += '</tr>';
  }
  grid.innerHTML = html;
  document.getElementById('agree').textContent =
    'Both ways agree on ' + same + ' of 9 pairs.';

  // 2. Count results of a fair pick and a skewed one.
  const pickFloor = () => Math.floor(Math.random() * 3);
  const pickRound = () => Math.round(Math.random() * 2);

  function count(pick, n) {
    const t = [0, 0, 0];
    for (let i = 0; i < n; i++) t[pick()]++;
    return t;
  }

  function draw(title, cls, t, n) {
    let s = '<h3>' + title + '</h3><div class="' + cls + '">';
    for (let i = 0; i < 3; i++) {
      const pct = (t[i] / n * 100).toFixed(1);
      s += '<div class="row"><div>' + moves[i] + '</div><div class="bar"><div class="fill" style="width:' + pct + '%"></div><span>' + pct + '%</span></div></div>';
    }
    return s + '</div>';
  }

  document.getElementById('run').addEventListener('click', () => {
    const n = 30000;
    document.getElementById('bars').innerHTML =
      draw('Math.floor(Math.random() * 3)', 'good', count(pickFloor, n), n) +
      draw('Math.round(Math.random() * 2)', 'bad', count(pickRound, n), n);
  });
</script>
</body>
</html>
Top: all nine pairs from one formula, checked against a long if-chain. Bottom: pick counts for floor and for round.

Run it a few times. The floor counts stay close to a third each. The round counts settle near a quarter, a half and a quarter. For other random-number recipes, see random numbers in JavaScript.

A finished version: first to three

The same play() logic can sit inside a small match. This version adds a score, a first-to-three rule, a round history, a new-match button and keyboard keys.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Rock Paper Scissors - best of five</title>
<style>
  body { margin: 0; padding: 18px 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; text-align: center; }
  h1 { font-size: 20px; margin: 0 0 4px; }
  .sub { font-size: 13px; color: #6b7280; margin: 0 0 14px; }
  .score { display: flex; justify-content: center; align-items: center; gap: 18px; font-size: 15px; }
  .score b { font-size: 30px; display: block; line-height: 1.1; }
  .hands { display: flex; justify-content: center; gap: 14px; margin: 14px 0; }
  .hand { width: 92px; height: 92px; border-radius: 14px; background: #fff; box-shadow: 0 4px 14px rgba(0,0,0,.1);
          display: grid; place-items: center; font-size: 44px; }
  .hand.win { outline: 3px solid #16a34a; }
  .hand.lose { outline: 3px solid #ea580c; }
  .pick button, #again {
    font: inherit; font-size: 15px; padding: 10px 14px; margin: 3px; border: 1px solid #c9ced8;
    border-radius: 10px; background: #fff; cursor: pointer;
  }
  .pick button:hover:not(:disabled) { background: #eef2ff; }
  .pick button:disabled { opacity: .45; cursor: default; }
  #status { min-height: 1.6em; font-size: 17px; font-weight: 600; margin: 8px 0; }
  #again[hidden] { display: none; }
  ol { list-style: none; margin: 10px auto 0; padding: 0; max-width: 320px; max-height: 120px; overflow-y: auto; font-size: 13px; color: #374151; text-align: left; }
  li { padding: 3px 0; border-top: 1px solid #e5e7eb; }
</style>
</head>
<body>
<h1>Rock Paper Scissors</h1>
<p class="sub">First to 3 wins. Keys: R, P, S.</p>

<div class="score">
  <div>You<b id="you">0</b></div>
  <div>:</div>
  <div>Computer<b id="cpu">0</b></div>
</div>

<div class="hands">
  <div class="hand" id="handYou">?</div>
  <div class="hand" id="handCpu">?</div>
</div>

<div class="pick" id="pick">
  <button type="button" data-key="r">&#9994; Rock</button>
  <button type="button" data-key="p">&#9995; Paper</button>
  <button type="button" data-key="s">&#9996;&#65039; Scissors</button>
</div>

<div id="status" aria-live="polite">Pick a move.</div>
<button type="button" id="again" hidden>New match</button>
<ol id="log"></ol>

<script>
  const MOVES = ['rock', 'paper', 'scissors'];
  const ICONS = ['✊', '✋', '✌️'];
  const WINS_NEEDED = 3;

  const buttons = document.querySelectorAll('#pick button');
  const el = (id) => document.getElementById(id);
  let you = 0, cpu = 0, round = 0;

  function play(player) {
    const computer = Math.floor(Math.random() * 3);
    const diff = (player - computer + 3) % 3;   // 0 draw, 1 win, 2 lose
    round++;
    if (diff === 1) you++;
    if (diff === 2) cpu++;

    el('handYou').textContent = ICONS[player];
    el('handCpu').textContent = ICONS[computer];
    el('handYou').className = 'hand' + (diff === 1 ? ' win' : diff === 2 ? ' lose' : '');
    el('handCpu').className = 'hand' + (diff === 2 ? ' win' : diff === 1 ? ' lose' : '');
    el('you').textContent = you;
    el('cpu').textContent = cpu;

    const li = document.createElement('li');
    li.textContent = 'Round ' + round + ': ' + MOVES[player] + ' vs ' + MOVES[computer] +
      ' - ' + ['draw', 'you win', 'you lose'][diff];
    el('log').prepend(li);

    if (you === WINS_NEEDED || cpu === WINS_NEEDED) {
      el('status').textContent = you > cpu ? 'You win the match!' : 'Computer wins the match.';
      buttons.forEach(b => b.disabled = true);
      el('again').hidden = false;
      el('again').focus();
    } else {
      el('status').textContent = ['Draw.', 'You win the round.', 'You lose the round.'][diff];
    }
  }

  buttons.forEach((btn, i) => btn.addEventListener('click', () => play(i)));

  document.addEventListener('keydown', (e) => {
    if (e.ctrlKey || e.metaKey || e.altKey) return;
    const i = Array.from(buttons).findIndex(b => b.dataset.key === e.key.toLowerCase());
    if (i !== -1 && !buttons[i].disabled) play(i);
  });

  el('again').addEventListener('click', () => {
    you = cpu = round = 0;
    el('you').textContent = 0;
    el('cpu').textContent = 0;
    el('handYou').textContent = '?';
    el('handCpu').textContent = '?';
    el('handYou').className = el('handCpu').className = 'hand';
    el('log').innerHTML = '';
    el('status').textContent = 'Pick a move.';
    buttons.forEach(b => b.disabled = false);
    el('again').hidden = true;
  });
</script>
</body>
</html>
Click, or press R, P and S. Draws score nobody. The first side to three wins, then the buttons lock until New match.
  • Score: two counters, raised only when diff is 1 or 2.
  • End of match: when a counter reaches 3, disable the buttons and show New match.
  • Keys: a keydown listener reads e.key and finds the button whose data-key matches. For more on key handling, see keyboard shortcuts in JavaScript.
  • History: each round adds a line to a list with createElement and prepend.

No state is saved between visits. Reload the page and the score starts again at zero.

Rock paper scissors in the console

You can play without any buttons. Open your browser's developer tools, as in HTML DevTools, paste this into the Console tab and call play('rock'):

function play(player) {
  const moves = ['rock', 'paper', 'scissors'];
  if (!moves.includes(player)) return console.log('Use rock, paper or scissors');
  const computer = Math.floor(Math.random() * 3);
  const diff = (moves.indexOf(player) - computer + 3) % 3;
  console.log('Computer: ' + moves[computer] + ' - ' + ['draw', 'you win', 'you lose'][diff]);
}

The includes check stops a typo from being treated as rock. If you read the move with prompt() instead, remember it returns null when the player cancels, and calling a string method on null throws a TypeError.

When it does not work

What you see Cause Fix
The computer's move shows undefined The pick is a fraction such as 1.7, which is not an array position Wrap the pick in Math.floor
Paper comes up twice as often as rock The pick uses Math.round Use Math.floor(Math.random() * 3)
Some rounds give no verdict The difference is -1 or -2, which matches no case Add 3 before the %
The page reloads when you click The button is inside a form and defaults to submit Add type="button"
TypeError on addEventListener The script runs before the buttons are parsed Move the script to the end of body
Wrong move credited to a button Button order differs from the array order Match the orders, or read data-move
TypeError after pressing Cancel prompt() returned null Check for null before using the value

A game is easier to show than to describe. For more on sharing games, see single-file HTML games.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its script runs, so the people you send it to can play it themselves. If you change the code later, the same link shows the new version.

Questions people ask

How do I make rock paper scissors in JavaScript?

Put the three moves in an array, so each move has a number: 0, 1 or 2. On a click, pick the computer's number with Math.floor(Math.random() * 3), then compute (player - computer + 3) % 3. A result of 0 is a draw, 1 means you win, and 2 means the computer wins. Write the text on the page with textContent.

Do I need a server, a library or a GitHub repository?

No. The examples on this page are single HTML files with the script inside them. They run when opened straight from a file, with no server, no library and no build step. A repository is only useful if you want version history.

How do I play rock paper scissors in the browser console?

Open the developer tools, paste a short function that returns the result with console.log, and call it with play('rock'). If you read the move with prompt(), check for null first, because prompt returns null when the player presses Cancel.

Why does the computer pick paper so often?

Most likely the pick uses Math.round(Math.random() * 2). Rounding gives the middle number twice the chance of 0 or 2. Use Math.floor(Math.random() * 3) so each move gets an equal share.

How do I send the game to someone so they can play it?

Paste the page into a NOS document and choose Create share link. The page renders as written and its script runs, so the other person can click the buttons. Editing the code later keeps the same link.

Keep reading