Skip to content

Instantly share code, notes, and snippets.

@Techcable
Last active June 25, 2017 22:10
Show Gist options
  • Select an option

  • Save Techcable/c2e9f0824b3785e5d85f0984fe3ec1da to your computer and use it in GitHub Desktop.

Select an option

Save Techcable/c2e9f0824b3785e5d85f0984fe3ec1da to your computer and use it in GitHub Desktop.
Risk attack simulator
#!/usr/bin/env python3
from argh import ArghParser, arg
from random import Random
from statistics import median
class Territory:
__slots__ = "name", "troops", "random"
def __init__(self, name, troops, random=Random()):
self.name = name
self.troops = troops
self.random = random
@property
def attackDice(self):
return max(min(3, self.troops - 1), 1)
@property
def defenseDice(self):
return min(self.troops, 2)
def roll(self, times):
assert times >= 0
random = self.random
return [random.randrange(1, 7) for _ in range(times)]
def attack(self, defender, troopThreshold=1):
assert troopThreshold >= 1
while self.troops >= troopThreshold:
defenseDice = defender.roll(defender.defenseDice)
if not defenseDice:
return True # We win
attackDice = self.roll(self.attackDice)
for attackDie, defenseDie in zip(attackDice, defenseDice):
if attackDie > defenseDie:
defender.troops -= 1
else:
self.troops -= 1
assert self.troops >= 0
assert defender.troops >= 0
return False # We loose or give up
@arg('attackingTroops', type=int, help="The number of attacking troops")
@arg('defendingTroops', type=int, help="The number of defending troops")
@arg('--runs', help="The number of runs to simulate")
def analyse(attackingTroops, defendingTroops, runs=500):
"""Analyse the probability of a successful attack"""
assert attackingTroops > 1, f"Invalid attacking troops: {attackingTroops}"
assert defendingTroops > 0, f"Invalid attacking troops: {defendingTroops}"
attacker = Territory("Attacker", None)
defender = Territory("Defender", None)
survivingAttackTroops, survivingDefenseTroops = [], []
for run in range(runs):
attacker.troops = attackingTroops
defender.troops = defendingTroops
if attacker.attack(defender):
survivingAttackTroops.append(attacker.troops)
else:
survivingDefenseTroops.append(defender.troops)
assert len(survivingAttackTroops) + len(survivingDefenseTroops) == runs
percentage = (len(survivingAttackTroops) / runs) * 100
print(f"Attacker wins {percentage}% of the time")
if survivingAttackTroops:
print(f"Median troops surviving a successful attack: {median(survivingAttackTroops)}")
else:
print("Attacker never wins")
if survivingDefenseTroops:
print(f"Median troops surviving a successful defense: {median(survivingDefenseTroops)}")
else:
print("Defender never survives")
@arg('attackingTroops', type=int, help="The number of attacking troops")
@arg('defendingTroops', type=int, help="The number of defending troops")
def attack(attackingTroops, defendingTroops):
attacker = Territory("Attacker", attackingTroops)
defender = Territory("Defender", defendingTroops)
if attacker.attack(defender):
print(f"Attacker wins with {attacker.troops} troops remaining")
else:
print(f"Defender survives with {defender.troops} troops remaining")
def analyse_loop():
"""Interactively prompts for attacks to analyse"""
print("Enter EOF (ctrl-d/ctrl-z) at prompt to terminate")
while True:
try:
attackingTroops = int(input("Attacking troops: "))
if attackingTroops <= 1:
raise ValueError()
defendingTroops = int(input("Defending troops: "))
if defendingTroops <= 0:
raise ValueError()
except EOFError:
break
except ValueError:
print("Invalid troops! Remember to enter EOF (ctrl-d/ctrl-z) to terminate")
continue
analyse(attackingTroops, defendingTroops)
def main():
parser = ArghParser(description="A risk attack simulator")
parser.add_commands([attack, analyse, analyse_loop])
parser.dispatch()
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment