How a Markov Chain Beats You at Rock, Paper, Scissors

How a Markov Chain Beats You at Rock, Paper, Scissors

Play ten rounds of rock, paper, scissors against the Mind Reader on the home page of this site. Try hard to be random. If a match ends before you reach ten, press Rematch: it keeps everything it has learned about you.

Then look at the score. When it wins on a prediction, it tells you how sure it was. It never sees your click before it moves, and there is no trick in it beyond counting. This post is about what it counts, and why counting is enough.

What it is counting

Every throw adds one to a tally that answers a single question: after you played this, what did you play next? With one move of memory that is a table of three rows, one for each last move. With two moves of memory there are nine rows, one for each pair, like rock-then-paper.

Before each round it looks up the row for your last two moves. If one answer there has come up more than 40 percent of the time, it backs that answer. If not, it falls back to the row for your last move alone. If it has never seen the situation, it throws at random, which is why the first couple of rounds are a three-sided coin toss. Whatever it expects you to play, it plays the move that beats it.

That is a Markov chain: a model that assumes what happens next depends only on the last step or two. Predictive text grew from the same idea, counting which word tends to follow which. The fallback has a name too, backoff: the two-move table is more specific but fills up slowly, so use it when it has evidence and the one-move table when it does not. The 40 percent bar exists because with three moves, the top answer always has at least a third of the count. Just above a third is noise. Well above it is a habit.

The whole idea in a page of code

Here is the core, trimmed from the script that runs the home page game. Moves are single letters: R, P and S.

const BEATS = { R: 'P', P: 'S', S: 'R' };  // what beats each move
const order1 = {};  // "R"  -> counts of what you played next
const order2 = {};  // "RP" -> counts of what you played next
const history = [];

function count(table, key, move) {
  table[key] = table[key] || {};
  table[key][move] = (table[key][move] || 0) + 1;
}

function learn(move) {
  if (history.length >= 1) count(order1, history.slice(-1).join(''), move);
  if (history.length >= 2) count(order2, history.slice(-2).join(''), move);
  history.push(move);
}

function likeliest(counts = {}) {
  let best = null, total = 0;
  for (const m of ['R', 'P', 'S']) {
    total += counts[m] || 0;
    if (best === null || (counts[m] || 0) > (counts[best] || 0)) best = m;
  }
  if (!total) return null;
  return { move: best, confidence: Math.round(100 * counts[best] / total) };
}

function predict() {
  const two = likeliest(order2[history.slice(-2).join('')]);
  if (two && two.confidence > 40) return two;
  return likeliest(order1[history.slice(-1).join('')]);
}

// Decided before you click: counter the guess, or throw at random
const guess = predict();
const myMove = guess ? BEATS[guess.move] : 'RPS'[Math.floor(Math.random() * 3)];

Notice what is missing. There is no training phase and no maths beyond division. Learning is adding one to a number. The counts last as long as the page stays open, so a rematch starts with everything the last match taught it.

Why you are easier to read than you think

Against a perfectly random player, rock, paper, scissors cannot be won: every strategy wins, draws and loses a third of the time over the long run. The Mind Reader is betting you are not that player. People repeat a move that just won. They alternate. They overcorrect after a loss. And when they try to be random, they switch too often, because three rocks in a row feels deliberate, even though genuinely random sequences produce runs like that regularly.

It does not need to read your mind. It only needs you to lean.

The lean can be small. Picture a player who, after paper, throws rock half the time and paper or scissors a quarter each. The Mind Reader answers paper, and in those rounds it wins half, draws a quarter and loses a quarter. A small habit has become a steady edge.

One honest limit: it looks only at your moves, not at who won, so a habit like “change after losing” reaches it only indirectly. Adding the last result to the lookup key is an easy next step if you build your own.

How to beat it

  • Stop choosing. Let a die pick: one or two is rock, three or four paper, five or six scissors. Your habits never reach the tally, and the best it can do is break even.
  • Play against its prediction. If your habit after paper is rock, it is about to throw paper, so throw scissors. That works until your new behaviour fills the tally, so keep shifting.
  • Use its memory against it. The counts never fade. Follow paper with rock for a while, then switch to following paper with scissors. It keeps throwing paper, and losing, until the new count overtakes the old one.
  • Reload the page. Its memory lives in the page. That is not beating it so much as making it forget you.

A version that let old rounds fade would follow your changes faster, and would be fooled more easily by a short streak. Any model of a person trades memory against adaptability.

Your move

The Mind Reader is on the home page, first to five, with confetti if you win. Play it twice: once naturally, once with a die on the desk. If the second match goes better, you have just met your own habit.

Its older sibling, the RPS Mind Reader in the Y2K Lab, comes from my college coursework and works on the same principle: track what you tend to play next given what you just played, and throw the counter.