def test_rv_create_rv(): # given a = value(6) b = value('D6') c = value(DiceValue(6)) # assert assert a.max({}) == b.max({}) == c.max({})
def compute_potential_wounds(context, potential_hits, towound): potential_wounds = [{ **hit, 'wounds': value(nb) + context.get(EXTRA_WOUND_ON_CRIT, 0) * nb_crit, 'crit_wounds': nb_crit, 'mortal_wounds': hit['mortal_wounds'] + context.get(MW_ON_WOUND_CRIT, 0) * nb_crit, 'proba': hit['proba'] * probability_of_wound_and_crit(hit['hits'], nb, nb_crit, towound, context, crit_hit=hit['crit_hits']) } for hit in potential_hits for nb in range(hit['hits'] + 1) for nb_crit in range(nb + 1)] potential_wounds = [{ **wnd, 'wounds': nb, 'proba': wnd['proba'] * proba, } for wnd in potential_wounds for (nb, proba) in wnd['wounds'].potential_values(context)] return potential_wounds
def buff(data): enemy_wounds = data.get(ENEMY_WOUNDS, 1) if enemy_wounds <= 1: return 0 bravery = data.get(ENEMY_BRAVERY, 7) roll_higher = sum([proba for val, proba in value('2D6').potential_values(data) if val > bravery]) return RandomValue({0: 1 - roll_higher, enemy_wounds - 1: roll_higher})
def __init__(self, base_value) -> None: if isinstance(base_value, Roll): self.base_value = base_value.base_value else: self.base_value = value(base_value) self.rerolled = 0 self.rules: List[Callable] = [ ] # function take dict, return mod and reroll self.mod_ignored = []
def buff(data): possible_success = roll.success(data) possible_damage = { val: proba * possible_success for val, proba in value(mortal_wounds).potential_values(data) } possible_damage[0] = possible_damage.get(0, 0) + 1 - possible_success data[MORTAL_WOUNDS_PER_ATTACK] = RandomValue(possible_damage)
def test_average_apply_extras_on_context(): # given def bonus(context): return context.get('bonus', 0) random_value = value(1) random_value.rules.append(bonus) # assert assert random_value.average({'bonus': 21}, mod=20) == 42
def buff(data): # roll a dice (-1 if monster). if is equal to or less than the number of minis in the unit, D3 MW mod = 0 if MONSTER in data.get(ENEMY_KEYWORDS, []): mod = -1 possibilities = { val: proba for val, proba in value('D6').potential_values(data, mod) } nb_enemies = data.get(ENEMY_NUMBERS, 1) possible_success = sum([ proba for val, proba in possibilities.items() if val <= nb_enemies ]) possible_damage = { val: proba * possible_success for val, proba in value('D3').potential_values(data) } possible_damage[0] = 1 - possible_success data[MORTAL_WOUNDS_PER_ATTACK] = RandomValue(possible_damage)
def compute_potential_hits(context, potential_attacks, tohit): potential_hits = [{ **att, 'hits': value(nb) * context.get(NUMBER_OF_HITS, 1) + context.get(EXTRA_HIT_ON_CRIT, 0) * nb_crit, 'crit_hits': nb_crit, 'second_attacks': nb * context.get(EXTRA_ATTACK_ON_HIT, 0), 'proba': att['proba'] * probability_of_hit_and_crit(att['attacks'], nb, nb_crit, tohit, context) } for att in potential_attacks for nb in range(att['attacks'] + 1) for nb_crit in range(nb + 1)] potential_hits = [{ **att, 'hits': att['hits'] + value(nb) + context.get(EXTRA_HIT_ON_CRIT, 0) * nb_crit, 'crit_hits': nb_crit + att['crit_hits'], 'proba': att['proba'] * probability_of_hit_and_crit(att['second_attacks'], nb, nb_crit, tohit, context) } for att in potential_hits for nb in range(att['second_attacks'] + 1) for nb_crit in range(nb + 1)] potential_hits = [{ **hit, 'hits': nb, 'mortal_wounds': hit['mortal_wounds'] + context.get(MW_ON_HIT_CRIT, 0) * hit['crit_hits'], 'proba': hit['proba'] * proba, } for hit in potential_hits for (nb, proba) in hit['hits'].potential_values(context)] if context.get(STOP_ON_CRIT_HIT, False): potential_hits = [{ **hit, 'hits': hit['hits'] - hit['crit_hits'], 'crit_hits': 0, } for hit in potential_hits] return potential_hits
def __init__( self, name: str, weapons: Union[List[Weapon]], move: Union[int, str, Value, Dict[int, Union[int, str, Value]]], save: int, bravery: int, wounds: int, min_size: int, base: Base, rules: List[Rule], keywords: List[str], cast=0, unbind=0, named=False, max_size=None, ): self.name = name self.weapons = weapons self.move = value(move) self.save = Roll(save) self.extra_save = Roll(7) self.bravery = bravery self.wounds = wounds self.min_size = min_size self.size = min_size self.max_size = max_size if self.max_size is None: self.max_size = min_size if 'MONSTER' in keywords or 'HERO' in keywords else min_size self.base = base self.keywords = keywords keywords.append(self.name.upper()) self.named = named self.can_fly = False self.run_distance = value('D6') self.charge_range = value('2D6') self.can_run_and_charge = False self.special_users = [] self.casting_value = value('2D6') self.unbinding_value = value('2D6') self.morale_roll = value('D6') self.notes = [] self.spells_per_turn = value(cast) self.unbind_per_turn = value(unbind) self.spells: List[Spell] = [ARCANE_BOLT, MAGIC_SHIELD] self.command_abilities: List[CommandAbility] = [] self.rules = rules for r in self.rules: r.apply(self)
def buff(data): # roll a dice against you opponent # if rolled higher, deal the difference as mortal wounds instead of regular wound # if rolled lower, no damage data[MW_ON_WOUND_CRIT] = RandomValue({ 5: 1 / 36, 4: 2 / 36, 3: 3 / 36, 2: 4 / 36, 1: 5 / 36, 0: 21 / 36 }) data[EXTRA_DAMAGE_ON_CRIT_WOUND] = value(-1)
def __init__( self, name: str, range_: Union[int, str, Value, Dict[int, Union[int, str, Value]]], attacks: Union[int, str, Value, Dict[int, Union[int, str, Value]]], tohit, towound, rend, damage: Union[int, str, Value, Dict[int, Union[int, str, Value]]], rules: List[Rule], ) -> None: self.name = name self.range = value(range_) self.attacks = value(attacks) self.tohit = Roll(tohit) self.towound = Roll(towound) self.rend = value(rend) self.damage = value(damage) self.attack_rules: List[Callable] = [] self.rules = rules for r in rules: r.apply(self)
def test_max_d6_is_6(): # given random_value = value('D6') # assert assert random_value.max({}, mod=-1) == 5
def test_average_1_is_1(): # given random_value = value(1) # assert assert random_value.average({}, mod=1) == 2
def test_max_1_is_1(): # given random_value = value(1) # assert assert random_value.max({}, mod=-1) == 0
def test_average_d6_is_35(): # given random_value = value('D6') # assert assert random_value.average({}, mod=1) == 4.5
def cloak_of_feathers(u: Unit): fly(u) u.move = value(14) u.save = Roll(4)
def morale_grade(self, context: dict): if HERO in self.keywords or MONSTER in self.keywords: return self.bravery mod = value('D6').average(context) - self.morale_roll.average(context) return self.bravery + mod
def rule_func(u: Unit): value(range_) pass
def charge_at_3d6(u: Unit): u.charge_range = value('3D6')
'Kairos Fateweaver', [ [Weapon('Staff of Tomorrow', 3, 2, 4, {11: 2, 5: 3, 0: 4}, -1, 2, []), Weapon('Beaks and Talons', 1, 5, 4, 3, -1, 2, [])], ], {11: 10, 8: 9, 5: 8, 2: 7, 0: 6}, 4, 10, 14, 1, monster_base, rules=[ FLIGHT, Rule('Mastery of Magic', mastery_of_magic), Rule('Oracle of Eternity', can_reroll_x_dice_during_game(1)), Spell('Gift of Change', 8, None), ], keywords=[CHAOS, DAEMON, TZEENTCH, WIZARD, HERO, MONSTER, 'LORD OF CHANGE'], cast=2, unbind=2, named=True)) def arcane_tome(u: Unit): u.casting_value = u.casting_value + OncePerGame('D6') sky_sharks = Rule('Sky-sharks', extra_damage_on_keyword(value('D3') - 1, MONSTER)) TZEENTCH_WS.append(Warscroll( 'Herald of Tzeentch on Burning Chariot', [ [Weapon('Staff of Change', 2, 1, 4, 3, -1, 'D3', []), Weapon('Wake of Fire', 'move across', 1, 7, 7, 0, 0, [Rule('', deal_x_mortal_wound_on_roll('D3', Roll(4)))]), Weapon('Screamer`s Lamprey Bites', 1, 6, 4, 3, 0, 1, [sky_sharks])], [Weapon('Ritual Dagger', 1, 2, 4, 4, 0, 1, []), Weapon('Wake of Fire', 'move across', 1, 7, 7, 0, 0, [Rule('', deal_x_mortal_wound_on_roll('D3', Roll(4)))]), Weapon('Screamer`s Lamprey Bites', 1, 6, 4, 3, 0, 1, [sky_sharks])], ], 14, 5, 10, 8, 1, monster_base, rules=[ FLIGHT, Rule('Arcane Tome', arcane_tome), Spell('Tzeentch`s Firestorm', 9, None), ], keywords=[CHAOS, DAEMON, HORROR, TZEENTCH, WIZARD, HERO, 'HERALD ON CHARIOT'], cast=1, unbind=1))
def buff(data): data[NUMBER_OF_HITS] = value(hits)
def buff(data): if keyword in data.get(ENEMY_KEYWORDS, []): return value(extra_damage) return 0
def buff(data): if data.get(CHARGING, False): data[MORTAL_WOUNDS_PER_ATTACK] = value(mortal_wounds)
def buff(data): data[MW_ON_HIT_CRIT] = value(mortal_wounds) data[STOP_ON_CRIT_HIT] = True
def hellfire(data): data[MW_IF_DAMAGE] = value('D3') * RandomValue({1: 0.5, 0: 0.5})
def buff(data): data[EXTRA_DAMAGE_ON_CRIT_WOUND] = value('D6') - 1
def buff(data): if data.get(CHARGING, False): return value(extra_attacks) return 0
def rule_func(u: Unit): value(range_) u.notes.append( f'Spell stealer ({round(value(chances).average({}) * value(tries_per_turn).average({}), 1)})' )
def buff(data): data[EXTRA_HIT_ON_CRIT] = value(amount) - 1
def attack_round(self, context, users=1): my_context = copy(context) for r in self.attack_rules: r(my_context) potential_attacks = {} potential_hits = {} potential_wounds = {} potential_unsaved = {} potential_damage = {} cleaned_damage = [] try: potential_attacks = [{ 'attacks': nb * users, 'proba': proba, 'mortal_wounds': my_context.get(MORTAL_WOUNDS, value(0)) + my_context.get(MORTAL_WOUNDS_PER_ATTACK, 0) * nb * users, } for (nb, proba) in self.attacks.potential_values(my_context)] assert abs(sum([att['proba'] for att in potential_attacks]) - 1) <= pow(0.1, 5) potential_attacks = cleaned_dict_list(potential_attacks, ['attacks', 'mortal_wounds']) potential_hits = compute_potential_hits(my_context, potential_attacks, self.tohit) assert abs(sum([hit['proba'] for hit in potential_hits]) - 1) <= pow(0.1, 5) potential_hits = cleaned_dict_list( potential_hits, ['hits', 'crit_hits', 'mortal_wounds']) potential_wounds = compute_potential_wounds( my_context, potential_hits, self.towound) assert abs(sum([wnd['proba'] for wnd in potential_wounds]) - 1) <= pow(0.1, 5) potential_wounds = cleaned_dict_list( potential_wounds, ['wounds', 'crit_wounds', 'mortal_wounds']) potential_unsaved = [ { **wnd, 'unsaved': nb, 'unsaved_crit_wound': nb_crit, 'proba': wnd['proba'] * probability_of_save_fail( wnd['wounds'], nb, nb_crit, my_context[ENEMY_SAVE], my_context, rend=self.rend.average(my_context), crit_wnd=wnd['crit_wounds']) } for wnd in potential_wounds for nb in range(wnd['wounds'] + 1) for nb_crit in range( max(0, wnd['crit_wounds'] - (wnd['wounds'] - nb)), min(wnd['crit_wounds'], nb) + 1) ] assert abs( sum([unsvd['proba'] for unsvd in potential_unsaved]) - 1) <= pow(0.1, 5) potential_unsaved = cleaned_dict_list( potential_unsaved, ['unsaved', 'unsaved_crit_wound', 'mortal_wounds']) potential_damage = compute_potential_damage( self.damage, my_context, potential_unsaved) assert abs(sum([dmg['proba'] for dmg in potential_damage]) - 1) <= pow(0.1, 5) potential_full_damage = [{ **dmg, 'damage': dmg['damage'] + nb, 'mortal_wounds': nb, 'proba': dmg['proba'] * proba, } for dmg in potential_damage for ( nb, proba) in dmg['mortal_wounds'].potential_values(my_context) ] potential_full_damage = cleaned_dict_list(potential_full_damage, ['damage']) cleaned_damage = [{ 'damage': pick * (1 + context.get(MW_ON_DAMAGE, 0)), 'proba': sum([ dmg['proba'] for dmg in potential_full_damage if dmg['damage'] == pick ]) } for pick in set(dmg['damage'] for dmg in potential_full_damage)] # raise AssertionError # testing except AssertionError: info = { 'potential_attacks': potential_attacks, 'potential_hits': potential_hits, 'potential_wounds': potential_wounds, 'potential_unsaved': potential_unsaved, 'potential_damage': potential_damage, 'cleaned_damage': cleaned_damage, } print(self.name) for k, potent in info.items(): print(f'- {k}:') for e in potent: print( str({k: str(v) for k, v in e.items() }).replace("'", "").replace("\"", "")) sum_proba = sum(e['proba'] for e in potent) print(f' total={sum_proba}') average = sum( d.get('damage') * d.get('proba') for d in cleaned_damage) print(f'AVERAGE: {average}') return cleaned_damage