Crystaroller Stairs fixed More Full ER support Added DungeonGen check for hangers without enough hooks DungeonGen doesn't consider BK door problems unless starting from origin --This could cause some longer gen times - as the origin is hooked up last Skull 3 Exit - attempt to fix
803 lines
33 KiB
Python
803 lines
33 KiB
Python
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.hanger_crystal = 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
|
|
itr = 0
|
|
finished = False
|
|
# last_choice = None
|
|
while not finished:
|
|
# what are my choices?
|
|
itr += 1
|
|
if itr > 5000:
|
|
raise Exception('Generation taking too long. Ref %s' % entrance_region_names[0])
|
|
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:
|
|
if len(proposed_map) == len(doors_to_connect):
|
|
finished = True
|
|
continue
|
|
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
|
|
if depth < 0:
|
|
raise Exception('Invalid dungeon. Ref %s' % entrance_region_names[0])
|
|
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, True, world, player)
|
|
dungeon['Origin'] = create_graph_piece_from_state(None, original_state, original_state, proposed_map)
|
|
doors_to_connect = set()
|
|
hanger_set = set()
|
|
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():
|
|
hanger_set.add(door)
|
|
parent = parent_region(door, world, player).parent_region
|
|
o_state = extend_reachable_state_improved([parent], ExplorationState(), proposed_map, valid_doors, False, 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
|
|
check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, valid_doors, world, player)
|
|
|
|
# catalog hooks: Dict<Hook, Set<Door, Crystal, Door>>
|
|
# and hangers: Dict<Hang, Set<Door>>
|
|
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 check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, valid_doors, world, player):
|
|
not_blue = set()
|
|
not_blue.update(hanger_set)
|
|
doors_to_check = set()
|
|
doors_to_check.update(hanger_set) # doors to check, check everything on first pass
|
|
blue_hooks = []
|
|
blue_hangers = []
|
|
new_blues = True
|
|
while new_blues:
|
|
new_blues = False
|
|
for door in doors_to_check:
|
|
piece = dungeon[door.name]
|
|
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:
|
|
new_blues = True
|
|
blue_hooks.append(h_type)
|
|
if piece.hanger_crystal == CrystalBarrier.Either:
|
|
h_type = hanger_from_door(piece.hanger_info)
|
|
if h_type not in blue_hangers:
|
|
new_blues = True
|
|
blue_hangers.append(h_type)
|
|
doors_to_check = set()
|
|
for door in not_blue: # am I now blue?
|
|
hang_type = hanger_from_door(door) # am I hangable on a hook?
|
|
hook_type = hook_from_door(door) # am I hookable onto a hanger?
|
|
if (hang_type in blue_hooks and not door.stonewall) or hook_type in blue_hangers:
|
|
explore_blue_state(door, dungeon, o_state_cache[door.name], proposed_map, valid_doors, world, player)
|
|
doors_to_check.add(door)
|
|
not_blue.difference_update(doors_to_check)
|
|
|
|
|
|
def explore_blue_state(door, dungeon, o_state, proposed_map, valid_doors, world, player):
|
|
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, False, world, player)
|
|
dungeon[door.name] = create_graph_piece_from_state(door, o_state, b_state, proposed_map)
|
|
|
|
|
|
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
|
|
must_hang = defaultdict(list)
|
|
all_hooks = set()
|
|
for key in hooks.keys():
|
|
for hook in hooks[key]:
|
|
all_hooks.add(hook[0])
|
|
for key in hangers.keys():
|
|
for hanger in hangers[key]:
|
|
if hanger not in all_hooks:
|
|
must_hang[key].append(hanger)
|
|
for key in must_hang.keys():
|
|
if len(must_hang[key]) > len(hooks[key]):
|
|
return False
|
|
outstanding_doors = defaultdict(list)
|
|
for d in doors_to_connect:
|
|
if d not in proposed_map.keys():
|
|
outstanding_doors[hook_from_door(d)].append(d)
|
|
for key in outstanding_doors.keys():
|
|
opp_key = opposite_h_type(key)
|
|
if len(outstanding_doors[key]) > 0 and len(hangers[key]) == 0 and len(hooks[opp_key]) == 0:
|
|
return False
|
|
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
|
|
break
|
|
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
|
|
elif all_unattached[d] == CrystalBarrier.Blue and exp_d.crystal == CrystalBarrier.Orange:
|
|
# the swapping case
|
|
logging.getLogger('').warning('Mismatched state @ %s (o:%s b:%s)', d.name, all_unattached[d], exp_d.crystal)
|
|
elif all_unattached[d] == CrystalBarrier.Either:
|
|
all_unattached[d] = exp_d.crystal # pessimism, and if not this, leave it alone
|
|
else:
|
|
all_unattached[exp_d.door] = exp_d.crystal
|
|
h_crystal = door.crystal if door is not None else None
|
|
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
|
|
if d == door:
|
|
h_crystal = crystal
|
|
graph_piece.hanger_info = door
|
|
graph_piece.hanger_crystal = h_crystal
|
|
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 opposite_h_type(h_type):
|
|
type_map = {
|
|
Hook.Stairs: Hook.Stairs,
|
|
Hook.North: Hook.South,
|
|
Hook.South: Hook.North,
|
|
Hook.West: Hook.East,
|
|
Hook.East: Hook.West,
|
|
|
|
}
|
|
return type_map[h_type]
|
|
|
|
|
|
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)
|
|
dep_doors, target = [], None
|
|
if len(a.dependents) > 0:
|
|
dep_doors, target = a.dependents, b
|
|
elif len(b.dependents) > 0:
|
|
dep_doors, target = b.dependents, a
|
|
if target is not None:
|
|
target_region = world.get_entrance(target.name, player).parent_region
|
|
for dep in dep_doors:
|
|
connect_simple_door(dep, target_region, world, 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
|
|
|
|
|
|
def connect_simple_door(exit_door, region, world, player):
|
|
world.get_entrance(exit_door.name, player).connect(region)
|
|
exit_door.dest = region
|
|
|
|
|
|
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, isOrigin, world, player):
|
|
for door in get_doors(world, region, player):
|
|
if self.can_traverse_bk_check(door, isOrigin):
|
|
if door.controller is not None:
|
|
door = door.controller
|
|
if door.dest is None and door not in proposed_map.keys() and door in valid_doors:
|
|
if not self.in_door_list_ic(door, self.unattached_doors):
|
|
self.append_door_to_list(door, self.unattached_doors)
|
|
else:
|
|
other = self.find_door_in_list(door, self.unattached_doors)
|
|
if self.crystal != other.crystal:
|
|
other.crystal = CrystalBarrier.Either
|
|
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 can_traverse_bk_check(self, door, isOrigin):
|
|
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 not isOrigin or not door.bigKey or len(self.found_locations) > 0
|
|
# return not door.bigKey or len([x for x in self.found_locations if '- Prize' not in x.name]) > 0
|
|
|
|
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
|
|
|
|
@staticmethod
|
|
def find_door_in_list(door, door_list):
|
|
for d in door_list:
|
|
if d.door == door:
|
|
return d
|
|
return None
|
|
|
|
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, isOrigin, 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, isOrigin, 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, isOrigin, world, player)
|
|
return local_state
|
|
|
|
|
|
# cross-utility methods
|
|
def valid_region_to_explore(region, world):
|
|
return region is not None and (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
|