Cross Dungeon initial work
This commit is contained in:
@@ -6,37 +6,6 @@ from Dungeons import dungeon_keys, dungeon_bigs
|
||||
from DungeonGenerator import ExplorationState
|
||||
|
||||
|
||||
class KeySphere(object):
|
||||
|
||||
def __init__(self):
|
||||
self.access_door = None
|
||||
self.free_locations = {}
|
||||
self.prize_region = False
|
||||
self.key_only_locations = {}
|
||||
self.child_doors = {}
|
||||
self.bk_locked = False
|
||||
self.parent_sphere = None
|
||||
self.other_locations = {}
|
||||
|
||||
def __eq__(self, other):
|
||||
if self.prize_region != other.prize_region:
|
||||
return False
|
||||
# already have merge function for this
|
||||
# if self.bk_locked != other.bk_locked:
|
||||
# return False
|
||||
if len(self.free_locations) != len(other.free_locations):
|
||||
return False
|
||||
if len(self.key_only_locations) != len(other.key_only_locations):
|
||||
return False
|
||||
if len(set(self.free_locations).symmetric_difference(set(other.free_locations))) > 0:
|
||||
return False
|
||||
if len(set(self.key_only_locations).symmetric_difference(set(other.key_only_locations))) > 0:
|
||||
return False
|
||||
if len(set(self.child_doors).symmetric_difference(set(other.child_doors))) > 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class KeyLayout(object):
|
||||
|
||||
def __init__(self, sector, starts, proposal):
|
||||
@@ -45,20 +14,21 @@ class KeyLayout(object):
|
||||
self.proposal = proposal
|
||||
self.key_logic = KeyLogic(sector.name)
|
||||
|
||||
self.key_spheres = None
|
||||
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
|
||||
|
||||
# bk special?
|
||||
# bk required? True if big chests or big doors exists
|
||||
|
||||
def reset(self, proposal):
|
||||
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)
|
||||
|
||||
|
||||
class KeyLogic(object):
|
||||
@@ -69,6 +39,8 @@ class KeyLogic(object):
|
||||
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 = {}
|
||||
|
||||
@@ -96,34 +68,7 @@ class KeyCounter(object):
|
||||
self.used_keys = 0
|
||||
self.big_key_opened = False
|
||||
self.important_location = False
|
||||
|
||||
def update(self, key_sphere):
|
||||
self.free_locations.update(key_sphere.free_locations)
|
||||
self.key_only_locations.update(key_sphere.key_only_locations)
|
||||
self.child_doors.update(dict.fromkeys([x for x in key_sphere.child_doors if x not in self.open_doors and x.dest not in self.open_doors]))
|
||||
self.important_location = self.important_location or key_sphere.prize_region or self.special_region(key_sphere)
|
||||
|
||||
@staticmethod
|
||||
def special_region(key_sphere):
|
||||
for other in key_sphere.other_locations:
|
||||
# todo: zelda's cell is special in standard, and probably crossed too
|
||||
if other.name in ['Attic Cracked Floor', 'Suspicious Maiden']:
|
||||
return True
|
||||
return False
|
||||
|
||||
def open_door(self, door, flat_proposal):
|
||||
if door in flat_proposal:
|
||||
self.used_keys += 1
|
||||
del self.child_doors[door]
|
||||
self.open_doors[door] = None
|
||||
if door.dest in flat_proposal:
|
||||
self.open_doors[door.dest] = None
|
||||
if door.dest in self.child_doors:
|
||||
del self.child_doors[door.dest]
|
||||
elif door.bigKey:
|
||||
self.big_key_opened = True
|
||||
del self.child_doors[door]
|
||||
self.open_doors[door] = None
|
||||
self.other_locations = {}
|
||||
|
||||
def used_smalls_loc(self, reserve=0):
|
||||
return max(self.used_keys + reserve - len(self.key_only_locations), 0)
|
||||
@@ -140,29 +85,35 @@ class KeyCounter(object):
|
||||
return ret
|
||||
|
||||
|
||||
def build_key_layout(sector, start_regions, proposal, world, player):
|
||||
key_layout = KeyLayout(sector, start_regions, proposal)
|
||||
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_chests = len(world.get_dungeon(key_layout.sector.name, player).small_keys)
|
||||
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 = 'Hyrule Dungeon Cellblock' in key_layout.sector.region_set()
|
||||
return key_layout
|
||||
|
||||
|
||||
def calc_max_chests(builder, key_layout, world, player):
|
||||
if world.doorShuffle != 'crossed':
|
||||
return len(world.get_dungeon(key_layout.sector.name, player).small_keys)
|
||||
return 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_layout.key_spheres = create_key_spheres(key_layout, world, player)
|
||||
key_logic = key_layout.key_logic
|
||||
|
||||
find_bk_locked_sections(key_layout, world)
|
||||
key_logic.bk_chests.update(find_big_chest_locations(key_layout.all_chest_locations))
|
||||
|
||||
init_bk = check_special_locations(key_layout.key_spheres['Origin'].free_locations.keys())
|
||||
key_counter = key_layout.key_counters[counter_id({}, init_bk, key_layout.flat_prop)]
|
||||
queue = collections.deque([(key_layout.key_spheres['Origin'], key_counter)])
|
||||
original_key_counter = find_counter({}, False, key_layout)
|
||||
queue = collections.deque([(None, original_key_counter)])
|
||||
doors_completed = set()
|
||||
|
||||
while len(queue) > 0:
|
||||
queue = collections.deque(sorted(queue, key=queue_sorter))
|
||||
key_sphere, key_counter = queue.popleft()
|
||||
parent_door, key_counter = queue.popleft()
|
||||
chest_keys = available_chest_small_keys(key_counter, world)
|
||||
raw_avail = chest_keys + len(key_counter.key_only_locations)
|
||||
available = raw_avail - key_counter.used_keys
|
||||
@@ -170,29 +121,28 @@ def analyze_dungeon(key_layout, world, player):
|
||||
if not key_counter.big_key_opened:
|
||||
if chest_keys == count_locations_big_optional(key_counter.free_locations) and available <= possible_smalls:
|
||||
key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations))
|
||||
if not key_sphere.bk_locked and big_chest_in_locations(key_counter.free_locations):
|
||||
if not key_counter.big_key_opened and big_chest_in_locations(key_counter.free_locations):
|
||||
key_logic.sm_restricted.update(find_big_chest_locations(key_counter.free_locations))
|
||||
# todo: detect forced subsequent keys - see keypuzzles
|
||||
# try to relax the rules here? - smallest requirement that doesn't force a softlock
|
||||
child_queue = collections.deque()
|
||||
for child in sorted(list(key_sphere.child_doors), key=lambda x: x.name):
|
||||
next_sphere = key_layout.key_spheres[child.name]
|
||||
# todo: empty_sphere are not always empty, Mire spike barrier is not empty if other doors open first
|
||||
if not empty_sphere(next_sphere) and child not in doors_completed:
|
||||
child_queue.append((child, next_sphere))
|
||||
for child in key_counter.child_doors.keys():
|
||||
if not child.bigKey or not key_layout.big_key_special or key_counter.big_key_opened:
|
||||
odd_counter = create_odd_key_counter(child, key_counter, key_layout, world)
|
||||
if not empty_counter(odd_counter) and child not in doors_completed:
|
||||
child_queue.append((child, odd_counter))
|
||||
while len(child_queue) > 0:
|
||||
child, next_sphere = child_queue.popleft()
|
||||
child, odd_counter = child_queue.popleft()
|
||||
if not child.bigKey:
|
||||
best_counter = find_best_counter(child, key_counter, key_layout, world, False)
|
||||
best_counter = find_best_counter(child, odd_counter, key_counter, key_layout, world, False)
|
||||
rule = create_rule(best_counter, key_counter, key_layout, world)
|
||||
check_for_self_lock_key(rule, next_sphere, key_layout, world)
|
||||
bk_restricted_rules(rule, next_sphere, key_counter, key_layout, world)
|
||||
check_for_self_lock_key(rule, child, best_counter, key_layout, world)
|
||||
bk_restricted_rules(rule, child, odd_counter, key_counter, key_layout, world)
|
||||
key_logic.door_rules[child.name] = rule
|
||||
doors_completed.add(next_sphere.access_door)
|
||||
next_counter = find_next_counter(child, key_counter, next_sphere, key_layout)
|
||||
queue.append((next_sphere, next_counter))
|
||||
check_rules(key_layout)
|
||||
return key_layout
|
||||
doors_completed.add(child)
|
||||
next_counter = find_next_counter(child, key_counter, key_layout)
|
||||
queue.append((child, next_counter))
|
||||
check_rules(original_key_counter, key_layout)
|
||||
|
||||
|
||||
def count_key_drops(sector):
|
||||
@@ -205,43 +155,42 @@ def count_key_drops(sector):
|
||||
|
||||
|
||||
def queue_sorter(queue_item):
|
||||
sphere, counter = queue_item
|
||||
if sphere.access_door is None:
|
||||
door, counter = queue_item
|
||||
if door is None:
|
||||
return 0
|
||||
return 1 if sphere.access_door.bigKey else 0
|
||||
return 1 if door.bigKey else 0
|
||||
|
||||
|
||||
def find_bk_locked_sections(key_layout, world):
|
||||
key_spheres = key_layout.key_spheres
|
||||
key_counters = key_layout.key_counters
|
||||
key_logic = key_layout.key_logic
|
||||
|
||||
bk_key_not_required = set()
|
||||
big_chest_allowed_big_key = world.accessibility != 'locations'
|
||||
for key in key_spheres.keys():
|
||||
sphere = key_spheres[key]
|
||||
key_layout.all_chest_locations.update(sphere.free_locations)
|
||||
if sphere.bk_locked and (sphere.prize_region or KeyCounter.special_region(sphere)):
|
||||
for counter in key_counters.values():
|
||||
key_layout.all_chest_locations.update(counter.free_locations)
|
||||
if counter.big_key_opened and counter.important_location:
|
||||
big_chest_allowed_big_key = False
|
||||
if not sphere.bk_locked:
|
||||
bk_key_not_required.update(sphere.free_locations)
|
||||
if not counter.big_key_opened:
|
||||
bk_key_not_required.update(counter.free_locations)
|
||||
key_logic.bk_restricted.update(dict.fromkeys(set(key_layout.all_chest_locations).difference(bk_key_not_required)))
|
||||
if not big_chest_allowed_big_key:
|
||||
key_logic.bk_restricted.update(find_big_chest_locations(key_layout.all_chest_locations))
|
||||
|
||||
|
||||
def empty_sphere(sphere):
|
||||
if len(sphere.key_only_locations) != 0 or len(sphere.free_locations) != 0 or len(sphere.child_doors) != 0:
|
||||
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 sphere.prize_region
|
||||
return not counter.important_location
|
||||
|
||||
|
||||
def relative_empty_sphere(sphere, key_counter):
|
||||
if len(set(sphere.key_only_locations).difference(key_counter.key_only_locations)) > 0:
|
||||
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(sphere.free_locations).difference(key_counter.free_locations)) > 0:
|
||||
if len(set(odd_counter.free_locations).difference(key_counter.free_locations)) > 0:
|
||||
return False
|
||||
new_child_door = False
|
||||
for child in sphere.child_doors:
|
||||
for child in odd_counter.child_doors:
|
||||
if unique_child_door(child, key_counter):
|
||||
new_child_door = True
|
||||
break
|
||||
@@ -260,23 +209,15 @@ def unique_child_door(child, key_counter):
|
||||
return True
|
||||
|
||||
|
||||
def increment_key_counter(door, sphere, key_counter, flat_proposal):
|
||||
new_counter = key_counter.copy()
|
||||
new_counter.open_door(door, flat_proposal)
|
||||
new_counter.update(sphere)
|
||||
return new_counter
|
||||
|
||||
|
||||
def find_best_counter(door, key_counter, key_layout, world, skip_bk): # try to waste as many keys as possible?
|
||||
door_sphere = key_layout.key_spheres[door.name]
|
||||
ignored_doors = {door, door.dest}
|
||||
def find_best_counter(door, odd_counter, key_counter, key_layout, world, 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, skip_bk)
|
||||
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
|
||||
@@ -286,7 +227,7 @@ def find_best_counter(door, key_counter, key_layout, world, skip_bk): # try to
|
||||
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 relative_empty_sphere(door_sphere, new_counter):
|
||||
if relative_empty_counter(odd_counter, new_counter):
|
||||
ignored_doors.add(new_door)
|
||||
else:
|
||||
if not key_wasted(new_door, last_counter, new_counter, key_layout, world):
|
||||
@@ -298,17 +239,20 @@ def find_best_counter(door, key_counter, key_layout, world, skip_bk): # try to
|
||||
return last_counter
|
||||
|
||||
|
||||
def find_potential_open_doors(key_counter, ignored_doors, skip_bk):
|
||||
def find_potential_open_doors(key_counter, ignored_doors, key_layout, skip_bk):
|
||||
small_doors = []
|
||||
big_doors = []
|
||||
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:
|
||||
if not skip_bk and (not key_layout.big_key_special or key_counter.big_key_opened):
|
||||
big_doors.append(other)
|
||||
elif other.dest not in small_doors:
|
||||
small_doors.append(other)
|
||||
big_key_available = len(key_counter.free_locations) - key_counter.used_smalls_loc(1) > 0
|
||||
if key_layout.big_key_special:
|
||||
big_key_available = key_counter.big_key_opened
|
||||
else:
|
||||
big_key_available = len(key_counter.free_locations) - key_counter.used_smalls_loc(1) > 0
|
||||
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
|
||||
@@ -338,10 +282,10 @@ def key_wasted(new_door, old_counter, new_counter, key_layout, world):
|
||||
return False
|
||||
|
||||
|
||||
def find_next_counter(new_door, old_counter, next_sphere, key_layout):
|
||||
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 or check_special_locations(next_sphere.free_locations)
|
||||
return key_layout.key_counters[counter_id(proposed_doors, bk_open, key_layout.flat_prop)]
|
||||
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):
|
||||
@@ -358,8 +302,8 @@ def calc_avail_keys(key_counter, world):
|
||||
|
||||
|
||||
def create_rule(key_counter, prev_counter, key_layout, world):
|
||||
prev_chest_keys = available_chest_small_keys(prev_counter, world)
|
||||
prev_avail = prev_chest_keys + len(prev_counter.key_only_locations)
|
||||
# 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)
|
||||
key_gain = len(key_counter.key_only_locations) - len(prev_counter.key_only_locations)
|
||||
raw_avail = chest_keys + len(key_counter.key_only_locations)
|
||||
@@ -377,29 +321,78 @@ def create_rule(key_counter, prev_counter, key_layout, world):
|
||||
return DoorRules(rule_num)
|
||||
|
||||
|
||||
def check_for_self_lock_key(rule, sphere, key_layout, world):
|
||||
def check_for_self_lock_key(rule, door, parent_counter, key_layout, world):
|
||||
if world.accessibility != 'locations':
|
||||
counter = KeyCounter(key_layout.max_chests)
|
||||
counter.update(sphere)
|
||||
counter = find_inverted_counter(door, parent_counter, key_layout, world)
|
||||
if not self_lock_possible(counter):
|
||||
return
|
||||
queue = collections.deque(counter.child_doors)
|
||||
already_queued = set(counter.child_doors)
|
||||
while len(queue) > 0:
|
||||
child = queue.popleft()
|
||||
if child not in counter.open_doors:
|
||||
counter = increment_key_counter(child, key_layout.key_spheres[child.name], counter, key_layout.flat_prop)
|
||||
if not self_lock_possible(counter):
|
||||
return
|
||||
for new_door in counter.child_doors:
|
||||
if new_door not in already_queued:
|
||||
queue.append(new_door)
|
||||
already_queued.add(new_door)
|
||||
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):
|
||||
# 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.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):
|
||||
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
|
||||
|
||||
@@ -415,136 +408,27 @@ def available_chest_small_keys(key_counter, world):
|
||||
return key_counter.max_chests
|
||||
|
||||
|
||||
def bk_restricted_rules(rule, sphere, key_counter, key_layout, world):
|
||||
if sphere.bk_locked:
|
||||
def bk_restricted_rules(rule, door, odd_counter, key_counter, key_layout, world):
|
||||
if key_counter.big_key_opened:
|
||||
return
|
||||
best_counter = find_best_counter(sphere.access_door, key_counter, key_layout, world, True)
|
||||
best_counter = find_best_counter(door, odd_counter, key_counter, key_layout, world, True)
|
||||
bk_number = create_rule(best_counter, key_counter, key_layout, world).small_key_num
|
||||
if bk_number == rule.small_key_num:
|
||||
return
|
||||
post_counter = KeyCounter(key_layout.max_chests)
|
||||
post_counter.update(sphere)
|
||||
other_doors_beyond_me = [x for x in post_counter.child_doors if not x.bigKey]
|
||||
queue = collections.deque(other_doors_beyond_me)
|
||||
already_queued = set(other_doors_beyond_me)
|
||||
while len(queue) > 0:
|
||||
child = queue.popleft()
|
||||
if child not in post_counter.open_doors:
|
||||
post_counter = increment_key_counter(child, key_layout.key_spheres[child.name], post_counter, key_layout.flat_prop)
|
||||
for new_door in post_counter.child_doors:
|
||||
if not new_door.bigKey and new_door not in already_queued and new_door.dest not in already_queued:
|
||||
queue.append(new_door)
|
||||
already_queued.add(new_door)
|
||||
unique_loc = set(post_counter.free_locations).difference(set(best_counter.free_locations))
|
||||
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)
|
||||
if len(unique_loc) > 0:
|
||||
rule.alternate_small_key = bk_number
|
||||
rule.alternate_big_key_loc.update(unique_loc)
|
||||
|
||||
|
||||
def expand_counter_no_big_doors(door, key_counter, key_layout, ignored_doors):
|
||||
door_sphere = key_layout.key_spheres[door.name]
|
||||
small_doors = set()
|
||||
for other in key_counter.child_doors:
|
||||
if other != door and other not in ignored_doors:
|
||||
if other.dest not in small_doors and not other.bigKey:
|
||||
small_doors.add(other)
|
||||
if len(small_doors) == 0:
|
||||
return key_counter
|
||||
new_counter = key_counter
|
||||
last_counter = key_counter
|
||||
new_ignored = set(ignored_doors)
|
||||
for new_door in small_doors:
|
||||
new_sphere = key_layout.key_spheres[new_door.name]
|
||||
new_counter = increment_key_counter(new_door, new_sphere, new_counter, key_layout.flat_prop)
|
||||
# this means the new_door invalidates the door / leads to the same stuff
|
||||
if relative_empty_sphere(door_sphere, new_counter):
|
||||
new_counter = last_counter
|
||||
new_ignored.add(new_door)
|
||||
else:
|
||||
last_counter = new_counter
|
||||
old_counter = None
|
||||
while old_counter != new_counter:
|
||||
old_counter = new_counter
|
||||
new_counter = expand_counter_no_big_doors(door, old_counter, key_layout, new_ignored)
|
||||
return new_counter
|
||||
|
||||
|
||||
def create_key_spheres(key_layout, world, player):
|
||||
key_spheres = {}
|
||||
flat_proposal = key_layout.flat_prop
|
||||
state = ExplorationState(dungeon=key_layout.sector.name)
|
||||
state.key_locations = len(world.get_dungeon(key_layout.sector.name, player).small_keys)
|
||||
state.big_key_special = world.get_region('Hyrule Dungeon Cellblock', player) in key_layout.sector.regions
|
||||
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)
|
||||
key_spheres['Origin'] = create_key_sphere(state, None, None)
|
||||
queue = collections.deque([(key_spheres['Origin'], state)])
|
||||
while len(queue) > 0:
|
||||
next_key_sphere, parent_state = queue.popleft()
|
||||
for door in next_key_sphere.child_doors:
|
||||
child_state = parent_state.copy()
|
||||
# open the door
|
||||
open_a_door(door, child_state, flat_proposal)
|
||||
expand_key_state(child_state, flat_proposal, world, player)
|
||||
child_kr = create_key_sphere(child_state, next_key_sphere, door)
|
||||
if door.name not in key_spheres.keys():
|
||||
key_spheres[door.name] = child_kr
|
||||
queue.append((child_kr, child_state))
|
||||
else:
|
||||
merge_sphere = old_sphere = key_spheres[door.name]
|
||||
if empty_sphere(old_sphere) and not empty_sphere(child_kr):
|
||||
key_spheres[door.name] = merge_sphere = child_kr
|
||||
queue.append((child_kr, child_state))
|
||||
if not empty_sphere(old_sphere) and not empty_sphere(child_kr) and not old_sphere == child_kr:
|
||||
# ugly sphere merge function - just union locations - ugh
|
||||
if old_sphere.bk_locked != child_kr.bk_locked:
|
||||
if old_sphere.bk_locked:
|
||||
merge_sphere.child_doors = child_kr.child_doors
|
||||
merge_sphere.free_locations = child_kr.free_locations
|
||||
merge_sphere.key_only_locations = child_kr.key_only_locations
|
||||
else:
|
||||
merge_sphere.child_doors = {**old_sphere.child_doors, **child_kr.child_doors}
|
||||
merge_sphere.free_locations = {**old_sphere.free_locations, **child_kr.free_locations}
|
||||
merge_sphere.key_only_locations = {**old_sphere.key_only_locations, **child_kr.key_only_locations}
|
||||
merge_sphere.bk_locked = old_sphere.bk_locked and child_kr.bk_locked
|
||||
# this feels so ugly, key counters are much smarter than this - would love to get rid of spheres
|
||||
return key_spheres
|
||||
|
||||
|
||||
def create_key_sphere(state, parent_sphere, door):
|
||||
key_sphere = KeySphere()
|
||||
key_sphere.parent_sphere = parent_sphere
|
||||
p_region = parent_sphere
|
||||
parent_doors = set()
|
||||
parent_locations = set()
|
||||
while p_region is not None:
|
||||
parent_doors.update(p_region.child_doors)
|
||||
parent_locations.update(p_region.free_locations)
|
||||
parent_locations.update(p_region.key_only_locations)
|
||||
parent_locations.update(p_region.other_locations)
|
||||
p_region = p_region.parent_sphere
|
||||
u_doors = [x for x in unique_doors(state.small_doors+state.big_doors) if x not in parent_doors]
|
||||
key_sphere.child_doors.update(dict.fromkeys(u_doors))
|
||||
region_locations = [x for x in state.found_locations if x not in parent_locations]
|
||||
for loc in region_locations:
|
||||
if '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']:
|
||||
key_sphere.prize_region = True
|
||||
key_sphere.other_locations[loc] = None
|
||||
elif loc.event and 'Small Key' in loc.item.name:
|
||||
key_sphere.key_only_locations[loc] = None
|
||||
elif loc.name not in dungeon_events:
|
||||
key_sphere.free_locations[loc] = None
|
||||
else:
|
||||
key_sphere.other_locations[loc] = None
|
||||
# todo: Cellblock in a dungeon with a big_key door or chest - Crossed Mode
|
||||
key_sphere.bk_locked = state.big_key_opened if not state.big_key_special else False
|
||||
if door is not None:
|
||||
key_sphere.access_door = door
|
||||
return key_sphere
|
||||
|
||||
|
||||
def open_a_door(door, child_state, flat_proposal):
|
||||
if door.bigKey:
|
||||
child_state.big_key_opened = True
|
||||
@@ -610,7 +494,7 @@ def filter_big_chest(locations):
|
||||
def count_locations_exclude_big_chest(state):
|
||||
cnt = 0
|
||||
for loc in state.found_locations:
|
||||
if '- Big Chest' not in loc.name and '- Prize' not in loc.name:
|
||||
if '- Big Chest' not in loc.name and '- Prize' not in loc.name and loc.name not in dungeon_events and loc.name not in ['Agahnim 1', 'Agahnim 2', 'Hyrule Castle - Big Key Drop']:
|
||||
cnt += 1
|
||||
return cnt
|
||||
|
||||
@@ -648,21 +532,33 @@ def flatten_pair_list(paired_list):
|
||||
return flat_list
|
||||
|
||||
|
||||
def check_rules(key_layout):
|
||||
def check_rules(original_counter, key_layout):
|
||||
all_key_only = set()
|
||||
key_only_map = {}
|
||||
for sphere in key_layout.key_spheres.values():
|
||||
for loc in sphere.key_only_locations:
|
||||
queue = collections.deque([(None, original_counter)])
|
||||
completed = set()
|
||||
completed.add(cid(original_counter, key_layout))
|
||||
while len(queue) > 0:
|
||||
queue = collections.deque(sorted(queue, key=queue_sorter))
|
||||
access_door, counter = queue.popleft()
|
||||
for loc in counter.key_only_locations:
|
||||
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 sphere.access_door is None or sphere.access_door.name not in key_layout.key_logic.door_rules.keys():
|
||||
if access_door is None or access_door.name not in key_layout.key_logic.door_rules.keys():
|
||||
access_rules.append(DoorRules(0))
|
||||
else:
|
||||
access_rules.append(key_layout.key_logic.door_rules[sphere.access_door.name])
|
||||
access_rules.append(key_layout.key_logic.door_rules[access_door.name])
|
||||
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)
|
||||
queue.append((child, next_counter))
|
||||
min_rule_bk = defaultdict(list)
|
||||
min_rule_non_bk = defaultdict(list)
|
||||
check_non_bk = False
|
||||
@@ -712,30 +608,26 @@ def adjust_key_location_mins(key_layout, min_rules, getter, setter):
|
||||
|
||||
|
||||
# Soft lock stuff
|
||||
def validate_key_layout_ex(key_layout, world, player):
|
||||
return validate_key_layout_main_loop(key_layout, world, player)
|
||||
|
||||
|
||||
def validate_key_layout_main_loop(key_layout, world, player):
|
||||
def validate_key_layout(key_layout, world, player):
|
||||
flat_proposal = key_layout.flat_prop
|
||||
state = ExplorationState(dungeon=key_layout.sector.name)
|
||||
state.key_locations = len(world.get_dungeon(key_layout.sector.name, player).small_keys)
|
||||
state.key_locations = key_layout.max_chests
|
||||
state.big_key_special = world.get_region('Hyrule Dungeon Cellblock', player) in key_layout.sector.regions
|
||||
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(state, {}, flat_proposal, world, player)
|
||||
return validate_key_layout_sub_loop(key_layout, state, {}, flat_proposal, world, player)
|
||||
|
||||
|
||||
def validate_key_layout_sub_loop(state, checked_states, flat_proposal, world, player):
|
||||
def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposal, world, player):
|
||||
expand_key_state(state, flat_proposal, world, player)
|
||||
smalls_avail = len(state.small_doors) > 0
|
||||
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
|
||||
ttl_locations = state.ttl_locations if state.big_key_opened else count_locations_exclude_big_chest(state)
|
||||
available_small_locations = min(ttl_locations - state.used_locations, state.key_locations - state.used_smalls)
|
||||
available_big_locations = ttl_locations - state.used_locations if not state.big_key_special else 0
|
||||
available_small_locations = cnt_avail_small_locations(key_layout, ttl_locations, state, world)
|
||||
available_big_locations = cnt_avail_big_locations(ttl_locations, state, world)
|
||||
if (not smalls_avail or available_small_locations == 0) and (state.big_key_opened or num_bigs == 0 or available_big_locations == 0):
|
||||
return False
|
||||
else:
|
||||
@@ -747,7 +639,7 @@ def validate_key_layout_sub_loop(state, checked_states, flat_proposal, world, pl
|
||||
state_copy.used_smalls += 1
|
||||
code = state_id(state_copy, flat_proposal)
|
||||
if code not in checked_states.keys():
|
||||
valid = validate_key_layout_sub_loop(state_copy, checked_states, flat_proposal, world, player)
|
||||
valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal, world, player)
|
||||
checked_states[code] = valid
|
||||
else:
|
||||
valid = checked_states[code]
|
||||
@@ -759,7 +651,7 @@ def validate_key_layout_sub_loop(state, checked_states, flat_proposal, world, pl
|
||||
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(state_copy, checked_states, flat_proposal, world, player)
|
||||
valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal, world, player)
|
||||
checked_states[code] = valid
|
||||
else:
|
||||
valid = checked_states[code]
|
||||
@@ -768,6 +660,18 @@ def validate_key_layout_sub_loop(state, checked_states, flat_proposal, world, pl
|
||||
return True
|
||||
|
||||
|
||||
def cnt_avail_small_locations(key_layout, ttl_locations, state, world):
|
||||
if not world.keysanity and world.mode != 'retro':
|
||||
return min(ttl_locations - state.used_locations, state.key_locations - state.used_smalls)
|
||||
return key_layout.max_chests + state.key_locations - state.used_smalls
|
||||
|
||||
|
||||
def cnt_avail_big_locations(ttl_locations, state, world):
|
||||
if not world.keysanity:
|
||||
return 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
|
||||
@@ -779,36 +683,39 @@ def create_key_counters(key_layout, world, player):
|
||||
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_x(state, key_layout, world, player)
|
||||
key_counters[code] = create_key_counter(state, key_layout, world, player)
|
||||
queue = collections.deque([(key_counters[code], state)])
|
||||
while len(queue) > 0:
|
||||
next_key_sphere, parent_state = queue.popleft()
|
||||
for door in next_key_sphere.child_doors:
|
||||
next_key_counter, parent_state = queue.popleft()
|
||||
for door in next_key_counter.child_doors:
|
||||
child_state = parent_state.copy()
|
||||
# open the door
|
||||
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_x(child_state, key_layout, world, player)
|
||||
key_counters[code] = child_kr
|
||||
queue.append((child_kr, child_state))
|
||||
if door.bigKey:
|
||||
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.big_key_opened:
|
||||
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_x(state, key_layout, world, player):
|
||||
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 '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']:
|
||||
key_counter.important_location = True
|
||||
# todo: zelda's cell is special in standard, and probably crossed too
|
||||
elif loc.name in ['Attic Cracked Floor', 'Suspicious Maiden']:
|
||||
if important_location(loc, world):
|
||||
key_counter.important_location = True
|
||||
key_counter.other_locations[loc] = None
|
||||
elif loc.event and 'Small Key' in loc.item.name:
|
||||
key_counter.key_only_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)
|
||||
if state.big_key_special:
|
||||
@@ -823,6 +730,34 @@ def create_key_counter_x(state, key_layout, world, player):
|
||||
return key_counter
|
||||
|
||||
|
||||
def important_location(loc, world):
|
||||
important_locations = ['Agahnim 1', 'Agahnim 2', 'Attic Cracked Floor', 'Suspicious Maiden']
|
||||
if world.mode == 'standard' or world.doorShuffle == 'crossed':
|
||||
important_locations.append('Hyrule Dungeon Cellblock')
|
||||
return '- Prize' in loc.name or loc.name in important_locations
|
||||
|
||||
|
||||
def create_odd_key_counter(door, parent_counter, key_layout, world):
|
||||
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):
|
||||
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:
|
||||
@@ -857,6 +792,10 @@ def find_counter_hint(opened_doors, bk_hint, key_layout):
|
||||
return None
|
||||
|
||||
|
||||
def find_max_counter(key_layout):
|
||||
return find_counter_hint(dict.fromkeys(key_layout.flat_prop), False, key_layout)
|
||||
|
||||
|
||||
def counter_id(opened_doors, bk_unlocked, flat_proposal):
|
||||
s_id = '1' if bk_unlocked else '0'
|
||||
for d in flat_proposal:
|
||||
@@ -864,6 +803,10 @@ def counter_id(opened_doors, bk_unlocked, flat_proposal):
|
||||
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
|
||||
|
||||
@@ -900,7 +843,7 @@ def val_hyrule(key_logic, world, player):
|
||||
|
||||
|
||||
def val_eastern(key_logic, world, player):
|
||||
val_rule(key_logic.door_rules['Eastern Dark Square Key Door WN'], 2, False, None, 1, {'Eastern Palace - Big Key Chest'})
|
||||
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
|
||||
@@ -981,7 +924,7 @@ def val_ice(key_logic, world, player):
|
||||
|
||||
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 Spikes NW'], 5) # todo: is sometimes 3 or 5? best_counter order matters
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user