Files
alttpr-python/KeyDoorShuffle.py
aerinon 4dda394a90 Added option to keep original palettes in crossed dungeon mode
If sanc if in a DW dungeon because of crossed+ ER, then you start in bunny form
Mirroring from sanc to the portal is now in logic
Another fix for animated tiles (fairy fountains)
GT Big Key stat changed on credits

Some standard logic fixes for lobbies (more outstanding)
2020-11-16 10:51:26 -07:00

1751 lines
80 KiB
Python

import itertools
import logging
from collections import defaultdict, deque
from BaseClasses import DoorType
from Regions import dungeon_events
from Dungeons import dungeon_keys, dungeon_bigs
from DungeonGenerator import ExplorationState, special_big_key_doors
class KeyLayout(object):
def __init__(self, sector, starts, proposal):
self.sector = sector
self.start_regions = starts
self.proposal = proposal
self.key_logic = KeyLogic(sector.name)
self.key_counters = None
self.flat_prop = None
self.max_chests = None
self.max_drops = None
self.all_chest_locations = {}
self.big_key_special = False
self.all_locations = set()
self.item_locations = set()
# bk special?
# bk required? True if big chests or big doors exists
def reset(self, proposal, builder, world, player):
self.proposal = proposal
self.flat_prop = flatten_pair_list(self.proposal)
self.key_logic = KeyLogic(self.sector.name)
self.max_chests = calc_max_chests(builder, self, world, player)
self.all_locations = set()
self.item_locations = set()
class KeyLogic(object):
def __init__(self, dungeon_name):
self.door_rules = {}
self.bk_restricted = set() # subset of free locations
self.bk_locked = set() # includes potentially other locations and key only locations
self.sm_restricted = set()
self.small_key_name = dungeon_keys[dungeon_name]
self.bk_name = dungeon_bigs[dungeon_name]
self.bk_doors = set()
self.bk_chests = set()
self.logic_min = {}
self.logic_max = {}
self.placement_rules = []
self.location_rules = {}
self.outside_keys = 0
self.dungeon = dungeon_name
def check_placement(self, unplaced_keys, big_key_loc=None):
for rule in self.placement_rules:
if not rule.is_satisfiable(self.outside_keys, unplaced_keys):
return False
if big_key_loc:
for rule_a, rule_b in itertools.combinations(self.placement_rules, 2):
if rule_a.contradicts(rule_b, unplaced_keys, big_key_loc):
return False
return True
class DoorRules(object):
def __init__(self, number, is_valid):
self.small_key_num = number
self.is_valid = is_valid
# allowing a different number if bk is behind this door in a set of locations
self.alternate_small_key = None
self.alternate_big_key_loc = set()
# for a place with only 1 free location/key_only_location behind it ... no goals and locations
self.allow_small = False
self.small_location = None
self.opposite = None
class LocationRule(object):
def __init__(self):
self.small_key_num = 0
self.conditional_sets = []
class ConditionalLocationRule(object):
def __init__(self, conditional_set):
self.conditional_set = conditional_set
self.small_key_num = 0
class PlacementRule(object):
def __init__(self):
self.door_reference = None
self.small_key = None
self.bk_conditional_set = None # the location that means
self.needed_keys_w_bk = None
self.needed_keys_wo_bk = None
self.check_locations_w_bk = None
self.check_locations_wo_bk = None
self.bk_relevant = True
self.key_reduced = False
def contradicts(self, rule, unplaced_keys, big_key_loc):
bk_blocked = big_key_loc in self.bk_conditional_set if self.bk_conditional_set else False
rule_blocked = big_key_loc in rule.bk_conditional_set if rule.bk_conditional_set else False
check_locations = self.check_locations_wo_bk if bk_blocked else self.check_locations_w_bk
rule_locations = rule.check_locations_wo_bk if rule_blocked else rule.check_locations_w_bk
if check_locations is None or rule_locations is None:
return False
check_locations = check_locations - {big_key_loc}
rule_locations = rule_locations - {big_key_loc}
threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk
rule_threshold = rule.needed_keys_wo_bk if rule_blocked else rule.needed_keys_w_bk
common_locations = rule_locations & check_locations
shared = len(common_locations)
if min(rule_threshold, threshold) - shared > 0:
left = unplaced_keys - shared
check_locations = check_locations - common_locations
check_needed = threshold - shared
if len(check_locations) < check_needed or left < check_needed:
return True
else:
left -= check_needed
rule_locations = rule_locations - common_locations
rule_needed = rule_threshold - shared
if len(rule_locations) < rule_needed or left < rule_needed:
return True
else:
left -= rule_needed
return False
def is_satisfiable(self, outside_keys, unplaced_keys):
bk_blocked = False
if self.bk_conditional_set:
for loc in self.bk_conditional_set:
if loc.item and loc.item.bigkey:
bk_blocked = True
break
check_locations = self.check_locations_wo_bk if bk_blocked else self.check_locations_w_bk
if not bk_blocked and check_locations is None:
return True
available_keys = outside_keys
empty_chests = 0
threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk
for loc in check_locations:
if not loc.item:
empty_chests += 1
elif loc.item and loc.item.name == self.small_key:
available_keys += 1
place_able_keys = min(empty_chests, unplaced_keys)
available_keys += place_able_keys
return available_keys >= threshold
class KeyCounter(object):
def __init__(self, max_chests):
self.max_chests = max_chests
self.free_locations = {}
self.key_only_locations = {}
self.child_doors = {}
self.open_doors = {}
self.used_keys = 0
self.big_key_opened = False
self.important_location = False
self.other_locations = {}
def used_smalls_loc(self, reserve=0):
return max(self.used_keys + reserve - len(self.key_only_locations), 0)
def copy(self):
ret = KeyCounter(self.max_chests)
ret.free_locations.update(self.free_locations)
ret.key_only_locations.update(self.key_only_locations)
ret.child_doors.update(self.child_doors)
ret.used_keys = self.used_keys
ret.open_doors.update(self.open_doors)
ret.big_key_opened = self.big_key_opened
ret.important_location = self.important_location
return ret
def build_key_layout(builder, start_regions, proposal, world, player):
key_layout = KeyLayout(builder.master_sector, start_regions, proposal)
key_layout.flat_prop = flatten_pair_list(key_layout.proposal)
key_layout.max_drops = count_key_drops(key_layout.sector)
key_layout.max_chests = calc_max_chests(builder, key_layout, world, player)
key_layout.big_key_special = check_bk_special(key_layout.sector.region_set(), world, player)
key_layout.all_locations = find_all_locations(key_layout.sector)
return key_layout
def count_key_drops(sector):
cnt = 0
for region in sector.regions:
for loc in region.locations:
if loc.forced_item and 'Small Key' in loc.item.name:
cnt += 1
return cnt
def find_all_locations(sector):
all_locations = set()
for region in sector.regions:
for loc in region.locations:
all_locations.add(loc)
return all_locations
def calc_max_chests(builder, key_layout, world, player):
if world.doorShuffle[player] != 'crossed':
return len(world.get_dungeon(key_layout.sector.name, player).small_keys)
return max(0, builder.key_doors_num - key_layout.max_drops)
def analyze_dungeon(key_layout, world, player):
key_layout.key_counters = create_key_counters(key_layout, world, player)
key_logic = key_layout.key_logic
find_bk_locked_sections(key_layout, world, player)
key_logic.bk_chests.update(find_big_chest_locations(key_layout.all_chest_locations))
key_logic.bk_chests.update(find_big_key_locked_locations(key_layout.all_chest_locations))
if world.retro[player] and world.mode[player] != 'standard':
return
original_key_counter = find_counter({}, False, key_layout)
queue = deque([(None, original_key_counter)])
doors_completed = set()
visited_cid = set()
visited_cid.add(cid(original_key_counter, key_layout))
while len(queue) > 0:
queue = deque(sorted(queue, key=queue_sorter))
parent_door, key_counter = queue.popleft()
chest_keys = available_chest_small_keys(key_counter, world, player)
raw_avail = chest_keys + len(key_counter.key_only_locations)
available = raw_avail - key_counter.used_keys
possible_smalls = count_unique_small_doors(key_counter, key_layout.flat_prop)
avail_bigs = exist_relevant_big_doors(key_counter, key_layout) or exist_big_chest(key_counter)
non_big_locs = count_locations_big_optional(key_counter.free_locations)
big_avail = key_counter.big_key_opened or (key_layout.big_key_special and any(x for x in key_counter.other_locations.keys() if x.forced_item and x.forced_item.bigkey))
if not big_avail:
if chest_keys == non_big_locs and chest_keys > 0 and available <= possible_smalls and not avail_bigs:
key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations))
# try to relax the rules here? - smallest requirement that doesn't force a softlock
child_queue = deque()
for child in key_counter.child_doors.keys():
if not child.bigKey or not key_layout.big_key_special or big_avail:
odd_counter = create_odd_key_counter(child, key_counter, key_layout, world, player)
empty_flag = empty_counter(odd_counter)
child_queue.append((child, odd_counter, empty_flag))
while len(child_queue) > 0:
child, odd_counter, empty_flag = child_queue.popleft()
if not child.bigKey and child not in doors_completed:
best_counter = find_best_counter(child, odd_counter, key_counter, key_layout, world, player, False, empty_flag)
rule = create_rule(best_counter, key_counter, key_layout, world, player)
check_for_self_lock_key(rule, child, best_counter, key_layout, world, player)
bk_restricted_rules(rule, child, odd_counter, empty_flag, key_counter, key_layout, world, player)
key_logic.door_rules[child.name] = rule
doors_completed.add(child)
next_counter = find_next_counter(child, key_counter, key_layout)
ctr_id = cid(next_counter, key_layout)
if ctr_id not in visited_cid:
queue.append((child, next_counter))
visited_cid.add(ctr_id)
check_rules(original_key_counter, key_layout, world, player)
# Flip bk rules if more restrictive, to prevent placing a big key in a softlocking location
for rule in key_logic.door_rules.values():
if rule.alternate_small_key is not None and rule.alternate_small_key > rule.small_key_num:
max_counter = find_max_counter(key_layout)
rule.alternate_big_key_loc = set(max_counter.free_locations.keys()).difference(rule.alternate_big_key_loc)
rule.small_key_num, rule.alternate_small_key = rule.alternate_small_key, rule.small_key_num
create_exhaustive_placement_rules(key_layout, world, player)
set_paired_rules(key_logic, world, player)
def create_exhaustive_placement_rules(key_layout, world, player):
key_logic = key_layout.key_logic
max_ctr = find_max_counter(key_layout)
for code, key_counter in key_layout.key_counters.items():
accessible_loc = set()
accessible_loc.update(key_counter.free_locations)
accessible_loc.update(key_counter.key_only_locations)
blocked_loc = key_layout.item_locations.difference(accessible_loc)
valid_rule = True
# min_keys = max(count_unique_sm_doors(key_counter.child_doors), key_counter.used_keys + 1)
min_keys = key_counter.used_keys + 1
if len(blocked_loc) > 0 and len(key_counter.key_only_locations) < min_keys:
rule = PlacementRule()
rule.door_reference = code
rule.small_key = key_logic.small_key_name
if key_counter.big_key_opened or not big_key_progress(key_counter):
rule.needed_keys_w_bk = min_keys
rule.bk_relevant = key_counter.big_key_opened
if key_counter.big_key_opened and rule.needed_keys_w_bk + 1 > len(accessible_loc):
valid_rule = False # indicates that the big key cannot be in the accessible locations
key_logic.bk_restricted.update(accessible_loc.difference(max_ctr.key_only_locations))
else:
placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player)
rule.check_locations_w_bk = accessible_loc
check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc)
else:
if big_key_progress(key_counter) and only_sm_doors(key_counter):
create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, accessible_loc, min_keys, world, player)
rule.bk_conditional_set = blocked_loc
rule.needed_keys_wo_bk = min_keys
rule.check_locations_wo_bk = set(filter_big_chest(accessible_loc))
if valid_rule:
key_logic.placement_rules.append(rule)
adjust_locations_rules(key_logic, rule, accessible_loc, key_layout, key_counter, max_ctr)
refine_placement_rules(key_layout, max_ctr)
refine_location_rules(key_layout)
def placement_self_lock_adjustment(rule, max_ctr, blocked_loc, ctr, world, player):
if len(blocked_loc) == 1 and world.accessibility[player] != 'locations':
blocked_others = set(max_ctr.other_locations).difference(set(ctr.other_locations))
important_found = False
for loc in blocked_others:
if important_location(loc, world, player):
important_found = True
break
if not important_found:
rule.needed_keys_w_bk -= 1
def check_sm_restriction_needed(key_layout, max_ctr, rule, blocked):
if rule.needed_keys_w_bk == key_layout.max_chests + len(max_ctr.key_only_locations):
key_layout.key_logic.sm_restricted.update(blocked.difference(max_ctr.key_only_locations))
return True
return False
def adjust_locations_rules(key_logic, rule, accessible_loc, key_layout, key_counter, max_ctr):
if rule.bk_conditional_set:
test_set = (rule.bk_conditional_set - key_logic.bk_locked) - set(max_ctr.key_only_locations.keys())
needed = rule.needed_keys_wo_bk if test_set else 0
else:
test_set = None
needed = rule.needed_keys_w_bk
if needed > 0:
accessible_loc.update(key_counter.other_locations)
blocked_loc = key_layout.all_locations-accessible_loc
for location in blocked_loc:
if location not in key_logic.location_rules.keys():
loc_rule = LocationRule()
key_logic.location_rules[location] = loc_rule
else:
loc_rule = key_logic.location_rules[location]
if test_set:
if location not in key_logic.bk_locked:
cond_rule = None
for other in loc_rule.conditional_sets:
if other.conditional_set == test_set:
cond_rule = other
break
if not cond_rule:
cond_rule = ConditionalLocationRule(test_set)
loc_rule.conditional_sets.append(cond_rule)
cond_rule.small_key_num = max(needed, cond_rule.small_key_num)
else:
loc_rule.small_key_num = max(needed, loc_rule.small_key_num)
def refine_placement_rules(key_layout, max_ctr):
key_logic = key_layout.key_logic
changed = True
while changed:
changed = False
rules_to_remove = []
for rule in key_logic.placement_rules:
if rule.check_locations_w_bk:
rule.check_locations_w_bk.difference_update(key_logic.sm_restricted)
key_onlys = rule.check_locations_w_bk.intersection(max_ctr.key_only_locations)
if len(key_onlys) > 0:
rule.check_locations_w_bk.difference_update(key_onlys)
rule.needed_keys_w_bk -= len(key_onlys)
if rule.needed_keys_w_bk == 0:
rules_to_remove.append(rule)
if rule.bk_relevant and len(rule.check_locations_w_bk) == rule.needed_keys_w_bk + 1:
new_restricted = set(max_ctr.free_locations) - rule.check_locations_w_bk
if len(new_restricted - key_logic.bk_restricted) > 0:
key_logic.bk_restricted.update(new_restricted) # bk must be in one of the check_locations
changed = True
if rule.needed_keys_w_bk > key_layout.max_chests or len(rule.check_locations_w_bk) < rule.needed_keys_w_bk:
logging.getLogger('').warning('Invalid rule - what went wrong here??')
rules_to_remove.append(rule)
changed = True
if rule.bk_conditional_set is not None:
rule.bk_conditional_set.difference_update(key_logic.bk_restricted)
rule.bk_conditional_set.difference_update(max_ctr.key_only_locations)
if len(rule.bk_conditional_set) == 0:
rules_to_remove.append(rule)
if rule.check_locations_wo_bk:
rule.check_locations_wo_bk.difference_update(key_logic.sm_restricted)
key_onlys = rule.check_locations_wo_bk.intersection(max_ctr.key_only_locations)
if len(key_onlys) > 0:
rule.check_locations_wo_bk.difference_update(key_onlys)
rule.needed_keys_wo_bk -= len(key_onlys)
if rule.needed_keys_wo_bk == 0:
rules_to_remove.append(rule)
if len(rule.check_locations_wo_bk) < rule.needed_keys_wo_bk or rule.needed_keys_wo_bk > key_layout.max_chests:
if len(rule.bk_conditional_set) > 0:
key_logic.bk_restricted.update(rule.bk_conditional_set)
rules_to_remove.append(rule)
changed = True # impossible for bk to be here, I think
for rule_a, rule_b in itertools.combinations([x for x in key_logic.placement_rules if x not in rules_to_remove], 2):
if rule_b.bk_conditional_set and rule_a.check_locations_w_bk:
temp = rule_a
rule_a = rule_b
rule_b = temp
if rule_a.bk_conditional_set and rule_b.check_locations_w_bk:
common_needed = min(rule_a.needed_keys_wo_bk, rule_b.needed_keys_w_bk)
if len(rule_b.check_locations_w_bk & rule_a.check_locations_wo_bk) < common_needed:
key_logic.bk_restricted.update(rule_a.bk_conditional_set)
rules_to_remove.append(rule_a)
changed = True
break
equivalent_rules = []
for rule in key_logic.placement_rules:
for rule2 in key_logic.placement_rules:
if rule != rule2:
if rule.check_locations_w_bk and rule2.check_locations_w_bk:
if rule2.check_locations_w_bk == rule.check_locations_w_bk and rule2.needed_keys_w_bk > rule.needed_keys_w_bk:
rules_to_remove.append(rule)
elif rule2.needed_keys_w_bk == rule.needed_keys_w_bk and rule2.check_locations_w_bk < rule.check_locations_w_bk:
rules_to_remove.append(rule)
elif rule2.check_locations_w_bk == rule.check_locations_w_bk and rule2.needed_keys_w_bk == rule.needed_keys_w_bk:
equivalent_rules.append((rule, rule2))
if rule.check_locations_wo_bk and rule2.check_locations_wo_bk and rule.bk_conditional_set == rule2.bk_conditional_set:
if rule2.check_locations_wo_bk == rule.check_locations_wo_bk and rule2.needed_keys_wo_bk > rule.needed_keys_wo_bk:
rules_to_remove.append(rule)
elif rule2.needed_keys_wo_bk == rule.needed_keys_wo_bk and rule2.check_locations_wo_bk < rule.check_locations_wo_bk:
rules_to_remove.append(rule)
elif rule2.check_locations_wo_bk == rule.check_locations_wo_bk and rule2.needed_keys_wo_bk == rule.needed_keys_wo_bk:
equivalent_rules.append((rule, rule2))
if len(rules_to_remove) > 0:
key_logic.placement_rules = [x for x in key_logic.placement_rules if x not in rules_to_remove]
equivalent_rules = [x for x in equivalent_rules if x[0] not in rules_to_remove and x[1] not in rules_to_remove]
if len(equivalent_rules) > 0:
removed_rules = {}
for r1, r2 in equivalent_rules:
if r1 in removed_rules.keys():
r1 = removed_rules[r1]
if r2 in removed_rules.keys():
r2 = removed_rules[r2]
if r1 != r2:
r1.door_reference += ','+r2.door_reference
key_logic.placement_rules.remove(r2)
removed_rules[r2] = r1
def refine_location_rules(key_layout):
locs_to_remove = []
for loc, rule in key_layout.key_logic.location_rules.items():
conditions_to_remove = []
for cond_rule in rule.conditional_sets:
if cond_rule.small_key_num <= rule.small_key_num:
conditions_to_remove.append(cond_rule)
rule.conditional_sets = [x for x in rule.conditional_sets if x not in conditions_to_remove]
if rule.small_key_num == 0 and len(rule.conditional_sets) == 0:
locs_to_remove.append(loc)
for loc in locs_to_remove:
del key_layout.key_logic.location_rules[loc]
def create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, accessible_loc, min_keys, world, player):
key_logic = key_layout.key_logic
rule = PlacementRule()
rule.door_reference = code
rule.small_key = key_logic.small_key_name
rule.needed_keys_w_bk = min_keys
if key_counter.big_key_opened and rule.needed_keys_w_bk + 1 > len(accessible_loc):
# indicates that the big key cannot be in the accessible locations
key_logic.bk_restricted.update(accessible_loc.difference(max_ctr.key_only_locations))
else:
placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player)
rule.check_locations_w_bk = accessible_loc
check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc)
key_logic.placement_rules.append(rule)
adjust_locations_rules(key_logic, rule, accessible_loc, key_layout, key_counter, max_ctr)
def queue_sorter(queue_item):
door, counter = queue_item
if door is None:
return 0
return 1 if door.bigKey else 0
def queue_sorter_2(queue_item):
door, counter, key_only = queue_item
if door is None:
return 0
return 1 if door.bigKey else 0
def find_bk_locked_sections(key_layout, world, player):
key_counters = key_layout.key_counters
key_logic = key_layout.key_logic
bk_not_required = set()
big_chest_allowed_big_key = world.accessibility[player] != 'locations'
for counter in key_counters.values():
key_layout.all_chest_locations.update(counter.free_locations)
key_layout.item_locations.update(counter.free_locations)
key_layout.item_locations.update(counter.key_only_locations)
if counter.big_key_opened and counter.important_location:
big_chest_allowed_big_key = False
if not counter.big_key_opened:
bk_not_required.update(counter.free_locations)
bk_not_required.update(counter.key_only_locations)
bk_not_required.update(counter.other_locations)
# todo?: handle bk special differently in cross dungeon
# notably: things behind bk doors - relying on the bk door logic atm
if not key_layout.big_key_special:
key_logic.bk_restricted.update(dict.fromkeys(set(key_layout.all_chest_locations).difference(bk_not_required)))
key_logic.bk_locked.update(dict.fromkeys(set(key_layout.all_locations) - bk_not_required))
if not big_chest_allowed_big_key:
bk_required_locations = find_big_chest_locations(key_layout.all_chest_locations)
bk_required_locations += find_big_key_locked_locations(key_layout.all_chest_locations)
key_logic.bk_restricted.update(bk_required_locations)
key_logic.bk_locked.update(bk_required_locations)
def empty_counter(counter):
if len(counter.key_only_locations) != 0 or len(counter.free_locations) != 0 or len(counter.child_doors) != 0:
return False
return not counter.important_location
def relative_empty_counter(odd_counter, key_counter):
if len(set(odd_counter.key_only_locations).difference(key_counter.key_only_locations)) > 0:
return False
if len(set(odd_counter.free_locations).difference(key_counter.free_locations)) > 0:
return False
new_child_door = False
for child in odd_counter.child_doors:
if unique_child_door(child, key_counter):
new_child_door = True
break
if new_child_door:
return False
return True
def relative_empty_counter_2(odd_counter, key_counter):
if len(set(odd_counter.key_only_locations).difference(key_counter.key_only_locations)) > 0:
return False
if len(set(odd_counter.free_locations).difference(key_counter.free_locations)) > 0:
return False
for child in odd_counter.child_doors:
if unique_child_door_2(child, key_counter):
return False
return True
def progressive_ctr(new_counter, last_counter):
if len(set(new_counter.key_only_locations).difference(last_counter.key_only_locations)) > 0:
return True
if len(set(new_counter.free_locations).difference(last_counter.free_locations)) > 0:
return True
for child in new_counter.child_doors:
if unique_child_door_2(child, last_counter):
return True
return False
def unique_child_door(child, key_counter):
if child in key_counter.child_doors or child.dest in key_counter.child_doors:
return False
if child in key_counter.open_doors or child.dest in key_counter.child_doors:
return False
if child.bigKey and key_counter.big_key_opened:
return False
return True
def unique_child_door_2(child, key_counter):
if child in key_counter.child_doors or child.dest in key_counter.child_doors:
return False
if child in key_counter.open_doors or child.dest in key_counter.child_doors:
return False
return True
def find_best_counter(door, odd_counter, key_counter, key_layout, world, player, skip_bk, empty_flag): # try to waste as many keys as possible?
ignored_doors = {door, door.dest} if door is not None else {}
finished = False
opened_doors = dict(key_counter.open_doors)
bk_opened = key_counter.big_key_opened
# new_counter = key_counter
last_counter = key_counter
while not finished:
door_set = find_potential_open_doors(last_counter, ignored_doors, key_layout, skip_bk)
if door_set is None or len(door_set) == 0:
finished = True
continue
for new_door in door_set:
proposed_doors = {**opened_doors, **dict.fromkeys([new_door, new_door.dest])}
bk_open = bk_opened or new_door.bigKey
new_counter = find_counter(proposed_doors, bk_open, key_layout)
bk_open = new_counter.big_key_opened
# this means the new_door invalidates the door / leads to the same stuff
if not empty_flag and relative_empty_counter(odd_counter, new_counter):
ignored_doors.add(new_door)
elif empty_flag or key_wasted(new_door, door, last_counter, new_counter, key_layout, world, player):
last_counter = new_counter
opened_doors = proposed_doors
bk_opened = bk_open
else:
ignored_doors.add(new_door)
return last_counter
def find_worst_counter(door, odd_counter, key_counter, key_layout, skip_bk): # try to waste as many keys as possible?
ignored_doors = {door, door.dest} if door is not None else {}
finished = False
opened_doors = dict(key_counter.open_doors)
bk_opened = key_counter.big_key_opened
# new_counter = key_counter
last_counter = key_counter
while not finished:
door_set = find_potential_open_doors(last_counter, ignored_doors, key_layout, skip_bk, 0)
if door_set is None or len(door_set) == 0:
finished = True
continue
for new_door in door_set:
proposed_doors = {**opened_doors, **dict.fromkeys([new_door, new_door.dest])}
bk_open = bk_opened or new_door.bigKey
new_counter = find_counter(proposed_doors, bk_open, key_layout)
bk_open = new_counter.big_key_opened
if not new_door.bigKey and progressive_ctr(new_counter, last_counter) and relative_empty_counter_2(odd_counter, new_counter):
ignored_doors.add(new_door)
else:
last_counter = new_counter
opened_doors = proposed_doors
bk_opened = bk_open
# this means the new_door invalidates the door / leads to the same stuff
return last_counter
def find_potential_open_doors(key_counter, ignored_doors, key_layout, skip_bk, reserve=1):
small_doors = []
big_doors = []
if key_layout.big_key_special:
big_key_available = any(x for x in key_counter.other_locations.keys() if x.forced_item and x.forced_item.bigkey)
else:
big_key_available = len(key_counter.free_locations) - key_counter.used_smalls_loc(reserve) > 0
for other in key_counter.child_doors:
if other not in ignored_doors and other.dest not in ignored_doors:
if other.bigKey:
if not skip_bk and (not key_layout.big_key_special or big_key_available):
big_doors.append(other)
elif other.dest not in small_doors:
small_doors.append(other)
if len(small_doors) == 0 and (not skip_bk and (len(big_doors) == 0 or not big_key_available)):
return None
return small_doors + big_doors
def key_wasted(new_door, old_door, old_counter, new_counter, key_layout, world, player):
if new_door.bigKey: # big keys are not wastes - it uses up a location
return True
chest_keys = available_chest_small_keys(old_counter, world, player)
old_key_diff = len(old_counter.key_only_locations) - old_counter.used_keys
old_avail = chest_keys + old_key_diff
new_chest_keys = available_chest_small_keys(new_counter, world, player)
new_key_diff = len(new_counter.key_only_locations) - new_counter.used_keys
new_avail = new_chest_keys + new_key_diff
if new_key_diff < old_key_diff or new_avail < old_avail:
return True
if new_avail >= old_avail:
wasted_keys = 0
old_children = old_counter.child_doors.keys()
new_children = [x for x in new_counter.child_doors.keys() if x != old_door and x.dest != old_door and (not x.bigKey or x not in old_children)]
current_counter = new_counter
opened_doors = dict(current_counter.open_doors)
bk_opened = current_counter.big_key_opened
for new_child in new_children:
proposed_doors = {**opened_doors, **dict.fromkeys([new_child, new_child.dest])}
bk_open = bk_opened or new_door.bigKey
new_counter = find_counter(proposed_doors, bk_open, key_layout)
if key_wasted(new_child, old_door, current_counter, new_counter, key_layout, world, player):
wasted_keys += 1
if new_avail - wasted_keys < old_avail:
return True # waste is possible
return False
def find_next_counter(new_door, old_counter, key_layout):
proposed_doors = {**old_counter.open_doors, **dict.fromkeys([new_door, new_door.dest])}
bk_open = old_counter.big_key_opened or new_door.bigKey
return find_counter(proposed_doors, bk_open, key_layout)
def check_special_locations(locations):
for loc in locations:
if loc.name == 'Hyrule Castle - Zelda\'s Chest':
return True
return False
def calc_avail_keys(key_counter, world, player):
chest_keys = available_chest_small_keys(key_counter, world, player)
raw_avail = chest_keys + len(key_counter.key_only_locations)
return raw_avail - key_counter.used_keys
def create_rule(key_counter, prev_counter, key_layout, world, player):
# prev_chest_keys = available_chest_small_keys(prev_counter, world)
# prev_avail = prev_chest_keys + len(prev_counter.key_only_locations)
chest_keys = available_chest_small_keys(key_counter, world, player)
key_gain = len(key_counter.key_only_locations) - len(prev_counter.key_only_locations)
# previous method
# raw_avail = chest_keys + len(key_counter.key_only_locations)
# available = raw_avail - key_counter.used_keys
# possible_smalls = count_unique_small_doors(key_counter, key_layout.flat_prop)
# required_keys = min(available, possible_smalls) + key_counter.used_keys
required_keys = key_counter.used_keys + 1 # this makes more sense, if key_counter has wasted all keys
adj_chest_keys = min(chest_keys, required_keys)
needed_chests = required_keys - len(key_counter.key_only_locations)
is_valid = needed_chests <= chest_keys
unneeded_chests = min(key_gain, max(0, adj_chest_keys - needed_chests))
rule_num = required_keys - unneeded_chests
return DoorRules(rule_num, is_valid)
def check_for_self_lock_key(rule, door, parent_counter, key_layout, world, player):
if world.accessibility[player] != 'locations':
counter = find_inverted_counter(door, parent_counter, key_layout, world, player)
if not self_lock_possible(counter):
return
if len(counter.free_locations) == 1 and len(counter.key_only_locations) == 0 and not counter.important_location:
rule.allow_small = True
rule.small_location = next(iter(counter.free_locations))
def find_inverted_counter(door, parent_counter, key_layout, world, player):
# open all doors in counter
counter = open_all_counter(parent_counter, key_layout, door=door)
max_counter = find_max_counter(key_layout)
# find the difference
inverted_counter = KeyCounter(key_layout.max_chests)
inverted_counter.free_locations = dict_difference(max_counter.free_locations, counter.free_locations)
inverted_counter.key_only_locations = dict_difference(max_counter.key_only_locations, counter.key_only_locations)
# child doors? used_keys?
# inverted_counter.child_doors = dict_difference(max_counter.child_doors, counter.child_doors)
inverted_counter.open_doors = dict_difference(max_counter.open_doors, counter.open_doors)
inverted_counter.other_locations = dict_difference(max_counter.other_locations, counter.other_locations)
for loc in inverted_counter.other_locations:
if important_location(loc, world, player):
inverted_counter.important_location = True
return inverted_counter
def open_all_counter(parent_counter, key_layout, door=None, skipBk=False):
changed = True
counter = parent_counter
proposed_doors = dict.fromkeys(parent_counter.open_doors.keys())
while changed:
changed = False
doors_to_open = {}
for child in counter.child_doors:
if door is None or (child != door and child != door.dest):
if skipBk:
if not child.bigKey:
doors_to_open[child] = None
elif not child.bigKey or not key_layout.big_key_special or counter.big_key_opened:
doors_to_open[child] = None
if len(doors_to_open.keys()) > 0:
proposed_doors = {**proposed_doors, **doors_to_open}
bk_hint = counter.big_key_opened
for d in doors_to_open.keys():
bk_hint = bk_hint or d.bigKey
counter = find_counter(proposed_doors, bk_hint, key_layout)
changed = True
return counter
def open_some_counter(parent_counter, key_layout, ignored_doors):
changed = True
counter = parent_counter
proposed_doors = dict.fromkeys(parent_counter.open_doors.keys())
while changed:
changed = False
doors_to_open = {}
for child in counter.child_doors:
if child not in ignored_doors:
if not child.bigKey:
doors_to_open[child] = None
if len(doors_to_open.keys()) > 0:
proposed_doors = {**proposed_doors, **doors_to_open}
bk_hint = counter.big_key_opened
for d in doors_to_open.keys():
bk_hint = bk_hint or d.bigKey
counter = find_counter(proposed_doors, bk_hint, key_layout)
changed = True
return counter
def self_lock_possible(counter):
return len(counter.free_locations) <= 1 and len(counter.key_only_locations) == 0 and not counter.important_location
def available_chest_small_keys(key_counter, world, player):
if not world.keyshuffle[player] and not world.retro[player]:
cnt = 0
for loc in key_counter.free_locations:
if key_counter.big_key_opened or '- Big Chest' not in loc.name:
cnt += 1
return min(cnt, key_counter.max_chests)
else:
return key_counter.max_chests
def available_chest_small_keys_logic(key_counter, world, player, sm_restricted):
if not world.keyshuffle[player] and not world.retro[player]:
cnt = 0
for loc in key_counter.free_locations:
if loc not in sm_restricted and (key_counter.big_key_opened or '- Big Chest' not in loc.name):
cnt += 1
return min(cnt, key_counter.max_chests)
else:
return key_counter.max_chests
def big_key_drop_available(key_counter):
for loc in key_counter.other_locations:
if loc.forced_big_key():
return True
return False
def bk_restricted_rules(rule, door, odd_counter, empty_flag, key_counter, key_layout, world, player):
if key_counter.big_key_opened:
return
best_counter = find_best_counter(door, odd_counter, key_counter, key_layout, world, player, True, empty_flag)
bk_rule = create_rule(best_counter, key_counter, key_layout, world, player)
if bk_rule.small_key_num >= rule.small_key_num:
return
door_open = find_next_counter(door, best_counter, key_layout)
ignored_doors = dict_intersection(best_counter.child_doors, door_open.child_doors)
dest_ignored = []
for door in ignored_doors.keys():
if door.dest not in ignored_doors:
dest_ignored.append(door.dest)
ignored_doors = {**ignored_doors, **dict.fromkeys(dest_ignored)}
post_counter = open_some_counter(door_open, key_layout, ignored_doors.keys())
unique_loc = dict_difference(post_counter.free_locations, best_counter.free_locations)
# todo: figure out the intention behind this change - better way to detect the big key is blocking needed key onlys?
if len(unique_loc) > 0: # and bk_rule.is_valid
rule.alternate_small_key = bk_rule.small_key_num
rule.alternate_big_key_loc.update(unique_loc)
# elif not bk_rule.is_valid:
# key_layout.key_logic.bk_restricted.update(unique_loc)
def find_worst_counter_wo_bk(small_key_num, accessible_set, door, odd_ctr, key_counter, key_layout):
if key_counter.big_key_opened:
return None, None, None
worst_counter = find_worst_counter(door, odd_ctr, key_counter, key_layout, True)
bk_rule_num = worst_counter.used_keys + 1
bk_access_set = set()
bk_access_set.update(worst_counter.free_locations)
bk_access_set.update(worst_counter.key_only_locations)
if bk_rule_num == small_key_num and len(bk_access_set ^ accessible_set) == 0:
return None, None, None
door_open = find_next_counter(door, worst_counter, key_layout)
ignored_doors = dict_intersection(worst_counter.child_doors, door_open.child_doors)
dest_ignored = []
for door in ignored_doors.keys():
if door.dest not in ignored_doors:
dest_ignored.append(door.dest)
ignored_doors = {**ignored_doors, **dict.fromkeys(dest_ignored)}
post_counter = open_some_counter(door_open, key_layout, ignored_doors.keys())
return worst_counter, post_counter, bk_rule_num
def open_a_door(door, child_state, flat_proposal):
if door.bigKey or door.name in special_big_key_doors:
child_state.big_key_opened = True
child_state.avail_doors.extend(child_state.big_doors)
child_state.opened_doors.extend(set([d.door for d in child_state.big_doors]))
child_state.big_doors.clear()
else:
child_state.opened_doors.append(door)
doors_to_open = [x for x in child_state.small_doors if x.door == door]
child_state.small_doors[:] = [x for x in child_state.small_doors if x.door != door]
child_state.avail_doors.extend(doors_to_open)
dest_door = door.dest
if dest_door in flat_proposal and door.type != DoorType.SpiralStairs:
child_state.opened_doors.append(dest_door)
if child_state.in_door_list_ic(dest_door, child_state.small_doors):
now_available = [x for x in child_state.small_doors if x.door == dest_door]
child_state.small_doors[:] = [x for x in child_state.small_doors if x.door != dest_door]
child_state.avail_doors.extend(now_available)
# allows dest doors
def unique_doors(doors):
unique_d_set = []
for d in doors:
if d.door not in unique_d_set:
unique_d_set.append(d.door)
return unique_d_set
# does not allow dest doors
def count_unique_sm_doors(doors):
unique_d_set = set()
for d in doors:
if d not in unique_d_set and (d.dest not in unique_d_set or d.type == DoorType.SpiralStairs) and not d.bigKey:
unique_d_set.add(d)
return len(unique_d_set)
def big_key_progress(key_counter):
return not only_sm_doors(key_counter) or exist_big_chest(key_counter)
def only_sm_doors(key_counter):
for door in key_counter.child_doors:
if door.bigKey:
return False
return True
# doesn't count dest doors
def count_unique_small_doors(key_counter, proposal):
cnt = 0
counted = set()
for door in key_counter.child_doors:
if door in proposal and door not in counted:
cnt += 1
counted.add(door)
if door.type != DoorType.SpiralStairs:
counted.add(door.dest)
return cnt
def exist_relevant_big_doors(key_counter, key_layout):
bk_counter = find_counter(key_counter.open_doors, True, key_layout, False)
if bk_counter is not None:
diff = dict_difference(bk_counter.free_locations, key_counter.free_locations)
if len(diff) > 0:
return True
diff = dict_difference(bk_counter.key_only_locations, key_counter.key_only_locations)
if len(diff) > 0:
return True
diff = dict_difference(bk_counter.child_doors, key_counter.child_doors)
if len(diff) > 0:
return True
return False
def exist_big_chest(key_counter):
for loc in key_counter.free_locations:
if '- Big Chest' in loc.name:
return True
return False
def count_locations_big_optional(locations, bk=False):
cnt = 0
for loc in locations:
if bk or '- Big Chest' not in loc.name:
cnt += 1
return cnt
def filter_big_chest(locations):
return [x for x in locations if '- Big Chest' not in x.name]
def count_locations_exclude_logic(locations, key_logic):
cnt = 0
for loc in locations:
if loc not in key_logic.bk_restricted and not loc.forced_item and not prize_or_event(loc):
cnt += 1
return cnt
def prize_or_event(loc):
return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']
def count_free_locations(state):
cnt = 0
for loc in state.found_locations:
if not prize_or_event(loc) and not loc.forced_item:
cnt += 1
return cnt
def count_locations_exclude_big_chest(state):
cnt = 0
for loc in state.found_locations:
if '- Big Chest' not in loc.name and not loc.forced_item and not prize_or_event(loc):
cnt += 1
return cnt
def count_small_key_only_locations(state):
cnt = 0
for loc in state.found_locations:
if loc.forced_item and loc.item.smallkey:
cnt += 1
return cnt
def big_chest_in_locations(locations):
return len(find_big_chest_locations(locations)) > 0
def find_big_chest_locations(locations):
ret = []
for loc in locations:
if 'Big Chest' in loc.name:
ret.append(loc)
return ret
def find_big_key_locked_locations(locations):
ret = []
for loc in locations:
if loc.name in ["Thieves' Town - Blind's Cell", "Hyrule Castle - Zelda's Chest"]:
ret.append(loc)
return ret
def expand_key_state(state, flat_proposal, world, player):
while len(state.avail_doors) > 0:
exp_door = state.next_avail_door()
door = exp_door.door
connect_region = world.get_entrance(door.name, player).connected_region
if state.validate(door, connect_region, world, player):
state.visit_region(connect_region, key_checks=True)
state.add_all_doors_check_keys(connect_region, flat_proposal, world, player)
def flatten_pair_list(paired_list):
flat_list = []
for d in paired_list:
if type(d) is tuple:
flat_list.append(d[0])
flat_list.append(d[1])
else:
flat_list.append(d)
return flat_list
def check_rules(original_counter, key_layout, world, player):
all_key_only = set()
key_only_map = {}
queue = deque([(None, original_counter, original_counter.key_only_locations)])
completed = set()
completed.add(cid(original_counter, key_layout))
while len(queue) > 0:
queue = deque(sorted(queue, key=queue_sorter_2))
access_door, counter, key_only_loc = queue.popleft()
for loc in key_only_loc:
if loc not in all_key_only:
all_key_only.add(loc)
access_rules = []
key_only_map[loc] = access_rules
else:
access_rules = key_only_map[loc]
if access_door is None or access_door.name not in key_layout.key_logic.door_rules.keys():
if access_door is None or not access_door.bigKey:
access_rules.append(DoorRules(0, True))
else:
rule = key_layout.key_logic.door_rules[access_door.name]
if rule not in access_rules:
access_rules.append(rule)
for child in counter.child_doors.keys():
if not child.bigKey or not key_layout.big_key_special or counter.big_key_opened:
next_counter = find_next_counter(child, counter, key_layout)
c_id = cid(next_counter, key_layout)
if c_id not in completed:
completed.add(c_id)
new_key_only = dict_difference(next_counter.key_only_locations, counter.key_only_locations)
queue.append((child, next_counter, new_key_only))
min_rule_bk = defaultdict(list)
min_rule_non_bk = defaultdict(list)
check_non_bk = False
for loc, rule_list in key_only_map.items():
m_bk = None
m_nbk = None
for rule in rule_list:
if m_bk is None or rule.small_key_num <= m_bk:
min_rule_bk[loc].append(rule)
m_bk = rule.small_key_num
if rule.alternate_small_key is None:
ask = rule.small_key_num
else:
check_non_bk = True
ask = rule.alternate_small_key
if m_nbk is None or ask <= m_nbk:
min_rule_non_bk[loc].append(rule)
m_nbk = rule.alternate_small_key
adjust_key_location_mins(key_layout, min_rule_bk, lambda r: r.small_key_num, lambda r, v: setattr(r, 'small_key_num', v))
if check_non_bk:
adjust_key_location_mins(key_layout, min_rule_non_bk, lambda r: r.small_key_num if r.alternate_small_key is None else r.alternate_small_key,
lambda r, v: r if r.alternate_small_key is None else setattr(r, 'alternate_small_key', v))
check_rules_deep(original_counter, key_layout, world, player)
def adjust_key_location_mins(key_layout, min_rules, getter, setter):
collected_keys = key_layout.max_chests
collected_locs = set()
changed = True
while changed:
changed = False
for_removal = []
for loc, rules in min_rules.items():
if loc in collected_locs:
for_removal.append(loc)
for rule in rules:
if getter(rule) <= collected_keys and loc not in collected_locs:
changed = True
collected_keys += 1
collected_locs.add(loc)
for_removal.append(loc)
for loc in for_removal:
del min_rules[loc]
if len(min_rules) > 0:
for loc, rules in min_rules.items():
for rule in rules:
setter(rule, collected_keys)
def check_rules_deep(original_counter, key_layout, world, player):
key_logic = key_layout.key_logic
big_locations = {x for x in key_layout.all_chest_locations if x not in key_logic.bk_restricted}
queue = deque([original_counter])
completed = set()
completed.add(cid(original_counter, key_layout))
last_counter = None
bail = 0
while len(queue) > 0:
counter = queue.popleft()
if counter == last_counter:
bail += 1
if bail > 10:
raise Exception('Key logic issue, during deep rule check: %s' % key_layout.sector.name)
else:
bail = 0
last_counter = counter
chest_keys = available_chest_small_keys_logic(counter, world, player, key_logic.sm_restricted)
bk_drop = big_key_drop_available(counter)
big_avail = counter.big_key_opened or bk_drop
big_maybe_not_found = not counter.big_key_opened and not bk_drop # better named as big_missing?
if not key_layout.big_key_special and not big_avail:
if world.bigkeyshuffle[player]:
big_avail = True
else:
for location in counter.free_locations:
if location not in key_logic.bk_restricted:
big_avail = True
break
outstanding_big_locs = {x for x in big_locations if x not in counter.free_locations}
if big_maybe_not_found:
if len(outstanding_big_locs) == 0 and not key_layout.big_key_special:
big_maybe_not_found = False
big_uses_chest = big_avail and not key_layout.big_key_special
collected_alt = len(counter.key_only_locations) + chest_keys
if big_uses_chest and chest_keys == count_locations_big_optional(counter.free_locations, counter.big_key_opened):
chest_keys -= 1
collected = len(counter.key_only_locations) + chest_keys
can_progress = len(counter.child_doors) == 0
smalls_opened, big_opened = False, False
small_rules = []
for door in counter.child_doors.keys():
can_open = False
if door.bigKey and big_avail:
can_open = True
elif door.name in key_logic.door_rules.keys():
rule = key_logic.door_rules[door.name]
small_rules.append(rule)
if rule_satisfied(rule, collected, collected_alt, outstanding_big_locs, chest_keys, key_layout):
can_open = True
smalls_opened = True
elif not door.bigKey:
can_open = True
if can_open:
can_progress = smalls_opened or not big_maybe_not_found
next_counter = find_next_counter(door, counter, key_layout)
c_id = cid(next_counter, key_layout)
if c_id not in completed:
completed.add(c_id)
queue.append(next_counter)
if not can_progress:
if len(small_rules) > 0: # zero could be indicative of a problem, but also, the big key is now required
reduce_rules(small_rules, collected, collected_alt)
queue.append(counter) # run it through again
else:
raise Exception('Possible problem with generation or bk rules')
def rule_satisfied(rule, collected, collected_alt, outstanding_big_locs, chest_keys, key_layout):
if collected >= rule.small_key_num:
return True
if rule.allow_small and collected >= rule.small_key_num-1 and chest_keys < key_layout.max_chests:
return True
rule_diff = outstanding_big_locs.difference(rule.alternate_big_key_loc)
if rule.alternate_small_key is not None and len(rule_diff) == 0 and collected >= rule.alternate_small_key:
return True
if collected_alt > collected:
if collected_alt >= rule.small_key_num:
return True
if rule.allow_small and collected_alt >= rule.small_key_num-1 and chest_keys+1 < key_layout.max_chests:
return True
if rule.alternate_small_key is not None and len(rule_diff) == 0 and collected_alt >= rule.alternate_small_key:
return True
return False
def reduce_rules(small_rules, collected, collected_alt):
smallest_rules = []
min_num = None
for rule in small_rules:
if min_num is None or rule.small_key_num <= min_num:
if min_num is not None and rule.small_key_num < min_num:
min_num = rule.small_key_num
smallest_rules.clear()
elif min_num is None:
min_num = rule.small_key_num
smallest_rules.append(rule)
for rule in smallest_rules:
if rule.allow_small: # we are already reducing it
rule.allow_small = False
if min_num > collected_alt > collected:
rule.small_key_num = collected_alt
else:
rule.small_key_num = collected
def set_paired_rules(key_logic, world, player):
for d_name, rule in key_logic.door_rules.items():
door = world.get_door(d_name, player)
if door.dest.name in key_logic.door_rules.keys():
rule.opposite = key_logic.door_rules[door.dest.name]
def check_bk_special(regions, world, player):
for r_name in regions:
region = world.get_region(r_name, player)
for loc in region.locations:
if loc.forced_big_key():
return True
return False
# Soft lock stuff
def validate_key_layout(key_layout, world, player):
# retro is all good - except for hyrule castle in standard mode
if (world.retro[player] and (world.mode[player] != 'standard' or key_layout.sector.name != 'Hyrule Castle')) or world.logic[player] == 'nologic':
return True
flat_proposal = key_layout.flat_prop
state = ExplorationState(dungeon=key_layout.sector.name)
state.key_locations = key_layout.max_chests
state.big_key_special = check_bk_special(key_layout.sector.regions, world, player)
for region in key_layout.start_regions:
state.visit_region(region, key_checks=True)
state.add_all_doors_check_keys(region, flat_proposal, world, player)
return validate_key_layout_sub_loop(key_layout, state, {}, flat_proposal, None, 0, world, player)
def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposal, prev_state, prev_avail, world, player):
expand_key_state(state, flat_proposal, world, player)
smalls_avail = len(state.small_doors) > 0 # de-dup crystal repeats
num_bigs = 1 if len(state.big_doors) > 0 else 0 # all or nothing
if not smalls_avail and num_bigs == 0:
return True # I think that's the end
# todo: fix state to separate out these types
ttl_locations = count_free_locations(state) if state.big_key_opened else count_locations_exclude_big_chest(state)
ttl_small_key_only = count_small_key_only_locations(state)
available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player)
available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player)
if invalid_self_locking_key(key_layout, state, prev_state, prev_avail, world, player):
return False
# todo: allow more key shuffles - refine placement rules
# if (not smalls_avail or available_small_locations == 0) and (state.big_key_opened or num_bigs == 0 or available_big_locations == 0):
found_forced_bk = state.found_forced_bk()
smalls_done = not smalls_avail or not enough_small_locations(state, available_small_locations)
bk_done = state.big_key_opened or num_bigs == 0 or (available_big_locations == 0 and not found_forced_bk)
if smalls_done and bk_done:
return False
else:
if smalls_avail and available_small_locations > 0:
for exp_door in state.small_doors:
state_copy = state.copy()
open_a_door(exp_door.door, state_copy, flat_proposal)
state_copy.used_smalls += 1
if state_copy.used_smalls > ttl_small_key_only:
state_copy.used_locations += 1
code = state_id(state_copy, flat_proposal)
if code not in checked_states.keys():
valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal,
state, available_small_locations, world, player)
checked_states[code] = valid
else:
valid = checked_states[code]
if not valid:
return False
if not state.big_key_opened and (available_big_locations >= num_bigs > 0 or (found_forced_bk and num_bigs > 0)):
state_copy = state.copy()
open_a_door(state.big_doors[0].door, state_copy, flat_proposal)
if not found_forced_bk:
state_copy.used_locations += 1
code = state_id(state_copy, flat_proposal)
if code not in checked_states.keys():
valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal,
state, available_small_locations, world, player)
checked_states[code] = valid
else:
valid = checked_states[code]
if not valid:
return False
return True
def invalid_self_locking_key(key_layout, state, prev_state, prev_avail, world, player):
if prev_state is None or state.used_smalls == prev_state.used_smalls:
return False
new_bk_doors = set(state.big_doors).difference(set(prev_state.big_doors))
state_copy = state.copy()
while len(new_bk_doors) > 0:
for door in new_bk_doors:
open_a_door(door.door, state_copy, key_layout.flat_prop)
new_bk_doors = set(state_copy.big_doors).difference(set(prev_state.big_doors))
expand_key_state(state_copy, key_layout.flat_prop, world, player)
new_locations = set(state_copy.found_locations).difference(set(prev_state.found_locations))
important_found = False
for loc in new_locations:
important_found |= important_location(loc, world, player)
if not important_found:
return False
new_small_doors = set(state.small_doors).difference(set(prev_state.small_doors))
if len(new_small_doors) > 0:
return False
return prev_avail - 1 == 0
def enough_small_locations(state, avail_small_loc):
unique_d_set = set()
for exp_door in state.small_doors:
door = exp_door.door
if door not in unique_d_set and door.dest not in unique_d_set:
unique_d_set.add(door)
return avail_small_loc >= len(unique_d_set)
def cnt_avail_small_locations(free_locations, key_only, state, world, player):
if not world.keyshuffle[player] and not world.retro[player]:
bk_adj = 1 if state.big_key_opened and not state.big_key_special else 0
avail_chest_keys = min(free_locations - bk_adj, state.key_locations - key_only)
return max(0, avail_chest_keys + key_only - state.used_smalls)
return state.key_locations - state.used_smalls
def cnt_avail_big_locations(ttl_locations, state, world, player):
if not world.bigkeyshuffle[player]:
return max(0, ttl_locations - state.used_locations) if not state.big_key_special else 0
return 1 if not state.big_key_special else 0
def create_key_counters(key_layout, world, player):
key_counters = {}
flat_proposal = key_layout.flat_prop
state = ExplorationState(dungeon=key_layout.sector.name)
if world.doorShuffle[player] == 'vanilla':
state.key_locations = len(world.get_dungeon(key_layout.sector.name, player).small_keys)
else:
state.key_locations = world.dungeon_layouts[player][key_layout.sector.name].key_doors_num
state.big_key_special, special_region = False, None
for region in key_layout.sector.regions:
for location in region.locations:
if location.forced_big_key():
state.big_key_special = True
special_region = region
for region in key_layout.start_regions:
state.visit_region(region, key_checks=True)
state.add_all_doors_check_keys(region, flat_proposal, world, player)
expand_key_state(state, flat_proposal, world, player)
code = state_id(state, key_layout.flat_prop)
key_counters[code] = create_key_counter(state, key_layout, world, player)
queue = deque([(key_counters[code], state)])
while len(queue) > 0:
next_key_counter, parent_state = queue.popleft()
for door in next_key_counter.child_doors:
child_state = parent_state.copy()
if door.bigKey or door.name in special_big_key_doors:
key_layout.key_logic.bk_doors.add(door)
# open the door, if possible
if not door.bigKey or not child_state.big_key_special or child_state.visited_at_all(special_region):
open_a_door(door, child_state, flat_proposal)
expand_key_state(child_state, flat_proposal, world, player)
code = state_id(child_state, key_layout.flat_prop)
if code not in key_counters.keys():
child_kr = create_key_counter(child_state, key_layout, world, player)
key_counters[code] = child_kr
queue.append((child_kr, child_state))
return key_counters
def create_key_counter(state, key_layout, world, player):
key_counter = KeyCounter(key_layout.max_chests)
key_counter.child_doors.update(dict.fromkeys(unique_doors(state.small_doors+state.big_doors)))
for loc in state.found_locations:
if important_location(loc, world, player):
key_counter.important_location = True
key_counter.other_locations[loc] = None
elif loc.forced_item and loc.item.name == key_layout.key_logic.small_key_name:
key_counter.key_only_locations[loc] = None
elif loc.forced_item and loc.item.name == key_layout.key_logic.bk_name:
key_counter.other_locations[loc] = None
elif loc.name not in dungeon_events:
key_counter.free_locations[loc] = None
else:
key_counter.other_locations[loc] = None
key_counter.open_doors.update(dict.fromkeys(state.opened_doors))
key_counter.used_keys = count_unique_sm_doors(state.opened_doors)
key_counter.big_key_opened = state.big_key_opened
return key_counter
imp_locations = None
def imp_locations_factory(world, player):
global imp_locations
if imp_locations:
return imp_locations
imp_locations = ['Agahnim 1', 'Agahnim 2', 'Attic Cracked Floor', 'Suspicious Maiden']
if world.mode[player] == 'standard':
imp_locations.append('Zelda Pickup')
imp_locations.append('Zelda Dropoff')
return imp_locations
def important_location(loc, world, player):
return '- Prize' in loc.name or loc.name in imp_locations_factory(world, player) or (loc.forced_big_key())
def create_odd_key_counter(door, parent_counter, key_layout, world, player):
odd_counter = KeyCounter(key_layout.max_chests)
next_counter = find_next_counter(door, parent_counter, key_layout)
odd_counter.free_locations = dict_difference(next_counter.free_locations, parent_counter.free_locations)
odd_counter.key_only_locations = dict_difference(next_counter.key_only_locations, parent_counter.key_only_locations)
odd_counter.child_doors = dict_difference(next_counter.child_doors, parent_counter.child_doors)
odd_counter.other_locations = dict_difference(next_counter.other_locations, parent_counter.other_locations)
for loc in odd_counter.other_locations:
if important_location(loc, world, player):
odd_counter.important_location = True
return odd_counter
def dict_difference(dict_a, dict_b):
return dict.fromkeys([x for x in dict_a.keys() if x not in dict_b.keys()])
def dict_intersection(dict_a, dict_b):
return dict.fromkeys([x for x in dict_a.keys() if x in dict_b.keys()])
def state_id(state, flat_proposal):
s_id = '1' if state.big_key_opened else '0'
for d in flat_proposal:
s_id += '1' if d in state.opened_doors else '0'
return s_id
def find_counter(opened_doors, bk_hint, key_layout, raise_on_error=True):
counter = find_counter_hint(opened_doors, bk_hint, key_layout)
if counter is not None:
return counter
more_doors = []
for door in opened_doors.keys():
more_doors.append(door)
if door.dest not in opened_doors.keys():
more_doors.append(door.dest)
if len(more_doors) > len(opened_doors.keys()):
counter = find_counter_hint(dict.fromkeys(more_doors), bk_hint, key_layout)
if counter is not None:
return counter
if raise_on_error:
raise Exception('Unable to find door permutation. Init CID: %s' % counter_id(opened_doors, bk_hint, key_layout.flat_prop))
return None
def find_counter_hint(opened_doors, bk_hint, key_layout):
cid = counter_id(opened_doors, bk_hint, key_layout.flat_prop)
if cid in key_layout.key_counters.keys():
return key_layout.key_counters[cid]
if not bk_hint:
cid = counter_id(opened_doors, True, key_layout.flat_prop)
if cid in key_layout.key_counters.keys():
return key_layout.key_counters[cid]
return None
def find_max_counter(key_layout):
max_counter = find_counter_hint(dict.fromkeys(key_layout.flat_prop), False, key_layout)
if max_counter is None:
raise Exception("Max Counter is none - something is amiss")
if len(max_counter.child_doors) > 0:
max_counter = find_counter_hint(dict.fromkeys(key_layout.flat_prop), True, key_layout)
return max_counter
def counter_id(opened_doors, bk_unlocked, flat_proposal):
s_id = '1' if bk_unlocked else '0'
for d in flat_proposal:
s_id += '1' if d in opened_doors.keys() else '0'
return s_id
def cid(counter, key_layout):
return counter_id(counter.open_doors, counter.big_key_opened, key_layout.flat_prop)
# class SoftLockException(Exception):
# pass
# vanilla validation code
def validate_vanilla_key_logic(world, player):
validators = {
'Hyrule Castle': val_hyrule,
'Eastern Palace': val_eastern,
'Desert Palace': val_desert,
'Tower of Hera': val_hera,
'Agahnims Tower': val_tower,
'Palace of Darkness': val_pod,
'Swamp Palace': val_swamp,
'Skull Woods': val_skull,
'Thieves Town': val_thieves,
'Ice Palace': val_ice,
'Misery Mire': val_mire,
'Turtle Rock': val_turtle,
'Ganons Tower': val_ganons
}
key_logic_dict = world.key_logic[player]
for key, key_logic in key_logic_dict.items():
validators[key](key_logic, world, player)
def val_hyrule(key_logic, world, player):
if world.mode[player] == 'standard':
val_rule(key_logic.door_rules['Hyrule Dungeon Map Room Key Door S'], 1)
val_rule(key_logic.door_rules['Hyrule Dungeon Armory Interior Key Door N'], 2)
val_rule(key_logic.door_rules['Sewers Dark Cross Key Door N'], 3)
val_rule(key_logic.door_rules['Sewers Key Rat Key Door N'], 4)
else:
val_rule(key_logic.door_rules['Sewers Secret Room Key Door S'], 2)
val_rule(key_logic.door_rules['Sewers Dark Cross Key Door N'], 2)
val_rule(key_logic.door_rules['Hyrule Dungeon Map Room Key Door S'], 2)
val_rule(key_logic.door_rules['Hyrule Dungeon Armory Interior Key Door N'], 4)
def val_eastern(key_logic, world, player):
val_rule(key_logic.door_rules['Eastern Dark Square Key Door WN'], 2, True, 'Eastern Palace - Big Key Chest', 1, {'Eastern Palace - Big Key Chest'})
val_rule(key_logic.door_rules['Eastern Darkness Up Stairs'], 2)
assert world.get_location('Eastern Palace - Big Chest', player) in key_logic.bk_restricted
assert world.get_location('Eastern Palace - Boss', player) in key_logic.bk_restricted
assert len(key_logic.bk_restricted) == 2
def val_desert(key_logic, world, player):
val_rule(key_logic.door_rules['Desert East Wing Key Door EN'], 4)
val_rule(key_logic.door_rules['Desert Tiles 1 Up Stairs'], 2)
val_rule(key_logic.door_rules['Desert Beamos Hall NE'], 3)
val_rule(key_logic.door_rules['Desert Tiles 2 NE'], 4)
assert world.get_location('Desert Palace - Big Chest', player) in key_logic.bk_restricted
assert world.get_location('Desert Palace - Boss', player) in key_logic.bk_restricted
assert len(key_logic.bk_restricted) == 2
def val_hera(key_logic, world, player):
val_rule(key_logic.door_rules['Hera Lobby Key Stairs'], 1, True, 'Tower of Hera - Big Key Chest')
assert world.get_location('Tower of Hera - Big Chest', player) in key_logic.bk_restricted
assert world.get_location('Tower of Hera - Compass Chest', player) in key_logic.bk_restricted
assert world.get_location('Tower of Hera - Boss', player) in key_logic.bk_restricted
assert len(key_logic.bk_restricted) == 3
def val_tower(key_logic, world, player):
val_rule(key_logic.door_rules['Tower Room 03 Up Stairs'], 1)
val_rule(key_logic.door_rules['Tower Dark Maze ES'], 2)
val_rule(key_logic.door_rules['Tower Dark Archers Up Stairs'], 3)
val_rule(key_logic.door_rules['Tower Circle of Pots ES'], 4)
def val_pod(key_logic, world, player):
val_rule(key_logic.door_rules['PoD Arena Main NW'], 4)
val_rule(key_logic.door_rules['PoD Basement Ledge Up Stairs'], 6, True, 'Palace of Darkness - Big Key Chest')
val_rule(key_logic.door_rules['PoD Compass Room SE'], 6, True, 'Palace of Darkness - Harmless Hellway')
val_rule(key_logic.door_rules['PoD Falling Bridge WN'], 6)
val_rule(key_logic.door_rules['PoD Dark Pegs WN'], 6)
assert world.get_location('Palace of Darkness - Big Chest', player) in key_logic.bk_restricted
assert world.get_location('Palace of Darkness - Boss', player) in key_logic.bk_restricted
assert len(key_logic.bk_restricted) == 2
def val_swamp(key_logic, world, player):
val_rule(key_logic.door_rules['Swamp Entrance Down Stairs'], 1)
val_rule(key_logic.door_rules['Swamp Pot Row WS'], 2)
val_rule(key_logic.door_rules['Swamp Trench 1 Key Ledge NW'], 3)
val_rule(key_logic.door_rules['Swamp Hub North Ledge N'], 5)
val_rule(key_logic.door_rules['Swamp Hub WN'], 6)
val_rule(key_logic.door_rules['Swamp Waterway NW'], 6)
assert world.get_location('Swamp Palace - Entrance', player) in key_logic.bk_restricted
assert len(key_logic.bk_restricted) == 1
def val_skull(key_logic, world, player):
val_rule(key_logic.door_rules['Skull 3 Lobby NW'], 4)
val_rule(key_logic.door_rules['Skull Spike Corner ES'], 5)
def val_thieves(key_logic, world, player):
val_rule(key_logic.door_rules['Thieves Hallway WS'], 1)
val_rule(key_logic.door_rules['Thieves Spike Switch Up Stairs'], 3)
val_rule(key_logic.door_rules['Thieves Conveyor Bridge WS'], 3, True, 'Thieves\' Town - Big Chest')
assert world.get_location('Thieves\' Town - Attic', player) in key_logic.bk_restricted
assert world.get_location('Thieves\' Town - Boss', player) in key_logic.bk_restricted
assert world.get_location('Thieves\' Town - Blind\'s Cell', player) in key_logic.bk_restricted
assert world.get_location('Thieves\' Town - Big Chest', player) in key_logic.bk_restricted
assert len(key_logic.bk_restricted) == 4
def val_ice(key_logic, world, player):
val_rule(key_logic.door_rules['Ice Jelly Key Down Stairs'], 1)
val_rule(key_logic.door_rules['Ice Conveyor SW'], 2)
val_rule(key_logic.door_rules['Ice Backwards Room Down Stairs'], 5)
assert world.get_location('Ice Palace - Boss', player) in key_logic.bk_restricted
assert world.get_location('Ice Palace - Big Chest', player) in key_logic.bk_restricted
assert len(key_logic.bk_restricted) == 2
def val_mire(key_logic, world, player):
mire_west_wing = {'Misery Mire - Big Key Chest', 'Misery Mire - Compass Chest'}
val_rule(key_logic.door_rules['Mire Spikes NW'], 3) # todo: is sometimes 3 or 5? best_counter order matters
# val_rule(key_logic.door_rules['Mire Spike Barrier NE'], 4) # kind of a waste mostly
val_rule(key_logic.door_rules['Mire Hub WS'], 5, False, None, 3, mire_west_wing)
val_rule(key_logic.door_rules['Mire Conveyor Crystal WS'], 6, False, None, 4, mire_west_wing)
assert world.get_location('Misery Mire - Boss', player) in key_logic.bk_restricted
assert world.get_location('Misery Mire - Big Chest', player) in key_logic.bk_restricted
assert len(key_logic.bk_restricted) == 2
def val_turtle(key_logic, world, player):
# todo: check vanilla key logic when TR back doors are accessible
if world.shuffle[player] == 'vanilla' and world.mode[player] != 'inverted':
val_rule(key_logic.door_rules['TR Hub NW'], 1)
val_rule(key_logic.door_rules['TR Pokey 1 NW'], 2)
val_rule(key_logic.door_rules['TR Chain Chomps Down Stairs'], 3)
val_rule(key_logic.door_rules['TR Pokey 2 ES'], 6, True, 'Turtle Rock - Big Key Chest', 4, {'Turtle Rock - Big Key Chest'})
val_rule(key_logic.door_rules['TR Crystaroller Down Stairs'], 5)
val_rule(key_logic.door_rules['TR Dash Bridge WS'], 6)
assert world.get_location('Turtle Rock - Eye Bridge - Bottom Right', player) in key_logic.bk_restricted
assert world.get_location('Turtle Rock - Eye Bridge - Top Left', player) in key_logic.bk_restricted
assert world.get_location('Turtle Rock - Eye Bridge - Top Right', player) in key_logic.bk_restricted
assert world.get_location('Turtle Rock - Eye Bridge - Bottom Left', player) in key_logic.bk_restricted
assert world.get_location('Turtle Rock - Boss', player) in key_logic.bk_restricted
assert world.get_location('Turtle Rock - Crystaroller Room', player) in key_logic.bk_restricted
assert world.get_location('Turtle Rock - Big Chest', player) in key_logic.bk_restricted
assert len(key_logic.bk_restricted) == 7
def val_ganons(key_logic, world, player):
rando_room = {'Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right'}
compass_room = {'Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right'}
gt_middle = {'Ganons Tower - Big Key Room - Left', 'Ganons Tower - Big Key Chest', 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest'}
val_rule(key_logic.door_rules['GT Double Switch EN'], 6, False, None, 4, rando_room.union({'Ganons Tower - Firesnake Room'}))
val_rule(key_logic.door_rules['GT Hookshot ES'], 7, False, 'Ganons Tower - Map Chest', 5, {'Ganons Tower - Map Chest'})
val_rule(key_logic.door_rules['GT Tile Room EN'], 6, False, None, 5, compass_room)
val_rule(key_logic.door_rules['GT Firesnake Room SW'], 7, False, None, 5, rando_room)
val_rule(key_logic.door_rules['GT Conveyor Star Pits EN'], 6, False, None, 5, gt_middle) # should be 7?
val_rule(key_logic.door_rules['GT Mini Helmasaur Room WN'], 6) # not sure about this 6...
val_rule(key_logic.door_rules['GT Crystal Circles SW'], 8)
assert world.get_location('Ganons Tower - Mini Helmasaur Room - Left', player) in key_logic.bk_restricted
assert world.get_location('Ganons Tower - Mini Helmasaur Room - Right', player) in key_logic.bk_restricted
assert world.get_location('Ganons Tower - Big Chest', player) in key_logic.bk_restricted
assert world.get_location('Ganons Tower - Pre-Moldorm Chest', player) in key_logic.bk_restricted
assert world.get_location('Ganons Tower - Validation Chest', player) in key_logic.bk_restricted
assert len(key_logic.bk_restricted) == 5
def val_rule(rule, skn, allow=False, loc=None, askn=None, setCheck=None):
if setCheck is None:
setCheck = set()
assert rule.small_key_num == skn
assert rule.allow_small == allow
assert rule.small_location == loc or rule.small_location.name == loc
assert rule.alternate_small_key == askn
assert len(setCheck) == len(rule.alternate_big_key_loc)
for loc in rule.alternate_big_key_loc:
assert loc.name in setCheck
# Soft lock stuff
def validate_key_placement(key_layout, world, player):
if world.retro[player] or world.accessibility[player] == 'none':
return True # Can't keylock in retro. Expected if beatable only.
max_counter = find_max_counter(key_layout)
keys_outside = 0
big_key_outside = False
smallkey_name = dungeon_keys[key_layout.sector.name]
bigkey_name = dungeon_bigs[key_layout.sector.name]
if world.keyshuffle[player]:
keys_outside = key_layout.max_chests - sum(1 for i in max_counter.free_locations if i.item is not None and i.item.name == smallkey_name and i.item.player == player)
if world.bigkeyshuffle[player]:
max_counter = find_max_counter(key_layout)
big_key_outside = bigkey_name not in (l.item.name for l in max_counter.free_locations if l.item)
for code, counter in key_layout.key_counters.items():
if len(counter.child_doors) == 0:
continue
if key_layout.big_key_special:
big_found = any(i.forced_item is not None and i.item.bigkey for i in counter.other_locations) or big_key_outside
else:
big_found = any(i.item is not None and i.item.name == bigkey_name for i in counter.free_locations if "- Big Chest" not in i.name) or big_key_outside
if counter.big_key_opened and not big_found:
continue # Can't get to this state
found_locations = set(i for i in counter.free_locations if big_found or "- Big Chest" not in i.name)
found_keys = sum(1 for i in found_locations if i.item is not None and i.item.name == smallkey_name and i.item.player == player) + \
len(counter.key_only_locations) + keys_outside
can_progress = (not counter.big_key_opened and big_found and any(d.bigKey for d in counter.child_doors)) or \
found_keys > counter.used_keys and any(not d.bigKey for d in counter.child_doors)
if not can_progress:
missing_locations = set(max_counter.free_locations.keys()).difference(found_locations)
missing_items = [l for l in missing_locations if l.item is None or (l.item.name != smallkey_name and l.item.name != bigkey_name) or "- Boss" in l.name]
# missing_key_only = set(max_counter.key_only_locations.keys()).difference(counter.key_only_locations.keys()) # do freestanding keys matter for locations?
if len(missing_items) > 0: # world.accessibility[player]=='locations' and (len(missing_locations)>0 or len(missing_key_only) > 0):
logging.getLogger('').error("Keylock - can't open locations: ")
logging.getLogger('').error("code: " + code)
for i in missing_locations:
logging.getLogger('').error(i)
return False
return True