Skip to content

Instantly share code, notes, and snippets.

@cbsmith
Last active September 11, 2016 08:54

Revisions

  1. cbsmith revised this gist Sep 11, 2016. 1 changed file with 4 additions and 4 deletions.
    8 changes: 4 additions & 4 deletions shadow_combat.py
    Original 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
    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']:
    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('{:>5.2f}% above {}P'.format(min_outcomes * 100.0 / simulation.iterations, min_damage))
    print('mean: {}'.format(simulation.mean()))

    if __name__ == '__main__':
  2. cbsmith revised this gist Sep 11, 2016. 1 changed file with 127 additions and 52 deletions.
    179 changes: 127 additions & 52 deletions shadow_combat.py
    Original file line number Diff line number Diff line change
    @@ -1,15 +1,16 @@
    """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 [--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 [--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
    from itertools import groupby, chain
    import re
    from numbers import Number
    from sys import argv, stderr
    @@ -45,10 +48,11 @@

    try:
    import random
    choice = random.SystemRandom().choice
    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
    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__ = ('misses', 'hits', 'phys', 'stun')
    __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} {:>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)
    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):
    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 _ in range(self.dice) if self.hit())
    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
    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
    return Outcome(misses=1)

    # 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
    return Outcome(hits=1)

    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 o
    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())
    o = Outcome(hits=1, stun=damage) if stun else Outcome(hits=1, phys=damage)
    assert o.hits > 0
    return o
    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, attack, iterations=10000):
    self.attack = attack
    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 = self.attack.damage()
    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'])
    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)
    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_attack(test, args):
    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')
    stun = matches.group(2) == 'S'
    attack_args['stun'] = matches.group(2) == 'S'
    ap_string = matches.group(3)
    ap = parse_ap_string(ap_string)
    dv = int(matches.group(1))
    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')
    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'])
    attack_args['armor'] = None if matches.group(1) is None else int(matches.group(1))
    attack_args['soak'] = int(matches.group(2))
    else:
    return Attack(test, args['--dv'], parse_ap_string(args['--ap']), args['--stun'], args['--soak'], args['--armor'], args['--contact'])

    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))

    attack_pool = get_attack_pool(args)
    threshold = get_threshold(args)
    test = Test(attack_pool, threshold=threshold)
    attack = get_attack(test, args)
    tests = get_tests(args)
    attacks = get_attacks(tests, args)

    if args['--once']:
    print(attack.damage())
    for attack in attacks:
    print(attack.damage())
    return

    simulation = Simulation(attack, iterations=args['--iterations'] or args['ITERATIONS'])
    simulation = Simulation(attacks, args['--iterations'] or args['ITERATIONS'], args['--debug'])

    distribution = simulation.distribution()
    for damage, total in distribution:
    min_damage = args['--min']
    if min_damage:
    min_damage = int(min_damage)
    min_outcomes = 0
    for outcome, 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))
    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__':
  3. cbsmith revised this gist Sep 10, 2016. 1 changed file with 85 additions and 12 deletions.
    97 changes: 85 additions & 12 deletions shadow_combat.py
    Original 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
    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 0
    return o

    stun = self.stun

    soak = self.soak

    if self.armor is not None:
    armor_value = self.armor - self.ap
    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 = 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'])
    attack_pool = get_attack_pool(args)
    threshold = get_threshold(args)
    test = Test(attack_pool, threshold=threshold)

    attack = Attack(test, args['--dv'], args['--ap'], args['--stun'], args['--soak'], args['--armor'], args['--contact'])
    attack = get_attack(test, args)

    if args['--once']:
    print(attack.damage())
    return

    simulation = Simulation(attack, iterations=args['--iterations'])
    simulation = Simulation(attack, iterations=args['--iterations'] or args['ITERATIONS'])

    distribution = simulation.distribution()
    for damage, total in distribution:
  4. cbsmith created this gist Sep 7, 2016.
    297 changes: 297 additions & 0 deletions shadow_combat.py
    Original 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)