Risk Analysis for Risk

In 2011 I was on vacation with friends, we played a lot of Risk. Somehow we ended up having fights of hundreds of armies against each other. Since with every dice rolling you can only eliminate up to three armies, you need a lot of rounds until a battle is settled. While my friends were occupied in a battle of 200 against 150, I used my freshly acquired Python skills to write a program to do the dice rolling, risk-auto-dice.

The program does exactly what the players do until one player runs out of armies:

  • The attacker rolls up to three dice, the defender up to two. The number of dice cannot exceed the number of armies. In the game a player can choose to use less dice, the program uses the maximum amount.

  • The results are ordered descending and paired up. For every pair which is unequal, the person with the lower number loses one unit.

At first, the program would only output the end result. The result turned out to be extremely unstable, a 100 vs 100 fight could end with either side having 50 armies left. If one re-ran the program, the result was completely different. At the time, we just accepted this as an intrinsic quirk of the game that gave it its name.

Revisiting the Problem

Now, six years later, I learned more about statistics. So I looked at this again. For this article, I have implemented the dice rolling function in R because I currently learn that language. The fight function is not very complicated:

sorted_roll <- function (number) {
    sort(sample(1:6, number, replace = TRUE), decreasing = TRUE)
}

fight <- function (attacking, defending) {
    armies_att <- attacking
    armies_def <- defending

    # Risk has “last man standing” fights, therefore
    while (armies_att > 0 && armies_def > 0) {
        # The number of dice that can be used is capped to 3 and 2.
        dice_att <- min(armies_att, 3)
        dice_def <- min(armies_def, 2, dice_att)
        dice_both <- min(dice_att, dice_def)

        # Roll the dice.
        roll_att <- sorted_roll(dice_att)
        roll_def <- sorted_roll(dice_def)

        #cat(armies_att, ' ', armies_def, '\n')
        #print(roll_att)
        #print(roll_def)
        #cat('\n')

        for (i in 1:dice_both) {
            # The attacker has won a single battle, therefore the defender loses a die.
            if (roll_att[i] > roll_def[i]) {
                armies_def <- armies_def - 1
            }
            # The defender has scored.
            else if (roll_att[i] < roll_def[i]) {
                armies_att <- armies_att - 1
            }
        }
    }

    return (data.frame(attacker = armies_att,
                       defender = armies_def,
                       diff = armies_att - armies_def))
}

Analysis

I have run it 300 times for each configuration of up to 25 armies. The whole implementation is not really fast, therefore this already took several minutes to run through. For a more sophisticated analysis one should probably re-implement this in C++ and use OpenMP to get some more speed. The amount of data generated seems to sufficiently see the trend, there is no indication that beyond 25 armies something qualitatively different will happen.

data <- expand.grid(attacking = 1:25, defending = 1:25, sample = 1:300)

data$x <- mapply(fight, data$attacking, data$defending, SIMPLIFY = FALSE)
data %<>% tidyr::unnest()
just_10_10 <- data %>%
  filter(attacking == 10,
         defending == 10)

First one has to note that the combat system is extremely unpredictable. There is a lot of spread in the result. Take for instance 10 attackers and 10 defenders. The resulting number of armies will be the following, where negative values mean surviving defenders.

ggplot(just_10_10) +
  geom_histogram(aes(x = diff), binwidth = 1) +
  labs('Fight of 10 vs. 10',
       x = 'Remaining attackers - remaining defenders',
       y = 'Count')

plot of chunk unnamed-chunk-4

Even though 10 vs. 10 starts with the same amount of armies on both sides, we can clearly see that the attacker on average has better chances of winning the battle. In most cases the attacker even gets to keep most of the armies. Keeping in mind that there are huge fluctuations in the fights we will take a look at averages.

grouped <- data %>%
  group_by(attacking, defending) %>%
  summarize(won = mean(attacker > 0),
            attacker_survival = mean(attacker / attacking),
            diff_mean = mean(diff),
            diff_sd = sd(diff),
            diff_q01 = quantile(diff, .01),
            diff_q30 = quantile(diff, .30),
            diff_q50 = quantile(diff, .50),
            diff_q70 = quantile(diff, .70),
            diff_q99 = quantile(diff, .99))

Winning Chance

The following shows the ratio of fights that the attacker wins given various number of attacking and defending armies.

ggplot(grouped) +
  geom_tile(aes(x = attacking, y = defending, fill = won)) +
  scale_fill_gradient2(midpoint = 0.5) +
  geom_abline(slope = 1) +
  labs(title = 'Ratio of attacker wins',
       x = 'Attacking Armies',
       y = 'Defending Armies',
       fill = 'Attacker wins')

plot of chunk unnamed-chunk-6

As we have seen before, it is heavily skewed towards the attacker. The slope of the white line, where the chances to win are equal for both sides, has a much higher slope than the black line which marks the same number of attackers and defenders. We see that defense is hard in this game and requires some 50% more armies in order to just give equal odds for both sides.

One can have a look at the surviving armies of the attacker normalized by armies put into the battle, this is a survival rate:

ggplot(grouped) +
  geom_tile(aes(x = attacking, y = defending, fill = attacker_survival)) +
  scale_fill_gradient() +
  geom_abline(slope = 1) +
  labs(title = 'Ratio of attacker survival',
       x = 'Attacking Armies',
       y = 'Defending Armies',
       fill = 'Attacker survival')

plot of chunk unnamed-chunk-7

Interestingly, the 50% line has almost unit slope, so when both players have the same amounts of armies, the survival rate of the attacker's armies is still 50%. Of course to win one only needs to have a single unit left, therefore the ratio of won battles has a larger slope.

Fluctuations

But there were those great fluctuations. If we look at the standard deviation of armies left, we see the following:

ggplot(grouped) +
  geom_tile(aes(x = attacking, y = defending, fill = diff_sd)) +
  scale_fill_gradient() +
  geom_abline(slope = 1) +
  labs(title = 'Spread of armies left',
       x = 'Attacking Armies',
       y = 'Defending Armies',
       fill = 'Standard devidation')

plot of chunk unnamed-chunk-8

On the bottom and on the left, there are no fluctuations. This is no surprise because these situations correspond to a near-certain victory or defeat. The fluctuations look rather symmetric. But that does not really tell much about the risk that one is taking in each battle.

So let's have a look at the 1% quantile of the armies that are left after the battle. I have chosen 1% instead of 0% because that should be somewhat stable against outliers whereas 0% will by definition be the most extreme outlier.

ggplot(grouped) +
  geom_tile(aes(x = attacking, y = defending, fill = diff_q01)) +
  scale_fill_gradient2() +
  geom_abline(slope = 1) +
  labs(title = '1% worst outcomes for attacker',
       x = 'Attacking Armies',
       y = 'Defending Armies',
       fill = 'Difference')

plot of chunk unnamed-chunk-9

Most of the chart corresponds to a defeat, zero armies left. For low numbers it is skewed towards the defender, it you want to be almost completely certain to win, you will need to have at least four armies to attack a single one. For large number of armies, it seems to become more even again. One could draw the line such that the chance of losing is very low. One would need more statistics here in order to get a better resolution because 300 samples are not going to give a very good 1% quantile.

In a game called "Risk", one cannot be that conservative. Therefore let's have a look at the 30% quantile:

ggplot(grouped) +
  geom_tile(aes(x = attacking, y = defending, fill = diff_q30)) +
  scale_fill_gradient2() +
  geom_abline(slope = 1) +
  labs(title = '30% worst outcomes for attacker',
       x = 'Attacking Armies',
       y = 'Defending Armies',
       fill = 'Difference')

plot of chunk unnamed-chunk-10

For very low numbers, the defender has the advantage. This changes at around 5--5, where the attacker has the advantage again.

The median (50% quantile) looks pretty good for the attacker:

ggplot(grouped) +
  geom_tile(aes(x = attacking, y = defending, fill = diff_q50)) +
  scale_fill_gradient2() +
  geom_abline(slope = 1) +
  labs(title = '50% worst outcomes for attacker',
       x = 'Attacking Armies',
       y = 'Defending Armies',
       fill = 'Difference')

plot of chunk unnamed-chunk-11

If you want to hedge your bets on something that will work out worse in 70% of the cases, you can have a look at the following chart:

ggplot(grouped) +
  geom_tile(aes(x = attacking, y = defending, fill = diff_q70)) +
  scale_fill_gradient2() +
  geom_abline(slope = 1) +
  labs(title = '70% worst outcomes for attacker',
       x = 'Attacking Armies',
       y = 'Defending Armies',
       fill = 'Difference')

plot of chunk unnamed-chunk-12

The best cases that could happen (only 1% even better) look like this:

ggplot(grouped) +
  geom_tile(aes(x = attacking, y = defending, fill = diff_q99)) +
  scale_fill_gradient2() +
  geom_abline(slope = 1) +
  labs(title = '99% worst outcomes for attacker',
       x = 'Attacking Armies',
       y = 'Defending Armies',
       fill = 'Difference')

plot of chunk unnamed-chunk-13

With three armies, one has a slim chance of winning against a defender with 17 armies. In the best case scenario, the number of defending armies only has weak impact on the result. This can be seen through the pretty vertical slope of the color boundaries in the plots. That should be compared to the worst case, where the slopes are only down to unity.

Fixed number of defending armies

Let us fix the number of defending armies to 10 and just vary the number of attackers. In the following plot the line denotes the median, the dark ribbon covers 30% to 70% and the gray ribbon from 1% to 99% of the cases. The red line marks 10 attackers, the case of equal power on each side. The blue line marks 0 surviving armies, wins for the attacker are below that line and wins for the defender below.

ggplot(filter(grouped, defending == 10), aes(x = attacking)) +
  geom_ribbon(aes(ymin = diff_q01, ymax = diff_q99), alpha = 0.3) +
  geom_ribbon(aes(ymin = diff_q30, ymax = diff_q70), alpha = 0.3) +
  geom_line(aes(y = diff_q50)) +
  geom_vline(xintercept = 10, color = 'red') +
  geom_hline(yintercept = 0, color = 'blue') +
  labs(title = 'Remaining armies for 10 defending armies',
       x = 'Attacking armies',
       y = 'Difference')

plot of chunk unnamed-chunk-14

We can nicely see that the attacker has the advantage in the median case until we get down to 6 attacking armies. But even if we aim to win 70% of our fights, we can just take around 7 or 8 armies against 10. And if we take 15 armies, the chances of losing are just around 1%, which is extremely slim.

Next we take a look at this from the other side, fixing 10 attackers and varying the number of defenders. The colors are all the same.

ggplot(filter(grouped, attacking == 10), aes(x = defending)) +
  geom_ribbon(aes(ymin = diff_q01, ymax = diff_q99), alpha = 0.3) +
  geom_ribbon(aes(ymin = diff_q30, ymax = diff_q70), alpha = 0.3) +
  geom_line(aes(y = diff_q50)) +
  geom_vline(xintercept = 10, color = 'red') +
  geom_hline(yintercept = 0, color = 'blue') +
  labs(title = 'Remaining armies for 10 attacking armies',
       x = 'Defending armies',
       y = 'Difference')

plot of chunk unnamed-chunk-15

We see that most of the chart is a win to the attacker. Even 25 defending armies are not enough to ensure a defend rate of 99%, we can only get to around 70%. With the median we see that it takes around 18 armies to defend more than 50% of the attacks.

Conclusion

It seems that the attacker usually has a great advantage in terms of numbers. The chance to win is higher. This probably means that an aggressive strategy will serve the player best.