Revamped dungeon generation
Revamped key logic generation Prevent key floods in playthrough/can_beat_game checks
This commit is contained in:
@@ -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'
|
||||||
|
}
|
||||||
|
|||||||
549
DoorShuffle.py
549
DoorShuffle.py
@@ -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):
|
||||||
|
|||||||
8
Doors.py
8
Doors.py
@@ -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
689
DungeonGenerator.py
Normal 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
|
||||||
23
Dungeons.py
23
Dungeons.py
@@ -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)'
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
3
Main.py
3
Main.py
@@ -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:
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
32
Rules.py
32
Rules.py
@@ -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('')
|
||||||
|
|||||||
Reference in New Issue
Block a user