The table below shows expected outcomes for large battles in risk. The row corresponds to the size of the attacking army, the column corresponds to the size of the defending army.
The greener a cell is, the more likely it is the attacker can take control of a territory. The redder a cell is, the more likely it is the defender will maintain control of a territory.
Each cell contains a number corresponding to an expected outcome. If the number is positive, it represents the number of armies the attacker will likely be left with. If the number is negative, it represents the number of armies the defender will likely be left with.
For a primer on the rules of risk: http://media.wizards.com/2015/downloads/ah/Risk_rules.pdf Each configuration was simulated 5,000 times to determine the frequency of outcomes.
from collections import namedtuple
import random
rand = random.Random()
Armies = namedtuple("Armies", ["attackers", "defenders"])
class Side:
ATTACKER = 1
DEFENDER = 2
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 roll_dice(num):
return [rand.randint(1, 6) for i in range(num)]
def run_battle(armies):
attacking, defending = armies
while attacking > 1 and defending > 0:
delta_attacking, delta_defending = eval_roll(
roll_dice(min(attacking - 1, 3)),
roll_dice(min(defending, 2)))
attacking += delta_attacking
defending += delta_defending
return Armies(attacking, defending)
run_battle(Armies(6, 5))
Armies(attackers=1, defenders=5)
def get_winner(armies):
if armies.defenders == 0:
return Side.ATTACKER
else:
return Side.DEFENDER
def run_many_battles(armies, num_iterations=2500):
"Given a starting configuration of |armies|, simulate |num_iterationgs| battles and return stats on the results"
attacker_wins, defender_wins = 0, 0
attacker_remainings, defender_remainings = [], []
for i in range(num_iterations):
res = run_battle(armies)
if get_winner(res) == Side.ATTACKER:
attacker_wins += 1
attacker_remainings.append(res.attackers)
else:
defender_wins += 1
defender_remainings.append(res.defenders)
attacker_mean = 0.0
defender_mean = 0.0
all_mean = sum(attacker_remainings + [-x for x in defender_remainings]) / len(attacker_remainings + defender_remainings)
try:
#attacker_mean = max(set(attacker_remainings), key=attacker_remainings.count)
attacker_mean = sum(attacker_remainings) / len(attacker_remainings)
except:
pass
try:
#defender_mean = max(set(defender_remainings), key=defender_remainings.count)
defender_mean = sum(defender_remainings) / len(defender_remainings)
except:
pass
return (attacker_wins / num_iterations, defender_wins / num_iterations, attacker_mean, defender_mean, all_mean)
def generate_battle_matrix(max_attackers=20, max_defenders=20):
"Call run_many_battles on various starting configurations"
matrix = [[0 for x in range(max_defenders + 1)] for y in range(max_attackers + 1)]
for attackers in range(max_defenders + 1):
for defenders in range(max_defenders + 1):
matrix[attackers][defenders] = run_many_battles(Armies(attackers, defenders))
return matrix
battle_results = generate_battle_matrix()
from IPython.display import HTML, display
def colorize(res, attackers_start, defenders_start):
"Assign a color to the stats collected by run_many_battles"
rate, _, attackers_left, defenders_left, all_left = res
"""
if rate > 0.5:
whiteness = int(255 * (1 - (attackers_left / attackers_start)**2.0))
return "#%02xff%02x" % (whiteness, whiteness)
else:
whiteness = int(255 * (1 - (defenders_left / defenders_start)**2.0))
return "#ff%02x%02x" % (whiteness, whiteness)
"""
whiteness = int(255 * (1 - (2 * abs(rate - 0.5))**1.5))
if rate > 0.5:
return "#%02xff%02x" % (whiteness, whiteness)
else:
return "#ff%02x%02x" % (whiteness, whiteness)
def render_matrix_html(matrix):
"Render the results of generate_battle_matrix as an html table"
rows = []
first_row = ["<th></th>"]
for num_defenders, _ in enumerate(matrix[0]):
if num_defenders == 0:
continue
first_row.append("<th>d=%s</th>" % (num_defenders))
rows.append(''.join(first_row))
for num_attackers, stats in enumerate(matrix):
if num_attackers == 0:
continue
cells = ["<th>a=%s</th>" % (num_attackers)]
for num_defenders, res in enumerate(stats):
if num_defenders == 0:
continue
successRate = res[0]
remainingArmies = 0
if successRate >= 0.5:
remainingArmies = "+%2.1f" % res[2]
else:
remainingArmies = "-%2.1f" % res[3]
color = colorize(res, num_attackers, num_defenders)
remainingArmies = "%2.1f" % res[4]
cells.append("<td style='background-color: %s'>%s</td>" % (color, remainingArmies))
rows.append(''.join(cells))
rows = ["<tr>%s</tr>" % (row) for row in rows]
return HTML("<table>%s</table>" % (''.join(rows)))
display(render_matrix_html(battle_results))
d=1 | d=2 | d=3 | d=4 | d=5 | d=6 | d=7 | d=8 | d=9 | d=10 | d=11 | d=12 | d=13 | d=14 | d=15 | d=16 | d=17 | d=18 | d=19 | d=20 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
a=1 | -1.0 | -2.0 | -3.0 | -4.0 | -5.0 | -6.0 | -7.0 | -8.0 | -9.0 | -10.0 | -11.0 | -12.0 | -13.0 | -14.0 | -15.0 | -16.0 | -17.0 | -18.0 | -19.0 | -20.0 |
a=2 | 0.3 | -1.4 | -2.6 | -3.6 | -4.7 | -5.7 | -6.7 | -7.6 | -8.7 | -9.7 | -10.7 | -11.7 | -12.7 | -13.7 | -14.7 | -15.6 | -16.6 | -17.7 | -18.7 | -19.7 |
a=3 | 1.8 | -0.1 | -1.4 | -2.7 | -3.8 | -4.8 | -5.9 | -6.8 | -7.9 | -8.8 | -9.9 | -10.8 | -11.8 | -12.8 | -13.8 | -14.8 | -15.8 | -16.8 | -17.9 | -18.8 |
a=4 | 3.2 | 1.7 | 0.4 | -1.0 | -2.1 | -3.2 | -4.3 | -5.4 | -6.5 | -7.5 | -8.5 | -9.5 | -10.5 | -11.5 | -12.5 | -13.5 | -14.4 | -15.6 | -16.5 | -17.5 |
a=5 | 4.4 | 3.0 | 1.7 | 0.5 | -0.8 | -1.9 | -3.1 | -4.1 | -5.3 | -6.3 | -7.4 | -8.4 | -9.3 | -10.4 | -11.5 | -12.4 | -13.4 | -14.4 | -15.4 | -16.4 |
a=6 | 5.5 | 4.2 | 3.2 | 1.9 | 0.7 | -0.5 | -1.5 | -2.8 | -3.9 | -5.0 | -6.2 | -7.0 | -8.2 | -9.2 | -10.2 | -11.3 | -12.1 | -13.2 | -14.1 | -15.2 |
a=7 | 6.5 | 5.2 | 4.3 | 3.2 | 2.0 | 1.0 | -0.4 | -1.5 | -2.7 | -3.9 | -4.8 | -5.8 | -7.0 | -8.0 | -9.0 | -9.9 | -10.9 | -12.1 | -13.1 | -14.1 |
a=8 | 7.5 | 6.4 | 5.4 | 4.5 | 3.3 | 2.3 | 1.0 | -0.1 | -1.2 | -2.3 | -3.4 | -4.5 | -5.6 | -6.7 | -7.8 | -8.8 | -9.7 | -10.8 | -11.8 | -12.9 |
a=9 | 8.5 | 7.4 | 6.6 | 5.5 | 4.5 | 3.4 | 2.4 | 1.1 | -0.2 | -1.1 | -2.4 | -3.5 | -4.3 | -5.3 | -6.4 | -7.6 | -8.5 | -9.7 | -10.6 | -11.7 |
a=10 | 9.5 | 8.4 | 7.6 | 6.7 | 5.7 | 4.8 | 3.4 | 2.3 | 1.2 | 0.1 | -1.0 | -2.0 | -3.0 | -4.1 | -5.3 | -6.3 | -7.3 | -8.4 | -9.5 | -10.7 |
a=11 | 10.5 | 9.4 | 8.7 | 7.6 | 6.7 | 5.7 | 4.7 | 3.7 | 2.6 | 1.5 | 0.3 | -0.6 | -1.8 | -2.9 | -3.8 | -5.0 | -6.2 | -7.2 | -8.1 | -9.3 |
a=12 | 11.5 | 10.4 | 9.7 | 8.8 | 7.9 | 7.0 | 5.7 | 4.8 | 3.7 | 2.8 | 1.6 | 0.6 | -0.6 | -1.7 | -2.9 | -3.8 | -4.8 | -6.1 | -6.9 | -8.1 |
a=13 | 12.5 | 11.5 | 10.7 | 9.7 | 8.8 | 8.0 | 7.1 | 6.0 | 5.1 | 3.8 | 2.9 | 1.8 | 0.3 | -0.3 | -1.6 | -2.5 | -3.5 | -4.6 | -5.7 | -6.9 |
a=14 | 13.5 | 12.5 | 11.7 | 10.8 | 9.9 | 8.9 | 8.1 | 7.1 | 6.4 | 5.0 | 4.0 | 2.9 | 1.6 | 1.2 | -0.1 | -1.2 | -2.3 | -3.5 | -4.4 | -5.5 |
a=15 | 14.5 | 13.5 | 12.7 | 11.8 | 10.8 | 10.1 | 9.1 | 8.2 | 7.3 | 6.3 | 5.3 | 4.1 | 3.2 | 2.4 | 1.0 | 0.1 | -1.2 | -2.0 | -3.2 | -4.1 |
a=16 | 15.5 | 14.4 | 13.6 | 12.7 | 11.9 | 11.0 | 10.3 | 9.1 | 8.3 | 7.5 | 6.4 | 5.5 | 4.4 | 3.4 | 2.3 | 1.4 | 0.2 | -1.1 | -1.8 | -2.9 |
a=17 | 16.5 | 15.5 | 14.6 | 13.8 | 12.9 | 12.2 | 11.3 | 10.3 | 9.4 | 8.4 | 7.7 | 6.5 | 5.5 | 4.5 | 3.5 | 2.4 | 1.5 | 0.6 | -0.7 | -2.3 |
a=18 | 17.5 | 16.5 | 15.7 | 14.9 | 14.0 | 13.1 | 12.2 | 11.2 | 10.5 | 9.6 | 8.4 | 7.4 | 6.7 | 5.7 | 4.8 | 3.4 | 2.8 | 1.6 | 0.5 | -0.5 |
a=19 | 18.5 | 17.5 | 16.7 | 15.7 | 14.9 | 14.2 | 13.2 | 12.3 | 11.3 | 10.4 | 9.6 | 8.7 | 7.6 | 6.8 | 5.7 | 4.9 | 3.5 | 2.8 | 1.8 | 0.8 |
a=20 | 19.5 | 18.5 | 17.7 | 16.9 | 15.9 | 15.0 | 14.2 | 13.4 | 12.6 | 11.5 | 10.6 | 9.9 | 9.1 | 8.0 | 7.0 | 6.0 | 5.2 | 3.9 | 2.8 | 2.0 |