Risk stats

This notebook contains several visualizations to help predict the outcome of a battle in the game of risk.

For a primer on the rules of risk, check out http://media.wizards.com/2015/downloads/ah/Risk_rules.pdf

In this notebook, I use the following terminology:

  • Roll: a game event where the attacker rolls min(3, n-1) dice and the defender rolls min(2, n) dice
    • The outcome of a roll is determined by comparing the attacker's highest with the defender's highest dice and attacker's second highest with defender's second highest dice. Defender wins tie
    • Attacker can roll with a max of 3 dice, defender can roll with a max of 2
  • Battle: a series of rolls until either the defender is completely destroyed or the attacker can no longer attack
  • Success: a battle results in "success" if the attacker wins (i.e. if the defender is completely destroyed)

Graphs

There are three types of graphs in this notebook. You may need to scroll through a lot of code to see them.

Probability distribution for outcomes of a roll

There are a limited number of outcomes during a roll:

  • The defender loses two armies (e.g. attacker rolls 6, 6 and defender rolls 5, 5)
  • The defender loses one army (e.g. attacker rolls 6 and defender rolls 5)
  • The attacker and defender both lose one army (e.g. attacker rolls 6, 4 and defender rolls 6, 6)
  • The attacker loses one army (e.g. attacker rolls 5 and defender rolls 5)
  • The attacker loses two armies (e.g. attacker rolls 4, 4 and defender rolls 6, 4)

Probability distribution for outcomes of a battle

The height of each bar represents the probability that a battle will result in a certain outcome. Outcomes on the right are more favorable to the attacker. Outcomes on the left are more favorable to the defender.

You will notice that in the middle there is a dip. This is because if one army is almost destroyed, it has fewer dice and thus less chance to make a comeback.

Expected outcome matrix for outcomes of a battle

Each x, y location in the matrix corresponds to a certain starting configuration. The row/y corresponds with the number of attacking armies. The col/x corresponds with the number of defending armies.

The color of each cell maps to the probability of success for the attacker. Green means the attacker is likely to succeed, red means the defender is likely to succeed, and white means neither party is likely to succeed.

The first number in each cell is the probability of success described as a percentage. The second line shows the 25th percentile outcome (in terms of how well it went for the attacker). The third line shows the 50th percentile outcome. The fourth line shows the 75th percentile outcome. Each outcome is displayed as an ordered pair where the first element is the number of attacker armies remaining and the second element is the number of defender armies remaining.

In [1]:
from collections import namedtuple, defaultdict
import itertools

Armies = namedtuple("Armies", ["attackers", "defenders"])
def loss_str(armies):
    return "Attacker loses %d, Defender loses %d" % (-armies.attackers, -armies.defenders)
assert(loss_str(Armies(-4, -6)) == "Attacker loses 4, Defender loses 6")

def remains_str(armies):
    return "%d attackers and %d defenders" % (armies.attackers, armies.defenders)
assert(remains_str(Armies(6, 3)) == "6 attackers and 3 defenders")

def tuple_str(armies):
    return "(%d, %d)" % (armies.attackers, armies.defenders)

def assert_close(actual, expected):
    "assert that the two values are pretty darn close... like they should be equal if they weren't floats"
    if abs(actual - expected) > 0.00001:
        raise AssertionError("Bad stuff.\nActual: %s\nExpected: %s\n" % (actual, expected))

def eval_roll(attacker_dice, defender_dice):
    "Return the delta for each side... (delta_attackers, delta_defenders)"
    attacker_wins, defender_wins = 0, 0
    for attacker_roll, defender_roll in zip(sorted(attacker_dice)[::-1], sorted(defender_dice)[::-1]):
        if attacker_roll > defender_roll:
            attacker_wins += 1
        else:  #defender wins tie
            defender_wins += 1
    return Armies(-defender_wins, -attacker_wins)
assert(eval_roll([5, 4, 1], [5, 1]) == Armies(-1, -1))
assert(eval_roll([6, 6, 6], [6]) == Armies(-1, 0))

def generate_rolls(n_dice):
    "generate every possible roll given n_dice"
    return itertools.product((1, 2, 3, 4, 5, 6), repeat=n_dice)

def scale_dict(d, scale):
    "scale each value in a dict by |scale|"
    ret = defaultdict(lambda: 0)
    for key in d:
        ret[key] = d[key] * scale
    return ret

def normalize_dict(d):
    "scale down all dict values so they sum to 1.0"
    return scale_dict(d, 1.0 / sum(d.values()))

def get_roll_stats(n_attack_dice, n_defence_dice):
    "find the likelyhood of each outcome given x attack dice and y defence dice"
    res = defaultdict(lambda: 0)
    for attacker_roll in generate_rolls(n_attack_dice):
        for defender_roll in generate_rolls(n_defence_dice):
            res[eval_roll(attacker_roll, defender_roll)] += 1
    return normalize_dict(res)
assert_close(get_roll_stats(1, 1)[Armies(0, -1)], 15/36)
assert_close(get_roll_stats(1, 1)[Armies(-1, 0)], 21/36)
assert_close(get_roll_stats(3, 2)[Armies(-1, -1)], 2611/7776)
In [2]:
import plotly.plotly as py
import plotly.graph_objs as go
from plotly.offline import download_plotlyjs, init_notebook_mode, plot, iplot
init_notebook_mode(connected=False)

def make_roll_chart(n_attacker_dice, n_defender_dice):
    "Generate a pie chart showing the probability of each outcome for a an X by Y roll"
    outcomes = get_roll_stats(n_attacker_dice, n_defender_dice)
    labels = [loss_str(key) for key in outcomes]
    values = [val for (key, val) in outcomes.items()]
    fig = {
        'data': [{
                'labels': labels,
                'values': values,
                'type': 'pie'
            }],
        'layout': {
            'title': 'Possible outcomes when rolling %d attack die and %d defence die' %
                     (n_attacker_dice, n_defender_dice)
        }
    }
    iplot(fig)

make_roll_chart(1, 1)
make_roll_chart(1, 2)
make_roll_chart(2, 1)
make_roll_chart(2, 2)
make_roll_chart(3, 1)
make_roll_chart(3, 2)