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.dungeon_layouts = {}
self.inaccessible_regions = []
self.key_logic = {}
def intialize_regions(self):
for region in self.regions:
@@ -328,7 +329,7 @@ class World(object):
sphere = []
# 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:
if location.can_reach(state):
if location.can_reach(state) and state.not_flooding_a_key(state.world, location):
sphere.append(location)
if not sphere:
@@ -414,14 +415,17 @@ class CollectionState(object):
def _do_not_flood_the_keys(self, reachable_events):
adjusted_checks = list(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:
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:
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 len(adjusted_checks) < len(reachable_events):
return adjusted_checks
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):
if count == 1:
return (item, player) in self.prog_items
@@ -948,7 +952,8 @@ class Door(object):
# logical properties
# self.connected = False # combine with Dest?
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.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)
@@ -1004,6 +1009,10 @@ class Door(object):
self.blocked = True
return self
def no_entrance(self):
self.stonewall = True
return self
def trap(self, trapFlag):
self.trapFlag = trapFlag
return self
@@ -1024,6 +1033,12 @@ class Door(object):
self.crystal = CrystalBarrier.Either
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):
return str(self.__unicode__())
@@ -1419,3 +1434,9 @@ class Spoiler(object):
path_listings.append("{}\n {}".format(location, "\n => ".join(path_lines)))
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 collections
from collections import defaultdict
import logging
import operator as op
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 dungeon_regions, region_starts, split_region_starts
from Regions import key_only_locations, dungeon_events, flooded_keys, flooded_keys_reverse
from Dungeons import dungeon_regions, region_starts, split_region_starts, dungeon_keys, dungeon_bigs
from RoomData import DoorKind, PairedDoor
from DungeonGenerator import ExplorationState, extend_reachable_state, convert_regions, generate_dungeon
def link_doors(world, player):
@@ -129,6 +130,16 @@ paired_directions = {
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):
return oppositemap[direction]
@@ -455,12 +466,13 @@ def experiment(world, player):
split_sectors = split_up_sectors(sector_list, split_region_starts[key])
for idx, sub_sector_list in enumerate(split_sectors):
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:
dungeon_sectors.append((key, sector_list, region_starts[key]))
dungeon_layouts = []
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
dungeon_layouts.append((ds, entrance_list))
@@ -619,6 +631,14 @@ def shuffle_sectors(buckets, candidates):
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
def find_proposal_monte_carlo(proposal, buckets, candidates):
n = len(candidates)
@@ -666,253 +686,6 @@ def find_proposal(proposal, buckets, candidates):
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):
local_state = state.copy()
for region in search_regions:
@@ -929,6 +702,7 @@ def extend_state_backward(search_regions, state, world, player):
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: prevent crystal barrier dead ends
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:
random.shuffle(sector.outstanding_doors)
entrance_regions = []
# current_sector = None
for region_name in entrance_region_names:
entrance_regions.append(world.get_region(region_name, player))
entrance_regions = convert_regions(entrance_region_names, world, player)
state = extend_reachable_state(entrance_regions, ExplorationState(), world, player)
# 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
elif early_loop_dies(door_a, sector_a, sector_b, available_sectors):
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
elif door_a.blocked and door_b.blocked: # I can't see this going well unless we are in loop generation...
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)
if needed and len(visited_regions.intersection(region_set)) == 0 and door.direction in directions:
hooks_needed += 1
elif door.direction in hook_directions:
outstanding_hooks += 1
visited_regions.update(region_set)
elif door.direction in hook_directions and not door.blocked:
if opposing_hooks > 0 and more_than_one_hook(local_state, hook_directions):
needed = False
visited_regions.update(region_set)
if not needed:
outstanding_hooks += 1
visited_regions.update(region_set)
if not needed:
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):
@@ -1191,6 +964,179 @@ def logical_dead_end(door_a, door_b, state, world, player, available_sectors, re
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):
if door == door_b or door.type != d_type:
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)
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):
cnt = 0
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)
itr = 0
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
if itr >= combinations:
logging.getLogger('').info('Lowering key door count because no valid layouts: %s', dungeon_sector.name)
@@ -1275,10 +1230,23 @@ def shuffle_key_doors(dungeon_sector, entrances, world, player):
combinations = ncr(len(paired_candidates), num_key_doors)
itr = 0
proposal = kth_combination(itr, paired_candidates, num_key_doors)
key_logic = KeyLogic(dungeon_sector.name)
# 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)
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):
paired_list = []
queue = collections.deque(flat_list)
@@ -1361,7 +1329,7 @@ def ncr(n, r):
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)
state = ExplorationState()
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.add_all_doors_check_keys(region, flat_proposal, world, player)
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
# 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):
return False
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:
state_copy = state.copy()
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]
state_copy.small_doors[:] = [x for x in state_copy.small_doors if x.door != dest_door]
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_smalls += 1
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)
valid = validate_key_layout_r(state_copy, flat_proposal, checked_states, key_logic, world, player)
if valid:
checked_states.add(code)
if not 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
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):
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:
state_id += '1' if d in state.opened_doors else '0'
return state_id
s_id += '1' if d in state.opened_doors else '0'
return s_id
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 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),
# todo: we need a new flag for a door that has a wall on it - you have to traverse it one particular way first
# 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 Wall Slide NW', Nrml).dir(Direction.North, 0x43, Left, High).big_key().pos(0).no_entrance(),
create_door(player, 'Desert Boss SW', Nrml).dir(Direction.South, 0x33, Left, High).no_exit().trap(0x4).pos(0),
# 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 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),
# todo: we need a new flag for a door that has a wall on it - you have to traverse it one particular way first
# 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 Bow Statue Down Ladder', Lddr).no_entrance(),
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 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.inaccessible_regions = world.inaccessible_regions
ret.dungeon_layouts = world.dungeon_layouts
ret.key_logic = world.key_logic
for player in range(1, world.players + 1):
set_rules(ret, player)
@@ -337,7 +338,7 @@ def create_playthrough(world):
sphere = []
# 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:
if state.can_reach(location):
if state.can_reach(location) and state.not_flooding_a_key(world, location):
sphere.append(location)
for location in sphere:

View File

@@ -647,11 +647,6 @@ dungeon_events = [
'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 = {
'Swamp Palace - Trench 1 Pot Key': 'Trench 1 Switch',
'Swamp Palace - Trench 2 Pot Key': 'Trench 2 Switch'

View File

@@ -2,7 +2,8 @@ import collections
from collections import defaultdict
import logging
from BaseClasses import CollectionState, DoorType
from DoorShuffle import ExplorationState
from DungeonGenerator import ExplorationState
from Regions import key_only_locations
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))
# Start of door rando rules
# TODO: Do these need to flag off when door rando is off?
# If these generate fine rules with vanilla shuffle - then no.
# Escape/ Hyrule Castle
generate_key_logic('Hyrule Castle', 'Small Key (Escape)', world, player)
# TODO: Do these need to flag off when door rando is off? - some of them, yes
add_key_logic_rules(world, player) # todo - vanilla shuffle rules
# Eastern Palace
# 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)
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))
generate_key_logic('Eastern Palace', 'Small Key (Eastern Palace)', world, player)
# Boss rules. Same as below but no BK or arrow requirement.
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_defeat_dungeon_boss_rule(world.get_location('Desert Palace - Prize', 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
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_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))
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))
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 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_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))
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 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)
set_defeat_dungeon_boss_rule(world.get_location('Swamp Palace - Boss', 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_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_defeat_dungeon_boss_rule(world.get_location('Skull Woods - Boss', 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))
# 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':
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']:
@@ -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('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))
generate_key_logic('Thieves Town', 'Small Key (Thieves Town)', world, player)
# 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))
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):
sector, start_region_names = world.dungeon_layouts[player][dungeon_name]
logger = logging.getLogger('')