Files
alttpr-python/source/enemizer/EnemyLogic.py
aerinon 1985139cb5 Fixing generation errors
Update rom for multiworld/vanilla sewers problems
2023-08-09 15:33:11 -06:00

519 lines
23 KiB
Python

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'] not in [0, -3]: # fairy doesn't make it drop
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 once 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].kind == EnemySprite.Terrorpin:
if not hammer_required:
required_rules.append(has('Hammer', player))
hammer_required = True
picky_enemies.append(key)
continue
if key[0].kind == 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': [5, 8, 9, 11],
'Mimic Cave': [3, 4],
'Ice Hookshot Ledge': None,
'TR Hub Ledges': [3, 4, 5, 6, 7],
'TR Dark Ride': [1, 2, 3],
'GT Speed Torch': [11, 13],
'Old Man Cave (West)': None,
'Old Man Cave (East)': None,
'Old Man House': [1, 3],
'Old Man House Back': [4, 5, 6],
'Death Mountain Return Cave (left)': None,
'Death Mountain Return Cave (right)': [1, 2, 3, 6, 7] # 2, 5, 6 are considered embedded
}
def special_rules_for_region(world, player, region_name, location, original_rule):
if region_name == 'Swamp Waterway':
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', 'TR Dark Ride']:
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
elif region_name in ['Old Man Cave (West)', 'Old Man Cave (East)', 'Old Man House Back', 'Old Man House',
'Death Mountain Return Cave (left)', 'Death Mountain Return Cave (right)']:
enemy_number = int(location.name.split('#')[1])
if region_name == 'Death Mountain Return Cave (left)':
if enemy_number in [1, 5]:
return and_rule(original_rule, or_rule(has_boomerang(player), has('Hookshot', player)))
else:
return and_rule(original_rule, has('Hookshot', player))
if special_rules_check[region_name] is None or enemy_number in special_rules_check[region_name]:
return and_rule(original_rule, has('Hookshot', player))
else:
return original_rule
return original_rule
def has_blunt_weapon(player):
return or_rule(has_sword(player), has('Hammer', player))
# Bombs, Arrows, Bombos, FireRod, Somaria, Byrna should be handled by the damage table logic
# Powder doesn't work (also handled by damage table)
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), has('Ice Rod', player))),
and_rule(has('Ether', player), has_sword(player)),
and_rule(has('Quake', player), has_sword(player)))
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 == -3:
if enemy_sprite.kind != EnemySprite.Buzzblob: # buzzblobs are special and don't die
vulnerability[method] = -1
elif 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 incinerated; -2 blobbed, -3 fairy-ed (depends on enemy if "killed" or not)
# F9: fairy, defeated, but doesn't drop anything: -3
# FA: blobbed - can you kill a blob? = -2
# FD: incinerated
return special_classes[damage_amount]
else:
return 0
special_classes = {0xF9: -3, 0xFA: -2, 0xFD: -1}