Rock, paper, scissors: two friends shake their fists, one, two, three, and show a fist, a flat hand or two fingers. Paper wraps rock, scissors cut paper, rock blunts scissors. It decides who bats first, who gets the window seat, who washes the plates.
It looks like pure luck. You choose, your friend chooses, and nobody can know what the other will show. But can a computer find out what you will show, before you show it? The computer below has already chosen its move when you choose yours, so it can’t peek. All it has is a list of what you played before.
Suppose you really did choose at random, like rolling a die. In 30 rounds, how many would the computer win, on average?
Pick an answer to see why.
You need: for the blocks, variables and choices, from Grade 7. For the Python (from Grade 8), loops, lists and functions.
Try it
Thirty rounds against the predictor
Play 30 rounds against Predictor (or more: the longer you play, the more it learns). Then look at the table under the game. It counts what you played after each of your own moves: the row “After Rock” says how many times you followed a rock with a rock, a paper or a scissors.
- a.Who is ahead after 30 rounds?
- b.Look at the row of your favorite move. Is one number in it much bigger than the others?
- c.Read the sentence under your buttons after a few rounds. What does the predictor look at to choose its move?
Against the counter and against Random
Press Counter. It is simpler: it counts only how many times you played each move in all (the row “In all”), guesses your favorite, and plays what beats it.
Then press Random. It chooses at random every time and looks at nothing.
Random has no pattern to find, and it doesn’t look at yours: however you play, you win, draw and lose about a third of the rounds each. You can’t beat it, but it can’t beat you either. So the only safe way to play is to be truly random. The predictor bets that you aren’t.
How it works
Each move beats one move and loses to the other. Going round the triangle below against the arrows, rock is beaten by paper, paper by scissors, and scissors by rock.
People are not random
A die has no memory: each roll knows nothing of the last. People remember, and react to what just happened.
The totals looked random; the habits were in what came after each round. The counter looks only at totals. The predictor looks at what comes after.
The table of nine counts
Here are eight moves a player made, in order:
R P R P S R P R
Look at each move and the one straight after it. After R came P, then after that P came R, and so on. There are seven pairs. Count them in a table: the row is the first move, the column the move that came next.
| then Rock | then Paper | then Scissors | |
|---|---|---|---|
| After Rock | 0 | 3 | 0 |
| After Paper | 2 | 0 | 1 |
| After Scissors | 1 | 0 | 0 |
The player’s last move was R. In the row “After Rock”, Paper has 3 and the others 0: three times out of three, this player followed a rock with paper. So the predictor guesses Paper, and plays Scissors, which beats it.
The table has nine counts, but each guess needs only one row: the row of the last move.
The same player now plays P, so their moves end R P. Which row does the predictor read, and what does it play?
Pick an answer to see why.
This is the same idea as the next-word model from Grade 8: count what came after each word, and guess the one that came most. A chatbot’s ancestor and a rock-paper-scissors predictor are cousins.
Program it
With blocks: a computer that counts
This program is the counter, in blocks. Before each round it looks at its three counts, r, p and s, guesses your most played move, and sets mine to the move that beats it (rock beaten by paper, and so on). Then it asks your move, says its own, counts yours, and adds a win if its move beats yours. After 10 rounds it says how many it won.
In Python (from Grade 8): the game file
The programs below share a file the page keeps for you, rps.py: the rules, and some players to test against. A move is the string 'R', 'P' or 'S'. A player is a function choose_move(theirs, mine): theirs is the list of the other player’s moves so far, mine its own, and it gives back its next move.
random.choice(['R', 'P', 'S']) picks one item of the list at random: that is how the players roll their dice.
Step 1: the counter
The counter counts the other player’s moves, then plays beat of the most common one. With nothing to count yet, it picks at random.
from rps import beat, mostimport random def choose_move(theirs, mine): r = 0 p = 0 s = 0 for i in range(len(theirs)): if theirs[i] == 'R': r = r + 1 elif theirs[i] == 'P': p = p + 1 else: s = s + 1 if r + p + s == 0: return random.choice(['R', 'P', 'S']) return beat(most(r, p, s)) print(choose_move(['R', 'R', 'P'], ['S', 'P', 'P']))print(choose_move(['R', 'P', 'S', 'R', 'P', 'S'], []))Output
PRAgainst R, R, P it guesses rock and plays paper. Against two of each, most gives S (a tie goes to the later move), so it plays rock. That is why “R, P, S in turn” left the blocks counter without a single win: the counts are always nearly equal, and the guess is always a step behind.
Step 2: the predictor
To count what came after the last move, look at every move except the last. Where a move is the same as the last one, the move straight after it, theirs[i + 1], is what the player did next that time: count it.
from rps import beat, mostimport random def choose_move(theirs, mine): if len(theirs) == 0: return random.choice(['R', 'P', 'S']) last = theirs[len(theirs) - 1] r = 0 p = 0 s = 0 for i in range(len(theirs) - 1): if theirs[i] == last: after = theirs[i + 1] if after == 'R': r = r + 1 elif after == 'P': p = p + 1 else: s = s + 1 if r + p + s == 0: return random.choice(['R', 'P', 'S']) print('After', last, 'you played R', r, 'P', p, 'S', s) return beat(most(r, p, s)) print(choose_move(['R', 'P', 'R', 'P', 'S', 'R', 'P', 'R'], []))Output
After R you played R 0 P 3 S 0SThat is the eight moves from the table above, and the program reads the same row: after R came P three times, so it plays scissors. The loop stops at len(theirs) - 1, so theirs[i + 1] never goes past the end of the list.
Now play it. The game below calls choose_move from the program just above (as you have it: press Edit to change it) before every round, with your moves as theirs. What it prints appears under the game.
What your program shows
You can’t play a thousand rounds by hand, but match can. Here your predictor plays four players from rps.py, 1,000 rounds each. random.seed(1) makes the dice roll the same way every time you press Run, so your numbers match the ones here; change the 1 for another set of games.
from rps import beat, most, matchfrom rps import always_rock, in_turn, never_twice, at_randomimport randomrandom.seed(1) def choose_move(theirs, mine): if len(theirs) == 0: return random.choice(['R', 'P', 'S']) last = theirs[len(theirs) - 1] r = 0 p = 0 s = 0 for i in range(len(theirs) - 1): if theirs[i] == last: after = theirs[i + 1] if after == 'R': r = r + 1 elif after == 'P': p = p + 1 else: s = s + 1 if r + p + s == 0: return random.choice(['R', 'P', 'S']) return beat(most(r, p, s)) players = [always_rock, in_turn, never_twice, at_random]names = ['Always rock', 'Rock, paper, scissors in turn', 'Never the same twice', 'At random']for i in range(len(players)): result = match(choose_move, players[i], 1000) print(names[i] + ':', result[0], 'won,', result[1], 'drawn,', result[2], 'lost')Output
Always rock: 998 won, 1 drawn, 1 lostRock, paper, scissors in turn: 998 won, 2 drawn, 0 lostNever the same twice: 489 won, 324 drawn, 187 lostAt random: 358 won, 323 drawn, 319 lost(match(choose_move, players[i], 1000) hands the functions themselves to match: a function’s name without brackets is the function, which match then calls 1,000 times.)
- A habit is a weakness. “Always rock” and “in turn”, which beat the counter, lose almost every round: after one or two rounds, every row of the table has the answer in it.
- Trying hard to look random is a habit too. “Never the same twice” is what many people do when they try to be random. It makes every row of the table say “not this one”, and the predictor wins about half the rounds, not a third. (How the rest split between draws and losses changes a lot with the seed; Challenge ★★ fixes that.)
- Only real randomness is safe. Against “At random”, nothing is learned: about a third each, as the choose at the start said. Change the seed and the three numbers wobble round 333, now one ahead, now another.
The predictor knows nothing about rock-paper-scissors strategy. It only counts, and the counts are enough, because players have habits and habits show up in counts. Much of what is called artificial intelligence works the same way: a model learns from what happened before and guesses what comes next.
Going further still
Beat the predictor. Once you know its rule, the predictor is as predictable as “always rock”: work out what it will play, and play what beats that. Try it by hand in the first game: next it reads the row of your last move. Challenge ★★★ makes a program do it.
The habit it misses. Run this, the habit from the Zhejiang study taken to the extreme: after a win, the same move; after a loss, the move that beats the winner.
from rps import beat, most, match, win_stay_lose_shiftimport randomrandom.seed(1) def choose_move(theirs, mine): if len(theirs) == 0: return random.choice(['R', 'P', 'S']) last = theirs[len(theirs) - 1] r = 0 p = 0 s = 0 for i in range(len(theirs) - 1): if theirs[i] == last: after = theirs[i + 1] if after == 'R': r = r + 1 elif after == 'P': p = p + 1 else: s = s + 1 if r + p + s == 0: return random.choice(['R', 'P', 'S']) return beat(most(r, p, s)) result = match(choose_move, win_stay_lose_shift, 1000)print('Win-stay, lose-shift:', result[0], 'won,', result[1], 'drawn,', result[2], 'lost')Output
Win-stay, lose-shift: 492 won, 13 drawn, 495 lostThe predictor gets no better than even. This player’s next move depends on whether it won or lost, which the predictor never looks at.
A robot that doesn’t need to guess. At the University of Tokyo, Masatoshi Ishikawa’s laboratory built a robot hand that wins every round. It doesn’t predict anything: a high-speed camera sees what shape your hand is making within about a thousandth of a second, as you start to show it, and the robot makes the winning shape almost at the same moment. Fast enough to look fair, but it isn’t.
Challenges
★ The judge (blocks)
Finish the program so that it says who wins: Player 1 wins, Player 2 wins or Draw. Then press Test.
★★ The best reply (Python, from Grade 8)
“Never the same twice” makes a row like Rock 0, Paper 5, Scissors 5. The predictor guesses one of the two and plays what beats it, but half the time the player plays the other, and then the predictor can lose. Is there a move that never loses here?
Write best_reply(r, p, s): given how often the other player has played rock, paper and scissors, give back the move that would have scored most against them, counting each win as 1 and each loss as −1. If two moves score the same, either is right.
def best_reply(r, p, s): # r, p, s: how many times the other player played rock, paper, scissors # give back 'R', 'P' or 'S': the move that scores most against them return 'R' print(best_reply(0, 5, 5))★★★ Beat the predictor (Python, from Grade 8)
Your program now plays against the predictor. The program below has it as predictor, with beat and most from rps.py copied in. Write choose_move(theirs, mine) so that it wins: here theirs is the predictor’s moves so far and mine yours.
import random def beat(move): if move == 'R': return 'P' if move == 'P': return 'S' return 'R' def most(r, p, s): if r > p and r > s: return 'R' if p > s: return 'P' return 'S' def predictor(theirs, mine): if len(theirs) == 0: return random.choice(['R', 'P', 'S']) last = theirs[len(theirs) - 1] r = 0 p = 0 s = 0 for i in range(len(theirs) - 1): if theirs[i] == last: after = theirs[i + 1] if after == 'R': r = r + 1 elif after == 'P': p = p + 1 else: s = s + 1 if r + p + s == 0: return random.choice(['R', 'P', 'S']) return beat(most(r, p, s)) def choose_move(theirs, mine): # theirs: the predictor's moves so far; mine: yours # give back the move that beats the predictor's next move return 'R' print(choose_move(['P', 'R', 'S'], ['R', 'P', 'R']))Play it against the predictor here, then check it below.
Questions
Which player can the predictor in this topic not beat over many rounds?
Pick an answer to see why.
A player’s moves so far are S S R S S R S. What does the predictor play next? Show the row it reads.
Answer
Why did the counter win none of 10 rounds against “R, P, S in turn”?
Pick an answer to see why.
In the Zhejiang study, the totals were close to a third each, but the players still had habits. Explain how both can be true, with an example.
Answer
Where to go next
- Grade 7, How Machines Learn: a model trained on examples, tested on new ones. Your predictor trains on every round you play.
- Grade 8, Generative AI: the next-word model, which counts what comes after each word, as your predictor counts what comes after each move.
- Grade 8, Searching and Sorting: lists and their positions, which the predictor walks through.
- How do you always win at Nim? A game where, unlike this one, there is a perfect strategy, and it is written in binary.
- How does a computer roll a die? Where a computer’s random numbers come from, and why
random.seed(1)gives the same games every time. - Can a machine learn a game by itself? Matchboxes full of beads learn tic-tac-toe from their wins and losses. (Coming soon.)
