Skip to Content

Risk Battle Simulator

Risk is a game of strategy, diplomacy, and especially in the late game, excessive dice rolling. I wrote a utility to automate tedious battles and also compute the odds of winning under different circumstances.

Code available on GitHub

Rolling the Dice

Generating uniformly distributed random numbers is not trivial in C. The naive implementation, rand() % N, yields a skewed distribution in most cases. I wrote about how to do this properly in a separate post.

For each roll, we need to compare the attacker’s highest die with the defender’s highest die and so on until all pairs are exhausted. This is accomplished simply by generating a sorted array of random die rolls for each player and then comparing successive values between the arrays.

/* Simulates a single roll of the dice */
void risk_roll(int n_a, int n_d, int *loss_a, int *loss_d)
{
  int a[n_a];
  rand_dice_sorted(a, n_a);

  int d[n_d];
  rand_dice_sorted(d, n_d);

  *loss_a = 0;
  *loss_d = 0;

  int pairs = MIN(n_a, n_d);
  for (int i = 0; i < pairs; i++) {
    if (d[i] < a[i]) {
      (*loss_d)++;
    } else {
      (*loss_a)++;
    }
  }
}

Deciding Battles

For battles, we roll the dice repeatedly until one player has no more armies. Per Risk rules, the attacker is limited to three dice per roll while the defender is limited to two. The function returns the number of armies lost by each player via the loss_a and loss_d pointer parameters.

/* Simulates dice rolls until the attacker or defender wins */
void risk_battle(int n_a, int n_d, int *loss_a, int *loss_d)
{
  int curr_n_a = n_a;
  int curr_n_d = n_d;

  while (curr_n_a > 0 && curr_n_d > 0) {
    int roll_n_a = MIN(curr_n_a, ROLL_ATTACK_DICE_MAX);
    int roll_n_d = MIN(curr_n_d, ROLL_DEFEND_DICE_MAX);
    int roll_loss_a;
    int roll_loss_d;
    risk_roll(roll_n_a, roll_n_d, &roll_loss_a, &roll_loss_d);
    curr_n_a -= roll_loss_a;
    curr_n_d -= roll_loss_d;
  }

  *loss_a = n_a - curr_n_a;
  *loss_d = n_d - curr_n_d;
}

Measuring Probabilities

Given a function to simulate a single battle, it is straightforward to record a large number of trials and estimate the probability of different outcomes. Since each battle concludes with the losing player’s armies depleted, we just need to build a histogram of how many armies the winning player lost.

int *hist_loss_a = (int *)calloc(n_a, sizeof(int));
int *hist_loss_d = (int *)calloc(n_d, sizeof(int));

for (int i = 0; i < runs; i++) {
  int loss_a;
  int loss_d;
  risk_battle(n_a, n_d, &loss_a, &loss_d);

  if (loss_a < n_a) {
    hist_loss_a[loss_a]++;
  } else {
    hist_loss_d[loss_d]++;
  }
}

As an example, here is the output for a battle between five attacking armies and two defending armies:

$ risk prob 5 2 100000
calculating probabilities for 5 attackers vs. 2 defenders using 100000 runs
results:
attacker wins:
armies lost | probability | cumulative
  0            36.8%         36.8%
  1            22.8%         59.6%
  2            18.3%         77.9%
  3             7.9%         85.8%
  4             3.3%         89.1%
defender wins:
armies lost | probability | cumulative
  0             6.4%          6.4%
  1             4.5%         10.9%

Having played numerous games before computing any of these numbers, it was interesting to see how my intuition compared to reality. I think the most significant takeaway was the advantage the attacker gains by outnumbering the defender. Balanced battles of 3 vs. 3 or greater have roughly 1:1 odds, but if the attacker outnumbers the defender by just one army (e.g. 4 vs. 3), his odds increase to about 2:1. Outnumbering the defender by two armies (e.g. 5 vs. 3) yields an advantage of about 3:1.