Last active
September 11, 2016 08:54
Revisions
-
cbsmith revised this gist
Sep 11, 2016 . 1 changed file with 4 additions and 4 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -41,7 +41,7 @@ from itertools import groupby, chain import re from numbers import Number from sys import argv, stderr, stdout from warnings import warn from docopt import docopt @@ -431,13 +431,13 @@ def main(**args): min_damage = int(min_damage) min_outcomes = 0 for outcome, total in distribution: if min_damage and outcome.phys >= min_damage: min_outcomes += total if args['--distribution']: print('{} for {} or {:>5.2f}%'.format(outcome, total, total * 100.0 / simulation.iterations)) if min_damage: print('{:>5.2f}% above {}P'.format(min_outcomes * 100.0 / simulation.iterations, min_damage)) print('mean: {}'.format(simulation.mean())) if __name__ == '__main__': -
cbsmith revised this gist
Sep 11, 2016 . 1 changed file with 127 additions and 52 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,15 +1,16 @@ """Shadow run attack simulator. Usage: shadow_combat.py [--debug] [--limit LIMIT | -6 | --rule_of_six] [--threshold THRESHOLD | --opposed_pool OPPOSED [--opposed_limit OPPOSED_LIMIT]] [--dv DV --stun [--soak SOAK] [--armor ARMOR [--ap AP]]] [--contact] [--once | -o | [--iterations ITERATIONS] [-D | --distribution]] [--multi ATTACKS] [--min DAMAGE] ATTACK_POOL shadow_combat.py [--debug] [--contact] [-6 | --rule_of_six] --threshold THRESHOLD ATTACK_STRING DAMAGE_STRING SOAK_STRING shadow_combat.py [--debug] [--contact] [-D | --distribution] [-6 | --rule_of_six] --threshold THRESHOLD ATTACK_STRING DAMAGE_STRING SOAK_STRING ITERATIONS shadow_combat.py [--debug] [--contact] [-6 | --rule_of_six] ATTACK_STRING DAMAGE_STRING DEFENSE_STRING SOAK_STRING shadow_combat.py [--debug] [--contact] [-D | --distribution] [-6 | --rule_of_six] ATTACK_STRING DAMAGE_STRING DEFENSE_STRING SOAK_STRING ITERATIONS shadow_combat.py (-h | --help) shadow_combat.py (-v | --version) Options: -6 --rule_of_six rule of six applies --limit LIMIT attack dice limit --threshold THRESHOLD threshold for test --opposed_pool OPPOSED opposed dice pool @@ -20,12 +21,14 @@ --ap AP armor piercing [default: 0] --stun dv is stun damage, not physical --contact if this is a contact only attack [default: False] --multi ATTACKS split attack over ATTACKS [default: 1] -o --once simulate a single action only -D --distribution print the distribution of attacks --iterations ITERATIONS total iterations to run through [default: 10000] -h --help show this -v --version print version --debug turn on debug printing --min DAMAGE minimum_damage goal ATTACK_STRING format is 18[7] meaning 18 dice in the pool, with a limit of 7 DAMAGE_STRING format is 7Pv-4 meaning 7 physical damage, with -4 AP @@ -35,7 +38,7 @@ """ from functools import total_ordering from itertools import groupby, chain import re from numbers import Number from sys import argv, stderr @@ -45,10 +48,11 @@ try: import random sys_random = random.SystemRandom() choice, randint = sys_random.choice, sys_random.randint except: warn('Could not use SystemRandom; using default random instead', RuntimeWarning) from random import choice, randint class InputError(Exception): """Exception raised for errors in input @@ -67,7 +71,7 @@ def __init__(self, expr, mesg): class Outcome(object): 'Represents the outcome of one or more attacks' __slots__ = ('hits', 'phys', 'stun', 'misses') SINGULAR = { 'misses': 'miss', @@ -138,6 +142,14 @@ def __mul__(self, other): def __truediv__(self, other): return Outcome(copy=self).__itruediv__(other) def __radd__(self, other): assert other == 0 return Outcome(copy=self) def __rsub__(self, other): assert other == 0 return Outcome() - Outcome(copy=self) def __rmul__(self, other): return self.__mul__(other) @@ -151,7 +163,7 @@ def singular(attribute): def __str__(self): if self.hits > 0 and self.stun == 0 and self.phys == 0: return '{:>5} {:>4}, no damage'.format(self.hits, 'hit' if self.hits == 1 else 'hits') answer = ', '.join('{:>5} {:>8}'.format(*x) for x in ((getattr(self, attr), (self.singular(attr) if getattr(self, attr) == 1 else attr)) for attr in self.__slots__) if x[1] == 'miss' or x[0] > 0) return '{:22}'.format(answer) def __repr__(self): @@ -176,17 +188,45 @@ def __eq__(self, other): class Pool(object): OUTCOMES = (False, False, True) # we could do False, False, False, False, True, True... but that is needlessly inefficient def __init__(self, dice, limit=None, rule_of_six=False): if rule_of_six: assert limit is None self.dice = int(dice) self.limit = int(limit) if limit else None self.rule_of_six = rule_of_six def hit(self): return choice(self.OUTCOMES) def hit_generator(self): if self.rule_of_six: dice = self.dice while dice > 0: roll = randint(1, 6) yield roll > 4 if roll != 6: dice -= 1 else: for _ in range(self.dice): yield self.hit() def hits(self): hits = sum(1 for hit in self.hit_generator() if hit) return hits if self.limit is None else min(hits, self.limit) def split(self, parts=1): base = self.dice // parts more_than_base = (base + 1 for _ in range(self.dice % parts)) rest = (base for _ in range(parts - (self.dice % parts))) return (Pool(dice, self.limit, self.rule_of_six) for dice in chain(more_than_base, rest)) def __str__(self): return '{}[{}]'.format(self.dice, self.limit) if self.limit else '{}{}'.format(self.dice, '*' if self.rule_of_six else '') def __repr__(self): return 'Pool(dice={}, limit={}, rule_of_six={})'.format(self.dice, self.limit, self.rule_of_six) class FixedThreshold(object): def __init__(self, threshold): @@ -195,6 +235,12 @@ def __init__(self, threshold): def hits(self): return self.threshold def __str__(self): return '({})'.format(self.threshold) def __repr__(self): return 'FixedThreshold({})'.format(self.threshold) class Test(object): def __init__(self, pool, threshold=None): @@ -208,6 +254,12 @@ def test(self): def net_hits(self): return max(0, self.test()) def __str__(self): return '{} vs {}'.format(self.pool, self.threshold) def __repr__(self): return 'Test(pool={}, threshold={})'.format(repr(self.pool), repr(self.threshold)) class Attack(object): def __init__(self, test, dv=None, ap=0, stun=False, soak=None, armor=None, contact=False): @@ -216,29 +268,22 @@ def __init__(self, test, dv=None, ap=0, stun=False, soak=None, armor=None, conta self.stun = stun self.soak = int(soak) if soak else None self.armor = int(armor) if armor else None self.ap = int(ap) if ap else 0 self.contact = contact def damage(self): hits = self.test.net_hits() if (hits < 0) or (hits == 0 and not self.contact): return Outcome(misses=1) # if dv is not set, then we're just counting hits vs. not hits if self.dv is None: return Outcome(hits=1) modified_dv = self.dv + hits if self.soak is None: return Outcome(stun=modified_dv) if self.stun else Outcome(phys=modified_dv) stun = self.stun @@ -253,19 +298,32 @@ def damage(self): soak_pool = Pool(soak) damage = max(0, modified_dv - soak_pool.hits()) return Outcome(hits=1, stun=damage) if stun else Outcome(hits=1, phys=damage) def __str__(self): ap_val = self.ap if self.ap < 0 else ('+' + self.ap if self.ap > 0 else '') base = '{}{} {}{}{}'.format(self.test, ' contact' if self.contact else '', self.dv, 'S' if self.stun else 'P', ap_val) return base + 'soak: {}v{}'.format(self.armor, self.soak) if self.armor or self.soak else base def __repr__(self): return 'Attack(test={}, dv={}, ap={}, stun={}, soak={}, armor={}, contact={})'.format(repr(self.test), repr(self.dv), repr(self.ap), repr(self.stun), repr(self.soak), repr(self.armor), repr(self.contact)) class Simulation(object): def __init__(self, attacks, iterations=10000, debug=False): self.attacks = tuple(attacks) if debug: stderr.write('Performing {} iterations of [{}]\n'.format(iterations, ', '.join(map(str, self.attacks)))) self.iterations = int(iterations) if iterations else 10000 self.outcome = None def results(self): for _ in range(self.iterations): d = sum(attack.damage() for attack in self.attacks) assert d.misses != 0 or d.hits != 0 yield d @@ -290,13 +348,13 @@ def mean(self): def get_attack_pool(args): if args['ATTACK_POOL']: return Pool(args['ATTACK_POOL'], args['--limit'], args['--rule_of_six']) elif args['ATTACK_STRING']: matcher = ATTACK_RE.match(args['ATTACK_STRING']) if matcher is None: raise InputError(args['ATTACK_STRING'], 'Invalid Attack String') limit = matcher.group(2) return Pool(matcher.group(1), limit or None, args['--rule_of_six']) else: raise InputError(' '.join(args), 'No attacker parameters found') @@ -319,50 +377,67 @@ def parse_ap_string(ap_string): return int(ap_string[1:]) return -int((ap_string[1:] if ap_string[0] == '-' else ap_string)) def get_tests(args): attack_pool = get_attack_pool(args) threshold = get_threshold(args) multi_attacks = int(args['--multi']) if args['--multi'] else 1 if args['--debug']: stderr.write('Splitting in to {} attacks\n'.format(multi_attacks)) return (Test(pool, threshold) for pool in attack_pool.split(multi_attacks)) def get_attacks(tests, args): attack_args = {} if args['DAMAGE_STRING'] and args['SOAK_STRING']: matches = DAMAGE_RE.match(args['DAMAGE_STRING']) if matches is None: raise InputError(args['DAMAGE_STRING'], 'Invalid Damage String') assert matches.group(2) in ('S', 'P') attack_args['stun'] = matches.group(2) == 'S' ap_string = matches.group(3) attack_args['ap'] = None if ap_string is None else parse_ap_string(ap_string) attack_args['dv'] = int(matches.group(1)) matches = SOAK_RE.match(args['SOAK_STRING']) if matches is None: raise InputError(args['SOAK_STRING'], 'Invalid Soak String') attack_args['armor'] = None if matches.group(1) is None else int(matches.group(1)) attack_args['soak'] = int(matches.group(2)) else: attack_args['dv'] = args['--dv'] attack_args['ap'] = parse_ap_string(args['--ap']) attack_args['stun'] = args['--stun'] attack_args['soak'] = args['--soak'] attack_args['armor'] = args['--armor'] attack_args['contact'] = args['--contact'] for test in tests: yield Attack(test, **attack_args) def main(**args): if args['--debug']: stderr.write('Args: {}\n'.format(args)) tests = get_tests(args) attacks = get_attacks(tests, args) if args['--once']: for attack in attacks: print(attack.damage()) return simulation = Simulation(attacks, args['--iterations'] or args['ITERATIONS'], args['--debug']) distribution = simulation.distribution() min_damage = args['--min'] if min_damage: min_damage = int(min_damage) min_outcomes = 0 for outcome, total in distribution: if args['--distribution']: if min_damage and outcome.phys >= min_damage: min_outcomes += total print('{} for {} or {:>5.2f}%'.format(outcome, total, total * 100.0 / simulation.iterations)) if min_damage: print('{:>5.2f}% above {}P'.format(total * 100.0 / simulation.iterations, min_damage)) print('mean: {}'.format(simulation.mean())) if __name__ == '__main__': -
cbsmith revised this gist
Sep 10, 2016 . 1 changed file with 85 additions and 12 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -2,6 +2,10 @@ Usage: shadow_combat.py [--debug] [--limit LIMIT] [--threshold THRESHOLD | --opposed_pool OPPOSED [--opposed_limit OPPOSED_LIMIT]] [--dv DV --stun [--soak SOAK] [--armor ARMOR [--ap AP]]] [--contact] [--once | -o | [--iterations ITERATIONS] [-D | --distribution]] ATTACK_POOL shadow_combat.py [--debug] [--contact] --threshold THRESHOLD ATTACK_STRING DAMAGE_STRING SOAK_STRING shadow_combat.py [--debug] [--contact] [-D | --distribution] --threshold THRESHOLD ATTACK_STRING DAMAGE_STRING SOAK_STRING ITERATIONS shadow_combat.py [--debug] [--contact] ATTACK_STRING DAMAGE_STRING DEFENSE_STRING SOAK_STRING shadow_combat.py [--debug] [--contact] [-D | --distribution] ATTACK_STRING DAMAGE_STRING DEFENSE_STRING SOAK_STRING ITERATIONS shadow_combat.py (-h | --help) shadow_combat.py (-v | --version) @@ -22,10 +26,17 @@ -h --help show this -v --version print version --debug turn on debug printing ATTACK_STRING format is 18[7] meaning 18 dice in the pool, with a limit of 7 DAMAGE_STRING format is 7Pv-4 meaning 7 physical damage, with -4 AP DEFENSE_STRING format is same as ATTACK_STRING... without a limit it'd just be 18 SOAK_STRING format is 12v3 meaning 12 armor, 3 soak dice or just v3 for the case where there is no armor involved """ from functools import total_ordering from itertools import groupby import re from numbers import Number from sys import argv, stderr from warnings import warn @@ -39,6 +50,18 @@ warn('Could not use SystemRandom; using default random instead', RuntimeWarning) from random import choice class InputError(Exception): """Exception raised for errors in input Attributes: expr -- input expression in which the error occurred msg -- explanation of the error """ def __init__(self, expr, mesg): self.expr = expr self.msg = mesg @total_ordering class Outcome(object): @@ -144,7 +167,7 @@ def __lt__(self, other): if self.hits < other.hits: return True if self.hits == other.hits: return self.misses > other.misses # more misses means overall you did worse return False def __eq__(self, other): @@ -215,14 +238,14 @@ def damage(self): if self.soak is None: o = Outcome(stun=modified_dv) if self.stun else Outcome(phys=modified_dv) assert o.hits > 0 return o stun = self.stun soak = self.soak if self.armor is not None: armor_value = self.armor + self.ap if armor_value > 0: if armor_value >= modified_dv: stun = True @@ -260,25 +283,75 @@ def distribution(self): def mean(self): return self.outcome * 1.0 / self.iterations ATTACK_RE = re.compile(r'^(\d+)(?:\[(\d+)\])?$') DEFENSE_RE = ATTACK_RE DAMAGE_RE = re.compile(r'^(\d+)(P|S)(?:v((?:\+|-)\d+))?$') SOAK_RE = re.compile(r'^(\d+)?v(\d+)$') def get_attack_pool(args): if args['ATTACK_POOL']: return Pool(args['ATTACK_POOL'], args['--limit']) elif args['ATTACK_STRING']: matcher = ATTACK_RE.match(args['ATTACK_STRING']) if matcher is None: raise InputError(args['ATTACK_STRING'], 'Invalid Attack String') limit = matcher.group(2) return Pool(matcher.group(1), limit or None) else: raise InputError(' '.join(args), 'No attacker parameters found') def get_threshold(args): if args['--threshold']: return FixedThreshold(args['--threshold']) if args['--opposed_pool']: return Pool(args['--opposed_pool'], args['--opposed_limit']) if args['DEFENSE_STRING']: matches = DEFENSE_RE.match(args['DEFENSE_STRING']) if matches is None: raise InputError(args['DEFENSE_STRING'], 'Invalid Defense String') limit = matches.group(2) return Pool(matches.group(1), limit or None) else: return None def parse_ap_string(ap_string): if ap_string[0] == '+': return int(ap_string[1:]) return -int((ap_string[1:] if ap_string[0] == '-' else ap_string)) def get_attack(test, args): if args['DAMAGE_STRING'] and args['SOAK_STRING']: matches = DAMAGE_RE.match(args['DAMAGE_STRING']) if matches is None: raise InputError(args['DAMAGE_STRING'], 'Invalid Damage String') assert matches.group(2) in ('S', 'P') stun = matches.group(2) == 'S' ap_string = matches.group(3) ap = parse_ap_string(ap_string) dv = int(matches.group(1)) matches = SOAK_RE.match(args['SOAK_STRING']) if matches is None: raise InputError(args['SOAK_STRING'], 'Invalid Soak String') armor = None if matches.group(1) is None else int(matches.group(1)) soak = int(matches.group(2)) return Attack(test, dv, ap, stun, soak, armor, args['--contact']) else: return Attack(test, args['--dv'], parse_ap_string(args['--ap']), args['--stun'], args['--soak'], args['--armor'], args['--contact']) def main(**args): if args['--debug']: stderr.write('Args: {}\n'.format(args)) attack_pool = get_attack_pool(args) threshold = get_threshold(args) test = Test(attack_pool, threshold=threshold) attack = get_attack(test, args) if args['--once']: print(attack.damage()) return simulation = Simulation(attack, iterations=args['--iterations'] or args['ITERATIONS']) distribution = simulation.distribution() for damage, total in distribution: -
cbsmith created this gist
Sep 7, 2016 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,297 @@ """Shadow run attack simulator. Usage: shadow_combat.py [--debug] [--limit LIMIT] [--threshold THRESHOLD | --opposed_pool OPPOSED [--opposed_limit OPPOSED_LIMIT]] [--dv DV --stun [--soak SOAK] [--armor ARMOR [--ap AP]]] [--contact] [--once | -o | [--iterations ITERATIONS] [-D | --distribution]] ATTACK_POOL shadow_combat.py (-h | --help) shadow_combat.py (-v | --version) Options: --limit LIMIT attack dice limit --threshold THRESHOLD threshold for test --opposed_pool OPPOSED opposed dice pool --opposed_limit OPPOSED_LIMIT opposed dice limit --dv DV the damage value of the attack --soak SOAK defense base soak dice (does not include armor) --armor ARMOR armor dice --ap AP armor piercing [default: 0] --stun dv is stun damage, not physical --contact if this is a contact only attack [default: False] -o --once simulate a single action only -D --distribution print the distribution of attacks --iterations ITERATIONS total iterations to run through [default: 10000] -h --help show this -v --version print version --debug turn on debug printing """ from functools import total_ordering from itertools import groupby from numbers import Number from sys import argv, stderr from warnings import warn from docopt import docopt try: import random choice = random.SystemRandom().choice except: warn('Could not use SystemRandom; using default random instead', RuntimeWarning) from random import choice @total_ordering class Outcome(object): 'Represents the outcome of one or more attacks' __slots__ = ('misses', 'hits', 'phys', 'stun') SINGULAR = { 'misses': 'miss', 'hits': 'hit', 'phys': 'phys', 'stun': 'stun' } def __init__(self, misses=0, hits=None, phys=0, stun=0, copy=None): if copy is None: assert isinstance(misses, int) and isinstance(phys, int) and isinstance(stun, int) self.misses = misses self.phys = phys self.stun = stun # if hits is None, set it to an implied value based on damage if hits is None: self.hits = 1 if (self.phys > 0 or self.stun > 0) else 0 else: self.hits = hits assert isinstance(self.hits, int) else: assert misses == 0 and hits is None and phys == 0 and stun == 0 assert isinstance(copy, Outcome) self.misses = copy.misses self.hits = copy.hits self.phys = copy.phys self.stun = copy.stun def __iadd__(self, other): self.misses += other.misses self.hits += other.hits self.phys += other.phys self.stun += other.stun return self def __isub__(self, other): self.misses -= other.misses self.hits -= other.hits self.phys -= other.phys self.stun -= other.stun return self def __imul__(self, other): assert isinstance(other, Number) self.misses *= other self.hits *= other self.phys *= other self.stun *= other return self def __itruediv__(self, other): assert isinstance(other, Number) self.misses /= other self.hits /= other self.phys /= other self.stun /= other return self def __add__(self, other): return Outcome(copy=self).__iadd__(other) def __sub__(self, other): return Outcome(copy=self).__isub__(other) def __mul__(self, other): return Outcome(copy=self).__imul__(other) def __truediv__(self, other): return Outcome(copy=self).__itruediv__(other) def __rmul__(self, other): return self.__mul__(other) def __rtruediv__(self, other): return self.__truediv__(other) @staticmethod def singular(attribute): return Outcome.SINGULAR.get(attribute, attribute) def __str__(self): if self.hits > 0 and self.stun == 0 and self.phys == 0: return '{:>5} {:>4}, no damage'.format(self.hits, 'hit' if self.hits == 1 else 'hits') answer = ', '.join('{:>5} {:>4}'.format(*x) for x in ((getattr(self, attr), (self.singular(attr) if getattr(self, attr) == 1 else attr)) for attr in self.__slots__) if x[0] > 0) return '{:22}'.format(answer) def __repr__(self): return 'Outcome({}, {}, {}, {})'.format(self.misses, self.hits, self.phys, self.stun) def __lt__(self, other): if self.phys < other.phys: return True if self.phys == other.phys: if self.stun < other.stun: return True if self.stun == other.stun: if self.hits < other.hits: return True if self.hits == other.hits: return self.misses < other.misses return False def __eq__(self, other): return self.misses == other.misses and self.hits == other.hits and self.phys == other.phys and self.stun == other.stun class Pool(object): OUTCOMES = (False, False, True) # we could do False, False, False, False, True, True... but that is needlessly inefficient def __init__(self, dice, limit=None): self.dice = int(dice) self.limit = int(limit) if limit else None def hit(self): return choice(self.OUTCOMES) def hits(self): hits = sum(1 for _ in range(self.dice) if self.hit()) return hits if self.limit is None else min(hits, self.limit) class FixedThreshold(object): def __init__(self, threshold): self.threshold = int(threshold) def hits(self): return self.threshold class Test(object): def __init__(self, pool, threshold=None): self.pool = pool self.threshold = threshold def test(self): hits = self.pool.hits() return hits if self.threshold is None else hits - self.threshold.hits() def net_hits(self): return max(0, self.test()) class Attack(object): def __init__(self, test, dv=None, ap=0, stun=False, soak=None, armor=None, contact=False): self.test = test self.dv = int(dv) if dv else None self.stun = stun self.soak = int(soak) if soak else None self.armor = int(armor) if armor else None stderr.write('self.armor = {} and armor = {}\n'.format(self.armor, armor)) self.ap = int(ap) if ap else 0 self.contact = contact def damage(self): hits = self.test.net_hits() if (hits < 0) or (hits == 0 and not self.contact): o = Outcome(misses=1) assert o.misses > 0 return o # if dv is not set, then we're just counting hits vs. not hits if self.dv is None: o = Outcome(hits=1) assert o.hits > 0 return o modified_dv = self.dv + hits if self.soak is None: o = Outcome(stun=modified_dv) if self.stun else Outcome(phys=modified_dv) assert o.hits > 0 return 0 stun = self.stun soak = self.soak if self.armor is not None: armor_value = self.armor - self.ap if armor_value > 0: if armor_value >= modified_dv: stun = True soak += armor_value soak_pool = Pool(soak) damage = max(0, modified_dv - soak_pool.hits()) o = Outcome(hits=1, stun=damage) if stun else Outcome(hits=1, phys=damage) assert o.hits > 0 return o class Simulation(object): def __init__(self, attack, iterations=10000): self.attack = attack self.iterations = int(iterations) if iterations else 10000 self.outcome = None def results(self): for _ in range(self.iterations): d = self.attack.damage() assert d.misses != 0 or d.hits != 0 yield d def distribution(self): pred = lambda x: (x.phys, x.stun) results = sorted(self.results(), reverse=True) for damage, g in groupby(results): total = sum(1 for _ in g) yield damage, total if self.outcome is None: self.outcome = damage * total else: self.outcome += damage * total def mean(self): return self.outcome * 1.0 / self.iterations def main(**args): if args['--debug']: stderr.write('Args: {}\n'.format(args)) attack_pool = Pool(args['ATTACK_POOL'], args['--limit']) threshold = None if args['--threshold']: threshold = FixedThreshold(args['--threshold']) if args['--opposed_pool']: threshold = Pool(args['--opposed_pool'], args['--opposed_limit']) test = Test(attack_pool, threshold=threshold) attack = Attack(test, args['--dv'], args['--ap'], args['--stun'], args['--soak'], args['--armor'], args['--contact']) if args['--once']: print(attack.damage()) return simulation = Simulation(attack, iterations=args['--iterations']) distribution = simulation.distribution() for damage, total in distribution: if args['--distribution']: outcome = Outcome(copy=damage) if outcome.hits == 1: outcome.hits = total if outcome.misses == 1: outcome.misses = total print('{} or {:>5.2f}%'.format(outcome, total * 100.0 / simulation.iterations)) print('mean: {}'.format(simulation.mean())) if __name__ == '__main__': arguments = docopt(__doc__, version='Rolls 0.0') main(**arguments)