import math from collections import defaultdict import RaceRandom as random from source.logic.Rule import RuleFactory from source.dungeon.EnemyList import EnemySprite # these are for drops only def defeat_rule_single(world, player, enemy_sprite, region): if enemy_sprite.kind == EnemySprite.Terrorpin: # must be flipped return has('Hammer', player) elif enemy_sprite.kind == EnemySprite.RedBari: # must be burned to drop return or_rule(has('Fire Rod', player), and_rule(has_sword(player), has('Bombos', player))) vln = enemy_vulnerability(world, player, enemy_sprite, region) rules = [] if vln['Blunt'] != 0: rules.append(has_blunt_weapon(player)) if vln['Stun'] != 0: rules.append(buzzblob_rule(player)) if vln['Somaria'] != 0: rules.append(somaria_rule(world, player, vln['Somaria'])) if vln['Byrna'] != 0: rules.append(byrna_rule(world, player, vln['Byrna'])) if vln['Master'] != 0: rules.append(has_class_2_weapon(player)) if vln['Bow'] != 0: rules.append(bow_rule(world, player, vln['Bow'])) if vln['Silvers'] != 0: rules.append(silvers_rule(world, player, vln['Silvers'])) if vln['Bomb'] != 0: rules.append(bombs_rule(world, player, vln['Bomb'])) if vln['Hookshot'] != 0: rules.append(has('Hookshot', player)) if vln['IceRod'] != 0: rules.append(ice_rod_rule(world, player, vln['IceRod'])) if vln['FireRod'] != 0: rules.append(fire_rod_rule(world, player, vln['FireRod'])) if vln['Boomerang'] != 0: rules.append(has_boomerang(player)) if vln['Powder'] != 0: rules.append(magic_powder_rule(world, player, vln['Powder'])) # skip medallions if vln to Blunt? if vln['Bombos'] != 0 and vln['Blunt'] == 0: rules.append(medallion_rule(world, player, 'Bombos', vln['Bombos'])) if vln['Ether'] != 0 and vln['Blunt'] == 0: rules.append(medallion_rule(world, player, 'Ether', vln['Ether'])) if vln['Quake'] != 0 and vln['Blunt'] == 0: rules.append(medallion_rule(world, player, 'Quake', vln['Quake'])) if enemy_sprite.kind == EnemySprite.StalfosKnight: # must be bombed to be made vulnerable return and_rule(can_use_bombs(world, player), or_rule(*rules)) return or_rule(*rules) damage_cost = { 'Bomb': 1, 'Bow': 1, 'Silvers': 1, 'Powder': .5, 'Somaria': .5, 'Byrna': 1.125, 'FireRod': 1, 'IceRod': 1, 'Bombos': 2, 'Ether': 2, 'Quake': 2 } # damage_set = ['Blunt', 'Stun', 'Master', 'Tempered', 'Boomerang', 'Hookshot', 'Bomb', 'Silvers', 'Bow', # 'Somaria', 'Powder', 'FireRod', 'IceRod', 'Byrna', 'Bombos', 'Ether', 'Quake'] # these are for "challenge" rooms def defeat_rule_multiple(world, player, enemy_sprite_region_pairs): vln_list = {} for sprite, region in enemy_sprite_region_pairs: vln_list[(sprite, region)] = enemy_vulnerability(world, player, sprite, region) # damage_accounting = {x: list(y) for x, y in damage_types.items()} used_resources = {'Bomb': 0, 'Arrow': 0, 'Magic': 0} required_rules = [] picky_enemies = [] hammer_required = False bombs_required = False for key, vln in vln_list.items(): if key[0] == EnemySprite.Terrorpin: if not hammer_required: required_rules.append(has('Hammer', player)) hammer_required = True picky_enemies.append(key) continue if key[0] == EnemySprite.StalfosKnight: if not bombs_required: required_rules.append(bombs_rule(world, player, 1)) bombs_required = True used_resources['Bomb'] += 1 picky_enemies.append(key) continue vln_types = [k for k in vln.keys() if vln[k] != 0] if len(vln_types) == 1: d_type = vln_types[0] required_rules.append(defeat_rule_single(world, player, key[0], key[1])) picky_enemies.append(key) if d_type in damage_cost: cost = damage_cost[d_type] if d_type == 'Bomb': used_resources['Bomb'] += cost elif d_type in ['Bow', 'Silvers']: used_resources['Arrow'] += cost else: used_resources['Magic'] += cost vln_list = {k: v for k, v in vln_list.items() if k not in picky_enemies} while len(vln_list) > 0: optional_clears = find_possible_rules(vln_list, used_resources, world, player) if len(optional_clears) == 0: raise Exception('Kill rules seems to be insufficient for this enemy set, please report:' + ', '.join([str(x) for x, y in enemy_sprite_region_pairs])) # find rules which kill the most # idea: this could be multiple criteria: most-constrained then which method kills the most best_rules = {} best_size = 0 for vln_option in optional_clears.keys(): if len(vln_option) > best_size: best_size = len(vln_option) best_rules.clear() best_rules[vln_option] = optional_clears[vln_option] elif len(vln_option) == best_size: # assumes vln_option is different from prior options best_rules[vln_option] = optional_clears[vln_option] if len(best_rules) == 1: vln_option, rule_pair_list = next(iter(best_rules.items())) else: vln_option, rule_pair_list = random.choice(list(best_rules.items())) if best_size == 0: raise Exception('Invulnerable enemy? rules seems to be insufficient for this enemy set, please report:' + ', '.join([str(x) for x, y in enemy_sprite_region_pairs])) new_vln_list = {vln_kv[0]: vln_kv[1] for idx, vln_kv in enumerate(vln_list.items()) if idx not in vln_option} rules_to_add = [rule for rule, resources in rule_pair_list] resources_to_use = [resources for rule, resources in rule_pair_list] required_rules.append(or_rule(*rules_to_add)) for r in resources_to_use: for k, v in r.items(): used_resources[k] += v vln_list = new_vln_list return and_rule(*required_rules) def find_possible_rules(vln_list, used_resources, world, player): optional_clears = defaultdict(list) blunt_marker = defaultdict(bool) for damage_type in ['Blunt', 'Stun', 'Master', 'Boomerang', 'Hookshot']: # all_vln = all(vln[damage_type] != 0 for vln in vln_list.values()) vln_sub_list = frozenset({idx for idx, vln in enumerate(vln_list.values()) if vln[damage_type] != 0}) if vln_sub_list: if damage_type == 'Blunt': optional_clears[vln_sub_list].append((has_blunt_weapon(player), {})) blunt_marker[vln_sub_list] = True if damage_type == 'Stun': optional_clears[vln_sub_list].append((buzzblob_rule(player), {})) if damage_type == 'Master' and not blunt_marker[vln_sub_list]: optional_clears[vln_sub_list].append((has_class_2_weapon(player), {})) if damage_type == 'Boomerang': optional_clears[vln_sub_list].append((has('Hookshot', player), {})) elif damage_type == 'Hookshot': optional_clears[vln_sub_list].append((has_boomerang(player), {})) damage_type = 'Bomb' vln_sub_list = frozenset({idx for idx, vln in enumerate(vln_list.values()) if vln[damage_type] != 0}) if vln_sub_list: hits = needed_resources(damage_type, vln_list) if hits + used_resources['Bomb'] <= 8: optional_clears[vln_sub_list].append( (bombs_rule(world, player, hits + used_resources['Bomb']), {'Bomb': hits})) for damage_type in ['Bow', 'Silvers']: vln_sub_list = frozenset({idx for idx, vln in enumerate(vln_list.values()) if vln[damage_type] != 0}) if vln_sub_list: hits = needed_resources(damage_type, vln_list) resources = {'Arrow': hits} if damage_type == 'Bow' and hits + used_resources['Arrow'] <= 25: optional_clears[vln_sub_list].append( (bow_rule(world, player, hits + used_resources['Arrow']), resources)) if damage_type == 'Silvers' and hits + used_resources['Arrow'] <= 25: optional_clears[vln_sub_list].append( (silvers_rule(world, player, hits + used_resources['Arrow']), resources)) for damage_type in ['Powder', 'Somaria', 'Byrna', 'FireRod', 'IceRod', 'Bombos', 'Ether', 'Quake']: vln_sub_list = frozenset({idx for idx, vln in enumerate(vln_list.values()) if vln[damage_type] != 0}) if vln_sub_list: hits = needed_resources(damage_type, vln_list) resources = {'Magic': damage_cost[damage_type] * hits} if damage_type == 'Powder' and math.ceil(hits / 16) * 8 + used_resources['Magic'] <= 160: flag = min(vln[damage_type] for vln in vln_list.values()) flag = flag if flag < 0 else (hits + used_resources['Magic'] * 2) optional_clears[vln_sub_list].append((magic_powder_rule(world, player, flag), resources)) elif damage_type == 'Somaria' and math.ceil(hits / 64) * 8 + used_resources['Magic'] <= 160: flag = min(vln[damage_type] for vln in vln_list.values()) flag = flag if flag < 0 else (hits + used_resources['Magic'] * 8) optional_clears[vln_sub_list].append((somaria_rule(world, player, flag), resources)) elif damage_type == 'Byrna' and math.ceil(hits / 7) * 8 + used_resources['Magic'] <= 160: flag = min(vln[damage_type] for vln in vln_list.values()) flag = flag if flag < 0 else (hits + used_resources['Magic'] * 7 / 8) optional_clears[vln_sub_list].append((byrna_rule(world, player, flag), resources)) elif damage_type == 'FireRod' and hits + used_resources['Magic'] <= 160: flag = min(vln[damage_type] for vln in vln_list.values()) flag = flag if flag < 0 else (hits + used_resources['Magic']) optional_clears[vln_sub_list].append((fire_rod_rule(world, player, flag), resources)) elif damage_type == 'IceRod' and hits + used_resources['Magic'] <= 160: flag = min(vln[damage_type] for vln in vln_list.values()) flag = flag if flag < 0 else (hits + used_resources['Magic']) optional_clears[vln_sub_list].append((ice_rod_rule(world, player, flag), resources)) elif hits * 2 + used_resources['Magic'] <= 160 and not blunt_marker[vln_sub_list]: flag = min(vln[damage_type] for vln in vln_list.values()) flag = flag if flag < 0 else (hits + used_resources['Magic'] / 2) optional_clears[vln_sub_list].append((medallion_rule(world, player, damage_type, flag), resources)) return optional_clears def needed_resources(damage_type, vln_list): return sum(vln[damage_type] if vln[damage_type] >= 0 else 1 for vln in vln_list.values() if vln[damage_type] != 0) special_rules_check = { 'Swamp Waterway': None, 'Hera Back': [5, 6], 'GT Petting Zoo': [1, 4, 5, 7], 'Mimic Cave': [3, 4], 'Ice Hookshot Ledge': None, 'TR Hub Ledges': [3, 4, 5, 6, 7], 'Old Man Cave': None, 'Old Man House Back': [4, 5, 6], 'Death Mountain Return Cave (left)': None, 'Death Mountain Return Cave (right)': [1, 2, 3, 6, 7] } def special_rules_for_region(world, player, region_name, location, original_rule): if region_name == 'Swamp Waterway': # todo: check on enemizer interaction return or_rule(medallion_rule(world, player, 'Quake', 1), medallion_rule(world, player, 'Ether', 1), medallion_rule(world, player, 'Bombos', 1)) elif region_name in ['Hera Back', 'GT Petting Zoo', 'Mimic Cave']: enemy_number = int(location.name.split('#')[1]) if enemy_number in special_rules_check[region_name]: return and_rule(original_rule, has_boomerang(player)) else: return original_rule elif region_name in ['TR Hub Ledges', 'Ice Hookshot Ledge', 'Old Man Cave', 'Old Man House Back', 'Death Mountain Return Cave (left)', 'Death Mountain Return Cave (right)']: enemy_number = int(location.name.split('#')[1]) if special_rules_check[region_name] is None or enemy_number in special_rules_check[region_name]: return and_rule(original_rule, or_rule(has_boomerang(player), has('Hookshot', player))) else: return original_rule return original_rule def has_blunt_weapon(player): return or_rule(has_sword(player), has('Hammer', player)) def buzzblob_rule(player): return or_rule(has('Golden Sword', player), and_rule(has_blunt_weapon(player), or_rule(has_boomerang(player), has('Hookshot', player)))) # freeze it? def has_class_2_weapon(player): return or_rule(has_beam_sword(player), has('Hammer', player)) def somaria_rule(world, player, somaria_hits): if somaria_hits == -1: return has('Cane of Somaria', player) # insta-kill somaria? - not in vanilla else: magic_needed = math.ceil(somaria_hits / 64) * 8 # 64 hits per magic bar - 80 max? if magic_needed > 8: return and_rule(has('Cane of Somaria', player), can_extend_magic(world, player, magic_needed)) else: return has('Cane of Somaria', player) def byrna_rule(world, player, byrna_hits): if byrna_hits == -1: return has('Cane of Byrna', player) # insta-kill byrna? - not in vanilla else: magic_needed = math.ceil(byrna_hits / 7) * 8 # 7 hits per magic bar - generous? if magic_needed > 8: return and_rule(has('Cane of Byrna', player), can_extend_magic(world, player, magic_needed)) else: return has('Cane of Byrna', player) def bow_rule(world, player, arrows): if arrows == -1 or 0 < arrows <= 25: return can_shoot_normal_arrows(world, player) return RuleFactory.static_rule(False) def silvers_rule(world, player, arrows): if arrows == -1 or 0 < arrows <= 25: return can_shoot_silver_arrows(world, player) return RuleFactory.static_rule(False) def bombs_rule(world, player, bombs): if bombs == -1 or 0 < bombs <= 8: return can_use_bombs(world, player) return RuleFactory.static_rule(False) def ice_rod_rule(world, player, shots): if shots == -1: return has('Ice Rod', player) if shots > 8: return and_rule(has('Ice Rod', player), can_extend_magic(world, player, shots)) else: return has('Ice Rod', player) def fire_rod_rule(world, player, shots): if shots == -1: return has('Fire Rod', player) if shots > 8: return and_rule(has('Fire Rod', player), can_extend_magic(world, player, shots)) else: return has('Fire Rod', player) def magic_powder_rule(world, player, shots): if shots == -1: return has('Magic Powder', player) if shots == -2: # todo: other resources possible I guess - harder to keep track of though return and_rule(has('Magic Powder', player), or_rule(has_blunt_weapon(player), has('Hookshot', player))) magic_needed = math.ceil(shots / 16) * 8 # 16 tries per magic bar, that could be tight... if magic_needed > 8: return and_rule(has('Magic Powder', player), can_extend_magic(world, player, shots)) else: return has('Magic Powder', player) def medallion_rule(world, player, medallion, shots): if shots == -1: return and_rule(has(medallion, player), has_sword(player)) if shots == -2: return and_rule(has(medallion, player), has_sword(player)) magic_needed = shots * 2 if magic_needed > 8: return and_rule(has(medallion, player), has_sword(player), can_extend_magic(world, player, shots)) else: return and_rule(has(medallion, player), has_sword(player)) def or_rule(*rules): return RuleFactory.disj(rules) def and_rule(*rules): return RuleFactory.conj(rules) def has(item, player, count=1): return RuleFactory.item(item, player, count) def has_sword(player): return or_rule( has('Fighter Sword', player), has('Master Sword', player), has('Tempered Sword', player), has('Golden Sword', player) ) def has_beam_sword(player): return or_rule( has('Master Sword', player), has('Tempered Sword', player), has('Golden Sword', player) ) def has_class_3_sword(player): return or_rule( has('Tempered Sword', player), has('Golden Sword', player) ) def can_extend_magic(world, player, magic, flag_t=False): potion_shops = (find_shops_that_sell('Blue Potion', world, player) | find_shops_that_sell('Green Potion', world, player)) return RuleFactory.extend_magic(player, magic, world.difficulty_adjustments[player], potion_shops, flag_t) # class 0 damage (subtypes 1 and 2) def has_boomerang(player): return or_rule(has('Blue Boomerang', player), has('Red_Boomerang', player)) def find_shops_that_sell(item, world, player): return {shop.region for shop in world.shops[player] if shop.has_unlimited(item) and shop.region.player == player} def can_shoot_normal_arrows(world, player): if world.bow_mode[player].startswith('retro'): shops = find_shops_that_sell('Single Arrow', world, player) # retro+shopsanity, shops may not sell the Single Arrow at all if world.bow_mode[player] == 'retro_silvers': # non-progressive silvers grant wooden arrows, so shop may not be needed return and_rule(has('Bow', player), or_rule(RuleFactory.unlimited('Single Arrow', player, shops), has('Single Arrow', player), has('Silver Arrows', player))) else: return and_rule(has('Bow', player), or_rule(RuleFactory.unlimited('Single Arrow', player, shops), has('Single Arrow', player))) return has('Bow', player) def can_shoot_silver_arrows(world, player): # retro_silver requires the silver arrows item which is sufficient for the quiver if world.bow_mode[player] == 'retro': shops = find_shops_that_sell('Single Arrow', world, player) # retro+shopsanity, shops may not sell the Single Arrow at all return and_rule(has('Silver Arrows', player), or_rule(RuleFactory.unlimited('Single Arrow', player, shops), has('Single Arrow', player))) return and_rule(has('Bow', player), has('Silver Arrows', player)) def can_use_bombs(world, player): return or_rule(RuleFactory.static_rule(not world.bombbag[player]), has('Bomb Upgrade (+10)', player)) def enemy_vulnerability(world, player, enemy_sprite, region): damage_table = world.damage_table[player].damage_table stats = world.data_tables[player].enemy_stats damage_src = damage_table['DamageSource'] sub_class_table = damage_table['SubClassTable'] enemy_sub_class = sub_class_table[enemy_sprite.kind] vulnerability = defaultdict(int) c1 = number_of_hits('Sword1', damage_src, enemy_sub_class, stats, enemy_sprite, region) if c1 != 0: if enemy_sprite.kind == EnemySprite.Buzzblob: vulnerability['Stun'] = -1 else: vulnerability['Blunt'] = -1 vulnerability['Master'] = -1 vulnerability['Somaria'] = c1 vulnerability['Byrna'] = c1 else: c2 = number_of_hits('Sword3', damage_src, enemy_sub_class, stats, enemy_sprite, region) if c2 != 0: vulnerability['Master'] = -1 # currently Lynels are only vulnerable to only master spins or above hits = number_of_hits('Arrow', damage_src, enemy_sub_class, stats, enemy_sprite, region) if hits != 0: vulnerability['Bow'] = hits hits = number_of_hits('SilverArrow', damage_src, enemy_sub_class, stats, enemy_sprite, region) if hits != 0: vulnerability['Silvers'] = hits for method in ['Bomb', 'Hookshot', 'FireRod', 'IceRod', 'Boomerang', 'Powder', 'Bombos', 'Ether', 'Quake']: hits = number_of_hits(method, damage_src, enemy_sub_class, stats, enemy_sprite, region) if hits != 0: vulnerability[method] = hits return vulnerability def number_of_hits(source_name, damage_src, enemy_sub_class, stats, enemy_sprite, region): damage_class = damage_src[source_name]['class'] sub_class = enemy_sub_class[damage_class] damage_amount = damage_src[source_name]['subclass'][sub_class] if damage_amount == 0: return 0 elif damage_amount <= 0x64: health = stats[enemy_sprite.kind].health if isinstance(health, tuple): if enemy_sprite.kind in [EnemySprite.Tektite, EnemySprite.HardhatBeetle]: idx = enemy_sprite.tile_x & 0x1 health = health[idx] elif region.is_light_world and region.is_dark_world: health = min(health) elif region.is_light_world: health = health[0] elif region.is_dark_world: health = health[1] else: health = max(health) return math.ceil(health / damage_amount) elif damage_amount in [0xF9, 0xFA, 0xFD]: # -1 faired or incinerated; -2 blobbed # F9: fairy, defeated, but doesn't drop anything # FA: blobbed - can you kill a blob? = -2 # FD: incinerated return -1 if damage_amount != 0xFA else -2 else: return 0