Revamped dungeon generation

Revamped key logic generation
Prevent key floods in playthrough/can_beat_game checks
This commit is contained in:
aerinon
2019-10-31 11:09:58 -06:00
parent bc30cc3b42
commit 90c3368f9d
8 changed files with 1029 additions and 311 deletions

View File

@@ -90,6 +90,7 @@ class World(object):
self._room_cache = {} self._room_cache = {}
self.dungeon_layouts = {} self.dungeon_layouts = {}
self.inaccessible_regions = [] self.inaccessible_regions = []
self.key_logic = {}
def intialize_regions(self): def intialize_regions(self):
for region in self.regions: for region in self.regions:
@@ -328,7 +329,7 @@ class World(object):
sphere = [] sphere = []
# build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres # build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
for location in prog_locations: for location in prog_locations:
if location.can_reach(state): if location.can_reach(state) and state.not_flooding_a_key(state.world, location):
sphere.append(location) sphere.append(location)
if not sphere: if not sphere:
@@ -414,14 +415,17 @@ class CollectionState(object):
def _do_not_flood_the_keys(self, reachable_events): def _do_not_flood_the_keys(self, reachable_events):
adjusted_checks = list(reachable_events) adjusted_checks = list(reachable_events)
for event in reachable_events: for event in reachable_events:
if event.name == 'Trench 2 Switch' and self.world.get_location('Swamp Palace - Trench 2 Pot Key', event.player) not in reachable_events: if event.name in flooded_keys.keys() and self.world.get_location(flooded_keys[event.name], event.player) not in reachable_events:
adjusted_checks.remove(event)
if event.name == 'Trench 1 Switch' and self.world.get_location('Swamp Palace - Trench 1 Pot Key', event.player) not in reachable_events:
adjusted_checks.remove(event) adjusted_checks.remove(event)
if len(adjusted_checks) < len(reachable_events): if len(adjusted_checks) < len(reachable_events):
return adjusted_checks return adjusted_checks
return reachable_events return reachable_events
def not_flooding_a_key(self, world, location):
if location.name in flooded_keys.keys():
return world.get_location(flooded_keys[location.name], location.player) in self.locations_checked
return True
def has(self, item, player, count=1): def has(self, item, player, count=1):
if count == 1: if count == 1:
return (item, player) in self.prog_items return (item, player) in self.prog_items
@@ -948,7 +952,8 @@ class Door(object):
# logical properties # logical properties
# self.connected = False # combine with Dest? # self.connected = False # combine with Dest?
self.dest = None self.dest = None
self.blocked = False # Indicates if the door is normally blocked off. (Sanc door or always closed) self.blocked = False # Indicates if the door is normally blocked off as an exit. (Sanc door or always closed)
self.stonewall = False # Indicate that the door cannot be enter until exited (Desert Torches, PoD Eye Statue)
self.smallKey = False # There's a small key door on this side self.smallKey = False # There's a small key door on this side
self.bigKey = False # There's a big key door on this side self.bigKey = False # There's a big key door on this side
self.ugly = False # Indicates that it can't be seen from the front (e.g. back of a big key door) self.ugly = False # Indicates that it can't be seen from the front (e.g. back of a big key door)
@@ -1004,6 +1009,10 @@ class Door(object):
self.blocked = True self.blocked = True
return self return self
def no_entrance(self):
self.stonewall = True
return self
def trap(self, trapFlag): def trap(self, trapFlag):
self.trapFlag = trapFlag self.trapFlag = trapFlag
return self return self
@@ -1024,6 +1033,12 @@ class Door(object):
self.crystal = CrystalBarrier.Either self.crystal = CrystalBarrier.Either
return self return self
def __eq__(self, other):
return isinstance(other, self.__class__) and self.name == other.name
def __hash__(self):
return hash(self.name)
def __str__(self): def __str__(self):
return str(self.__unicode__()) return str(self.__unicode__())
@@ -1419,3 +1434,9 @@ class Spoiler(object):
path_listings.append("{}\n {}".format(location, "\n => ".join(path_lines))) path_listings.append("{}\n {}".format(location, "\n => ".join(path_lines)))
outfile.write('\n'.join(path_listings)) outfile.write('\n'.join(path_listings))
flooded_keys = {
'Trench 1 Switch': 'Swamp Palace - Trench 1 Pot Key',
'Trench 2 Switch': 'Swamp Palace - Trench 2 Pot Key'
}

View File

@@ -1,14 +1,15 @@
import random import random
import collections import collections
from collections import defaultdict
import logging import logging
import operator as op import operator as op
from functools import reduce from functools import reduce
from BaseClasses import RegionType, Door, DoorType, Direction, Sector, CrystalBarrier, Polarity, pol_idx, pol_inc from BaseClasses import RegionType, Door, DoorType, Direction, Sector, Polarity, pol_idx, pol_inc
from Dungeons import hyrule_castle_regions, eastern_regions, desert_regions, hera_regions, tower_regions, pod_regions from Dungeons import hyrule_castle_regions, eastern_regions, desert_regions, hera_regions, tower_regions, pod_regions
from Dungeons import dungeon_regions, region_starts, split_region_starts from Dungeons import dungeon_regions, region_starts, split_region_starts, dungeon_keys, dungeon_bigs
from Regions import key_only_locations, dungeon_events, flooded_keys, flooded_keys_reverse
from RoomData import DoorKind, PairedDoor from RoomData import DoorKind, PairedDoor
from DungeonGenerator import ExplorationState, extend_reachable_state, convert_regions, generate_dungeon
def link_doors(world, player): def link_doors(world, player):
@@ -129,6 +130,16 @@ paired_directions = {
Direction.Down: [Direction.Down, Direction.Up], Direction.Down: [Direction.Down, Direction.Up],
} }
allied_directions = {
Direction.South: [Direction.North, Direction.South],
Direction.North: [Direction.North, Direction.South],
Direction.West: [Direction.East, Direction.West],
Direction.East: [Direction.East, Direction.West],
Direction.Up: [Direction.Down, Direction.Up],
Direction.Down: [Direction.Down, Direction.Up],
}
def switch_dir(direction): def switch_dir(direction):
return oppositemap[direction] return oppositemap[direction]
@@ -455,12 +466,13 @@ def experiment(world, player):
split_sectors = split_up_sectors(sector_list, split_region_starts[key]) split_sectors = split_up_sectors(sector_list, split_region_starts[key])
for idx, sub_sector_list in enumerate(split_sectors): for idx, sub_sector_list in enumerate(split_sectors):
dungeon_sectors.append((key, sub_sector_list, split_region_starts[key][idx])) dungeon_sectors.append((key, sub_sector_list, split_region_starts[key][idx]))
# todo: shuffable entrances like pinball, left pit need to be added to entrance list
else: else:
dungeon_sectors.append((key, sector_list, region_starts[key])) dungeon_sectors.append((key, sector_list, region_starts[key]))
dungeon_layouts = [] dungeon_layouts = []
for key, sector_list, entrance_list in dungeon_sectors: for key, sector_list, entrance_list in dungeon_sectors:
ds = shuffle_dungeon_no_repeats_new(world, player, sector_list, entrance_list) ds = generate_dungeon(sector_list, entrance_list, world, player)
ds.name = key ds.name = key
dungeon_layouts.append((ds, entrance_list)) dungeon_layouts.append((ds, entrance_list))
@@ -619,6 +631,14 @@ def shuffle_sectors(buckets, candidates):
buckets[solution[i]].append(candidates[i]) buckets[solution[i]].append(candidates[i])
# def find_proposal_greedy_backtrack(bucket, candidates):
# choices = []
#
# # todo: stick things on the queue in interesting order
# queue = collections.deque(candidates):
#
# monte carlo proposal generation # monte carlo proposal generation
def find_proposal_monte_carlo(proposal, buckets, candidates): def find_proposal_monte_carlo(proposal, buckets, candidates):
n = len(candidates) n = len(candidates)
@@ -666,253 +686,6 @@ def find_proposal(proposal, buckets, candidates):
return proposal return proposal
# code below is for an algorithm without restarts
class ExplorableDoor(object):
def __init__(self, door, crystal):
self.door = door
self.crystal = crystal
def __str__(self):
return str(self.__unicode__())
def __unicode__(self):
return '%s (%s)' % (self.door.name, self.crystal.name)
class ExplorationState(object):
def __init__(self):
self.unattached_doors = []
self.avail_doors = []
self.event_doors = []
self.visited_orange = []
self.visited_blue = []
self.events = set()
self.crystal = CrystalBarrier.Orange
# key region stuff
self.door_krs = {}
# key validation stuff
self.small_doors = []
self.big_doors = []
self.opened_doors = []
self.big_key_opened = False
self.big_key_special = False
self.found_locations = []
self.ttl_locations = 0
self.used_locations = 0
self.key_locations = 0
self.used_smalls = 0
self.non_door_entrances = []
def copy(self):
ret = ExplorationState()
ret.unattached_doors = list(self.unattached_doors)
ret.avail_doors = list(self.avail_doors)
ret.event_doors = list(self.event_doors)
ret.visited_orange = list(self.visited_orange)
ret.visited_blue = list(self.visited_blue)
ret.events = set(self.events)
ret.crystal = self.crystal
ret.door_krs = self.door_krs.copy()
ret.small_doors = list(self.small_doors)
ret.big_doors = list(self.big_doors)
ret.opened_doors = list(self.opened_doors)
ret.big_key_opened = self.big_key_opened
ret.big_key_special = self.big_key_special
ret.ttl_locations = self.ttl_locations
ret.key_locations = self.key_locations
ret.used_locations = self.used_locations
ret.used_smalls = self.used_smalls
ret.found_locations = list(self.found_locations)
ret.non_door_entrances = list(self.non_door_entrances)
return ret
def next_avail_door(self):
exp_door = self.avail_doors.pop()
self.crystal = exp_door.crystal
return exp_door
def visit_region(self, region, key_region=None, key_checks=False):
if self.crystal == CrystalBarrier.Either:
if region not in self.visited_blue:
self.visited_blue.append(region)
if region not in self.visited_orange:
self.visited_orange.append(region)
elif self.crystal == CrystalBarrier.Orange:
self.visited_orange.append(region)
elif self.crystal == CrystalBarrier.Blue:
self.visited_blue.append(region)
for location in region.locations:
if key_checks and location not in self.found_locations:
if location.name in key_only_locations:
self.key_locations += 1
if location.name not in dungeon_events and '- Prize' not in location.name:
self.ttl_locations += 1
if location not in self.found_locations:
self.found_locations.append(location)
if location.name in dungeon_events and location.name not in self.events:
if self.flooded_key_check(location):
self.perform_event(location.name, key_region)
if location.name in flooded_keys_reverse.keys() and self.location_found(flooded_keys_reverse[location.name]):
self.perform_event(flooded_keys_reverse[location.name], key_region)
if key_checks and region.name == 'Hyrule Dungeon Cellblock' and not self.big_key_opened:
self.big_key_opened = True
self.avail_doors.extend(self.big_doors)
self.big_doors.clear()
def flooded_key_check(self, location):
if location.name not in flooded_keys.keys():
return True
return flooded_keys[location.name] in self.found_locations
def location_found(self, location_name):
for l in self.found_locations:
if l.name == location_name:
return True
return False
def perform_event(self, location_name, key_region):
self.events.add(location_name)
queue = collections.deque(self.event_doors)
while len(queue) > 0:
exp_door = queue.pop()
if exp_door.door.req_event == location_name:
self.avail_doors.append(exp_door)
self.event_doors.remove(exp_door)
if key_region is not None:
d_name = exp_door.door.name
if d_name not in self.door_krs.keys():
self.door_krs[d_name] = key_region
def add_all_entrance_doors_check_unattached(self, region, world, player):
door_list = [x for x in get_doors(world, region, player) if x.type in [DoorType.Normal, DoorType.SpiralStairs]]
door_list.extend(get_entrance_doors(world, region, player))
for door in door_list:
if self.can_traverse(door):
if door.dest is None and not self.in_door_list_ic(door, self.unattached_doors):
self.append_door_to_list(door, self.unattached_doors)
elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, self.event_doors):
self.append_door_to_list(door, self.event_doors)
elif not self.in_door_list(door, self.avail_doors):
self.append_door_to_list(door, self.avail_doors)
for entrance in region.entrances:
door = world.check_for_door(entrance.name, player)
if door is None:
self.non_door_entrances.append(entrance)
def add_all_doors_check_unattached(self, region, world, player):
for door in get_doors(world, region, player):
if self.can_traverse(door):
if door.dest is None and not self.in_door_list_ic(door, self.unattached_doors):
self.append_door_to_list(door, self.unattached_doors)
elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, self.event_doors):
self.append_door_to_list(door, self.event_doors)
elif not self.in_door_list(door, self.avail_doors):
self.append_door_to_list(door, self.avail_doors)
def add_all_doors_check_key_region(self, region, key_region, world, player):
for door in get_doors(world, region, player):
if self.can_traverse(door):
if door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, self.event_doors):
self.append_door_to_list(door, self.event_doors)
elif not self.in_door_list(door, self.avail_doors):
self.append_door_to_list(door, self.avail_doors)
if door.name not in self.door_krs.keys():
self.door_krs[door.name] = key_region
else:
if door.name not in self.door_krs.keys():
self.door_krs[door.name] = key_region
def add_all_doors_check_keys(self, region, key_door_proposal, world, player):
for door in get_doors(world, region, player):
if self.can_traverse(door):
if door in key_door_proposal and door not in self.opened_doors:
if not self.in_door_list(door, self.small_doors):
self.append_door_to_list(door, self.small_doors)
elif door.bigKey and not self.big_key_opened:
if not self.in_door_list(door, self.big_doors):
self.append_door_to_list(door, self.big_doors)
elif door.req_event is not None and door.req_event not in self.events:
if not self.in_door_list(door, self.event_doors):
self.append_door_to_list(door, self.event_doors)
elif not self.in_door_list(door, self.avail_doors):
self.append_door_to_list(door, self.avail_doors)
def visited(self, region):
if self.crystal == CrystalBarrier.Either:
return region in self.visited_blue and region in self.visited_orange
elif self.crystal == CrystalBarrier.Orange:
return region in self.visited_orange
elif self.crystal == CrystalBarrier.Blue:
return region in self.visited_blue
return False
def visited_at_all(self, region):
return region in self.visited_blue or region in self.visited_orange
def can_traverse(self, door):
if door.blocked:
return False
if door.crystal not in [CrystalBarrier.Null, CrystalBarrier.Either]:
return self.crystal == CrystalBarrier.Either or door.crystal == self.crystal
return True
def validate(self, door, region, world):
return self.can_traverse(door) and not self.visited(region) and valid_region_to_explore(region, world)
def in_door_list(self, door, door_list):
for d in door_list:
if d.door == door and d.crystal == self.crystal:
return True
return False
@staticmethod
def in_door_list_ic(door, door_list):
for d in door_list:
if d.door == door:
return True
return False
def append_door_to_list(self, door, door_list):
if door.crystal == CrystalBarrier.Null:
door_list.append(ExplorableDoor(door, self.crystal))
else:
door_list.append(ExplorableDoor(door, door.crystal))
def key_door_sort(self, d):
if d.door.smallKey:
if d.door in self.opened_doors:
return 1
else:
return 0
return 2
def extend_reachable_state(search_regions, state, world, player):
local_state = state.copy()
for region in search_regions:
local_state.visit_region(region)
local_state.add_all_doors_check_unattached(region, world, player)
while len(local_state.avail_doors) > 0:
explorable_door = local_state.next_avail_door()
entrance = world.get_entrance(explorable_door.door.name, player)
connect_region = entrance.connected_region
if connect_region is not None:
if valid_region_to_explore(connect_region, world) and not local_state.visited(connect_region):
local_state.visit_region(connect_region)
local_state.add_all_doors_check_unattached(connect_region, world, player)
return local_state
def extend_state_backward(search_regions, state, world, player): def extend_state_backward(search_regions, state, world, player):
local_state = state.copy() local_state = state.copy()
for region in search_regions: for region in search_regions:
@@ -929,6 +702,7 @@ def extend_state_backward(search_regions, state, world, player):
return local_state return local_state
# code below is for an algorithm without restarts
# todo: this sometimes generates two independent parts - that could be valid if the entrances are accessible # todo: this sometimes generates two independent parts - that could be valid if the entrances are accessible
# todo: prevent crystal barrier dead ends # todo: prevent crystal barrier dead ends
def shuffle_dungeon_no_repeats_new(world, player, available_sectors, entrance_region_names): def shuffle_dungeon_no_repeats_new(world, player, available_sectors, entrance_region_names):
@@ -937,10 +711,7 @@ def shuffle_dungeon_no_repeats_new(world, player, available_sectors, entrance_re
for sector in available_sectors: for sector in available_sectors:
random.shuffle(sector.outstanding_doors) random.shuffle(sector.outstanding_doors)
entrance_regions = [] entrance_regions = convert_regions(entrance_region_names, world, player)
# current_sector = None
for region_name in entrance_region_names:
entrance_regions.append(world.get_region(region_name, player))
state = extend_reachable_state(entrance_regions, ExplorationState(), world, player) state = extend_reachable_state(entrance_regions, ExplorationState(), world, player)
# Loop until all available doors are used # Loop until all available doors are used
@@ -1058,7 +829,7 @@ def is_valid(door_a, door_b, sector_a, sector_b, available_sectors, reachable_do
return False return False
elif early_loop_dies(door_a, sector_a, sector_b, available_sectors): elif early_loop_dies(door_a, sector_a, sector_b, available_sectors):
return False return False
elif logical_dead_end(door_a, door_b, state, world, player, available_sectors, reachable_doors): elif logical_dead_end_3(door_a, door_b, state, world, player, available_sectors, reachable_doors):
return False return False
elif door_a.blocked and door_b.blocked: # I can't see this going well unless we are in loop generation... elif door_a.blocked and door_b.blocked: # I can't see this going well unless we are in loop generation...
return False return False
@@ -1179,11 +950,13 @@ def logical_dead_end(door_a, door_b, state, world, player, available_sectors, re
region_set = set(local_state.visited_orange+local_state.visited_blue) region_set = set(local_state.visited_orange+local_state.visited_blue)
if needed and len(visited_regions.intersection(region_set)) == 0 and door.direction in directions: if needed and len(visited_regions.intersection(region_set)) == 0 and door.direction in directions:
hooks_needed += 1 hooks_needed += 1
elif door.direction in hook_directions: visited_regions.update(region_set)
outstanding_hooks += 1 elif door.direction in hook_directions and not door.blocked:
if opposing_hooks > 0 and more_than_one_hook(local_state, hook_directions): if opposing_hooks > 0 and more_than_one_hook(local_state, hook_directions):
needed = False needed = False
visited_regions.update(region_set) if not needed:
outstanding_hooks += 1
visited_regions.update(region_set)
if not needed: if not needed:
only_dead_ends = False only_dead_ends = False
if outstanding_doors_of_type > 0 and ((number_of_hooks == 0 and only_dead_ends) or hooks_needed > number_of_hooks + outstanding_hooks): if outstanding_doors_of_type > 0 and ((number_of_hooks == 0 and only_dead_ends) or hooks_needed > number_of_hooks + outstanding_hooks):
@@ -1191,6 +964,179 @@ def logical_dead_end(door_a, door_b, state, world, player, available_sectors, re
return False return False
def logical_dead_end_3(door_a, door_b, state, world, player, available_sectors, reachable_doors):
region = world.get_entrance(door_b.name, player).parent_region
new_state = extend_reachable_state([region], state, world, player)
new_state.unattached_doors[:] = [x for x in new_state.unattached_doors if x.door not in [door_a, door_b]]
if len(new_state.unattached_doors) == 0:
return True
current_hooks = defaultdict(lambda: 0)
hooks_needed = defaultdict(set)
outstanding_hooks = defaultdict(lambda: 0)
outstanding_total = defaultdict(lambda: 0)
potential_hooks = []
only_dead_ends_vector = [True, True, True]
avail_dirs = set()
for exp_d in new_state.unattached_doors:
hook_key = hook_id(exp_d.door)
current_hooks[hook_key] += 1
avail_dirs.update(allied_directions[exp_d.door.direction])
for sector in available_sectors:
for door in sector.outstanding_doors:
if door != door_b and not state.in_door_list_ic(door, new_state.unattached_doors):
opp_hook_key = opp_hook_id(door)
outstanding_total[opp_hook_key] += 1
if door.blocked:
hooks_needed[opp_hook_key].add(door)
else:
dead_end, cross_interaction = True, False
region = world.get_entrance(door.name, player).parent_region
local_state = extend_state_backward([region], ExplorationState(), world, player)
if len(local_state.non_door_entrances) > 0:
dead_end = False # not a dead end
cross_interaction = True
elif len(local_state.unattached_doors) > 1:
dead_end = False
for exp_d in local_state.unattached_doors:
if cross_door_interaction(exp_d.door, door, reachable_doors, avail_dirs):
cross_interaction = True
break
if dead_end:
hooks_needed[opp_hook_key].add(door)
else:
door_set = set([x.door for x in local_state.unattached_doors if x.door != door])
potential_hooks.append((door, door_set))
if cross_interaction:
only_dead_ends_vector[pol_idx[door.direction][0]] = False
logically_valid = False
satisfying_hooks = []
while not logically_valid:
check_good = True
for key in [Direction.North, Direction.South, Direction.East, Direction.West, DoorType.SpiralStairs]:
dir = key if isinstance(key, Direction) else Direction.Up
vector_idx = pol_idx[dir][0]
ttl_hooks = current_hooks[key] + current_hooks[switch_dir(dir) if isinstance(key, Direction) else DoorType.SpiralStairs]
if outstanding_total[key] > 0 and ttl_hooks == 0 and only_dead_ends_vector[vector_idx]:
return True # no way to get to part of the dungeon
hooks_wanted = hooks_needed[key]
if outstanding_total[key] > 0 and len(hooks_wanted) > current_hooks[key] + outstanding_hooks[key]:
check_good = False
fixer = find_fixer(potential_hooks, key, hooks_wanted, [])
if fixer is None:
return True # couldn't find a fix
else:
apply_fix(fixer, potential_hooks, outstanding_hooks, hooks_needed, satisfying_hooks, key)
if check_good and len(satisfying_hooks) > 0:
check_good = False
votes = defaultdict(lambda: 0) # I really don't like this as other votes could lead to a valid configuration
door_dict = {}
for hook_set in satisfying_hooks:
if len(hook_set) == 0:
return True # unsatisfiable condition
for hookable in hook_set:
votes[hookable.name] += 1
door_dict[hookable.name] = hookable
winner = None
for door_name in votes.keys():
if winner is None or votes[door_name] > votes[winner]:
winner = door_name
winning_hook = door_dict[winner]
key = opp_hook_id(winning_hook)
hooks_needed[key].add(winning_hook)
satisfying_hooks[:] = [x for x in satisfying_hooks if winning_hook not in x]
logically_valid = check_good
return False # no logical dead ends!
# potential_fixers = []
# skip = False
# for hookable in hook_set:
# key = opp_hook_id(hookable)
# if len(hooks_needed[key]) < current_hooks[key] + outstanding_hooks[key]:
# hooks_needed[key].add(hookable)
# hooks_to_remove.append(hook_set)
# hooks_to_remove.extend([x for x in satisfying_hooks if hookable in x])
# skip = True
# break
# fixer = find_fixer(potential_hooks, key, hooks_needed[key], hook_set)
# if fixer is not None:
# potential_fixers.append(fixer)
# if skip:
# break
# if len(potential_fixers) == 0:
# return True # can't find fixers for this set
# elif len(potential_fixers) >= 1:
# check_good = False
# fixer = potential_fixers[0] # just pick the first for now
# apply_fix(fixer, potential_hooks, outstanding_hooks, hooks_needed, satisfying_hooks, hook_id(fixer[0]))
# hooks_to_remove.append(hook_set)
# # what's left - multiple set with multiple available fixes
# if len(hooks_to_remove) == 0:
# return True # can't seem to make progress yet
# satisfying_hooks[:] = [x for x in satisfying_hooks if x not in hooks_to_remove]
# logically_valid = check_good
# return False # no logical dead ends!
def hook_id(door):
if door.type == DoorType.Normal:
return door.direction
if door.type == DoorType.SpiralStairs:
return door.type
return 'Some new door type'
def opp_hook_id(door):
if door.type == DoorType.Normal:
return switch_dir(door.direction)
if door.type == DoorType.SpiralStairs:
return door.type
return 'Some new door type'
def find_fixer(potential_hooks, key, hooks_wanted, invalid_options):
fixer = None
for door, door_set in potential_hooks:
if match_hook_key(door, key) and (len(hooks_wanted) > 1 or len(hooks_wanted.union(door_set)) > 1) and door not in invalid_options:
if fixer is None or len(door_set) > len(fixer[1]): # choose the one with most options
fixer = (door, door_set)
return fixer
def apply_fix(fixer, potential_hooks, outstanding_hooks, hooks_needed, satisfying_hooks, key):
outstanding_hooks[key] += 1
potential_hooks.remove(fixer)
winnow_potential_hooks(potential_hooks, fixer[0]) #
winnow_satisfying_hooks(satisfying_hooks, fixer[0])
if len(fixer[1]) == 1:
new_need = fixer[1].pop()
hooks_needed[opp_hook_id(new_need)].add(new_need)
# removes any hooks that are now fulfilled
satisfying_hooks[:] = [x for x in satisfying_hooks if new_need not in x]
else:
satisfying_hooks.append(fixer[1])
def match_hook_key(door, key):
if isinstance(key, DoorType):
return door.type == key
if isinstance(key, Direction):
return door.direction == key
return False
def winnow_potential_hooks(hooks, door_removal):
for door, door_set in hooks:
if door_removal in door_set:
door_set.remove(door_removal)
hooks[:] = [x for x in hooks if len(x[1]) > 0]
def winnow_satisfying_hooks(satisfying, door_removal):
for hook_set in satisfying:
if door_removal in hook_set:
hook_set.remove(door_removal)
def door_of_interest(door, door_b, d_type, directions, hook_directions, state): def door_of_interest(door, door_b, d_type, directions, hook_directions, state):
if door == door_b or door.type != d_type: if door == door_b or door.type != d_type:
return False return False
@@ -1205,6 +1151,14 @@ def different_direction(door, d_type, directions, hook_directions, reachable_doo
return door.type != d_type or (door.direction not in directions and door.direction not in hook_directions) return door.type != d_type or (door.direction not in directions and door.direction not in hook_directions)
def cross_door_interaction(door, original_door, reachable_doors, avail_dir):
if door in reachable_doors or door == original_door:
return False
if door.type == original_door.type and door.direction in allied_directions[original_door.direction]: # revisit if cross-type linking ever happens
return False
return door.direction in avail_dir
def more_than_one_hook(state, hook_directions): def more_than_one_hook(state, hook_directions):
cnt = 0 cnt = 0
for exp_d in state.unattached_doors: for exp_d in state.unattached_doors:
@@ -1267,7 +1221,8 @@ def shuffle_key_doors(dungeon_sector, entrances, world, player):
combinations = ncr(len(paired_candidates), num_key_doors) combinations = ncr(len(paired_candidates), num_key_doors)
itr = 0 itr = 0
proposal = kth_combination(itr, paired_candidates, num_key_doors) proposal = kth_combination(itr, paired_candidates, num_key_doors)
while not validate_key_layout(dungeon_sector, start_regions, proposal, world, player): key_logic = KeyLogic(dungeon_sector.name)
while not validate_key_layout(dungeon_sector, start_regions, proposal, key_logic, world, player):
itr += 1 itr += 1
if itr >= combinations: if itr >= combinations:
logging.getLogger('').info('Lowering key door count because no valid layouts: %s', dungeon_sector.name) logging.getLogger('').info('Lowering key door count because no valid layouts: %s', dungeon_sector.name)
@@ -1275,8 +1230,21 @@ def shuffle_key_doors(dungeon_sector, entrances, world, player):
combinations = ncr(len(paired_candidates), num_key_doors) combinations = ncr(len(paired_candidates), num_key_doors)
itr = 0 itr = 0
proposal = kth_combination(itr, paired_candidates, num_key_doors) proposal = kth_combination(itr, paired_candidates, num_key_doors)
key_logic = KeyLogic(dungeon_sector.name)
# make changes # make changes
if player not in world.key_logic.keys():
world.key_logic[player] = {}
world.key_logic[player][dungeon_sector.name] = key_logic
reassign_key_doors(current_doors, proposal, world, player) reassign_key_doors(current_doors, proposal, world, player)
class KeyLogic(object):
def __init__(self, dungeon_name):
self.door_rules = {}
self.bk_restricted = []
self.small_key_name = dungeon_keys[dungeon_name]
self.bk_name = dungeon_bigs[dungeon_name]
def build_pair_list(flat_list): def build_pair_list(flat_list):
@@ -1361,7 +1329,7 @@ def ncr(n, r):
return numerator / denominator return numerator / denominator
def validate_key_layout(sector, start_regions, key_door_proposal, world, player): def validate_key_layout(sector, start_regions, key_door_proposal, key_logic, world, player):
flat_proposal = flatten_pair_list(key_door_proposal) flat_proposal = flatten_pair_list(key_door_proposal)
state = ExplorationState() state = ExplorationState()
state.key_locations = len(world.get_dungeon(sector.name, player).small_keys) state.key_locations = len(world.get_dungeon(sector.name, player).small_keys)
@@ -1371,10 +1339,10 @@ def validate_key_layout(sector, start_regions, key_door_proposal, world, player)
state.visit_region(region, key_checks=True) state.visit_region(region, key_checks=True)
state.add_all_doors_check_keys(region, flat_proposal, world, player) state.add_all_doors_check_keys(region, flat_proposal, world, player)
checked_states = set() checked_states = set()
return validate_key_layout_r(state, flat_proposal, checked_states, world, player) return validate_key_layout_r(state, flat_proposal, checked_states, key_logic, world, player)
def validate_key_layout_r(state, flat_proposal, checked_states, world, player): def validate_key_layout_r(state, flat_proposal, checked_states, key_logic, world, player):
# improvements: remove recursion to make this iterative # improvements: remove recursion to make this iterative
# store a cache of various states of opened door to increase speed of checks - many are repetitive # store a cache of various states of opened door to increase speed of checks - many are repetitive
@@ -1395,7 +1363,21 @@ def validate_key_layout_r(state, flat_proposal, checked_states, world, player):
if (not smalls_avail or available_small_locations == 0) and (state.big_key_opened or num_bigs == 0 or available_big_locations == 0): 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 return False
else: else:
if smalls_avail and available_small_locations > 0: if not state.big_key_opened and available_big_locations >= num_bigs > 0: # bk first for better key rules
state_copy = state.copy()
state_copy.big_key_opened = True
state_copy.used_locations += 1
state_copy.avail_doors.extend(state.big_doors)
state_copy.big_doors.clear()
code = state_id(state_copy, flat_proposal)
if code not in checked_states:
valid = validate_key_layout_r(state_copy, flat_proposal, checked_states, key_logic, world, player)
if valid:
checked_states.add(code)
elif smalls_avail and available_small_locations > 0:
key_rule_num = min(state.key_locations, count_unique_doors(state.small_doors) + state.used_smalls)
if key_rule_num == len(state.found_locations):
key_logic.bk_restricted.extend([x for x in state.found_locations if x not in key_logic.bk_restricted])
for exp_door in state.small_doors: for exp_door in state.small_doors:
state_copy = state.copy() state_copy = state.copy()
state_copy.opened_doors.append(exp_door.door) state_copy.opened_doors.append(exp_door.door)
@@ -1408,34 +1390,43 @@ def validate_key_layout_r(state, flat_proposal, checked_states, world, player):
now_available = [x for x in state_copy.small_doors if x.door == dest_door] now_available = [x for x in state_copy.small_doors if x.door == dest_door]
state_copy.small_doors[:] = [x for x in state_copy.small_doors if x.door != dest_door] state_copy.small_doors[:] = [x for x in state_copy.small_doors if x.door != dest_door]
state_copy.avail_doors.extend(now_available) state_copy.avail_doors.extend(now_available)
set_key_rules(key_logic, dest_door, key_rule_num)
set_key_rules(key_logic, exp_door.door, key_rule_num)
state_copy.used_locations += 1 state_copy.used_locations += 1
state_copy.used_smalls += 1 state_copy.used_smalls += 1
code = state_id(state_copy, flat_proposal) code = state_id(state_copy, flat_proposal)
if code not in checked_states: if code not in checked_states:
valid = validate_key_layout_r(state_copy, flat_proposal, checked_states, world, player) valid = validate_key_layout_r(state_copy, flat_proposal, checked_states, key_logic, world, player)
if valid: if valid:
checked_states.add(code) checked_states.add(code)
if not valid: if not valid:
return valid return valid
if not state.big_key_opened and available_big_locations >= num_bigs > 0:
state_copy = state.copy()
state_copy.big_key_opened = True
state_copy.used_locations += 1
state_copy.avail_doors.extend(state.big_doors)
state_copy.big_doors.clear()
code = state_id(state_copy, flat_proposal)
if code not in checked_states:
valid = validate_key_layout_r(state_copy, flat_proposal, checked_states, world, player)
if valid:
checked_states.add(code)
return valid return valid
def count_unique_doors(doors_to_count):
cnt = 0
counted = set()
for d in doors_to_count:
if d.door not in counted:
cnt += 1
counted.add(d.door)
counted.add(d.door.dest)
return cnt
def set_key_rules(key_logic, door, number):
if door.name not in key_logic.door_rules.keys():
key_logic.door_rules[door.name] = number
else:
key_logic.door_rules[door.name] = min(number, key_logic.door_rules[door.name])
def state_id(state, flat_proposal): def state_id(state, flat_proposal):
state_id = '1' if state.big_key_opened else '0' s_id = '1' if state.big_key_opened else '0'
for d in flat_proposal: for d in flat_proposal:
state_id += '1' if d in state.opened_doors else '0' s_id += '1' if d in state.opened_doors else '0'
return state_id return s_id
def reassign_key_doors(current_doors, proposal, world, player): def reassign_key_doors(current_doors, proposal, world, player):

View File

@@ -184,9 +184,7 @@ def create_doors(world, player):
create_door(player, 'Desert Tiles 2 SE', Nrml).dir(Direction.South, 0x43, Right, High).small_key().pos(2), create_door(player, 'Desert Tiles 2 SE', Nrml).dir(Direction.South, 0x43, Right, High).small_key().pos(2),
create_door(player, 'Desert Tiles 2 NE', Intr).dir(Direction.North, 0x43, Right, High).small_key().pos(1), create_door(player, 'Desert Tiles 2 NE', Intr).dir(Direction.North, 0x43, Right, High).small_key().pos(1),
create_door(player, 'Desert Wall Slide SE', Intr).dir(Direction.South, 0x43, Right, High).small_key().pos(1), create_door(player, 'Desert Wall Slide SE', Intr).dir(Direction.South, 0x43, Right, High).small_key().pos(1),
# todo: we need a new flag for a door that has a wall on it - you have to traverse it one particular way first create_door(player, 'Desert Wall Slide NW', Nrml).dir(Direction.North, 0x43, Left, High).big_key().pos(0).no_entrance(),
# the above is not a problem until we get to crossed mode
create_door(player, 'Desert Wall Slide NW', Nrml).dir(Direction.North, 0x43, Left, High).big_key().pos(0),
create_door(player, 'Desert Boss SW', Nrml).dir(Direction.South, 0x33, Left, High).no_exit().trap(0x4).pos(0), create_door(player, 'Desert Boss SW', Nrml).dir(Direction.South, 0x33, Left, High).no_exit().trap(0x4).pos(0),
# Hera # Hera
@@ -323,9 +321,7 @@ def create_doors(world, player):
create_door(player, 'PoD Mimics 2 SW', Nrml).dir(Direction.South, 0x1b, Left, High).pos(1), create_door(player, 'PoD Mimics 2 SW', Nrml).dir(Direction.South, 0x1b, Left, High).pos(1),
create_door(player, 'PoD Mimics 2 NW', Intr).dir(Direction.North, 0x1b, Left, High).pos(0), create_door(player, 'PoD Mimics 2 NW', Intr).dir(Direction.North, 0x1b, Left, High).pos(0),
create_door(player, 'PoD Bow Statue SW', Intr).dir(Direction.South, 0x1b, Left, High).pos(0), create_door(player, 'PoD Bow Statue SW', Intr).dir(Direction.South, 0x1b, Left, High).pos(0),
# todo: we need a new flag for a door that has a wall on it - you have to traverse it one particular way first create_door(player, 'PoD Bow Statue Down Ladder', Lddr).no_entrance(),
# the above is not a problem until we get to crossed mode
create_door(player, 'PoD Bow Statue Down Ladder', Lddr),
create_door(player, 'PoD Dark Pegs Up Ladder', Lddr), create_door(player, 'PoD Dark Pegs Up Ladder', Lddr),
create_door(player, 'PoD Dark Pegs WN', Intr).dir(Direction.West, 0x0b, Mid, High).small_key().pos(2), create_door(player, 'PoD Dark Pegs WN', Intr).dir(Direction.West, 0x0b, Mid, High).small_key().pos(2),
create_door(player, 'PoD Lonely Turtle SW', Intr).dir(Direction.South, 0x0b, Mid, High).pos(0), create_door(player, 'PoD Lonely Turtle SW', Intr).dir(Direction.South, 0x0b, Mid, High).pos(0),

689
DungeonGenerator.py Normal file
View File

@@ -0,0 +1,689 @@
import random
import collections
from collections import defaultdict
from enum import Enum, unique
import logging
from BaseClasses import DoorType, Direction, CrystalBarrier, RegionType, flooded_keys
from Regions import key_only_locations, dungeon_events, flooded_keys_reverse
@unique
class Hook(Enum):
North = 0
West = 1
South = 2
East = 3
Stairs = 4
class GraphPiece:
def __init__(self):
self.hanger_info = None
self.hooks = {}
self.visited_regions = set()
def generate_dungeon(available_sectors, entrance_region_names, world, player):
logger = logging.getLogger('')
entrance_regions = convert_regions(entrance_region_names, world, player)
doors_to_connect = set()
all_regions = set()
for sector in available_sectors:
for door in sector.outstanding_doors:
doors_to_connect.add(door)
all_regions.update(sector.regions)
proposed_map = {}
choices_master = [[]]
depth = 0
dungeon_cache = {}
backtrack = False
# last_choice = None
while len(proposed_map) < len(doors_to_connect):
# what are my choices?
if depth not in dungeon_cache.keys():
dungeon, hangers, hooks = gen_dungeon_info(available_sectors, entrance_regions, proposed_map, doors_to_connect, world, player)
dungeon_cache[depth] = dungeon, hangers, hooks
valid = check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions)
else:
dungeon, hangers, hooks = dungeon_cache[depth]
valid = True
if valid:
prev_choices = choices_master[depth]
# make a choice
hanger, hook = make_a_choice(dungeon, hangers, hooks, prev_choices)
if hanger is None:
backtrack = True
else:
logger.debug(' '*depth+"%d: Linking %s to %s", depth, hanger.name, hook.name)
proposed_map[hanger] = hook
proposed_map[hook] = hanger
last_choice = (hanger, hook)
choices_master[depth].append(last_choice)
depth += 1
choices_master.append([])
else:
backtrack = True
if backtrack:
backtrack = False
choices_master.pop()
dungeon_cache.pop(depth, None)
depth -= 1
a, b = choices_master[depth][-1]
logger.debug(' '*depth+"%d: Rescinding %s, %s", depth, a.name, b.name)
proposed_map.pop(a, None)
proposed_map.pop(b, None)
queue = collections.deque(proposed_map.items())
while len(queue) > 0:
a, b = queue.pop()
connect_doors(a, b, world, player)
queue.remove((b, a))
master_sector = available_sectors.pop()
for sub_sector in available_sectors:
master_sector.regions.extend(sub_sector.regions)
return master_sector
def gen_dungeon_info(available_sectors, entrance_regions, proposed_map, valid_doors, world, player):
# step 1 create dungeon: Dict<DoorName|Origin, GraphPiece>
dungeon = {}
original_state = extend_reachable_state_improved(entrance_regions, ExplorationState(), proposed_map, valid_doors, world, player)
dungeon['Origin'] = create_graph_piece_from_state(None, original_state, original_state, proposed_map)
doors_to_connect = set()
blue_hooks = []
o_state_cache = {}
for sector in available_sectors:
for door in sector.outstanding_doors:
doors_to_connect.add(door)
if not door.stonewall and door not in proposed_map.keys():
parent = parent_region(door, world, player).parent_region
o_state = extend_reachable_state_improved([parent], ExplorationState(), proposed_map, valid_doors, world, player)
o_state_cache[door.name] = o_state
piece = create_graph_piece_from_state(door, o_state, o_state, proposed_map)
dungeon[door.name] = piece
for hook, crystal in piece.hooks.items():
if crystal == CrystalBarrier.Blue or crystal == CrystalBarrier.Either:
h_type = hook_from_door(hook)
if h_type not in blue_hooks:
blue_hooks.append(h_type) # todo: specific hooks and valid path to c_switch
if len(blue_hooks) > 0:
for sector in available_sectors:
for door in sector.outstanding_doors:
h_type = hanger_from_door(door)
if not door.stonewall and door not in proposed_map.keys() and h_type in blue_hooks:
parent = parent_region(door, world, player).parent_region
blue_start = ExplorationState(CrystalBarrier.Blue)
b_state = extend_reachable_state_improved([parent], blue_start, proposed_map, valid_doors, world, player)
o_state = o_state_cache[door.name]
dungeon[door.name] = create_graph_piece_from_state(door, o_state, b_state, proposed_map)
# catalog hooks: Dict<Hook, Set<>
# and hangers:
avail_hooks = defaultdict(set)
hangers = defaultdict(set)
for key, piece in dungeon.items():
door_hang = piece.hanger_info
if door_hang is not None:
hanger = hanger_from_door(door_hang)
hangers[hanger].add(door_hang)
for door, crystal in piece.hooks.items():
hook = hook_from_door(door)
avail_hooks[hook].add((door, crystal, door_hang))
# thin out invalid hanger
winnow_hangers(hangers, avail_hooks)
return dungeon, hangers, avail_hooks
def make_a_choice(dungeon, hangers, avail_hooks, prev_choices):
# choose a hanger
all_hooks = set()
origin = dungeon['Origin']
for key in avail_hooks.keys():
for hstuff in avail_hooks[key]:
all_hooks.add(hstuff[0])
candidate_hangers = []
for key in hangers.keys():
candidate_hangers.extend(hangers[key])
candidate_hangers.sort(key=lambda x: x.name) # sorting to create predictable seeds
random.shuffle(candidate_hangers) # randomize if equal preference
stage_2_hangers = []
hookable_hangers = collections.deque()
queue = collections.deque(candidate_hangers)
while len(queue) > 0:
c_hang = queue.pop()
if c_hang in all_hooks:
hookable_hangers.append(c_hang)
else:
stage_2_hangers.append(c_hang) # prefer hangers that are not hooks
# todo : prefer hangers with fewer hooks at some point? not sure about this
# this prefer hangers of the fewest type - to catch problems fast
hookable_hangers = sorted(hookable_hangers, key=lambda door: len(hangers[hanger_from_door(door)]), reverse=True)
origin_hangers = []
while len(hookable_hangers) > 0:
c_hang = hookable_hangers.pop()
if c_hang in origin.hooks.keys():
origin_hangers.append(c_hang)
else:
stage_2_hangers.append(c_hang) # prefer hangers that are not hooks on the 'origin'
stage_2_hangers.extend(origin_hangers)
hook = None
next_hanger = None
while hook is None:
if len(stage_2_hangers) == 0:
return None, None
next_hanger = stage_2_hangers.pop(0)
next_hanger_type = hanger_from_door(next_hanger)
hook_candidates = []
for door, crystal, orig_hang in avail_hooks[next_hanger_type]:
if filter_choices(next_hanger, door, orig_hang, prev_choices, hook_candidates):
hook_candidates.append(door)
if len(hook_candidates) > 0:
hook_candidates.sort(key=lambda x: x.name) # sort for deterministic seeds
hook = random.choice(tuple(hook_candidates))
return next_hanger, hook
def filter_choices(next_hanger, door, orig_hang, prev_choices, hook_candidates):
if (next_hanger, door) in prev_choices or (door, next_hanger) in prev_choices:
return False
return next_hanger != door and orig_hang != next_hanger and door not in hook_candidates
def check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions):
# evaluate if everything is still plausible
# only origin is left in the dungeon and not everything is connected
if len(dungeon.keys()) <= 1 and len(proposed_map.keys()) < len(doors_to_connect):
return False
# origin has no more hooks, but not all doors have been proposed
if len(dungeon['Origin'].hooks) == 0 and len(proposed_map.keys()) < len(doors_to_connect):
return False
for key in hangers.keys():
if len(hooks[key]) > 0 and len(hangers[key]) == 0:
return False
# todo: stonewall - check that there's no hook-only that is without a matching hanger
all_visited = set()
for piece in dungeon.values():
all_visited.update(piece.visited_regions)
if len(all_regions.difference(all_visited)) > 0:
return False
new_hangers_found = True
accessible_hook_types = []
hanger_matching = set()
all_hangers = set()
origin_hooks = set(dungeon['Origin'].hooks.keys())
for door_hook in origin_hooks:
h_type = hook_from_door(door_hook)
if h_type not in accessible_hook_types:
accessible_hook_types.append(h_type)
while new_hangers_found:
new_hangers_found = False
for hanger_set in hangers.values():
for hanger in hanger_set:
all_hangers.add(hanger)
h_type = hanger_from_door(hanger)
if (h_type in accessible_hook_types or hanger in origin_hooks) and hanger not in hanger_matching:
new_hangers_found = True
hanger_matching.add(hanger)
matching_hooks = dungeon[hanger.name].hooks.keys()
origin_hooks.update(matching_hooks)
for door_hook in matching_hooks:
new_h_type = hook_from_door(door_hook)
if new_h_type not in accessible_hook_types:
accessible_hook_types.append(new_h_type)
return len(all_hangers.difference(hanger_matching)) == 0
def winnow_hangers(hangers, hooks):
removal_info = []
for hanger, door_set in hangers.items():
for door in door_set:
hook_set = hooks[hanger]
if len(hook_set) == 0:
removal_info.append((hanger, door))
else:
found_valid = False
for door_hook, crystal, orig_hanger in hook_set:
if orig_hanger != door:
found_valid = True
if not found_valid:
removal_info.append((hanger, door))
for hanger, door in removal_info:
hangers[hanger].remove(door)
def create_graph_piece_from_state(door, o_state, b_state, proposed_map):
# todo: info about dungeon events - not sure about that
graph_piece = GraphPiece()
all_unattached = {}
for exp_d in o_state.unattached_doors:
all_unattached[exp_d.door] = exp_d.crystal
for exp_d in b_state.unattached_doors:
d = exp_d.door
if d in all_unattached.keys():
if all_unattached[d] != exp_d.crystal:
if all_unattached[d] == CrystalBarrier.Orange and exp_d.crystal == CrystalBarrier.Blue:
all_unattached[d] = CrystalBarrier.Null
else:
logging.getLogger('').warning('Mismatched state @ %s (o:%s b:%s)', d.name, all_unattached[d], exp_d.crystal)
else:
all_unattached[exp_d.door] = exp_d.crystal
for d, crystal in all_unattached.items():
if (door is None or d != door) and not d.blocked and d not in proposed_map.keys():
graph_piece.hooks[d] = crystal
graph_piece.hanger_info = door
graph_piece.visited_regions.update(o_state.visited_blue)
graph_piece.visited_regions.update(o_state.visited_orange)
graph_piece.visited_regions.update(b_state.visited_blue)
graph_piece.visited_regions.update(b_state.visited_orange)
return graph_piece
def parent_region(door, world, player):
return world.get_entrance(door.name, player)
def hook_from_door(door):
if door.type == DoorType.SpiralStairs:
return Hook.Stairs
if door.type == DoorType.Normal:
dir = {
Direction.North: Hook.North,
Direction.South: Hook.South,
Direction.West: Hook.West,
Direction.East: Hook.East,
}
return dir[door.direction]
return None
def hanger_from_door(door):
if door.type == DoorType.SpiralStairs:
return Hook.Stairs
if door.type == DoorType.Normal:
dir = {
Direction.North: Hook.South,
Direction.South: Hook.North,
Direction.West: Hook.East,
Direction.East: Hook.West,
}
return dir[door.direction]
return None
def connect_doors(a, b, world, player):
# Return on unsupported types.
if a.type in [DoorType.Open, DoorType.StraightStairs, DoorType.Hole, DoorType.Warp, DoorType.Ladder,
DoorType.Interior, DoorType.Logical]:
return
# Connect supported types
if a.type == DoorType.Normal or a.type == DoorType.SpiralStairs:
if a.blocked:
connect_one_way(world, b.name, a.name, player)
elif b.blocked:
connect_one_way(world, a.name, b.name, player)
else:
connect_two_way(world, a.name, b.name, player)
return
# If we failed to account for a type, panic
raise RuntimeError('Unknown door type ' + a.type.name)
def connect_two_way(world, entrancename, exitname, player):
entrance = world.get_entrance(entrancename, player)
ext = world.get_entrance(exitname, player)
# if these were already connected somewhere, remove the backreference
if entrance.connected_region is not None:
entrance.connected_region.entrances.remove(entrance)
if ext.connected_region is not None:
ext.connected_region.entrances.remove(ext)
entrance.connect(ext.parent_region)
ext.connect(entrance.parent_region)
if entrance.parent_region.dungeon:
ext.parent_region.dungeon = entrance.parent_region.dungeon
x = world.check_for_door(entrancename, player)
y = world.check_for_door(exitname, player)
if x is not None:
x.dest = y
if y is not None:
y.dest = x
def connect_one_way(world, entrancename, exitname, player):
entrance = world.get_entrance(entrancename, player)
ext = world.get_entrance(exitname, player)
# if these were already connected somewhere, remove the backreference
if entrance.connected_region is not None:
entrance.connected_region.entrances.remove(entrance)
if ext.connected_region is not None:
ext.connected_region.entrances.remove(ext)
entrance.connect(ext.parent_region)
if entrance.parent_region.dungeon:
ext.parent_region.dungeon = entrance.parent_region.dungeon
x = world.check_for_door(entrancename, player)
y = world.check_for_door(exitname, player)
if x is not None:
x.dest = y
if y is not None:
y.dest = x
class ExplorationState(object):
def __init__(self, init_crystal=CrystalBarrier.Orange):
self.unattached_doors = []
self.avail_doors = []
self.event_doors = []
self.visited_orange = []
self.visited_blue = []
self.events = set()
self.crystal = init_crystal
# key region stuff
self.door_krs = {}
# key validation stuff
self.small_doors = []
self.big_doors = []
self.opened_doors = []
self.big_key_opened = False
self.big_key_special = False
self.found_locations = []
self.ttl_locations = 0
self.used_locations = 0
self.key_locations = 0
self.used_smalls = 0
self.non_door_entrances = []
def copy(self):
ret = ExplorationState()
ret.unattached_doors = list(self.unattached_doors)
ret.avail_doors = list(self.avail_doors)
ret.event_doors = list(self.event_doors)
ret.visited_orange = list(self.visited_orange)
ret.visited_blue = list(self.visited_blue)
ret.events = set(self.events)
ret.crystal = self.crystal
ret.door_krs = self.door_krs.copy()
ret.small_doors = list(self.small_doors)
ret.big_doors = list(self.big_doors)
ret.opened_doors = list(self.opened_doors)
ret.big_key_opened = self.big_key_opened
ret.big_key_special = self.big_key_special
ret.ttl_locations = self.ttl_locations
ret.key_locations = self.key_locations
ret.used_locations = self.used_locations
ret.used_smalls = self.used_smalls
ret.found_locations = list(self.found_locations)
ret.non_door_entrances = list(self.non_door_entrances)
return ret
def next_avail_door(self):
exp_door = self.avail_doors.pop()
self.crystal = exp_door.crystal
return exp_door
def visit_region(self, region, key_region=None, key_checks=False):
if self.crystal == CrystalBarrier.Either:
if region not in self.visited_blue:
self.visited_blue.append(region)
if region not in self.visited_orange:
self.visited_orange.append(region)
elif self.crystal == CrystalBarrier.Orange:
self.visited_orange.append(region)
elif self.crystal == CrystalBarrier.Blue:
self.visited_blue.append(region)
for location in region.locations:
if key_checks and location not in self.found_locations:
if location.name in key_only_locations:
self.key_locations += 1
if location.name not in dungeon_events and '- Prize' not in location.name:
self.ttl_locations += 1
if location not in self.found_locations:
self.found_locations.append(location)
if location.name in dungeon_events and location.name not in self.events:
if self.flooded_key_check(location):
self.perform_event(location.name, key_region)
if location.name in flooded_keys_reverse.keys() and self.location_found(flooded_keys_reverse[location.name]):
self.perform_event(flooded_keys_reverse[location.name], key_region)
if key_checks and region.name == 'Hyrule Dungeon Cellblock' and not self.big_key_opened:
self.big_key_opened = True
self.avail_doors.extend(self.big_doors)
self.big_doors.clear()
def flooded_key_check(self, location):
if location.name not in flooded_keys.keys():
return True
return flooded_keys[location.name] in [x.name for x in self.found_locations]
def location_found(self, location_name):
for l in self.found_locations:
if l.name == location_name:
return True
return False
def perform_event(self, location_name, key_region):
self.events.add(location_name)
queue = collections.deque(self.event_doors)
while len(queue) > 0:
exp_door = queue.pop()
if exp_door.door.req_event == location_name:
self.avail_doors.append(exp_door)
self.event_doors.remove(exp_door)
if key_region is not None:
d_name = exp_door.door.name
if d_name not in self.door_krs.keys():
self.door_krs[d_name] = key_region
def add_all_entrance_doors_check_unattached(self, region, world, player):
door_list = [x for x in get_doors(world, region, player) if x.type in [DoorType.Normal, DoorType.SpiralStairs]]
door_list.extend(get_entrance_doors(world, region, player))
for door in door_list:
if self.can_traverse(door):
if door.dest is None and not self.in_door_list_ic(door, self.unattached_doors):
self.append_door_to_list(door, self.unattached_doors)
elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, self.event_doors):
self.append_door_to_list(door, self.event_doors)
elif not self.in_door_list(door, self.avail_doors):
self.append_door_to_list(door, self.avail_doors)
for entrance in region.entrances:
door = world.check_for_door(entrance.name, player)
if door is None:
self.non_door_entrances.append(entrance)
def add_all_doors_check_unattached(self, region, world, player):
for door in get_doors(world, region, player):
if self.can_traverse(door):
if door.dest is None and not self.in_door_list_ic(door, self.unattached_doors):
self.append_door_to_list(door, self.unattached_doors)
elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, self.event_doors):
self.append_door_to_list(door, self.event_doors)
elif not self.in_door_list(door, self.avail_doors):
self.append_door_to_list(door, self.avail_doors)
def add_all_doors_check_proposed(self, region, proposed_map, valid_doors, world, player):
for door in get_dungeon_doors(region, world, player):
if self.can_traverse(door):
if door.dest is None and not self.in_door_list_ic(door, self.unattached_doors)\
and door not in proposed_map.keys() and door in valid_doors:
self.append_door_to_list(door, self.unattached_doors)
elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, self.event_doors):
self.append_door_to_list(door, self.event_doors)
elif not self.in_door_list(door, self.avail_doors):
self.append_door_to_list(door, self.avail_doors)
def add_all_doors_check_key_region(self, region, key_region, world, player):
for door in get_doors(world, region, player):
if self.can_traverse(door):
if door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, self.event_doors):
self.append_door_to_list(door, self.event_doors)
elif not self.in_door_list(door, self.avail_doors):
self.append_door_to_list(door, self.avail_doors)
if door.name not in self.door_krs.keys():
self.door_krs[door.name] = key_region
else:
if door.name not in self.door_krs.keys():
self.door_krs[door.name] = key_region
def add_all_doors_check_keys(self, region, key_door_proposal, world, player):
for door in get_doors(world, region, player):
if self.can_traverse(door):
if door in key_door_proposal and door not in self.opened_doors:
if not self.in_door_list(door, self.small_doors):
self.append_door_to_list(door, self.small_doors)
elif door.bigKey and not self.big_key_opened:
if not self.in_door_list(door, self.big_doors):
self.append_door_to_list(door, self.big_doors)
elif door.req_event is not None and door.req_event not in self.events:
if not self.in_door_list(door, self.event_doors):
self.append_door_to_list(door, self.event_doors)
elif not self.in_door_list(door, self.avail_doors):
self.append_door_to_list(door, self.avail_doors)
def visited(self, region):
if self.crystal == CrystalBarrier.Either:
return region in self.visited_blue and region in self.visited_orange
elif self.crystal == CrystalBarrier.Orange:
return region in self.visited_orange
elif self.crystal == CrystalBarrier.Blue:
return region in self.visited_blue
return False
def visited_at_all(self, region):
return region in self.visited_blue or region in self.visited_orange
def can_traverse(self, door):
if door.blocked:
return False
if door.crystal not in [CrystalBarrier.Null, CrystalBarrier.Either]:
return self.crystal == CrystalBarrier.Either or door.crystal == self.crystal
return True
def validate(self, door, region, world):
return self.can_traverse(door) and not self.visited(region) and valid_region_to_explore(region, world)
def in_door_list(self, door, door_list):
for d in door_list:
if d.door == door and d.crystal == self.crystal:
return True
return False
@staticmethod
def in_door_list_ic(door, door_list):
for d in door_list:
if d.door == door:
return True
return False
def append_door_to_list(self, door, door_list):
if door.crystal == CrystalBarrier.Null:
door_list.append(ExplorableDoor(door, self.crystal))
else:
door_list.append(ExplorableDoor(door, door.crystal))
def key_door_sort(self, d):
if d.door.smallKey:
if d.door in self.opened_doors:
return 1
else:
return 0
return 2
class ExplorableDoor(object):
def __init__(self, door, crystal):
self.door = door
self.crystal = crystal
def __str__(self):
return str(self.__unicode__())
def __unicode__(self):
return '%s (%s)' % (self.door.name, self.crystal.name)
def extend_reachable_state(search_regions, state, world, player):
local_state = state.copy()
for region in search_regions:
local_state.visit_region(region)
local_state.add_all_doors_check_unattached(region, world, player)
while len(local_state.avail_doors) > 0:
explorable_door = local_state.next_avail_door()
connect_region = world.get_entrance(explorable_door.door.name, player).connected_region
if connect_region is not None:
if valid_region_to_explore(connect_region, world) and not local_state.visited(connect_region):
local_state.visit_region(connect_region)
local_state.add_all_doors_check_unattached(connect_region, world, player)
return local_state
def extend_reachable_state_improved(search_regions, state, proposed_map, valid_doors, world, player):
local_state = state.copy()
for region in search_regions:
local_state.visit_region(region)
local_state.add_all_doors_check_proposed(region, proposed_map, valid_doors, world, player)
while len(local_state.avail_doors) > 0:
explorable_door = local_state.next_avail_door()
if explorable_door.door in proposed_map:
connect_region = world.get_entrance(proposed_map[explorable_door.door].name, player).parent_region
else:
connect_region = world.get_entrance(explorable_door.door.name, player).connected_region
if connect_region is not None:
if valid_region_to_explore(connect_region, world) and not local_state.visited(connect_region):
local_state.visit_region(connect_region)
local_state.add_all_doors_check_proposed(connect_region, proposed_map, valid_doors, world, player)
return local_state
# cross-utility methods
def valid_region_to_explore(region, world):
return region.type == RegionType.Dungeon or region.name in world.inaccessible_regions
def get_doors(world, region, player):
res = []
for exit in region.exits:
door = world.check_for_door(exit.name, player)
if door is not None:
res.append(door)
return res
def get_dungeon_doors(region, world, player):
res = []
for ext in region.exits:
door = world.check_for_door(ext.name, player)
if door is not None and ext.parent_region.type == RegionType.Dungeon:
res.append(door)
return res
def get_entrance_doors(world, region, player):
res = []
for exit in region.entrances:
door = world.check_for_door(exit.name, player)
if door is not None:
res.append(door)
return res
def convert_regions(region_names, world, player):
region_list = []
for name in region_names:
region_list.append(world.get_region(name, player))
return region_list

View File

@@ -287,4 +287,27 @@ split_region_starts = {
] ]
} }
dungeon_keys = {
'Hyrule Castle': 'Small Key (Escape)',
'Eastern Palace': 'Small Key (Eastern Palace)',
'Desert Palace': 'Small Key (Desert Palace)',
'Tower of Hera': 'Small Key (Tower of Hera)',
'Agahnims Tower': 'Small Key (Agahnims Tower)',
'Palace of Darkness': 'Small Key (Palace of Darkness)',
'Swamp Palace': 'Small Key (Swamp Palace)',
'Skull Woods': 'Small Key (Skull Woods)',
'Thieves Town': 'Small Key (Thieves Town)'
}
dungeon_bigs = {
'Hyrule Castle': 'Big Key (Escape)',
'Eastern Palace': 'Big Key (Eastern Palace)',
'Desert Palace': 'Big Key (Desert Palace)',
'Tower of Hera': 'Big Key (Tower of Hera)',
'Agahnims Tower': 'Big Key (Agahnims Tower)',
'Palace of Darkness': 'Big Key (Palace of Darkness)',
'Swamp Palace': 'Big Key (Swamp Palace)',
'Skull Woods': 'Big Key (Skull Woods)',
'Thieves Town': 'Big Key (Thieves Town)'
}

View File

@@ -282,6 +282,7 @@ def copy_world(world):
ret.rooms = world.rooms ret.rooms = world.rooms
ret.inaccessible_regions = world.inaccessible_regions ret.inaccessible_regions = world.inaccessible_regions
ret.dungeon_layouts = world.dungeon_layouts ret.dungeon_layouts = world.dungeon_layouts
ret.key_logic = world.key_logic
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
set_rules(ret, player) set_rules(ret, player)
@@ -337,7 +338,7 @@ def create_playthrough(world):
sphere = [] sphere = []
# build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres # build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
for location in sphere_candidates: for location in sphere_candidates:
if state.can_reach(location): if state.can_reach(location) and state.not_flooding_a_key(world, location):
sphere.append(location) sphere.append(location)
for location in sphere: for location in sphere:

View File

@@ -647,11 +647,6 @@ dungeon_events = [
'Revealing Light' 'Revealing Light'
] ]
flooded_keys = {
'Trench 1 Switch': 'Swamp Palace - Trench 1 Pot Key',
'Trench 2 Switch': 'Swamp Palace - Trench 2 Pot Key'
}
flooded_keys_reverse = { flooded_keys_reverse = {
'Swamp Palace - Trench 1 Pot Key': 'Trench 1 Switch', 'Swamp Palace - Trench 1 Pot Key': 'Trench 1 Switch',
'Swamp Palace - Trench 2 Pot Key': 'Trench 2 Switch' 'Swamp Palace - Trench 2 Pot Key': 'Trench 2 Switch'

View File

@@ -2,7 +2,8 @@ import collections
from collections import defaultdict from collections import defaultdict
import logging import logging
from BaseClasses import CollectionState, DoorType from BaseClasses import CollectionState, DoorType
from DoorShuffle import ExplorationState from DungeonGenerator import ExplorationState
from Regions import key_only_locations
def set_rules(world, player): def set_rules(world, player):
@@ -257,11 +258,8 @@ def global_rules(world, player):
set_rule(world.get_location('Mimic Cave', player), lambda state: state.has('Hammer', player)) set_rule(world.get_location('Mimic Cave', player), lambda state: state.has('Hammer', player))
# Start of door rando rules # Start of door rando rules
# TODO: Do these need to flag off when door rando is off? # TODO: Do these need to flag off when door rando is off? - some of them, yes
# If these generate fine rules with vanilla shuffle - then no. add_key_logic_rules(world, player) # todo - vanilla shuffle rules
# Escape/ Hyrule Castle
generate_key_logic('Hyrule Castle', 'Small Key (Escape)', world, player)
# Eastern Palace # Eastern Palace
# Eyegore room needs a bow # Eyegore room needs a bow
@@ -272,7 +270,6 @@ def global_rules(world, player):
forbid_item(world.get_location('Eastern Palace - Big Chest', player), 'Big Key (Eastern Palace)', player) forbid_item(world.get_location('Eastern Palace - Big Chest', player), 'Big Key (Eastern Palace)', player)
set_rule(world.get_entrance('Eastern Big Key NE', player), lambda state: state.has('Big Key (Eastern Palace)', player)) set_rule(world.get_entrance('Eastern Big Key NE', player), lambda state: state.has('Big Key (Eastern Palace)', player))
set_rule(world.get_entrance('Eastern Courtyard N', player), lambda state: state.has('Big Key (Eastern Palace)', player)) set_rule(world.get_entrance('Eastern Courtyard N', player), lambda state: state.has('Big Key (Eastern Palace)', player))
generate_key_logic('Eastern Palace', 'Small Key (Eastern Palace)', world, player)
# Boss rules. Same as below but no BK or arrow requirement. # Boss rules. Same as below but no BK or arrow requirement.
set_defeat_dungeon_boss_rule(world.get_location('Eastern Palace - Prize', player)) set_defeat_dungeon_boss_rule(world.get_location('Eastern Palace - Prize', player))
@@ -286,7 +283,6 @@ def global_rules(world, player):
set_rule(world.get_entrance('Desert Wall Slide NW', player), lambda state: state.has_fire_source(player)) set_rule(world.get_entrance('Desert Wall Slide NW', player), lambda state: state.has_fire_source(player))
set_defeat_dungeon_boss_rule(world.get_location('Desert Palace - Prize', player)) set_defeat_dungeon_boss_rule(world.get_location('Desert Palace - Prize', player))
set_defeat_dungeon_boss_rule(world.get_location('Desert Palace - Boss', player)) set_defeat_dungeon_boss_rule(world.get_location('Desert Palace - Boss', player))
generate_key_logic('Desert Palace', 'Small Key (Desert Palace)', world, player)
# Tower of Hera # Tower of Hera
set_rule(world.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player)) set_rule(world.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player))
@@ -296,10 +292,8 @@ def global_rules(world, player):
set_rule(world.get_entrance('Hera Startile Corner NW', player), lambda state: state.has('Big Key (Tower of Hera)', player)) set_rule(world.get_entrance('Hera Startile Corner NW', player), lambda state: state.has('Big Key (Tower of Hera)', player))
set_defeat_dungeon_boss_rule(world.get_location('Tower of Hera - Boss', player)) set_defeat_dungeon_boss_rule(world.get_location('Tower of Hera - Boss', player))
set_defeat_dungeon_boss_rule(world.get_location('Tower of Hera - Prize', player)) set_defeat_dungeon_boss_rule(world.get_location('Tower of Hera - Prize', player))
generate_key_logic('Tower of Hera', 'Small Key (Tower of Hera)', world, player)
set_rule(world.get_entrance('Tower Altar NW', player), lambda state: state.has_sword(player)) set_rule(world.get_entrance('Tower Altar NW', player), lambda state: state.has_sword(player))
generate_key_logic('Agahnims Tower', 'Small Key (Agahnims Tower)', world, player)
set_rule(world.get_entrance('PoD Mimics 1 NW', player), lambda state: state.can_shoot_arrows(player)) set_rule(world.get_entrance('PoD Mimics 1 NW', player), lambda state: state.can_shoot_arrows(player))
set_rule(world.get_entrance('PoD Mimics 2 NW', player), lambda state: state.can_shoot_arrows(player)) set_rule(world.get_entrance('PoD Mimics 2 NW', player), lambda state: state.can_shoot_arrows(player))
@@ -313,7 +307,6 @@ def global_rules(world, player):
set_rule(world.get_entrance('PoD Dark Pegs Up Ladder', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('PoD Dark Pegs Up Ladder', player), lambda state: state.has('Hammer', player))
set_defeat_dungeon_boss_rule(world.get_location('Palace of Darkness - Boss', player)) set_defeat_dungeon_boss_rule(world.get_location('Palace of Darkness - Boss', player))
set_defeat_dungeon_boss_rule(world.get_location('Palace of Darkness - Prize', player)) set_defeat_dungeon_boss_rule(world.get_location('Palace of Darkness - Prize', player))
generate_key_logic('Palace of Darkness', 'Small Key (Palace of Darkness)', world, player)
set_rule(world.get_entrance('Swamp Lobby Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player)) set_rule(world.get_entrance('Swamp Lobby Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player))
set_rule(world.get_entrance('Swamp Trench 1 Approach Dry', player), lambda state: not state.has('Trench 1 Filled', player)) set_rule(world.get_entrance('Swamp Trench 1 Approach Dry', player), lambda state: not state.has('Trench 1 Filled', player))
@@ -348,7 +341,6 @@ def global_rules(world, player):
forbid_item(world.get_location('Swamp Palace - Big Chest', player), 'Big Key (Swamp Palace)', player) forbid_item(world.get_location('Swamp Palace - Big Chest', player), 'Big Key (Swamp Palace)', player)
set_defeat_dungeon_boss_rule(world.get_location('Swamp Palace - Boss', player)) set_defeat_dungeon_boss_rule(world.get_location('Swamp Palace - Boss', player))
set_defeat_dungeon_boss_rule(world.get_location('Swamp Palace - Prize', player)) set_defeat_dungeon_boss_rule(world.get_location('Swamp Palace - Prize', player))
generate_key_logic('Swamp Palace', 'Small Key (Swamp Palace)', world, player)
set_rule(world.get_entrance('Skull Big Chest Hookpath', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Skull Big Chest Hookpath', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player)) set_rule(world.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player))
@@ -358,11 +350,10 @@ def global_rules(world, player):
set_rule(world.get_entrance('Skull Vines NW', player), lambda state: state.has_sword(player)) set_rule(world.get_entrance('Skull Vines NW', player), lambda state: state.has_sword(player))
set_defeat_dungeon_boss_rule(world.get_location('Skull Woods - Boss', player)) set_defeat_dungeon_boss_rule(world.get_location('Skull Woods - Boss', player))
set_defeat_dungeon_boss_rule(world.get_location('Skull Woods - Prize', player)) set_defeat_dungeon_boss_rule(world.get_location('Skull Woods - Prize', player))
generate_key_logic('Skull Woods', 'Small Key (Skull Woods)', world, player)
set_rule(world.get_entrance('Thieves BK Corner NE', player), lambda state: state.has('Big Key (Thieves Town)', player)) set_rule(world.get_entrance('Thieves BK Corner NE', player), lambda state: state.has('Big Key (Thieves Town)', player))
# blind can't have the small key? - not necessarily true anymore - but likely still # blind can't have the small key? - not necessarily true anymore - but likely still
set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: (state.has('Big Key (Thieves Town)') and state.has('Hammer', player))) set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: (state.has('Big Key (Thieves Town)', player) and state.has('Hammer', player)))
if world.accessibility == 'locations': if world.accessibility == 'locations':
forbid_item(world.get_location('Thieves\' Town - Big Chest', player), 'Big Key (Thieves Town)', player) forbid_item(world.get_location('Thieves\' Town - Big Chest', player), 'Big Key (Thieves Town)', player)
for entrance in ['Thieves Basement Block Path', 'Thieves Blocked Entry Path', 'Thieves Conveyor Block Path', 'Thieves Conveyor Bridge Block Path']: for entrance in ['Thieves Basement Block Path', 'Thieves Blocked Entry Path', 'Thieves Conveyor Block Path', 'Thieves Conveyor Bridge Block Path']:
@@ -375,7 +366,6 @@ def global_rules(world, player):
set_rule(world.get_location('Revealing Light', player), lambda state: state.has('Shining Light', player) and state.has('Maiden Rescued', player)) set_rule(world.get_location('Revealing Light', player), lambda state: state.has('Shining Light', player) and state.has('Maiden Rescued', player))
set_rule(world.get_location('Thieves\' Town - Boss', player), lambda state: state.has('Maiden Unmasked', player) and world.get_location('Thieves\' Town - Boss', player).parent_region.dungeon.boss.can_defeat(state)) set_rule(world.get_location('Thieves\' Town - Boss', player), lambda state: state.has('Maiden Unmasked', player) and world.get_location('Thieves\' Town - Boss', player).parent_region.dungeon.boss.can_defeat(state))
set_rule(world.get_location('Thieves\' Town - Prize', player), lambda state: state.has('Maiden Unmasked', player) and world.get_location('Thieves\' Town - Prize', player).parent_region.dungeon.boss.can_defeat(state)) set_rule(world.get_location('Thieves\' Town - Prize', player), lambda state: state.has('Maiden Unmasked', player) and world.get_location('Thieves\' Town - Prize', player).parent_region.dungeon.boss.can_defeat(state))
generate_key_logic('Thieves Town', 'Small Key (Thieves Town)', world, player)
# End of door rando rules. # End of door rando rules.
@@ -1691,6 +1681,18 @@ def set_inverted_bunny_rules(world, player):
add_rule(location, get_rule_to_add(location.parent_region)) add_rule(location, get_rule_to_add(location.parent_region))
def add_key_logic_rules(world, player):
logger = logging.getLogger('')
key_logic = world.key_logic[player]
for d_name, d_logic in key_logic.items():
for door_name, keys in d_logic.door_rules.items():
logger.debug(' %s needs %s keys', door_name, keys)
add_rule(world.get_entrance(door_name, player), create_key_rule(d_logic.small_key_name, player, keys))
for location in d_logic.bk_restricted:
if location.name not in key_only_locations.keys():
forbid_item(location, d_logic.bk_name, player)
def generate_key_logic(dungeon_name, small_key_name, world, player): def generate_key_logic(dungeon_name, small_key_name, world, player):
sector, start_region_names = world.dungeon_layouts[player][dungeon_name] sector, start_region_names = world.dungeon_layouts[player][dungeon_name]
logger = logging.getLogger('') logger = logging.getLogger('')