Yesterday on Hacker News, there was a neat post showcasing a game where you play Rock-Paper-Scissors against an untrained neural network. The top comment was (and is) user QuadmasterXLII casting doubt on whether or not the game is fair, suggesting it must not be, because it is able to win against Random.org numbers.

Photo by Brett Jordan / Unsplash

Now, I know nothing about statistics or ro-sham-bo. I am a self-taught programmer, and I have no inclinations towards math. But, I felt I understood why random numbers can lose to AI, and I tried to explain it. It didn't go well. I think my comment came off as offensive to intelligent people, and resulted in many downvotes.

I realize now that the mental model I had trying to explain my intuitive understanding was wrong; speaking in terms of 'runs' doesn't make sense, because as people pointed out, past outputs have no impact on future outputs. But, despite that, I still didn't understand why you couldn't take advantage of the distribution of an RNG.

Normally I'd just admit defeat and move on, but by now I was just plain curious why this logic didn't work out, so I grabbed a sequence of 10,000 random numbers from 1-3 from Random.org, and wrote the following Python script:

from itertools import islice

# From https://docs.python.org/release/2.3.5/lib/itertools-example.html
def window(seq, n=2):
    it = iter(seq)
    result = tuple(islice(it, n))
    if len(result) == n:
        yield result
    for elem in it:
        result = result[1:] + (elem,)
        yield result

# Random data truncated.
RNG_DATA = [int(n) for n in "321132332122331332..."]

# Count the number of times two adjacent values are not equal to eachother.
count = 0
total = 0
for a, b in window(RNG_DATA):
    total += 1
    if a != b:
        count += 1

print(f"{count/total}")
Yes, this is hastily written. It's 3 AM.

I ran the script, and it gave me the following output:

0.6675667566756676

This means that for any given value in the sequence, the odds that the next number in the sequence is different from the previous number is about 2:1. This is unsurprising because we're dealing with a true random generator, and therefore should have an equal chance of getting any particular value at any time. There are 2 values that aren't the last value, and 1 that is the last value.

So let's say I'm the AI, and I know that you are 66% likely to choose a different number from the last one you chose. I could adjust my choice based on that knowledge. How? Well.. If I think you will choose Paper or Scissors, I should choose Scissors, since it will tie with Scissors but beat Paper. If I think you will choose Rock or Scissors, I will choose Rock because Rock will tie with Rock but beat Scissors. And finally, If I think you will choose Rock or Paper, I will choose Paper because Paper will tie with Paper but beat Rock.

Because I will be right about your choice 66% of the time, that means I win, right?

Let's find out. I wrote a quick script to implement the rules of RPS and implemented my "AI."

import random

# Random data truncated.
OPPONENT_MOVES = [int(n) for n in "3211323321223313321323131111..."]

# Possible moves.
ROCK = 1
PAPER = 2
SCISSORS = 3

# Some simple tables to implement the game.
NAMES = {ROCK: 'rock', PAPER: 'paper', SCISSORS: 'scissors'}
WINS_AGAINST = {ROCK: SCISSORS, PAPER: ROCK, SCISSORS: PAPER}

ADVANCED_NEURAL_AI = {
    ROCK: SCISSORS,
    PAPER: ROCK,
    SCISSORS: PAPER,
}

last_opponent_move = random.choice([ROCK, PAPER, SCISSORS])
win_count, loss_count, tie_count = 0, 0, 0

for opponent_choice in OPPONENT_MOVES:
    my_choice = ADVANCED_NEURAL_AI[last_opponent_move]
    
    print(f". . . {NAMES[my_choice]} VS {NAMES[opponent_choice]}!")

    if WINS_AGAINST[my_choice] == opponent_choice:
        print(f"{NAMES[my_choice]} wins against {NAMES[opponent_choice]}")
        win_count += 1
    elif WINS_AGAINST[opponent_choice] == my_choice:
        print(f"{NAMES[my_choice]} loses to {NAMES[opponent_choice]}")
        loss_count += 1
    else:
        tie_count += 1
    
    # Make note of the opponent's last choice.
    last_opponent_move = opponent_choice

print(f"FINAL RESULTS: {win_count} wins, {loss_count} losses, {tie_count} ties.")

And...

...
FINAL RESULTS: 3356 wins, 3324 losses, 3320 ties.

Oh.

Of course, I am wrong. But why?

There's definitely a 'smart' explanation about how Nash equilibrium works, but here I am just wondering why my intuitive understanding turned out to be incorrect. And I think that I might understand why that is, now.

So our "AI" does do something. It basically is able to at least match evenly against the random number generator. But, the trouble is, our guess does not give us a competitive advantage. We have 2:1 odds to not lose... but so does the opponent. Every choice is evenly likely, because our decisions map 1:1 with the opponent's previous decisions, which are also evenly likely to be any possible move.

The thing is, even our logic doesn't really actually matter, because past choices don't influence future choices. As long as our mapping is 1:1 with the opponent's previous (random) choice, we will get the exact same distribution of wins/losses/ties. Our choices are worse than random, though, because they are much more predictable, being based on the opponent's moves; a simple strategy could beat this.

So back to the original story, how does the AI win? It cheats. Whoops.

So what is the moral of this story? I don't know. Probably more than anything, I should stop commenting on stuff I know nothing about.