# Rock, Paper, Scissors

January 23, 2022## Why Rock, Paper, Scissors?

🪨 📰 ✂️

Why this topic? Well, coincidentally, I stumbled over this twice this week.

First, I saw a video about it on Numberphile, a YouTube channel that I can highly recommend and I'am watching regularly since a friend recently told me about it.

Then, my favorite podcast Stuff You Should Know
did an episode about this game. I was listening to this episode
while I was on a run and during this run I "wrote" most of the **C# code simulating
Rock, Paper, Scissors** in my head.

Of course, I then had to actually write this code down and try it out. And since I enjoyed this so much, I even wrote this blog post about it.

## The Setup

All the code can be found here on GitHub. Throughout the blog post I tried to add enough snippets from the project, so that you can follow along. Note however, that these snippets are only a subset of the full project's code.

Let's start with the most important data type `Move`

.

```
enum Move
{
Rock,
Paper,
Scissors,
}
```

`Rock`

beats`Scissors`

but loses to`Paper`

.`Paper`

beats`Rock`

but loses to`Scissors`

.`Scissors`

beats`Paper`

but loses to`Rock`

.- Equal
`Move`

s are a tie. - ... you know the drill.

Then, let's define a couple of strategies a player can use:

```
enum Strategy
{
Random,
Keep,
Forward,
Backward,
Copy,
Rock,
Paper,
Scissors,
}
```

Here's how these strategies work.

`Random`

: As the name says, a random`Move`

.`Keep`

: Always keeps the last`Move`

. If the last`Move`

was e.g.`Rock`

, it will again be`Rock`

and so on.`Forward`

: If you think of the possible outcomes`Rock`

-`Paper`

-`Scissor`

as ordered, this strategy will simply select the next`Move`

. If e.g. the last`Move`

was`Rock`

, it will select`Paper`

, after`Paper`

it will select`Scissors`

, after`Scissors`

it starts with`Rock`

again.

**Note:**The produced`Move`

would**always win over the previous**one.`Backward`

: Like`Forward`

but in the other direction.

**Note:**The produced`Move`

would**always lose to the previous**one.`Copy`

: This imitates other the`Player`

s last`Move`

.`Rock`

: Always`Rock`

.`Paper`

: Always`Paper`

.`Scissors`

: Always`Scissors`

.

Then, we have a `Player`

class that selects one of these strategies and can compete against
another `Player`

(in fact, we are using an interface `IPlayer`

mostly, to keep that `Player`

implementation interchangeable).

So here is, how the simulation goes:

- All possible combinations of stragies are calculated (
`CreatePlayerTypes`

). - Each player (i.e.
`Strategy`

) competes against all other players. - Each game consists of
`n`

rounds (where`n`

is a large number to get good statistics).- In each round, the player selects a
`Move`

based on their`Strategy`

. **Note:**the first`Move`

is**random**(although we will change that later).

- In each round, the player selects a
- The
`Statistics`

class keeps track of the games' outcomes so we know which`Strategy`

is the most successful one.

## Randomness

*Rock, Paper, Scissors* is a game of randomness and a player selecting the `Random`

strategy should have equal outcomes of wins, losses and ties.

Let's test this theory first. For that, we create two `Player`

instances that
use `Strategy.Random`

and let them can compete against each other.

```
var player1 = new Player(Strategy.Random);
var player2 = new Player(Strategy.Random);
```

Calling the `Play`

method, we let these players compete a **million** times (rounds).
The `Statistics`

class keeps track of all the outcomes of these games.

```
var statistics = new Statistics();
Play(player1, player2, 1_000_000, statistics);
```

Finally, we call `PrintGame`

to get an overview of the game and all its rounds played.

```
statistics.PrintGame();
```

Yes, that output looks about right. One third of the games was won, another third was lost and the last third was ties.

```
--- [Random] vs [Random]
Number of games played: 1000000
Wins: 33% (333677)
Losses: 33% (333079)
Ties: 33% (333244)
```

In fact, as soon as one of the players employs the `Random`

strategy, this will always be
the outcome, regardless of the strategy chosen by the other player.

Let's code a verification for that in the `Play`

method.

```
if (player1.IsRandom || player2.IsRandom)
{
Debug.Assert(Math.Abs(statistics.Wins - 33) <= 1);
Debug.Assert(Math.Abs(statistics.Losses - 33) <= 1);
Debug.Assert(Math.Abs(statistics.Ties - 33) <= 1);
}
```

OK, this checks out. But let's ignore `Random`

from now on and and look for a `Strategy`

that is more *successful* and yields more than 33% wins when competing against all other strategies.

Let's define "successful" as yielding more than 50% wins after playing

nrounds.

Or written in code:`var successful = statistics.Wins > 50;`

## A Simple Player

The `Statistics`

class also tracks the number of wins for each strategy so we can ask for a
ranking (`PrintRanking`

) after having all `Player`

s (i.e. `Strategy`

s) competed against each other.

The `Simulate`

method takes care of that.

```
void Simulate(List<IPlayer> playerTypes, int rounds)
{
var statistics = new Statistics();
foreach (var player1 in playerTypes)
{
foreach (var player2 in playerTypes)
{
Play(player1, player2, rounds, statistics);
}
}
statistics.PrintRanking();
}
```

Let's see how all `Strategy`

s compare against each other. Looks like `Forward`

is
most successful.

Note:Thescorein these final rankings means that a`Strategy`

lead to more than 50% percent of won rounds in a game. Put differently, a score of e.g. 2 means that a given strategy was able to beat 2 other`Strategy`

s.

```
Top 5 strategies:
Forward - score: 2
Keep - score: 1
Copy - score: 1
Rock - score: 1
Paper - score: 1
```

So, let's run the simulation again and this time the result looks like this.

```
Rock - score: 2
Forward - score: 1
Copy - score: 1
Paper - score: 1
Scissors - score: 1
```

Apparently, a lot depends on the initial **random** move here. So let's get rid
of that randomness.

### Removing Randomness

Instead of starting the first round with a randomly selected `Move`

, we actually run
the simulation with all possible start combinations (calling the method `PlayWithStartMoves`

).

This means we have 9 different constellations for the initial round (and we play all of them).

`Rock`

vs`Rock`

`Rock`

vs`Paper`

`Rock`

vs`Scissors`

`Paper`

vs`Rock`

`Paper`

vs`Paper`

`Paper`

vs`Scissors`

`Scissors`

vs`Rock`

`Scissors`

vs`Paper`

`Scissors`

vs`Scissors`

Running the simulation again, this now gives us a consistent results.

```
--- [Forward] vs [Copy]
Number of games played: 900000
Wins: 99% (899994)
Losses: 0% (3)
Ties: 0% (3)
--- [Copy] vs [Backward]
Number of games played: 900000
Wins: 99% (899994)
Losses: 0% (3)
Ties: 0% (3)
Most successful strategies:
Forward - score: 1
Copy - score: 1
```

`Forward`

dominates`Copy`

:`Player`

1 selects the next best move while`Player`

2 copies what`Player`

1 just played, so naturally it's a win for`Player`

1.`Copy`

dominates`Backward`

:`Player`

1 selects what`Player`

2 just played, while`Player`

2 goes "backwards" and selects the inferior move. Surely,`Player`

1 will always win here.

So far - so boring, admittedly. The game dynamics are just
too limited to produce interesting results. So let's improve our `Player`

class.

## An Improved Player

In fact, we introduce a new class `ImprovedPlayer`

that implements the same interface
`IPlayer`

as the `Player`

class so we can use instances of these classes interchangeably.

The main difference in this new class is that it holds **3** `Strategy`

s instead of just
one and it applies a different `Strategy`

whether the previous round was **won, lost or tied**.

```
public ImprovedPlayer(Strategy winStrategy, Strategy loseStrategy, Strategy tieStrategy)
{
_winStrategy = winStrategy;
_loseStrategy = loseStrategy;
_tieStrategy = tieStrategy;
}
```

Based on the outcome of the last round, this `ImprovedPlayer`

selects one of these 3 `Strategy`

s
when the `NextMove`

method is called.

```
public Move NextMove(Result lastResult, Move lastMove, Move lastMoveOtherPlayer)
{
var strategy = lastResult switch
{
Result.Win => _winStrategy,
Result.Lose => _loseStrategy,
Result.Tie => _tieStrategy,
};
return Player.GetNextMoveForStrategy(strategy, lastMove, lastMoveOtherPlayer);
}
```

Using the same start conditions (randomness removed), this yields two dominating `Strategy`

s:

```
Most successful strategies:
Forward/Forward/Forward - score: 132
Forward/Copy/Forward - score: 132
Keep/Forward/Forward - score: 111
Keep/Copy/Forward - score: 111
Forward/Forward/Rock - score: 111
```

`Forward/Forward/Forward`

: So again, just selecting the next`Move`

in all cases seems to be the most successful strategy.`Forward/Copy/Forward`

: Interestingly, this combination of strategies is just as successful. Selecting`Copy`

in case of a lost round yields the same score as the plain`Forward`

strategy.

Let's see how these two strategies compare against each other:

```
--- [Forward/Forward/Forward] vs [Forward/Copy/Forward]
Number of games played: 9000
Wins: 33% (3000)
Losses: 33% (3000)
Ties: 33% (3000)
```

As expected, there's no advantage for either of those.

But why is `Forward/Copy/Forward`

more successful than e.g. `Copy/Forward/Forward`

or `Forward/Forward/Copy`

? Let's have a closer look at how these strategies perform against
all other combinations and more specifically: which (and how many) others they can defeat.
The method `CompareImproved`

takes care of that.

**Note:** I removed the "trivial" (`Rock`

, `Paper`

, `Scissors`

) strategies for this
since it does not change the overall result but makes it easier to grasp since
it reduces the number of combinations we have to look at.

This is the result of comparing `Forward/Copy/Forward`

to `Forward/Forward/Copy`

:

**Note** that we look only at **unique wins** here, i.e. we exclude losing strategies
that both `Forward/Copy/Forward`

and `Forward/Forward/Copy`

have defeated.

```
Only [Forward/Copy/Forward] wins against:
- Keep/Forward/Keep
- Keep/Forward/Copy
- Keep/Copy/Keep
- Keep/Copy/Copy
- Forward/Forward/Keep
- Forward/Forward/Copy
- Forward/Copy/Keep
- Forward/Copy/Copy
Only [Forward/Forward/Copy] wins against:
- Keep/Forward/Backward
- Keep/Copy/Backward
- Forward/Forward/Backward
- Forward/Copy/Backward
```

We can immediately see that:

`Forward/Forward/Copy`

has more unique wins.`Forward/Copy/Forward`

wins directly against`Forward/Forward/Copy`

.

Comparing `Forward/Copy/Forward`

and `Copy/Forward/Forward`

, the result is even more onesided:

```
Only [Forward/Copy/Forward] wins against:
- Keep/Forward/Keep
- Keep/Forward/Copy
- Keep/Copy/Keep
- Keep/Copy/Copy
- Forward/Forward/Keep
- Forward/Forward/Copy
- Forward/Copy/Keep
- Forward/Copy/Copy
- Backward/Forward/Keep
- Backward/Forward/Forward
- Backward/Forward/Backward
- Backward/Forward/Copy
- Backward/Copy/Keep
- Backward/Copy/Forward
- Backward/Copy/Backward
- Backward/Copy/Copy
- Copy/Forward/Keep
- Copy/Forward/Forward
- Copy/Forward/Backward
- Copy/Forward/Copy
- Copy/Copy/Keep
- Copy/Copy/Forward
- Copy/Copy/Backward
- Copy/Copy/Copy
Only [Copy/Forward/Forward] wins against:
- Keep/Backward/Keep
- Keep/Backward/Copy
- Forward/Backward/Keep
- Forward/Backward/Copy
- Backward/Backward/Keep
- Backward/Backward/Forward
- Backward/Backward/Backward
- Backward/Backward/Copy
- Copy/Backward/Keep
- Copy/Backward/Forward
- Copy/Backward/Backward
- Copy/Backward/Copy
```

Again, we see that:

`Forward/Forward/Copy`

has more unique wins.`Forward/Copy/Forward`

wins directly against`Copy/Forward/Forward`

.

So, after all, the `Forward`

`Strategy`

seems to be the best bet as
soon as a player deviates from employing pure randomness (which against
a human opponent is always the case). This underlines the psychological, and
game-theoretical aspect here. Because, what happens if every player knows that using
`Forward`

gives and advantage? Right, they will chose a strategy that counters a
`Forward`

move. Which in turn leads to another strategy that counters that counter-move.
And so on, and so forth ... and endless game of counter-moves until someone
"chickens" out. You see, we actually just scratched the very surface of
tactics so far.

## Next?

It's fascinating how deep into a rabbit-hole one can go with such a simple game as Rock, Paper, Scissors. But I guess, that is part of the fascination with such simple setups (similar to fractals, cellular automata, ...) to see how far one can go from there.

Of course, even though this was quite a lengthy post already, we only scratched the surface of this topic. The next step would be more advanced strategies that e.g.

- have a longer "memory" and act accordingly
- change their strategy depending on the course of the game (e.g. adapt to losing or tie streaks).
- use
**machine learning**to try and figure out the opponent's strategy. In fact, I wrote a very simple neural network (DotNeuralNet) a while ago, that I wanted to port to .NET Core anyway. This will be the perfect opportunity to dust off this project and put it to use again. So stay tuned for a follow-up post on this using a`SmartPlayer`

strategy.