4244 lines
189 KiB
Python
4244 lines
189 KiB
Python
import RaceRandom as random
|
|
import collections
|
|
import itertools
|
|
from collections import defaultdict, deque
|
|
from functools import reduce
|
|
import logging
|
|
import math
|
|
import operator as op
|
|
import time
|
|
from typing import List
|
|
|
|
from BaseClasses import DoorType, Direction, CrystalBarrier, RegionType, Polarity, PolSlot, flooded_keys, Sector
|
|
from BaseClasses import Hook, hook_from_door, Door
|
|
from Regions import dungeon_events, flooded_keys_reverse
|
|
from Dungeons import split_region_starts
|
|
from RoomData import DoorKind
|
|
|
|
from source.dungeon.DungeonStitcher import generate_dungeon_find_proposal
|
|
from source.dungeon.DungeonStitcher import GenerationException as OtherGenException
|
|
|
|
|
|
class GraphPiece:
|
|
|
|
def __init__(self):
|
|
self.hanger_info = None
|
|
self.hanger_crystal = None
|
|
self.hooks = {}
|
|
self.visited_regions = set()
|
|
self.possible_bk_locations = set()
|
|
self.pinball_used = False
|
|
|
|
|
|
# Dungeons shouldn't be generated until all entrances are appropriately accessible
|
|
def pre_validate(builder, entrance_region_names, split_dungeon, world, player):
|
|
entrance_regions = convert_regions(entrance_region_names, world, player)
|
|
excluded = {}
|
|
for region in entrance_regions:
|
|
portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None)
|
|
if portal and portal.destination:
|
|
excluded[region] = None
|
|
entrance_regions = [x for x in entrance_regions if x not in excluded.keys()]
|
|
proposed_map = {}
|
|
doors_to_connect = {}
|
|
all_regions = set()
|
|
bk_special = False
|
|
for sector in builder.sectors:
|
|
for door in sector.outstanding_doors:
|
|
doors_to_connect[door.name] = door
|
|
if world.mode[player] == 'standard' and builder.name == 'Hyrule Castle Dungeon':
|
|
all_regions.update([x for x in sector.regions if x.name != 'Hyrule Castle Behind Tapestry'])
|
|
else:
|
|
all_regions.update(sector.regions)
|
|
bk_special |= check_for_special(sector.regions)
|
|
bk_needed = False
|
|
for sector in builder.sectors:
|
|
bk_needed |= determine_if_bk_needed(sector, split_dungeon, bk_special, world, player)
|
|
paths = determine_paths_for_dungeon(world, player, all_regions, builder.name)
|
|
dungeon, hangers, hooks = gen_dungeon_info(builder.name, builder.sectors, entrance_regions, all_regions,
|
|
proposed_map, doors_to_connect, bk_needed, bk_special, world, player)
|
|
return check_valid(builder.name, dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions,
|
|
bk_needed, bk_special, paths, entrance_regions, world, player)
|
|
|
|
|
|
def generate_dungeon(builder, entrance_region_names, split_dungeon, world, player):
|
|
if builder.valid_proposal: # we made this earlier in gen, just use it
|
|
proposed_map = builder.valid_proposal
|
|
else:
|
|
proposed_map = generate_dungeon_find_proposal_old(builder, entrance_region_names, split_dungeon, world, player)
|
|
builder.valid_proposal = proposed_map
|
|
queue = collections.deque(proposed_map.items())
|
|
while len(queue) > 0:
|
|
a, b = queue.popleft()
|
|
connect_doors(a, b)
|
|
queue.remove((b, a))
|
|
if len(builder.sectors) == 0:
|
|
return Sector()
|
|
available_sectors = list(builder.sectors)
|
|
master_sector = available_sectors.pop()
|
|
for sub_sector in available_sectors:
|
|
master_sector.regions.extend(sub_sector.regions)
|
|
master_sector.outstanding_doors.clear()
|
|
master_sector.r_name_set = None
|
|
return master_sector
|
|
|
|
|
|
def generate_dungeon_find_proposal_old(builder, entrance_region_names, split_dungeon, world, player):
|
|
logger = logging.getLogger('')
|
|
name = builder.name
|
|
entrance_regions = convert_regions(entrance_region_names, world, player)
|
|
excluded = {}
|
|
for region in entrance_regions:
|
|
portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None)
|
|
if portal:
|
|
if portal.destination:
|
|
excluded[region] = None
|
|
elif len(entrance_regions) > 1:
|
|
p_region = portal.door.entrance.connected_region
|
|
access_region = next(x.parent_region for x in p_region.entrances
|
|
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld])
|
|
if ((access_region.name in world.inaccessible_regions[player] and
|
|
region.name not in world.enabled_entrances[player])
|
|
or (world.mode[player] == 'standard' and access_region.name != 'Hyrule Castle Courtyard'
|
|
and 'Hyrule Castle' in builder.name)):
|
|
excluded[region] = None
|
|
else: # for non-portals, holes and sewers in std
|
|
access_region = next((x.parent_region for x in region.entrances
|
|
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]
|
|
or x.parent_region.name == 'Sewer Drop'), None)
|
|
if access_region is None:
|
|
if builder.sewers_access is None:
|
|
excluded[region] = None
|
|
else:
|
|
if access_region.name == 'Sewer Drop':
|
|
if world.mode[player] == 'standard' and (builder.sewers_access is None
|
|
or builder.sewers_access.entrance.parent_region != region):
|
|
excluded[region] = None
|
|
access_region = next(x.parent_region for x in access_region.entrances)
|
|
if (access_region.name in world.inaccessible_regions[player] and
|
|
region.name not in world.enabled_entrances[player]):
|
|
excluded[region] = None
|
|
entrance_regions = [x for x in entrance_regions if x not in excluded.keys()]
|
|
doors_to_connect = {}
|
|
all_regions = set()
|
|
bk_special = False
|
|
for sector in builder.sectors:
|
|
for door in sector.outstanding_doors:
|
|
doors_to_connect[door.name] = door
|
|
all_regions.update(sector.regions)
|
|
bk_special |= check_for_special(sector.regions)
|
|
bk_needed = False
|
|
for sector in builder.sectors:
|
|
bk_needed |= determine_if_bk_needed(sector, split_dungeon, bk_special, world, player)
|
|
proposed_map = {}
|
|
choices_master = [[]]
|
|
depth = 0
|
|
dungeon_cache = {}
|
|
backtrack = False
|
|
itr = 0
|
|
attempt = 1
|
|
finished = False
|
|
# flag if standard and this is hyrule castle
|
|
paths = determine_paths_for_dungeon(world, player, all_regions, name)
|
|
while not finished:
|
|
# what are my choices?
|
|
itr += 1
|
|
if itr > 1000:
|
|
if attempt > 9:
|
|
raise GenerationException('Generation taking too long. Ref %s' % name)
|
|
proposed_map = {}
|
|
choices_master = [[]]
|
|
depth = 0
|
|
dungeon_cache = {}
|
|
backtrack = False
|
|
itr = 0
|
|
attempt += 1
|
|
logger.debug(f'Starting new attempt {attempt}')
|
|
if depth not in dungeon_cache.keys():
|
|
dungeon, hangers, hooks = gen_dungeon_info(name, builder.sectors, entrance_regions, all_regions, proposed_map,
|
|
doors_to_connect, bk_needed, bk_special, world, player)
|
|
dungeon_cache[depth] = dungeon, hangers, hooks
|
|
if len(proposed_map) != len(doors_to_connect) and builder.name == 'Hyrule Castle Dungeon':
|
|
check_regions = all_regions.difference({world.get_region('Hyrule Castle Behind Tapestry', player)})
|
|
else:
|
|
check_regions = all_regions
|
|
valid = check_valid(name, dungeon, hangers, hooks, proposed_map, doors_to_connect, check_regions,
|
|
bk_needed, bk_special, paths, entrance_regions, world, player)
|
|
else:
|
|
dungeon, hangers, hooks = dungeon_cache[depth]
|
|
valid = True
|
|
if valid:
|
|
if len(proposed_map) == len(doors_to_connect):
|
|
if dungeon['Origin'].pinball_used:
|
|
door = world.get_door('Skull Pinball WS', player)
|
|
room = world.get_room(door.roomIndex, player)
|
|
if room.doorList[door.doorListPos][1] == DoorKind.Trap:
|
|
room.change(door.doorListPos, DoorKind.Normal)
|
|
door.trapFlag = 0x0
|
|
door.blocked = False
|
|
finished = True
|
|
continue
|
|
prev_choices = choices_master[depth]
|
|
# make a choice
|
|
hanger, hook = make_a_choice(dungeon, hangers, hooks, prev_choices, name)
|
|
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 GenerationException('Invalid dungeon. Ref %s' % name)
|
|
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)
|
|
return proposed_map
|
|
|
|
|
|
def determine_if_bk_needed(sector, split_dungeon, bk_special, world, player):
|
|
if not split_dungeon or bk_special:
|
|
for region in sector.regions:
|
|
for ext in region.exits:
|
|
door = world.check_for_door(ext.name, player)
|
|
if door is not None and door.bigKey:
|
|
return True
|
|
return False
|
|
|
|
|
|
def check_for_special(regions):
|
|
for region in regions:
|
|
for loc in region.locations:
|
|
if loc.forced_big_key():
|
|
return True
|
|
return False
|
|
|
|
|
|
def gen_dungeon_info(name, available_sectors, entrance_regions, all_regions, proposed_map, valid_doors, bk_needed, bk_special, world, player):
|
|
# step 1 create dungeon: Dict<DoorName|Origin, GraphPiece>
|
|
dungeon = {}
|
|
start = ExplorationState(dungeon=name)
|
|
start.big_key_special = bk_special
|
|
group_flags, door_map = find_bk_groups(name, available_sectors, proposed_map, bk_special)
|
|
bk_flag = False if world.bigkeyshuffle[player] and not bk_special else bk_needed
|
|
|
|
def exception(d):
|
|
return name == 'Skull Woods 2' and d.name == 'Skull Pinball WS'
|
|
original_state = extend_reachable_state_improved(entrance_regions, start, proposed_map, all_regions,
|
|
valid_doors, bk_flag, world, player, exception)
|
|
dungeon['Origin'] = create_graph_piece_from_state(None, original_state, original_state, proposed_map, exception,
|
|
world, player)
|
|
either_crystal = True # if all hooks from the origin are either, explore all bits with either
|
|
for hook, crystal in dungeon['Origin'].hooks.items():
|
|
if crystal != CrystalBarrier.Either:
|
|
either_crystal = False
|
|
break
|
|
init_crystal = CrystalBarrier.Either if either_crystal else CrystalBarrier.Orange
|
|
hanger_set = set()
|
|
o_state_cache = {}
|
|
for sector in available_sectors:
|
|
for door in sector.outstanding_doors:
|
|
if door not in proposed_map.keys():
|
|
hanger_set.add(door)
|
|
bk_flag = group_flags[door_map[door]]
|
|
parent = door.entrance.parent_region
|
|
crystal_start = CrystalBarrier.Either if parent.crystal_switch else init_crystal
|
|
init_state = ExplorationState(crystal_start, dungeon=name)
|
|
init_state.big_key_special = start.big_key_special
|
|
o_state = extend_reachable_state_improved([parent], init_state, proposed_map, all_regions,
|
|
valid_doors, bk_flag, world, player, exception)
|
|
o_state_cache[door.name] = o_state
|
|
piece = create_graph_piece_from_state(door, o_state, o_state, proposed_map, exception, world, player)
|
|
dungeon[door.name] = piece
|
|
check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, all_regions, valid_doors,
|
|
group_flags, door_map, world, player, exception)
|
|
|
|
# catalog hooks: Dict<Hook, List<Door, Crystal, Door>>
|
|
# and hangers: Dict<Hang, List<Door>>
|
|
avail_hooks = defaultdict(list)
|
|
hangers = defaultdict(list)
|
|
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].append(door_hang)
|
|
for door, crystal in piece.hooks.items():
|
|
hook = hook_from_door(door)
|
|
avail_hooks[hook].append((door, crystal, door_hang))
|
|
|
|
# thin out invalid hanger
|
|
winnow_hangers(hangers, avail_hooks)
|
|
return dungeon, hangers, avail_hooks
|
|
|
|
|
|
def find_bk_groups(name, available_sectors, proposed_map, bk_special):
|
|
groups = {}
|
|
door_ids = {}
|
|
gid = 1
|
|
for sector in available_sectors:
|
|
if bk_special:
|
|
my_gid = None
|
|
for door in sector.outstanding_doors:
|
|
if door in proposed_map and proposed_map[door] in door_ids:
|
|
if my_gid:
|
|
merge_gid = door_ids[proposed_map[door]]
|
|
for door in door_ids.keys():
|
|
if door_ids[door] == merge_gid:
|
|
door_ids[door] = my_gid
|
|
groups[my_gid] = groups[my_gid] or groups[merge_gid]
|
|
else:
|
|
my_gid = door_ids[proposed_map[door]]
|
|
if not my_gid:
|
|
my_gid = gid
|
|
gid += 1
|
|
for door in sector.outstanding_doors:
|
|
door_ids[door] = my_gid
|
|
if my_gid not in groups.keys():
|
|
groups[my_gid] = False
|
|
for region in sector.regions:
|
|
for loc in region.locations:
|
|
if loc.forced_item and loc.item.bigkey and name in loc.item.name:
|
|
groups[my_gid] = True
|
|
else:
|
|
for door in sector.outstanding_doors:
|
|
door_ids[door] = gid
|
|
groups[gid] = False
|
|
return groups, door_ids
|
|
|
|
|
|
def check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, all_regions, valid_doors, group_flags, door_map,
|
|
world, player, exception):
|
|
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.Orange:
|
|
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:
|
|
bk_flag = group_flags[door_map[door]]
|
|
explore_blue_state(door, dungeon, o_state_cache[door.name], proposed_map, all_regions, valid_doors,
|
|
bk_flag, world, player, exception)
|
|
doors_to_check.add(door)
|
|
not_blue.difference_update(doors_to_check)
|
|
|
|
|
|
def explore_blue_state(door, dungeon, o_state, proposed_map, all_regions, valid_doors, bk_flag, world, player, exception):
|
|
parent = door.entrance.parent_region
|
|
blue_start = ExplorationState(CrystalBarrier.Blue, o_state.dungeon)
|
|
blue_start.big_key_special = o_state.big_key_special
|
|
b_state = extend_reachable_state_improved([parent], blue_start, proposed_map, all_regions, valid_doors, bk_flag,
|
|
world, player, exception)
|
|
dungeon[door.name] = create_graph_piece_from_state(door, o_state, b_state, proposed_map, exception, world, player)
|
|
|
|
|
|
def make_a_choice(dungeon, hangers, avail_hooks, prev_choices, name):
|
|
# choose a hanger
|
|
all_hooks = {}
|
|
origin = dungeon['Origin']
|
|
for key in avail_hooks.keys():
|
|
for hstuff in avail_hooks[key]:
|
|
all_hooks[hstuff[0]] = None
|
|
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 = []
|
|
if len(prev_choices) > 0:
|
|
prev_hanger = prev_choices[0][0]
|
|
if prev_hanger in candidate_hangers:
|
|
stage_2_hangers.append(prev_hanger)
|
|
candidate_hangers.remove(prev_hanger)
|
|
hookable_hangers = collections.deque()
|
|
queue = collections.deque(candidate_hangers)
|
|
while len(queue) > 0:
|
|
c_hang = queue.popleft()
|
|
if c_hang in all_hooks.keys():
|
|
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))
|
|
elif name == 'Skull Woods 2' and next_hanger.name == 'Skull Pinball WS':
|
|
continue
|
|
else:
|
|
return None, None
|
|
|
|
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(name, dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions,
|
|
bk_needed, bk_special, paths, entrance_regions, world, player):
|
|
# 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 not world.bigkeyshuffle[player]:
|
|
possible_bks = len(dungeon['Origin'].possible_bk_locations)
|
|
if bk_special and check_for_special(dungeon['Origin'].visited_regions):
|
|
possible_bks = 1
|
|
true_origin_hooks = [x for x in dungeon['Origin'].hooks.keys() if not x.bigKey or possible_bks > 0 or not bk_needed]
|
|
if len(true_origin_hooks) == 0 and len(proposed_map.keys()) < len(doors_to_connect):
|
|
return False
|
|
if len(true_origin_hooks) == 0 and bk_needed and possible_bks == 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.values():
|
|
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()
|
|
bk_possible = not bk_needed or (world.bigkeyshuffle[player] and not bk_special)
|
|
for piece in dungeon.values():
|
|
all_visited.update(piece.visited_regions)
|
|
if ((not bk_possible and len(piece.possible_bk_locations) > 0) or
|
|
(bk_special and check_for_special(piece.visited_regions))):
|
|
bk_possible = True
|
|
if len(all_regions.difference(all_visited)) > 0:
|
|
return False
|
|
if not bk_possible:
|
|
return False
|
|
if not valid_paths(name, paths, entrance_regions, doors_to_connect, all_regions, proposed_map,
|
|
bk_needed, bk_special, world, player):
|
|
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 valid_paths(name, paths, entrance_regions, valid_doors, all_regions, proposed_map,
|
|
bk_needed, bk_special, world, player):
|
|
for path in paths:
|
|
if type(path) is tuple:
|
|
target = path[1]
|
|
start_regions = []
|
|
for region in all_regions:
|
|
if path[0] == region.name:
|
|
start_regions.append(region)
|
|
break
|
|
else:
|
|
target = path
|
|
start_regions = entrance_regions
|
|
if not valid_path(name, start_regions, target, valid_doors, proposed_map, all_regions,
|
|
bk_needed, bk_special, world, player):
|
|
return False
|
|
return True
|
|
|
|
|
|
def valid_path(name, starting_regions, target, valid_doors, proposed_map, all_regions,
|
|
bk_needed, bk_special, world, player):
|
|
target_regions = set()
|
|
if type(target) is not list:
|
|
for region in all_regions:
|
|
if target == region.name:
|
|
target_regions.add(region)
|
|
break
|
|
else:
|
|
for region in all_regions:
|
|
if region.name in target:
|
|
target_regions.add(region)
|
|
|
|
start = ExplorationState(dungeon=name)
|
|
start.big_key_special = bk_special
|
|
bk_flag = False if world.bigkeyshuffle[player] and not bk_special else bk_needed
|
|
|
|
def exception(d):
|
|
return name == 'Skull Woods 2' and d.name == 'Skull Pinball WS'
|
|
original_state = extend_reachable_state_improved(starting_regions, start, proposed_map, all_regions,
|
|
valid_doors, bk_flag, world, player, exception)
|
|
|
|
for exp_door in original_state.unattached_doors:
|
|
if not exp_door.door.blocked:
|
|
return True # outstanding connection possible
|
|
for target in target_regions:
|
|
if original_state.visited_at_all(target):
|
|
return True
|
|
return False # couldn't find an outstanding door or the target
|
|
|
|
|
|
def determine_required_paths(world, player):
|
|
paths = {}
|
|
for name, builder in world.dungeon_layouts[player].items():
|
|
all_regions = builder.master_sector.regions
|
|
paths[name] = determine_paths_for_dungeon(world, player, all_regions, name)
|
|
return paths
|
|
|
|
|
|
boss_path_checks = ['Eastern Boss', 'Desert Boss', 'Hera Boss', 'Tower Agahnim 1', 'PoD Boss', 'Swamp Boss',
|
|
'Skull Boss', 'Ice Boss', 'Mire Boss', 'TR Boss', 'GT Agahnim 2']
|
|
|
|
# pinball is allowed to orphan you
|
|
drop_path_checks = ['Skull Pot Circle', 'Skull Left Drop', 'Skull Back Drop', 'Sewers Rat Path']
|
|
|
|
|
|
def determine_paths_for_dungeon(world, player, all_regions, name):
|
|
all_r_names = set(x.name for x in all_regions)
|
|
paths = []
|
|
non_hole_portals = []
|
|
for portal in world.dungeon_portals[player]:
|
|
if portal.door.entrance.parent_region in all_regions:
|
|
non_hole_portals.append(portal.door.entrance.parent_region.name)
|
|
if portal.destination:
|
|
paths.append(portal.door.entrance.parent_region.name)
|
|
if world.mode[player] == 'standard':
|
|
if name == 'Hyrule Castle':
|
|
paths.append('Hyrule Dungeon Cellblock')
|
|
paths.append(('Hyrule Dungeon Cellblock', 'Sanctuary'))
|
|
if name == 'Hyrule Castle Sewers':
|
|
paths.append('Sanctuary')
|
|
if name == 'Hyrule Castle Dungeon':
|
|
paths.append('Hyrule Dungeon Cellblock')
|
|
paths.append(('Hyrule Dungeon Cellblock', 'Hyrule Castle Throne Room'))
|
|
if world.doorShuffle[player] in ['basic'] and name == 'Thieves Town':
|
|
paths.append('Thieves Attic Window')
|
|
elif 'Thieves Attic Window' in all_r_names:
|
|
paths.append('Thieves Attic Window')
|
|
for boss in boss_path_checks:
|
|
if boss in all_r_names:
|
|
paths.append(boss)
|
|
if 'Thieves Boss' in all_r_names:
|
|
paths.append('Thieves Boss')
|
|
if world.get_dungeon("Thieves Town", player).boss.enemizer_name == 'Blind':
|
|
paths.append(('Thieves Blind\'s Cell', 'Thieves Boss'))
|
|
for drop_check in drop_path_checks:
|
|
if drop_check in all_r_names:
|
|
paths.append((drop_check, non_hole_portals))
|
|
return paths
|
|
|
|
|
|
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, exception, world, player):
|
|
# 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 or exception(d))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)
|
|
graph_piece.possible_bk_locations.update(filter_for_potential_bk_locations(o_state.bk_found, world, player))
|
|
graph_piece.possible_bk_locations.update(filter_for_potential_bk_locations(b_state.bk_found, world, player))
|
|
graph_piece.pinball_used = o_state.pinball_used or b_state.pinball_used
|
|
return graph_piece
|
|
|
|
|
|
def filter_for_potential_bk_locations(locations, world, player):
|
|
return [x for x in locations if '- Big Chest' not in x.name and not reserved_location(x, world, player) and
|
|
not x.forced_item and not prize_or_event(x) and not blind_boss_unavail(x, locations, world, player)]
|
|
|
|
|
|
type_map = {
|
|
Hook.Stairs: Hook.Stairs,
|
|
Hook.North: Hook.South,
|
|
Hook.South: Hook.North,
|
|
Hook.West: Hook.East,
|
|
Hook.East: Hook.West
|
|
}
|
|
|
|
|
|
def opposite_h_type(h_type) -> Hook:
|
|
return type_map[h_type]
|
|
|
|
|
|
hang_dir_map = {
|
|
Direction.North: Hook.South,
|
|
Direction.South: Hook.North,
|
|
Direction.West: Hook.East,
|
|
Direction.East: Hook.West,
|
|
}
|
|
|
|
|
|
def hanger_from_door(door):
|
|
if door.type == DoorType.SpiralStairs:
|
|
return Hook.Stairs
|
|
if door.type in [DoorType.Normal, DoorType.Open, DoorType.StraightStairs, DoorType.Ladder]:
|
|
return hang_dir_map[door.direction]
|
|
return None
|
|
|
|
|
|
def connect_doors(a, b):
|
|
# Return on unsupported types.
|
|
if a.type in [DoorType.Hole, DoorType.Warp, DoorType.Interior, DoorType.Logical]:
|
|
return
|
|
# Connect supported types
|
|
if a.type in [DoorType.Normal, DoorType.SpiralStairs, DoorType.Open, DoorType.StraightStairs, DoorType.Ladder]:
|
|
if a.blocked:
|
|
connect_one_way(b.entrance, a.entrance)
|
|
elif b.blocked:
|
|
connect_one_way(a.entrance, b.entrance)
|
|
else:
|
|
connect_two_way(a.entrance, b.entrance)
|
|
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 = target.entrance.parent_region
|
|
for dep in dep_doors:
|
|
connect_simple_door(dep, target_region)
|
|
return
|
|
# If we failed to account for a type, panic
|
|
raise RuntimeError('Unknown door type ' + a.type.name)
|
|
|
|
|
|
def connect_two_way(entrance, ext):
|
|
|
|
# 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 = entrance.door
|
|
y = ext.door
|
|
if x is not None:
|
|
x.dest = y
|
|
if y is not None:
|
|
y.dest = x
|
|
|
|
|
|
def connect_one_way(entrance, ext):
|
|
|
|
# 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 = entrance.door
|
|
y = ext.door
|
|
if x is not None:
|
|
x.dest = y
|
|
if y is not None:
|
|
y.dest = x
|
|
|
|
|
|
def connect_simple_door(exit_door, region):
|
|
exit_door.entrance.connect(region)
|
|
exit_door.dest = region
|
|
|
|
|
|
special_big_key_doors = ['Hyrule Dungeon Cellblock Door', "Thieves Blind's Cell Door"]
|
|
std_special_big_key_doors = ['Hyrule Castle Throne Room Tapestry'] + special_big_key_doors
|
|
|
|
|
|
def get_special_big_key_doors(world, player):
|
|
if world.mode[player] == 'standard':
|
|
return std_special_big_key_doors
|
|
return special_big_key_doors
|
|
|
|
|
|
class ExplorationState(object):
|
|
|
|
def __init__(self, init_crystal=CrystalBarrier.Orange, dungeon=None):
|
|
|
|
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.bk_found = set()
|
|
|
|
self.non_door_entrances = []
|
|
self.dungeon = dungeon
|
|
self.pinball_used = False
|
|
|
|
self.prize_door_set = {}
|
|
self.prize_doors = []
|
|
self.prize_doors_opened = False
|
|
self.prize_received = False
|
|
|
|
def copy(self):
|
|
ret = ExplorationState(dungeon=self.dungeon)
|
|
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.bk_found = set(self.bk_found)
|
|
|
|
ret.non_door_entrances = list(self.non_door_entrances)
|
|
ret.dungeon = self.dungeon
|
|
ret.pinball_used = self.pinball_used
|
|
|
|
ret.prize_door_set = dict(self.prize_door_set)
|
|
ret.prize_doors = list(self.prize_doors)
|
|
ret.prize_doors_opened = self.prize_doors_opened
|
|
ret.prize_received = self.prize_received
|
|
return ret
|
|
|
|
def init_zelda_event_doors(self, event_starts, player):
|
|
for entrance in event_starts:
|
|
event_door = Door(player, entrance.name, DoorType.Logical)
|
|
event_door.req_event = 'Zelda Drop Off'
|
|
event_door.entrance = entrance
|
|
event_door.crystal = CrystalBarrier.Orange # always start in orange
|
|
self.append_door_to_list(event_door, self.event_doors)
|
|
|
|
def next_avail_door(self):
|
|
self.avail_doors.sort(key=lambda x: 0 if x.flag else 1 if x.door.bigKey else 2)
|
|
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, bk_flag=False):
|
|
if region.type != RegionType.Dungeon:
|
|
self.crystal = CrystalBarrier.Orange
|
|
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)
|
|
if region.type == RegionType.Dungeon:
|
|
for location in region.locations:
|
|
if key_checks and location not in self.found_locations:
|
|
if location.forced_item and 'Small Key' in location.item.name:
|
|
self.key_locations += 1
|
|
if location.name not in dungeon_events and '- Prize' not in location.name and location.name not in ['Agahnim 1', 'Agahnim 2']:
|
|
self.ttl_locations += 1
|
|
if location not in self.found_locations:
|
|
self.found_locations.append(location)
|
|
if not bk_flag and (not location.forced_item or 'Big Key' in location.item.name):
|
|
self.bk_found.add(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 '- Prize' in location.name:
|
|
self.prize_received = True
|
|
|
|
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.popleft()
|
|
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.controller is not None:
|
|
door = door.controller
|
|
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, flag, world, player, exception):
|
|
for door in get_doors(world, region, player):
|
|
if door.blocked and exception(door):
|
|
self.pinball_used = True
|
|
if self.can_traverse(door, exception):
|
|
if door.controller is not None:
|
|
door = door.controller
|
|
if door.dest is None and door not in proposed_map.keys() and door.name in valid_doors.keys():
|
|
if not self.in_door_list_ic(door, self.unattached_doors):
|
|
self.append_door_to_list(door, self.unattached_doors, flag)
|
|
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, flag)
|
|
elif not self.in_door_list(door, self.avail_doors):
|
|
self.append_door_to_list(door, self.avail_doors, flag)
|
|
|
|
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.controller:
|
|
door = door.controller
|
|
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 or door.name in special_big_key_doors) 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 add_all_doors_check_big_keys(self, region, big_key_door_proposal, world, player):
|
|
for door in get_doors(world, region, player):
|
|
if self.can_traverse(door):
|
|
if door.controller:
|
|
door = door.controller
|
|
if (door in big_key_door_proposal
|
|
or door.name in get_special_big_key_doors(world, player)) 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 found_forced_bk(self):
|
|
for location in self.found_locations:
|
|
if location.forced_big_key():
|
|
return True
|
|
return False
|
|
|
|
def can_traverse(self, door, exception=None):
|
|
if door.blocked:
|
|
return exception(door) if exception else False
|
|
if door.crystal not in [CrystalBarrier.Null, CrystalBarrier.Either]:
|
|
return self.crystal == CrystalBarrier.Either or door.crystal == self.crystal
|
|
return True
|
|
|
|
def count_locations_exclude_specials(self, world, player):
|
|
return count_locations_exclude_big_chest(self.found_locations, world, player)
|
|
|
|
def validate(self, door, region, world, player):
|
|
return self.can_traverse(door) and not self.visited(region) and valid_region_to_explore(region, self.dungeon,
|
|
world, player)
|
|
|
|
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, flag=False):
|
|
if door.crystal == CrystalBarrier.Null:
|
|
door_list.append(ExplorableDoor(door, self.crystal, flag))
|
|
else:
|
|
door_list.append(ExplorableDoor(door, door.crystal, flag))
|
|
|
|
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 count_locations_exclude_big_chest(locations, world, player):
|
|
cnt = 0
|
|
for loc in locations:
|
|
if ('- Big Chest' not in loc.name and not loc.forced_item and not reserved_location(loc, world, player)
|
|
and not prize_or_event(loc) and not blind_boss_unavail(loc, locations, world, player)):
|
|
cnt += 1
|
|
return cnt
|
|
|
|
|
|
def prize_or_event(loc):
|
|
return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']
|
|
|
|
|
|
def reserved_location(loc, world, player):
|
|
return hasattr(world, 'item_pool_config') and loc.name in world.item_pool_config.reserved_locations[player]
|
|
|
|
|
|
def blind_boss_unavail(loc, locations, world, player):
|
|
if loc.name == "Thieves' Town - Boss":
|
|
return (loc.parent_region.dungeon.boss.name == 'Blind' and
|
|
(not any(x for x in locations if x.name == 'Suspicious Maiden') or
|
|
(world.get_region('Thieves Attic Window', player).dungeon.name == 'Thieves Town' and
|
|
not any(x for x in locations if x.name == 'Attic Cracked Floor'))))
|
|
return False
|
|
|
|
|
|
class ExplorableDoor(object):
|
|
|
|
def __init__(self, door, crystal, flag):
|
|
self.door = door
|
|
self.crystal = crystal
|
|
self.flag = flag
|
|
|
|
def __str__(self):
|
|
return str(self.__unicode__())
|
|
|
|
def __unicode__(self):
|
|
return '%s (%s)' % (self.door.name, self.crystal.name)
|
|
|
|
|
|
def extend_reachable_state_improved(search_regions, state, proposed_map, all_regions, valid_doors, bk_flag, world, player, exception):
|
|
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, False, world, player, exception)
|
|
while len(local_state.avail_doors) > 0:
|
|
explorable_door = local_state.next_avail_door()
|
|
if explorable_door.door.bigKey:
|
|
if bk_flag:
|
|
big_not_found = (not special_big_key_found(local_state) if local_state.big_key_special
|
|
else local_state.count_locations_exclude_specials(world, player) == 0)
|
|
if big_not_found:
|
|
continue # we can't open this 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_in_regions(connect_region, all_regions, world, player) and not local_state.visited(
|
|
connect_region):
|
|
flag = explorable_door.flag or explorable_door.door.bigKey
|
|
local_state.visit_region(connect_region, bk_Flag=flag)
|
|
local_state.add_all_doors_check_proposed(connect_region, proposed_map, valid_doors, flag, world, player, exception)
|
|
return local_state
|
|
|
|
|
|
def special_big_key_found(state):
|
|
for location in state.found_locations:
|
|
if location.forced_item and location.forced_item.bigkey:
|
|
return True
|
|
return False
|
|
|
|
|
|
def valid_region_to_explore_in_regions(region, all_regions, world, player):
|
|
if region is None:
|
|
return False
|
|
return (region.type == RegionType.Dungeon and region in all_regions)\
|
|
or region.name in world.inaccessible_regions[player]\
|
|
or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard')
|
|
|
|
|
|
# cross-utility methods
|
|
def valid_region_to_explore(region, name, world, player):
|
|
if region is None:
|
|
return False
|
|
return ((region.type == RegionType.Dungeon and region.dungeon and region.dungeon.name in name)
|
|
or region.name in world.inaccessible_regions[player]
|
|
or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard'))
|
|
|
|
|
|
def get_doors(world, region, player):
|
|
res = []
|
|
for ext in region.exits:
|
|
door = world.check_for_door(ext.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 ext in region.entrances:
|
|
door = world.check_for_door(ext.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
|
|
|
|
|
|
# Begin crossed mode sector shuffle
|
|
|
|
class DungeonBuilder(object):
|
|
|
|
def __init__(self, name):
|
|
self.name = name
|
|
self.sectors = []
|
|
self.location_cnt = 0
|
|
self.location_set = set()
|
|
self.key_drop_cnt = 0
|
|
self.dungeon_items = None # during fill how many dungeon items are left
|
|
self.free_items = None # during fill how many dungeon items are left
|
|
self.bk_required = False
|
|
self.bk_provided = False
|
|
self.c_switch_required = False
|
|
self.c_switch_present = False
|
|
self.c_locked = False
|
|
self.dead_ends = 0
|
|
self.branches = 0
|
|
self.forced_loops = 0
|
|
self.total_conn_lack = 0
|
|
self.conn_needed = defaultdict(int)
|
|
self.conn_supplied = defaultdict(int)
|
|
self.conn_balance = defaultdict(int)
|
|
self.mag_needed = {}
|
|
self.unfulfilled = defaultdict(int)
|
|
self.all_entrances = None # used for sector segregation/branching
|
|
self.entrance_list = None # used for overworld accessibility
|
|
self.layout_starts = None # used for overworld accessibility
|
|
self.master_sector = None
|
|
self.path_entrances = None # used for pathing/key doors, I think
|
|
self.split_flag = False
|
|
|
|
self.candidates = None
|
|
self.total_keys = None
|
|
self.key_doors_num = None
|
|
self.combo_size = None
|
|
self.flex = 0
|
|
self.key_door_proposal = None
|
|
self.bk_door_proposal = None
|
|
self.trap_door_proposal = None
|
|
|
|
self.allowance = None
|
|
if 'Stonewall' in name:
|
|
self.allowance = 1
|
|
elif 'Prewall' in name:
|
|
orig_name = name[:-8]
|
|
if orig_name in dungeon_dead_end_allowance.keys():
|
|
self.allowance = dungeon_dead_end_allowance[orig_name]
|
|
if self.allowance is None:
|
|
self.allowance = 1
|
|
|
|
self.valid_proposal = None
|
|
self.split_dungeon_map = None
|
|
self.exception_list = []
|
|
|
|
self.throne_door = None
|
|
self.throne_sector = None
|
|
self.chosen_lobby = None
|
|
self.sewers_access = None
|
|
|
|
def polarity_complement(self):
|
|
pol = Polarity()
|
|
for sector in self.sectors:
|
|
pol += sector.polarity()
|
|
return pol.complement()
|
|
|
|
def polarity(self):
|
|
pol = Polarity()
|
|
for sector in self.sectors:
|
|
pol += sector.polarity()
|
|
return pol
|
|
|
|
def __str__(self):
|
|
return str(self.__unicode__())
|
|
|
|
def __unicode__(self):
|
|
return '%s' % self.name
|
|
|
|
|
|
def simple_dungeon_builder(name, sector_list):
|
|
define_sector_features(sector_list)
|
|
builder = DungeonBuilder(name)
|
|
dummy_pool = dict.fromkeys(sector_list)
|
|
global_pole = GlobalPolarity(dummy_pool)
|
|
for sector in sector_list:
|
|
assign_sector(sector, builder, dummy_pool, global_pole)
|
|
return builder
|
|
|
|
|
|
def create_dungeon_builders(all_sectors, connections_tuple, world, player, dungeon_pool,
|
|
dungeon_entrances=None, split_dungeon_entrances=None):
|
|
logger = logging.getLogger('')
|
|
logger.info('Shuffling Dungeon Sectors')
|
|
|
|
if dungeon_entrances is None:
|
|
dungeon_entrances = default_dungeon_entrances
|
|
if split_dungeon_entrances is None:
|
|
split_dungeon_entrances = split_region_starts
|
|
define_sector_features(all_sectors)
|
|
finished, dungeon_map, attempts = False, {}, 0
|
|
while not finished:
|
|
candidate_sectors = dict.fromkeys(all_sectors)
|
|
global_pole = GlobalPolarity(candidate_sectors)
|
|
|
|
dungeon_map = {}
|
|
for key in dungeon_pool:
|
|
current_dungeon = dungeon_map[key] = DungeonBuilder(key)
|
|
for r_name in dungeon_boss_sectors[key]:
|
|
assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors, global_pole)
|
|
if key == 'Hyrule Castle' and world.mode[player] == 'standard':
|
|
for r_name in ['Hyrule Dungeon Cellblock', 'Sanctuary', 'Hyrule Castle Throne Room']: # need to deliver zelda
|
|
assign_sector(find_sector(r_name, candidate_sectors), current_dungeon,
|
|
candidate_sectors, global_pole)
|
|
if key == 'Thieves Town' and world.get_dungeon("Thieves Town", player).boss.enemizer_name == 'Blind':
|
|
assign_sector(find_sector("Thieves Blind's Cell", candidate_sectors), current_dungeon,
|
|
candidate_sectors, global_pole)
|
|
entrances_map, potentials, connections = connections_tuple
|
|
accessible_sectors, reverse_d_map = set(), {}
|
|
for key in dungeon_pool:
|
|
current_dungeon = dungeon_map[key]
|
|
current_dungeon.all_entrances = dungeon_entrances[key]
|
|
for r_name in current_dungeon.all_entrances:
|
|
sector = find_sector(r_name, candidate_sectors)
|
|
assign_sector(sector, current_dungeon, candidate_sectors, global_pole)
|
|
if r_name in entrances_map[key]:
|
|
if sector:
|
|
accessible_sectors.add(sector)
|
|
else:
|
|
if not sector:
|
|
sector = find_sector(r_name, all_sectors)
|
|
reverse_d_map[sector] = key
|
|
|
|
complete_dungeons = {x: y for x, y in dungeon_map.items() if sum(len(sector.outstanding_doors) for sector in y.sectors) <= 0}
|
|
[dungeon_map.pop(key) for key in complete_dungeons.keys()]
|
|
|
|
if not dungeon_map:
|
|
dungeon_map.update(complete_dungeons)
|
|
return dungeon_map
|
|
|
|
if world.mode[player] == 'standard':
|
|
if 'Hyrule Castle' in dungeon_map:
|
|
current_dungeon = dungeon_map['Hyrule Castle']
|
|
standard_stair_check(dungeon_map, current_dungeon, candidate_sectors, global_pole)
|
|
|
|
# categorize sectors
|
|
identify_destination_sectors(accessible_sectors, reverse_d_map, dungeon_map, connections,
|
|
dungeon_entrances, split_dungeon_entrances)
|
|
for name, builder in dungeon_map.items():
|
|
calc_allowance_and_dead_ends(builder, connections_tuple, world, player)
|
|
|
|
if world.mode[player] == 'open' and world.shuffle[player] not in ['lean', 'swapped', 'crossed', 'insanity']:
|
|
sanc = find_sector('Sanctuary', candidate_sectors)
|
|
if sanc: # only run if sanc if a candidate
|
|
lw_builders = []
|
|
for name in dungeon_pool:
|
|
for portal_name in dungeon_portals[name]:
|
|
if world.get_portal(portal_name, player).light_world:
|
|
lw_builders.append(dungeon_map[name])
|
|
break
|
|
# portals only - not drops for mirror stuff
|
|
sanc_builder = random.choice(lw_builders)
|
|
assign_sector(sanc, sanc_builder, candidate_sectors, global_pole)
|
|
|
|
retro_std_flag = world.bow_mode[player].startswith('retro') and world.mode[player] == 'standard'
|
|
non_hc_sectors = {}
|
|
free_location_sectors = {}
|
|
crystal_switches = {}
|
|
crystal_barriers = {}
|
|
polarized_sectors = {}
|
|
neutral_sectors = {}
|
|
for sector in candidate_sectors:
|
|
if retro_std_flag and 'Bow' in sector.item_logic: # these need to be distributed outside of HC
|
|
non_hc_sectors[sector] = None
|
|
elif world.mode[player] == 'standard' and 'Open Floodgate' in sector.item_logic:
|
|
non_hc_sectors[sector] = None
|
|
elif sector.chest_locations > 0:
|
|
free_location_sectors[sector] = None
|
|
elif sector.c_switch:
|
|
crystal_switches[sector] = None
|
|
elif sector.blue_barrier:
|
|
crystal_barriers[sector] = None
|
|
elif sector.polarity().is_neutral():
|
|
neutral_sectors[sector] = None
|
|
else:
|
|
polarized_sectors[sector] = None
|
|
if non_hc_sectors:
|
|
assign_non_hc_sectors(dungeon_map, non_hc_sectors, global_pole)
|
|
leftover = assign_location_sectors_minimal(dungeon_map, free_location_sectors, global_pole, world, player)
|
|
free_location_sectors = scatter_extra_location_sectors(dungeon_map, leftover, global_pole)
|
|
for sector in free_location_sectors:
|
|
if sector.c_switch:
|
|
crystal_switches[sector] = None
|
|
elif sector.blue_barrier:
|
|
crystal_barriers[sector] = None
|
|
elif sector.polarity().is_neutral():
|
|
neutral_sectors[sector] = None
|
|
else:
|
|
polarized_sectors[sector] = None
|
|
leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole)
|
|
ensure_crystal_switches_reachable(dungeon_map, leftover, polarized_sectors, crystal_barriers, global_pole)
|
|
for sector in leftover:
|
|
if sector.polarity().is_neutral():
|
|
neutral_sectors[sector] = None
|
|
else:
|
|
polarized_sectors[sector] = None
|
|
# blue barriers
|
|
assign_crystal_barrier_sectors(dungeon_map, crystal_barriers, global_pole)
|
|
try:
|
|
# polarity:
|
|
if not global_pole.is_valid(dungeon_map):
|
|
# restart
|
|
raise NeutralizingException('Either free location/crystal assignment is already globally invalid')
|
|
logger.info(world.fish.translate("cli", "cli", "balance.doors"))
|
|
builder_info = dungeon_entrances, split_dungeon_entrances, connections_tuple, world, player
|
|
assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, builder_info)
|
|
# the rest
|
|
assign_the_rest(dungeon_map, neutral_sectors, global_pole, builder_info)
|
|
dungeon_map.update(complete_dungeons)
|
|
finished = True
|
|
except (NeutralizingException, GenerationException) as e:
|
|
attempts += 1
|
|
logger.debug(f'Attempt {attempts} failed with {str(e)}')
|
|
if attempts >= 10:
|
|
raise Exception('Could not find a valid seed quickly, something is likely horribly wrong.', e)
|
|
return dungeon_map
|
|
|
|
|
|
def standard_stair_check(dungeon_map, dungeon, candidate_sectors, global_pole):
|
|
# this is because there must be at least one non-dead stairway in hc to get out
|
|
# this check may not be necessary
|
|
filtered_sectors = [x for x in candidate_sectors if 'Open Floodgate' not in x.item_logic and
|
|
any(y for y in x.outstanding_doors if not y.dead and y.type == DoorType.SpiralStairs)]
|
|
valid = False
|
|
while not valid:
|
|
chosen_sector = random.choice(filtered_sectors)
|
|
filtered_sectors.remove(chosen_sector)
|
|
valid = global_pole.is_valid_choice(dungeon_map, dungeon, [chosen_sector])
|
|
if valid:
|
|
assign_sector(chosen_sector, dungeon, candidate_sectors, global_pole)
|
|
|
|
|
|
def identify_destination_sectors(accessible_sectors, reverse_d_map, dungeon_map, connections, dungeon_entrances, split_dungeon_entrances):
|
|
accessible_overworld, found_connections, explored = set(), set(), False
|
|
|
|
while not explored:
|
|
explored = True
|
|
for ent_name, region in connections.items():
|
|
if ent_name in found_connections:
|
|
continue
|
|
sector = find_sector(ent_name, reverse_d_map.keys())
|
|
if sector is None:
|
|
continue
|
|
if sector in accessible_sectors:
|
|
found_connections.add(ent_name)
|
|
accessible_overworld.add(region) # todo: drops don't give ow access
|
|
explored = False
|
|
elif region in accessible_overworld:
|
|
found_connections.add(ent_name)
|
|
accessible_sectors.add(sector)
|
|
explored = False
|
|
else:
|
|
d_name = reverse_d_map[sector]
|
|
if d_name not in dungeon_map:
|
|
return
|
|
if d_name not in split_dungeon_entrances:
|
|
for r_name in dungeon_entrances[d_name]:
|
|
ent_sector = find_sector(r_name, dungeon_map[d_name].sectors)
|
|
if ent_sector in accessible_sectors and ent_name not in dead_entrances:
|
|
sector.destination_entrance = True
|
|
found_connections.add(ent_name)
|
|
accessible_sectors.add(sector)
|
|
accessible_overworld.add(region)
|
|
explored = False
|
|
break
|
|
elif d_name in split_dungeon_entrances.keys():
|
|
split_section = None
|
|
for split_name, split_list in split_dungeon_entrances[d_name].items():
|
|
if ent_name in split_list:
|
|
split_section = split_name
|
|
break
|
|
if split_section:
|
|
for r_name in split_dungeon_entrances[d_name][split_section]:
|
|
ent_sector = find_sector(r_name, dungeon_map[d_name].sectors)
|
|
if ent_sector in accessible_sectors and ent_name not in dead_entrances:
|
|
sector.destination_entrance = True
|
|
found_connections.add(ent_name)
|
|
accessible_sectors.add(sector)
|
|
accessible_overworld.add(region)
|
|
explored = False
|
|
break
|
|
|
|
|
|
# todo: split version that adds allowance for potential entrances
|
|
def calc_allowance_and_dead_ends(builder, connections_tuple, world, player):
|
|
portals = world.dungeon_portals[player]
|
|
entrances_map, potentials, connections = connections_tuple
|
|
name = builder.name if not builder.split_flag else builder.name.rsplit(' ', 1)[0]
|
|
needed_connections = [x for x in builder.all_entrances if x not in entrances_map[name]]
|
|
starting_allowance = 0
|
|
used_sectors = set()
|
|
destination_entrances = [x.door.entrance.parent_region.name for x in portals if x.destination]
|
|
dead_ends = [x.door.entrance.parent_region.name for x in portals if x.deadEnd]
|
|
for entrance in entrances_map[name]:
|
|
sector = find_sector(entrance, builder.sectors)
|
|
if sector:
|
|
outflow_target = 0 if entrance not in drop_entrances_allowance else 1
|
|
if sector not in used_sectors and (sector.adj_outflow() > outflow_target or entrance in dead_ends):
|
|
if entrance not in destination_entrances:
|
|
starting_allowance += 1
|
|
else:
|
|
builder.branches -= 1
|
|
used_sectors.add(sector)
|
|
elif sector not in used_sectors:
|
|
if entrance in destination_entrances and sector.branches() > 0:
|
|
builder.branches -= 1
|
|
if entrance not in drop_entrances_allowance:
|
|
needed_connections.append(entrance)
|
|
if builder.sewers_access:
|
|
starting_allowance += 1
|
|
builder.allowance = starting_allowance
|
|
for entrance in needed_connections:
|
|
sector = find_sector(entrance, builder.sectors)
|
|
if sector and sector not in used_sectors: # ignore things on same sector
|
|
is_destination = entrance in destination_entrances
|
|
connect_able = False
|
|
if entrance in connections.keys():
|
|
enabling_region = connections[entrance]
|
|
check_list = list(potentials[enabling_region])
|
|
if enabling_region.name in ['Desert Ledge', 'Desert Ledge Keep']:
|
|
alternate = 'Desert Ledge Keep' if enabling_region.name == 'Desert Ledge' else 'Desert Ledge'
|
|
if world.get_region(alternate, player) in potentials:
|
|
check_list.extend(potentials[world.get_region(alternate, player)])
|
|
connecting_entrances = [x for x in check_list if x != entrance and x not in dead_entrances and x not in drop_entrances_allowance]
|
|
connect_able = len(connecting_entrances) > 0
|
|
if is_destination and sector.branches() == 0: #
|
|
builder.dead_ends += 1
|
|
if is_destination and sector.branches() > 0:
|
|
builder.branches -= 1
|
|
if connect_able and not is_destination:
|
|
builder.allowance += 1
|
|
used_sectors.add(sector)
|
|
|
|
|
|
def define_sector_features(sectors):
|
|
for sector in sectors:
|
|
for region in sector.regions:
|
|
for loc in region.locations:
|
|
if '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']:
|
|
pass
|
|
elif loc.forced_item and 'Small Key' in loc.item.name:
|
|
sector.key_only_locations += 1
|
|
elif loc.forced_item and loc.forced_item.bigkey:
|
|
sector.bk_provided = True
|
|
elif loc.name not in dungeon_events and not loc.forced_item:
|
|
sector.chest_locations += 1
|
|
sector.chest_location_set.add(loc.name)
|
|
if '- Big Chest' in loc.name or loc.name in ["Hyrule Castle - Zelda's Chest",
|
|
"Thieves' Town - Blind's Cell"]:
|
|
sector.bk_required = True
|
|
for ext in region.exits:
|
|
door = ext.door
|
|
if door is not None and not door.blocked:
|
|
if door.crystal == CrystalBarrier.Either:
|
|
sector.c_switch = True
|
|
elif door.crystal == CrystalBarrier.Orange:
|
|
sector.orange_barrier = True
|
|
elif door.crystal == CrystalBarrier.Blue:
|
|
sector.blue_barrier = True
|
|
if door.bigKey:
|
|
sector.bk_required = True
|
|
if region.name in ['PoD Mimics 2', 'PoD Bow Statue Right', 'PoD Mimics 1', 'GT Mimics 1', 'GT Mimics 2',
|
|
'Eastern Single Eyegore', 'Eastern Duo Eyegores']:
|
|
sector.item_logic.add('Bow')
|
|
if region.name in ['Swamp Lobby', 'Swamp Entrance']:
|
|
sector.item_logic.add('Open Floodgate')
|
|
|
|
|
|
def assign_sector(sector, dungeon, candidate_sectors, global_pole):
|
|
if sector:
|
|
del candidate_sectors[sector]
|
|
global_pole.consume(sector)
|
|
assign_sector_helper(sector, dungeon)
|
|
|
|
|
|
def assign_sector_helper(sector, builder):
|
|
builder.sectors.append(sector)
|
|
builder.location_cnt += sector.chest_locations
|
|
builder.key_drop_cnt += sector.key_only_locations
|
|
builder.location_set.update(sector.chest_location_set)
|
|
if sector.c_switch:
|
|
builder.c_switch_present = True
|
|
if sector.blue_barrier:
|
|
builder.c_switch_required = True
|
|
if sector.bk_required:
|
|
builder.bk_required = True
|
|
if sector.bk_provided:
|
|
builder.bk_provided = True
|
|
count_conn_needed_supplied(sector, builder.conn_needed, builder.conn_supplied)
|
|
builder.dead_ends += sector.dead_ends()
|
|
builder.branches += sector.branches()
|
|
if sector in builder.exception_list:
|
|
builder.exception_list.remove(sector)
|
|
else:
|
|
if builder.split_dungeon_map:
|
|
builder.split_dungeon_map = None
|
|
if builder.valid_proposal:
|
|
builder.valid_proposal = None
|
|
|
|
|
|
def count_conn_needed_supplied(sector, conn_needed, conn_supplied):
|
|
for door in sector.outstanding_doors:
|
|
# todo: destination sectors like skull 2 west should be
|
|
if (door.blocked or door.dead or sector.adj_outflow() <= 1) and not sector.is_entrance_sector():
|
|
conn_needed[hook_from_door(door)] += 1
|
|
# todo: stonewall
|
|
else: # todo: dungeons that need connections... skull, tr, hc, desert (when edges are done)
|
|
conn_supplied[hanger_from_door(door)] += 1
|
|
|
|
|
|
def find_sector(r_name, sectors):
|
|
for s in sectors:
|
|
if r_name in s.region_set():
|
|
return s
|
|
return None
|
|
|
|
|
|
def assign_non_hc_sectors(dungeon_map, non_hc_sectors, global_pole):
|
|
sector_list = list(non_hc_sectors)
|
|
random.shuffle(sector_list)
|
|
population = []
|
|
for name in dungeon_map:
|
|
if name != 'Hyrule Castle':
|
|
population.append(name)
|
|
choices = random.choices(population, k=len(sector_list))
|
|
for i, choice in enumerate(choices):
|
|
builder = dungeon_map[choice]
|
|
assign_sector(sector_list[i], builder, non_hc_sectors, global_pole)
|
|
|
|
|
|
def scatter_extra_location_sectors(dungeon_map, free_location_sectors, global_pole):
|
|
population = [n for n in dungeon_map.keys()]
|
|
k = round(len(free_location_sectors) * .50)
|
|
valid = False
|
|
choices = None
|
|
candidates = []
|
|
sector_list = list(free_location_sectors)
|
|
while not valid:
|
|
candidates = random.sample(sector_list, k=k)
|
|
choices = random.choices(population, k=len(candidates))
|
|
sector_dict = defaultdict(list)
|
|
for i, choice in enumerate(choices):
|
|
builder = dungeon_map[choice]
|
|
sector_dict[builder].append(candidates[i])
|
|
valid = global_pole.is_valid_multi_choice_2(dungeon_map, dungeon_map.values(), sector_dict)
|
|
for i, choice in enumerate(choices):
|
|
builder = dungeon_map[choice]
|
|
assign_sector(candidates[i], builder, free_location_sectors, global_pole)
|
|
return free_location_sectors
|
|
|
|
|
|
def assign_location_sectors_minimal(dungeon_map, free_location_sectors, global_pole, world, player):
|
|
valid = False
|
|
choices = defaultdict(list)
|
|
sector_list = list(free_location_sectors)
|
|
random.shuffle(sector_list)
|
|
orig_location_set = build_orig_location_set(dungeon_map)
|
|
num_dungeon_items = requested_dungeon_items(world, player)
|
|
locations_to_distribute = sum(sector.chest_locations for sector in free_location_sectors.keys())
|
|
reserved_per_dungeon = {d_name: count_reserved_locations(world, player, orig_location_set[d_name])
|
|
for d_name in dungeon_map.keys()}
|
|
base_free, found_enough = 2, False
|
|
while not found_enough:
|
|
needed = sum(max(0, max(base_free, reserved_per_dungeon[d]) + num_dungeon_items - len(orig_location_set[d]))
|
|
for d in dungeon_map.keys())
|
|
if needed > locations_to_distribute:
|
|
if base_free == 0:
|
|
raise Exception('Unable to meet minimum requirements, check for customizer problems')
|
|
base_free -= 1
|
|
else:
|
|
found_enough = True
|
|
d_idx = {builder.name: i for i, builder in enumerate(dungeon_map.values())}
|
|
next_sector = sector_list.pop()
|
|
while not valid:
|
|
choice, totals, location_set = weighted_random_location(dungeon_map, choices, orig_location_set,
|
|
base_free, world, player)
|
|
if not choice:
|
|
break
|
|
choices[choice].append(next_sector)
|
|
if global_pole.is_valid_multi_choice_2(dungeon_map, dungeon_map.values(), choices):
|
|
idx = d_idx[choice.name]
|
|
totals[idx] += next_sector.chest_locations
|
|
location_set[choice.name].update(next_sector.chest_location_set)
|
|
valid = True
|
|
for d_name, idx in d_idx.items():
|
|
free_items = count_reserved_locations(world, player, location_set[d_name])
|
|
target = max(free_items, base_free) + num_dungeon_items
|
|
if totals[idx] < target:
|
|
valid = False
|
|
break
|
|
if not valid:
|
|
if len(sector_list) == 0:
|
|
choices = defaultdict(list)
|
|
sector_list = list(free_location_sectors)
|
|
next_sector = sector_list.pop()
|
|
else:
|
|
choices[choice].remove(next_sector)
|
|
for builder, choice_list in choices.items():
|
|
for choice in choice_list:
|
|
assign_sector(choice, builder, free_location_sectors, global_pole)
|
|
return free_location_sectors
|
|
|
|
|
|
def weighted_random_location(dungeon_map, choices, orig_location_set, base_free, world, player):
|
|
population = []
|
|
totals = []
|
|
location_set = {x: set(y) for x, y in orig_location_set.items()}
|
|
num_dungeon_items = requested_dungeon_items(world, player)
|
|
for i, dungeon_builder in enumerate(dungeon_map.values()):
|
|
ttl = dungeon_builder.location_cnt + sum(sector.chest_locations for sector in choices[dungeon_builder])
|
|
totals.append(ttl)
|
|
builder_set = location_set[dungeon_builder.name]
|
|
builder_set.update(set().union(*(s.chest_location_set for s in choices[dungeon_builder])))
|
|
free_items = count_reserved_locations(world, player, builder_set)
|
|
target = max(free_items, base_free) + num_dungeon_items
|
|
if ttl < target:
|
|
population.append(dungeon_builder)
|
|
choice = random.choice(population) if len(population) > 0 else None
|
|
return choice, totals, location_set
|
|
|
|
|
|
# deprecated
|
|
def weighted_random_locations(dungeon_map, free_location_sectors):
|
|
population = []
|
|
ttl_assigned = 0
|
|
weights = []
|
|
totals = []
|
|
d_idx = {}
|
|
for i, dungeon_builder in enumerate(dungeon_map.values()):
|
|
population.append(dungeon_builder)
|
|
totals.append(dungeon_builder.location_cnt)
|
|
ttl_assigned += dungeon_builder.location_cnt
|
|
weights.append(6.375)
|
|
d_idx[dungeon_builder.name] = i
|
|
average = ttl_assigned / 13
|
|
for i, db in enumerate(population):
|
|
if db.location_cnt < average:
|
|
weights[i] += average - db.location_cnt
|
|
if db.location_cnt > average:
|
|
weights[i] = max(0, weights[i] - db.location_cnt + average)
|
|
|
|
choices = random.choices(population, weights, k=len(free_location_sectors))
|
|
return choices, d_idx, totals
|
|
|
|
|
|
def build_orig_location_set(dungeon_map):
|
|
orig_locations = {}
|
|
for name, builder in dungeon_map.items():
|
|
orig_locations[name] = set().union(*(s.chest_location_set for s in builder.sectors))
|
|
return orig_locations
|
|
|
|
|
|
def requested_dungeon_items(world, player):
|
|
num = 0
|
|
if not world.bigkeyshuffle[player]:
|
|
num += 1
|
|
if not world.compassshuffle[player]:
|
|
num += 1
|
|
if not world.mapshuffle[player]:
|
|
num += 1
|
|
return num
|
|
|
|
|
|
def count_reserved_locations(world, player, proposed_set):
|
|
if world.item_pool_config:
|
|
return len([x for x in proposed_set if x in world.item_pool_config.reserved_locations[player]])
|
|
return 2
|
|
|
|
|
|
def assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole):
|
|
population = []
|
|
some_c_switches_present = False
|
|
for name, builder in dungeon_map.items():
|
|
if builder.c_switch_required and not builder.c_switch_present and not builder.c_locked:
|
|
population.append(name)
|
|
if builder.c_switch_present and not builder.c_locked:
|
|
some_c_switches_present = True
|
|
if len(population) == 0: # nothing needs a switch
|
|
if len(crystal_barriers) > 0 and not some_c_switches_present: # something should have one
|
|
if len(crystal_switches) == 0:
|
|
raise GenerationException('No crystal switches to assign. Ref %s' % next(iter(dungeon_map.keys())))
|
|
valid, builder_choice, switch_choice = False, None, None
|
|
switch_candidates = list(crystal_switches)
|
|
switch_choice = random.choice(switch_candidates)
|
|
switch_candidates.remove(switch_choice)
|
|
builder_candidates = [name for name, builder in dungeon_map.items() if not builder.c_locked]
|
|
while not valid:
|
|
if len(builder_candidates) == 0:
|
|
if len(switch_candidates) == 0:
|
|
raise GenerationException('No where to assign crystal switch. Ref %s' % next(iter(dungeon_map.keys())))
|
|
switch_choice = random.choice(switch_candidates)
|
|
switch_candidates.remove(switch_choice)
|
|
builder_candidates = list(dungeon_map.keys())
|
|
choice = random.choice(builder_candidates)
|
|
builder_candidates.remove(choice)
|
|
builder_choice = dungeon_map[choice]
|
|
test_set = [switch_choice]
|
|
test_set.extend(crystal_barriers)
|
|
valid = global_pole.is_valid_choice(dungeon_map, builder_choice, test_set)
|
|
assign_sector(switch_choice, builder_choice, crystal_switches, global_pole)
|
|
return crystal_switches
|
|
if len(crystal_switches) == 0:
|
|
raise GenerationException('No crystal switches to assign')
|
|
sector_list = list(crystal_switches)
|
|
if len(population) > len(sector_list):
|
|
raise GenerationException('Not enough crystal switch sectors for those needed')
|
|
choices = random.sample(sector_list, k=len(population))
|
|
for i, choice in enumerate(choices):
|
|
builder = dungeon_map[population[i]]
|
|
assign_sector(choice, builder, crystal_switches, global_pole)
|
|
return crystal_switches
|
|
|
|
|
|
def ensure_crystal_switches_reachable(dungeon_map, crystal_switches, polarized_sectors, crystal_barriers, global_pole):
|
|
invalid_builders = []
|
|
for name, builder in dungeon_map.items():
|
|
if builder.c_switch_present and builder.c_switch_required and not builder.c_locked:
|
|
invalid_builders.append(builder)
|
|
random.shuffle(invalid_builders)
|
|
while len(invalid_builders) > 0:
|
|
valid_builders = []
|
|
for builder in invalid_builders:
|
|
entrance_sectors = []
|
|
reachable_crystals = defaultdict()
|
|
for sector in builder.sectors:
|
|
if sector.equations is None:
|
|
sector.equations = calc_sector_equations(sector)
|
|
if sector.is_entrance_sector() and not sector.destination_entrance:
|
|
need_switch = True
|
|
if sector.c_switch: # this relies on the fact that Mire Fishbone SE cannot be a portal
|
|
need_switch = False
|
|
any_benefit = False
|
|
for eq in sector.equations:
|
|
if len(eq.benefit) > 0:
|
|
any_benefit = True
|
|
break
|
|
if need_switch and any_benefit:
|
|
entrance_sectors.append(sector)
|
|
for eq in sector.equations:
|
|
if eq.c_switch:
|
|
reachable_crystals[hook_from_door(eq.door)] = True
|
|
valid_ent_sectors = []
|
|
random.shuffle(entrance_sectors)
|
|
for entrance_sector in entrance_sectors:
|
|
other_sectors = [x for x in builder.sectors if x != entrance_sector]
|
|
reachable, access = is_c_switch_reachable(entrance_sector, reachable_crystals, other_sectors)
|
|
if reachable:
|
|
valid_ent_sectors.append(entrance_sector)
|
|
else:
|
|
candidates = {}
|
|
for c in find_pol_cand_for_c_switch(access, reachable_crystals, polarized_sectors):
|
|
candidates[c] = 'Polarized'
|
|
for c in find_crystal_cand(access, crystal_switches):
|
|
candidates[c] = 'Crystals'
|
|
for c in find_pol_cand_for_c_switch(access, reachable_crystals, crystal_barriers):
|
|
candidates[c] = 'Barriers'
|
|
valid, sector, which_list = False, None, None
|
|
while not valid:
|
|
if len(candidates) <= 0:
|
|
raise GenerationException(f'need to provide more sophisticated crystal connection for {entrance_sector}')
|
|
# prioritize candidates
|
|
if any(x == 'Crystals' for x in candidates.values()):
|
|
cand_list = [x for x in candidates.items() if x[1] == 'Crystals']
|
|
else:
|
|
cand_list = list(candidates.items())
|
|
sector, which_list = random.choice(cand_list)
|
|
del candidates[sector]
|
|
valid = global_pole.is_valid_choice(dungeon_map, builder, [sector])
|
|
if which_list == 'Polarized':
|
|
assign_sector(sector, builder, polarized_sectors, global_pole)
|
|
elif which_list == 'Crystals':
|
|
assign_sector(sector, builder, crystal_switches, global_pole)
|
|
elif which_list == 'Barriers':
|
|
assign_sector(sector, builder, crystal_barriers, global_pole)
|
|
entrance_sectors = [x for x in entrance_sectors if x not in valid_ent_sectors]
|
|
if len(entrance_sectors) == 0:
|
|
valid_builders.append(builder)
|
|
invalid_builders = [x for x in invalid_builders if x not in valid_builders]
|
|
|
|
|
|
def is_c_switch_reachable(entrance_sector, reachable_crystals, other_sectors):
|
|
current_access = {}
|
|
for eq in entrance_sector.equations:
|
|
if eq.total_cost() <= 0:
|
|
for key, door_list in eq.benefit.items():
|
|
for door in door_list:
|
|
if door not in eq.crystal_blocked.keys() or eq.crystal_blocked[door] != CrystalBarrier.Blue:
|
|
current_access[key] = True
|
|
break
|
|
for key, flag in current_access.items():
|
|
if opposite_h_type(key) in reachable_crystals.keys():
|
|
return True, {}
|
|
changed = True
|
|
while changed:
|
|
changed = False
|
|
for sector in other_sectors:
|
|
for eq in sector.equations:
|
|
key, cost_door = eq.cost
|
|
if key in current_access.keys() and current_access[key]:
|
|
for bene_key, door_list in eq.benefit.items():
|
|
for door in door_list:
|
|
block_dict = eq.crystal_blocked
|
|
if door not in block_dict.keys() or block_dict[door] != CrystalBarrier.Blue:
|
|
if bene_key not in current_access.keys():
|
|
current_access[bene_key] = True
|
|
changed = True
|
|
break
|
|
for key, flag in current_access.items():
|
|
if opposite_h_type(key) in reachable_crystals.keys():
|
|
return True, {}
|
|
return False, current_access
|
|
|
|
|
|
def find_pol_cand_for_c_switch(access, reachable_crystals, polarized_candidates):
|
|
candidates = []
|
|
for sector in polarized_candidates:
|
|
if pol_cand_matches_access_reach(sector, access, reachable_crystals):
|
|
candidates.append(sector)
|
|
return candidates
|
|
|
|
|
|
def pol_cand_matches_access_reach(sector, access, reachable_crystals):
|
|
if sector.equations is None:
|
|
sector.equations = calc_sector_equations(sector)
|
|
for eq in sector.equations:
|
|
key, cost_door = eq.cost
|
|
if key in access.keys() and access[key]:
|
|
for bene_key, door_list in eq.benefit.items():
|
|
for door in door_list:
|
|
if door not in eq.crystal_blocked.keys() or eq.crystal_blocked[door] != CrystalBarrier.Blue:
|
|
if opposite_h_type(bene_key) in reachable_crystals.keys():
|
|
return True
|
|
return False
|
|
|
|
|
|
def find_crystal_cand(access, crystal_switches):
|
|
candidates = []
|
|
for sector in crystal_switches:
|
|
if crystal_cand_matches_access(sector, access):
|
|
candidates.append(sector)
|
|
return candidates
|
|
|
|
|
|
def crystal_cand_matches_access(sector, access):
|
|
if sector.equations is None:
|
|
sector.equations = calc_sector_equations(sector)
|
|
for eq in sector.equations:
|
|
key, cost_door = eq.cost
|
|
if key in access.keys() and access[key] and eq.c_switch and len(sector.outstanding_doors) > 1:
|
|
return True
|
|
return False
|
|
|
|
|
|
def assign_crystal_barrier_sectors(dungeon_map, crystal_barriers, global_pole):
|
|
population = []
|
|
for name, builder in dungeon_map.items():
|
|
if builder.c_switch_present and not builder.c_locked:
|
|
population.append(name)
|
|
sector_list = list(crystal_barriers)
|
|
random.shuffle(sector_list)
|
|
choices = random.choices(population, k=len(sector_list))
|
|
for i, choice in enumerate(choices):
|
|
builder = dungeon_map[choice]
|
|
assign_sector(sector_list[i], builder, crystal_barriers, global_pole)
|
|
|
|
|
|
def identify_polarity_issues(dungeon_map):
|
|
unconnected_builders = {}
|
|
for name, builder in dungeon_map.items():
|
|
identify_polarity_issues_internal(name, builder, unconnected_builders)
|
|
return unconnected_builders
|
|
|
|
|
|
def identify_polarity_issues_internal(name, builder, unconnected_builders):
|
|
if len(builder.sectors) == 1:
|
|
return
|
|
else:
|
|
def sector_filter(x, y):
|
|
return x != y
|
|
# else:
|
|
# def sector_filter(x, y):
|
|
# return x != y and (x.outflow() > 1 or is_entrance_sector(builder, x))
|
|
connection_flags = {}
|
|
for slot in PolSlot:
|
|
connection_flags[slot] = {}
|
|
for slot2 in PolSlot:
|
|
connection_flags[slot][slot2] = False
|
|
for sector in builder.sectors:
|
|
others = [x for x in builder.sectors if sector_filter(x, sector)]
|
|
other_mag = sum_magnitude(others)
|
|
sector_mag = sector.magnitude()
|
|
check_flags(sector_mag, connection_flags)
|
|
unconnected_sector = True
|
|
for i in PolSlot:
|
|
if sector_mag[i.value] == 0 or other_mag[i.value] > 0 or self_connecting(sector, i, sector_mag):
|
|
unconnected_sector = False
|
|
break
|
|
if unconnected_sector:
|
|
for i in PolSlot:
|
|
if sector_mag[i.value] > 0 and other_mag[i.value] == 0 and not self_connecting(sector, i, sector_mag):
|
|
builder.mag_needed[i] = [x for x in PolSlot if other_mag[x.value] > 0]
|
|
if name not in unconnected_builders.keys():
|
|
unconnected_builders[name] = builder
|
|
ttl_mag = sum_magnitude(builder.sectors)
|
|
for slot in PolSlot:
|
|
for slot2 in PolSlot:
|
|
if ttl_mag[slot.value] > 0 and ttl_mag[slot2.value] > 0 and not connection_flags[slot][slot2]:
|
|
builder.mag_needed[slot] = [slot2]
|
|
builder.mag_needed[slot2] = [slot]
|
|
if name not in unconnected_builders.keys():
|
|
unconnected_builders[name] = builder
|
|
|
|
def self_connecting(sector, slot, magnitude):
|
|
return sector.polarity()[slot.value] == 0 and sum(magnitude) > magnitude[slot.value]
|
|
|
|
|
|
def check_flags(sector_mag, connection_flags):
|
|
for slot in PolSlot:
|
|
for slot2 in PolSlot:
|
|
if sector_mag[slot.value] > 0 and sector_mag[slot2.value] > 0:
|
|
connection_flags[slot][slot2] = True
|
|
if slot != slot2:
|
|
for check_slot in PolSlot: # transitivity check
|
|
if check_slot not in [slot, slot2] and connection_flags[slot2][check_slot]:
|
|
connection_flags[slot][check_slot] = True
|
|
connection_flags[check_slot][slot] = True
|
|
|
|
|
|
def identify_simple_branching_issues(dungeon_map):
|
|
problem_builders = {}
|
|
for name, builder in dungeon_map.items():
|
|
if name == 'Skull Woods 2': # i dislike this special case todo: identify destination entrances
|
|
builder.conn_supplied[Hook.West] += 1
|
|
builder.conn_needed[Hook.East] -= 1
|
|
builder.forced_loops = calc_forced_loops(builder.sectors)
|
|
if builder.dead_ends + builder.forced_loops * 2 > builder.branches + builder.allowance:
|
|
problem_builders[name] = builder
|
|
for h_type in Hook:
|
|
lack = builder.conn_balance[h_type] = builder.conn_supplied[h_type] - builder.conn_needed[h_type]
|
|
if lack < 0:
|
|
builder.total_conn_lack += -lack
|
|
problem_builders[name] = builder
|
|
return problem_builders
|
|
|
|
|
|
def calc_forced_loops(sector_list):
|
|
forced_loops = 0
|
|
for sector in sector_list:
|
|
h_mag = sector.hook_magnitude()
|
|
other_sectors = [x for x in sector_list if x != sector]
|
|
other_mag = sum_hook_magnitude(other_sectors)
|
|
loop_parts = 0
|
|
for hook in Hook:
|
|
opp = opposite_h_type(hook).value
|
|
if h_mag[hook.value] > other_mag[opp] and loop_present(hook, opp, h_mag, other_mag):
|
|
loop_parts += (h_mag[hook.value] - other_mag[opp]) / 2
|
|
forced_loops += math.floor(loop_parts)
|
|
return forced_loops
|
|
|
|
|
|
def loop_present(hook, opp, h_mag, other_mag):
|
|
if hook == Hook.Stairs:
|
|
return h_mag[hook.value] - other_mag[opp] >= 2
|
|
else:
|
|
return h_mag[opp] >= h_mag[hook.value] - other_mag[opp]
|
|
|
|
|
|
def is_satisfied(door_dict_list):
|
|
for door_dict in door_dict_list:
|
|
for door_list in door_dict.values():
|
|
if len(door_list) > 0:
|
|
return False
|
|
return True
|
|
|
|
|
|
# todo: maybe filter by used doors too
|
|
# todo: I want the number of door that match is accessible by still
|
|
def filter_match_deps(candidate, match_deps):
|
|
return [x for x in match_deps if x != candidate]
|
|
|
|
|
|
def sum_magnitude(sector_list):
|
|
result = [0] * len(PolSlot)
|
|
for sector in sector_list:
|
|
vector = sector.magnitude()
|
|
for i in range(len(result)):
|
|
result[i] = result[i] + vector[i]
|
|
return result
|
|
|
|
|
|
def sum_hook_magnitude(sector_list):
|
|
result = [0] * len(Hook)
|
|
for sector in sector_list:
|
|
vector = sector.hook_magnitude()
|
|
for i in range(len(result)):
|
|
result[i] = result[i] + vector[i]
|
|
return result
|
|
|
|
|
|
def sum_polarity(sector_list):
|
|
pol = Polarity()
|
|
for sector in sector_list:
|
|
pol += sector.polarity()
|
|
return pol
|
|
|
|
|
|
def assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, builder_info):
|
|
# step 1: fix polarity connection issues
|
|
unconnected_builders = identify_polarity_issues(dungeon_map)
|
|
while len(unconnected_builders) > 0:
|
|
for name, builder in unconnected_builders.items():
|
|
candidates = find_connection_candidates(builder.mag_needed, polarized_sectors)
|
|
valid, sector = False, None
|
|
while not valid:
|
|
if len(candidates) == 0:
|
|
raise GenerationException('Cross Dungeon Builder: Cannot find a candidate for connectedness. %s' % name)
|
|
sector = random.choice(candidates)
|
|
candidates.remove(sector)
|
|
valid = global_pole.is_valid_choice(dungeon_map, builder, [sector])
|
|
assign_sector(sector, builder, polarized_sectors, global_pole)
|
|
builder.mag_needed = {}
|
|
unconnected_builders = identify_polarity_issues(unconnected_builders)
|
|
|
|
# step 2: fix dead ends
|
|
problem_builders = identify_simple_branching_issues(dungeon_map)
|
|
while len(problem_builders) > 0:
|
|
for name, builder in problem_builders.items():
|
|
candidates, charges = find_simple_branching_candidates(builder, polarized_sectors)
|
|
best = min(charges)
|
|
best_candidates = [x for i, x in enumerate(candidates) if charges[i] <= best]
|
|
valid, choice = False, None
|
|
while not valid:
|
|
if len(best_candidates) == 0:
|
|
if len(candidates) == 0:
|
|
raise GenerationException('Cross Dungeon Builder: Simple branch problems: %s' % name)
|
|
best = min(charges)
|
|
best_candidates = [x for i, x in enumerate(candidates) if charges[i] <= best]
|
|
choice = random.choice(best_candidates)
|
|
best_candidates.remove(choice)
|
|
i = candidates.index(choice)
|
|
candidates.pop(i)
|
|
charges.pop(i)
|
|
valid = global_pole.is_valid_choice(dungeon_map, builder, [choice]) and valid_connected_assignment(builder, [choice])
|
|
assign_sector(choice, builder, polarized_sectors, global_pole)
|
|
builder.total_conn_lack = 0
|
|
builder.conn_balance.clear()
|
|
problem_builders = identify_simple_branching_issues(problem_builders)
|
|
|
|
# step 3: fix neutrality issues
|
|
polarity_step_3(dungeon_map, polarized_sectors, global_pole)
|
|
|
|
# step 4: fix dead ends again
|
|
neutral_choices: List[List] = neutralize_the_rest(polarized_sectors)
|
|
problem_builders = identify_branching_issues(dungeon_map, builder_info)
|
|
while len(problem_builders) > 0:
|
|
for name, builder in problem_builders.items():
|
|
candidates = find_branching_candidates(builder, neutral_choices, builder_info)
|
|
valid, choice, package = False, None, None
|
|
while not valid:
|
|
if len(candidates) <= 0:
|
|
raise GenerationException('Cross Dungeon Builder: Complex branch problems: %s' % name)
|
|
choice, package = random.choice(candidates)
|
|
candidates.remove((choice, package))
|
|
valid = global_pole.is_valid_choice(dungeon_map, builder, choice) and valid_polarized_assignment(builder, choice)
|
|
neutral_choices.remove(choice)
|
|
for sector in choice:
|
|
assign_sector(sector, builder, polarized_sectors, global_pole)
|
|
if package:
|
|
builder.throne_door, builder.throne_sector, builder.chosen_lobby = package
|
|
builder.unfulfilled.clear()
|
|
problem_builders = identify_branching_issues(problem_builders, builder_info)
|
|
|
|
# step 5: assign randomly until gone - must maintain connectedness, neutral polarity, branching, lack, etc.
|
|
comb_w_replace = len(dungeon_map) ** len(neutral_choices)
|
|
combinations = None
|
|
if comb_w_replace <= 1000:
|
|
combinations = list(itertools.product(dungeon_map.keys(), repeat=len(neutral_choices)))
|
|
random.shuffle(combinations)
|
|
tries = 0
|
|
while len(polarized_sectors) > 0:
|
|
if tries > 1000 or (combinations and tries >= len(combinations)):
|
|
raise GenerationException('No valid assignment found. Ref: %s' % next(iter(dungeon_map.keys())))
|
|
if combinations:
|
|
choices = combinations[tries]
|
|
else:
|
|
choices = random.choices(list(dungeon_map.keys()), k=len(neutral_choices))
|
|
chosen_sectors = defaultdict(list)
|
|
for i, choice in enumerate(choices):
|
|
chosen_sectors[choice].extend(neutral_choices[i])
|
|
all_valid, package_map = True, {}
|
|
for name, sector_list in chosen_sectors.items():
|
|
flag, package = valid_assignment(dungeon_map[name], sector_list, builder_info)
|
|
if not flag:
|
|
all_valid = False
|
|
break
|
|
if package:
|
|
package_map[dungeon_map[name]] = package
|
|
if all_valid:
|
|
for i, choice in enumerate(choices):
|
|
builder = dungeon_map[choice]
|
|
for sector in neutral_choices[i]:
|
|
assign_sector(sector, builder, polarized_sectors, global_pole)
|
|
if builder in package_map:
|
|
builder.throne_door, builder.throne_sector, builder.chosen_lobby = package_map[builder]
|
|
tries += 1
|
|
|
|
|
|
def polarity_step_3(dungeon_map, polarized_sectors, global_pole):
|
|
# step 3a: fix odd builders
|
|
odd_builders = [x for x in dungeon_map.values() if sum_polarity(x.sectors).charge() % 2 != 0]
|
|
grouped_choices: List[List] = find_forced_groupings(polarized_sectors, dungeon_map)
|
|
random.shuffle(odd_builders)
|
|
odd_candidates = find_odd_sectors(grouped_choices)
|
|
tries = 0
|
|
while len(odd_builders) > 0:
|
|
if tries > 1000:
|
|
raise GenerationException('Unable to fix dungeon parity. Ref: %s' % next(iter(odd_builders)).name)
|
|
best_choices = None
|
|
best_charge = sum([x.polarity().charge() for x in dungeon_map.values()])
|
|
samples = 0
|
|
combos = ncr(len(odd_candidates), len(odd_builders))
|
|
sample_target = 100 if combos > 10 else combos * 2
|
|
while best_choices is None or samples < sample_target:
|
|
samples += 1
|
|
if len(odd_candidates) < len(odd_builders):
|
|
raise GenerationException(f'Unable to fix dungeon parity - not enough candidates.'
|
|
f' Ref: {next(iter(odd_builders)).name}')
|
|
choices = random.sample(odd_candidates, k=len(odd_builders))
|
|
valid = global_pole.is_valid_multi_choice(dungeon_map, odd_builders, choices)
|
|
charge = calc_total_charge(dungeon_map, odd_builders, choices)
|
|
if valid and charge < best_charge:
|
|
best_choices = choices
|
|
best_charge = charge
|
|
if samples > sample_target and best_choices is None:
|
|
best_choices = choices
|
|
best_charge = charge
|
|
samples = 0
|
|
all_valid = True
|
|
for i, candidate_list in enumerate(best_choices):
|
|
test_set = find_forced_connections(dungeon_map, candidate_list, polarized_sectors)
|
|
builder = odd_builders[i]
|
|
if ensure_test_set_connectedness(test_set, builder, polarized_sectors, dungeon_map, global_pole):
|
|
all_valid &= valid_branch_only(builder, candidate_list)
|
|
else:
|
|
all_valid = False
|
|
break
|
|
if not all_valid:
|
|
break
|
|
if all_valid:
|
|
for i, candidate_list in enumerate(best_choices):
|
|
builder = odd_builders[i]
|
|
for sector in candidate_list:
|
|
assign_sector(sector, builder, polarized_sectors, global_pole)
|
|
odd_builders = [x for x in dungeon_map.values() if sum_polarity(x.sectors).charge() % 2 != 0]
|
|
else:
|
|
tries += 1
|
|
|
|
# step 3b: neutralize all builders
|
|
parallel_full_neutralization(dungeon_map, polarized_sectors, global_pole)
|
|
|
|
|
|
def parallel_full_neutralization(dungeon_map, polarized_sectors, global_pole):
|
|
start = time.process_time()
|
|
builders = list(dungeon_map.values())
|
|
finished = all([x.polarity().is_neutral() for x in builders])
|
|
solution_list, current_depth = defaultdict(list), 1
|
|
complete_builders = [x for x in builders if x.polarity().is_neutral()]
|
|
avail_sectors = list(polarized_sectors)
|
|
while not finished:
|
|
builders_to_check = [x for x in builders if not (x.polarity()+sum_polarity(solution_list[x])).is_neutral()]
|
|
candidates, last_depth = find_exact_neutralizing_candidates_parallel_db(builders_to_check, solution_list,
|
|
avail_sectors, current_depth)
|
|
increment_depth = True
|
|
any_valid = False
|
|
for builder, candidate_list in candidates.items():
|
|
valid, sectors = False, None
|
|
while not valid:
|
|
if len(candidate_list) == 0:
|
|
increment_depth = False #need to look again at current level
|
|
break
|
|
sectors = random.choice(candidate_list)
|
|
candidate_list.remove(sectors)
|
|
proposal = solution_list.copy()
|
|
proposal[builder] = list(proposal[builder])
|
|
proposal[builder].extend(sectors)
|
|
valid = global_pole.is_valid_multi_choice_2(dungeon_map, builders, proposal)
|
|
if valid:
|
|
any_valid = True
|
|
solution_list[builder].extend(sectors)
|
|
for sector in sectors:
|
|
avail_sectors.remove(sector)
|
|
complete_builders.append(builder)
|
|
for other_builder, other_cand_list in candidates.items():
|
|
if other_builder not in complete_builders:
|
|
candidates_to_remove = list()
|
|
for candidate in other_cand_list:
|
|
for sector in sectors:
|
|
if sector in candidate:
|
|
candidates_to_remove.append(candidate)
|
|
break
|
|
other_cand_list[:] = [x for x in other_cand_list if x not in candidates_to_remove]
|
|
# remove sectors from other candidate lists
|
|
if not any_valid:
|
|
increment_depth = True
|
|
current_depth = last_depth + 1 if increment_depth else last_depth
|
|
finished = all([(x.polarity()+sum_polarity(solution_list[x])).is_neutral() for x in builders])
|
|
logging.getLogger('').info(f'-Balanced solution found in {time.process_time()-start}')
|
|
for builder, sectors in solution_list.items():
|
|
for sector in sectors:
|
|
assign_sector(sector, builder, polarized_sectors, global_pole)
|
|
|
|
|
|
def find_forced_connections(dungeon_map, candidate_list, polarized_sectors):
|
|
test_set = list(candidate_list)
|
|
other_sectors = [x for x in polarized_sectors if x not in candidate_list]
|
|
dungeon_hooks = defaultdict(int)
|
|
for name, builder in dungeon_map.items():
|
|
d_mag = sum_hook_magnitude(builder.sectors)
|
|
for val in Hook:
|
|
dungeon_hooks[val] += d_mag[val.value]
|
|
queue = deque(candidate_list)
|
|
while queue:
|
|
candidate = queue.popleft()
|
|
c_mag = candidate.hook_magnitude()
|
|
other_candidates = [x for x in candidate_list if x != candidate]
|
|
for val in Hook:
|
|
if c_mag[val.value] > 0:
|
|
opp = opposite_h_type(val)
|
|
o_val = opp.value
|
|
if sum_hook_magnitude(other_candidates)[o_val] == 0 and dungeon_hooks[opp] == 0 and not valid_self(c_mag, val, opp):
|
|
forced_sector = []
|
|
for sec in other_sectors:
|
|
if sec.hook_magnitude()[o_val] > 0:
|
|
forced_sector.append(sec)
|
|
if len(forced_sector) > 1:
|
|
break
|
|
if len(forced_sector) == 1:
|
|
test_set.append(forced_sector[0])
|
|
return test_set
|
|
|
|
|
|
def valid_self(c_mag, val, opp):
|
|
if val == Hook.Stairs:
|
|
return c_mag[val.value] > 2
|
|
else:
|
|
return c_mag[opp.value] > 0 and sum(c_mag) > 2
|
|
|
|
|
|
def ensure_test_set_connectedness(test_set, builder, polarized_sectors, dungeon_map, global_pole):
|
|
test_copy = list(test_set)
|
|
while not valid_connected_assignment(builder, test_copy):
|
|
dummy_builder = DungeonBuilder("Dummy Builder for " + builder.name)
|
|
dummy_builder.sectors = builder.sectors + test_copy
|
|
possibles = [x for x in polarized_sectors if x not in test_copy]
|
|
candidates = find_connected_candidates(possibles)
|
|
valid, sector = False, None
|
|
while not valid:
|
|
if len(candidates) == 0:
|
|
return False
|
|
sector = random.choice(candidates)
|
|
candidates.remove(sector)
|
|
t2 = test_copy+[sector]
|
|
valid = global_pole.is_valid_choice(dungeon_map, builder, t2) and valid_branch_only(builder, t2)
|
|
test_copy.append(sector)
|
|
dummy_builder.sectors = builder.sectors + test_copy
|
|
test_set[:] = test_copy
|
|
return True
|
|
|
|
|
|
def calc_total_charge(dungeon_map, builders, sector_lists):
|
|
polarity_list = [x.polarity() for x in dungeon_map.values() if x not in builders]
|
|
for i, sectors in enumerate(sector_lists):
|
|
builder = builders[i]
|
|
polarity = builder.polarity() + sum_polarity(sectors)
|
|
polarity_list.append(polarity)
|
|
return sum([x.charge() for x in polarity_list])
|
|
|
|
|
|
class GlobalPolarity:
|
|
|
|
def __init__(self, candidate_sectors):
|
|
self.positives = [0, 0, 0]
|
|
self.negatives = [0, 0, 0]
|
|
self.evens = 0
|
|
self.odds = 0
|
|
for sector in candidate_sectors:
|
|
pol = sector.polarity()
|
|
if pol.charge() % 2 == 0:
|
|
self.evens += 1
|
|
else:
|
|
self.odds += 1
|
|
for slot in PolSlot:
|
|
if pol.vector[slot.value] < 0:
|
|
self.negatives[slot.value] += -pol.vector[slot.value]
|
|
elif pol.vector[slot.value] > 0:
|
|
self.positives[slot.value] += pol.vector[slot.value]
|
|
|
|
def copy(self):
|
|
gp = GlobalPolarity([])
|
|
gp.positives = self.positives.copy()
|
|
gp.negatives = self.negatives.copy()
|
|
gp.evens = self.evens
|
|
gp.odds = self.odds
|
|
return gp
|
|
|
|
def is_valid(self, dungeon_map):
|
|
polarities = [x.polarity() for x in dungeon_map.values()]
|
|
return self._check_parity(polarities) and self._is_valid_polarities(polarities)
|
|
|
|
def _check_parity(self, polarities):
|
|
local_evens = 0
|
|
local_odds = 0
|
|
for pol in polarities:
|
|
if pol.charge() % 2 == 0:
|
|
local_evens += 1
|
|
else:
|
|
local_odds += 1
|
|
if local_odds > self.odds:
|
|
return False
|
|
return True
|
|
|
|
def _is_valid_polarities(self, polarities):
|
|
positives = self.positives.copy()
|
|
negatives = self.negatives.copy()
|
|
for polarity in polarities:
|
|
for slot in PolSlot:
|
|
if polarity[slot.value] > 0 and slot != PolSlot.Stairs:
|
|
if negatives[slot.value] >= polarity[slot.value]:
|
|
negatives[slot.value] -= polarity[slot.value]
|
|
else:
|
|
return False
|
|
elif polarity[slot.value] < 0 and slot != PolSlot.Stairs:
|
|
if positives[slot.value] >= -polarity[slot.value]:
|
|
positives[slot.value] += polarity[slot.value]
|
|
else:
|
|
return False
|
|
elif slot == PolSlot.Stairs:
|
|
if positives[slot.value] >= polarity[slot.value]:
|
|
positives[slot.value] -= polarity[slot.value]
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def consume(self, sector):
|
|
polarity = sector.polarity()
|
|
if polarity.charge() % 2 == 0:
|
|
self.evens -= 1
|
|
else:
|
|
self.odds -= 1
|
|
for slot in PolSlot:
|
|
if polarity[slot.value] > 0 and slot != PolSlot.Stairs:
|
|
if self.positives[slot.value] >= polarity[slot.value]:
|
|
self.positives[slot.value] -= polarity[slot.value]
|
|
else:
|
|
raise GenerationException('Invalid assignment of %s' % sector.name)
|
|
elif polarity[slot.value] < 0 and slot != PolSlot.Stairs:
|
|
if self.negatives[slot.value] >= -polarity[slot.value]:
|
|
self.negatives[slot.value] += polarity[slot.value]
|
|
else:
|
|
raise GenerationException('Invalid assignment of %s' % sector.name)
|
|
elif slot == PolSlot.Stairs:
|
|
if self.positives[slot.value] >= polarity[slot.value]:
|
|
self.positives[slot.value] -= polarity[slot.value]
|
|
else:
|
|
raise GenerationException('Invalid assignment of %s' % sector.name)
|
|
|
|
def is_valid_choice(self, dungeon_map, builder, sectors):
|
|
proposal = self.copy()
|
|
non_neutral_polarities = [x.polarity() for x in dungeon_map.values() if not x.polarity().is_neutral() and x != builder]
|
|
current_polarity = builder.polarity() + sum_polarity(sectors)
|
|
non_neutral_polarities.append(current_polarity)
|
|
for sector in sectors:
|
|
proposal.consume(sector)
|
|
return proposal._check_parity(non_neutral_polarities) and proposal._is_valid_polarities(non_neutral_polarities)
|
|
|
|
def is_valid_multi_choice(self, dungeon_map, builders, sector_lists):
|
|
proposal = self.copy()
|
|
non_neutral_polarities = [x.polarity() for x in dungeon_map.values() if not x.polarity().is_neutral()
|
|
and x not in builders]
|
|
for i, sectors in enumerate(sector_lists):
|
|
builder = builders[i]
|
|
current_polarity = builder.polarity() + sum_polarity(sectors)
|
|
non_neutral_polarities.append(current_polarity)
|
|
for sector in sectors:
|
|
proposal.consume(sector)
|
|
return proposal._check_parity(non_neutral_polarities) and proposal._is_valid_polarities(non_neutral_polarities)
|
|
|
|
def is_valid_multi_choice_2(self, dungeon_map, builders, sector_dict):
|
|
proposal = self.copy()
|
|
non_neutral_polarities = [x.polarity() for x in dungeon_map.values() if not x.polarity().is_neutral()
|
|
and x not in builders]
|
|
for builder, sectors in sector_dict.items():
|
|
current_polarity = builder.polarity() + sum_polarity(sectors)
|
|
non_neutral_polarities.append(current_polarity)
|
|
for sector in sectors:
|
|
proposal.consume(sector)
|
|
return proposal._check_parity(non_neutral_polarities) and proposal._is_valid_polarities(non_neutral_polarities)
|
|
|
|
# def check_odd_polarities(self, candidate_sectors, dungeon_map):
|
|
# odd_candidates = [x for x in candidate_sectors if x.polarity().charge() % 2 != 0]
|
|
# odd_map = {n: x for (n, x) in dungeon_map.items() if sum_polarity(x.sectors).charge() % 2 != 0}
|
|
# gp = GlobalPolarity(odd_candidates)
|
|
# return gp.is_valid(odd_map)
|
|
|
|
|
|
def find_connection_candidates(mag_needed, sector_pool):
|
|
candidates = []
|
|
for sector in sector_pool:
|
|
if sector.branching_factor() < 2:
|
|
continue
|
|
mag = sector.magnitude()
|
|
matches = False
|
|
for slot, match_slot in mag_needed.items():
|
|
if mag[slot.value] > 0:
|
|
for i in PolSlot:
|
|
if i in match_slot and mag[i.value] > 0:
|
|
matches = True
|
|
break
|
|
if matches:
|
|
candidates.append(sector)
|
|
return candidates
|
|
|
|
|
|
def find_simple_branching_candidates(builder, sector_pool):
|
|
candidates = defaultdict(list)
|
|
charges = defaultdict(list)
|
|
outflow_needed = builder.dead_ends + builder.forced_loops * 2 > builder.branches + builder.allowance
|
|
total_needed = builder.dead_ends + builder.forced_loops * 2 - builder.branches + builder.allowance
|
|
original_lack = builder.total_conn_lack
|
|
best_lack = original_lack
|
|
for sector in sector_pool:
|
|
if outflow_needed and sector.branching_factor() <= 2:
|
|
continue
|
|
calc_sector_balance(sector)
|
|
ttl_lack = 0
|
|
for hook in Hook:
|
|
lack = builder.conn_balance[hook] + sector.conn_balance[hook]
|
|
if lack < 0:
|
|
ttl_lack += -lack
|
|
forced_loops = calc_forced_loops(builder.sectors + [sector])
|
|
net_outflow = builder.dead_ends + forced_loops * 2 + sector.dead_ends() - builder.branches - builder.allowance - sector.branches()
|
|
valid_branches = net_outflow < total_needed
|
|
if valid_branches and (ttl_lack < original_lack or original_lack >= 0):
|
|
candidates[ttl_lack].append(sector)
|
|
charges[ttl_lack].append((builder.polarity()+sector.polarity()).charge())
|
|
if ttl_lack < best_lack:
|
|
best_lack = ttl_lack
|
|
if best_lack == original_lack and not outflow_needed:
|
|
raise GenerationException('These candidates may not help at all')
|
|
if len(candidates[best_lack]) <= 0:
|
|
raise GenerationException('Nothing can fix the simple branching issue. Panic ensues.')
|
|
return candidates[best_lack], charges[best_lack]
|
|
|
|
|
|
def calc_sector_balance(sector): # todo: move to base class?
|
|
if sector.conn_balance is None:
|
|
sector.conn_balance = defaultdict(int)
|
|
for door in sector.outstanding_doors:
|
|
if door.blocked or door.dead or sector.branching_factor() <= 1:
|
|
sector.conn_balance[hook_from_door(door)] -= 1
|
|
else:
|
|
sector.conn_balance[hanger_from_door(door)] += 1
|
|
|
|
|
|
def find_odd_sectors(grouped_candidates):
|
|
return [x for x in grouped_candidates if sum_polarity(x).charge() % 2 != 0]
|
|
|
|
|
|
# This is related to the perfect sum problem in CS
|
|
# * Best algorithm so far - db for dynamic programming
|
|
# * Keeps track of unique deviations from neutral in the index
|
|
# * Another assumption is that solutions that take fewer sector are more ideal
|
|
# * When attempting to add depth and there are no more possibilities, this raises an Exception
|
|
# * Each depth should be checked before asking for another one
|
|
# An alterative approach would be to trim the db after deciding the candidate at the current depth will be
|
|
# part of the propsoal
|
|
def find_exact_neutralizing_candidates_parallel_db(builders, proposal, avail_sectors, current_depth):
|
|
candidate_map = defaultdict(list)
|
|
polarity_map = {}
|
|
for builder in builders:
|
|
polarity_map[builder] = builder.polarity() + sum_polarity(proposal[builder])
|
|
finished = False
|
|
db, index = create_db_for_depth(current_depth, avail_sectors)
|
|
while not finished:
|
|
depth_map = db[current_depth]
|
|
for builder in builders:
|
|
target = polarity_map[builder].complement()
|
|
if target in depth_map.keys():
|
|
finished = True
|
|
candidate_map[builder].extend(depth_map[target].keys())
|
|
if finished:
|
|
for builder in list(candidate_map.keys()):
|
|
try:
|
|
candidate_map[builder] = weed_candidates(builder, {0: candidate_map[builder]}, 0)
|
|
except NeutralizingException:
|
|
del candidate_map[builder]
|
|
if len(candidate_map) == 0:
|
|
finished = False
|
|
if not finished:
|
|
current_depth += 1
|
|
add_depth_to_db(db, index, current_depth, avail_sectors)
|
|
return candidate_map, current_depth
|
|
|
|
|
|
def create_db_for_depth(depth, avail_sectors):
|
|
db = {0: {Polarity(): {OrderedFrozenSet(): None}}}
|
|
db_index = {Polarity()}
|
|
for i in range(1, depth+1):
|
|
add_depth_to_db(db, db_index, i, avail_sectors)
|
|
return db, db_index
|
|
|
|
|
|
def add_depth_to_db(db, db_index, i, avail_sectors):
|
|
previous = db[i-1]
|
|
depth_map = defaultdict(dict)
|
|
index_additions = set()
|
|
for sector in avail_sectors:
|
|
sector_set = {sector}
|
|
sector_pol = sector.polarity()
|
|
for polarity, choices in previous.items():
|
|
combo_pol = sector_pol + polarity
|
|
if combo_pol not in db_index:
|
|
index_additions.add(combo_pol)
|
|
for choice in choices:
|
|
if sector in choice.frozen_set:
|
|
continue
|
|
new_set = choice.new_with_element(sector_set)
|
|
depth_map[combo_pol][new_set] = None
|
|
for addition in index_additions:
|
|
if len(depth_map[addition]) > 0:
|
|
db_index.add(addition)
|
|
else:
|
|
del depth_map[addition]
|
|
if len(depth_map) == 0:
|
|
raise NeutralizingException('There is not a solution for this particular combination. Crystal switch issue?') # restart required
|
|
db[i] = depth_map
|
|
|
|
|
|
class OrderedFrozenSet:
|
|
|
|
def __init__(self):
|
|
self.frozen_set = frozenset()
|
|
self.order = []
|
|
|
|
def __eq__(self, other):
|
|
return self.frozen_set == other.frozen_set
|
|
|
|
def __hash__(self):
|
|
return hash(self.frozen_set)
|
|
|
|
def __iter__(self):
|
|
return self.order.__iter__()
|
|
|
|
def __len__(self):
|
|
return len(self.order)
|
|
|
|
def new_with_element(self, elements):
|
|
ret = OrderedFrozenSet()
|
|
ret.frozen_set = frozenset(self.frozen_set | elements)
|
|
ret.order = list(self.order)
|
|
ret.order.extend(elements)
|
|
return ret
|
|
|
|
|
|
# this could be re-worked for the more complete solution
|
|
# i'm not sure it does a whole lot now
|
|
def weed_candidates(builder, candidates, best_charge):
|
|
official_cand = []
|
|
while len(official_cand) == 0:
|
|
if len(candidates.keys()) == 0:
|
|
raise NeutralizingException('Cross Dungeon Builder: Weeded out all candidates %s' % builder.name)
|
|
while best_charge not in candidates.keys():
|
|
best_charge += 1
|
|
candidate_list = candidates.pop(best_charge)
|
|
best_lack = None
|
|
for cand in candidate_list:
|
|
ttl_deads = 0
|
|
ttl_branches = 0
|
|
for sector in cand:
|
|
calc_sector_balance(sector)
|
|
ttl_deads += sector.dead_ends()
|
|
ttl_branches += sector.branches()
|
|
ttl_lack = 0
|
|
ttl_balance = 0
|
|
for hook in Hook:
|
|
bal = 0
|
|
for sector in cand:
|
|
bal += sector.conn_balance[hook]
|
|
lack = builder.conn_balance[hook] + bal
|
|
ttl_balance += lack
|
|
if lack < 0:
|
|
ttl_lack += -lack
|
|
forced_loops = calc_forced_loops(builder.sectors + list(cand))
|
|
if ttl_balance >= 0 and builder.dead_ends + ttl_deads + forced_loops * 2 <= builder.branches + ttl_branches + builder.allowance:
|
|
if best_lack is None or ttl_lack < best_lack:
|
|
best_lack = ttl_lack
|
|
official_cand = [cand]
|
|
elif ttl_lack == best_lack:
|
|
official_cand.append(cand)
|
|
|
|
# choose from among those that use less
|
|
best_len = None
|
|
cand_len = []
|
|
for cand in official_cand:
|
|
size = len(cand)
|
|
if best_len is None or size < best_len:
|
|
best_len = size
|
|
cand_len = [cand]
|
|
elif size == best_len:
|
|
cand_len.append(cand)
|
|
return cand_len
|
|
|
|
|
|
def find_branching_candidates(builder, neutral_choices, builder_info):
|
|
candidates = []
|
|
for choice in neutral_choices:
|
|
resolved, problem_list, package = check_for_valid_layout(builder, choice, builder_info)
|
|
if resolved:
|
|
candidates.append((choice, package))
|
|
return candidates
|
|
|
|
|
|
def find_connected_candidates(sector_pool):
|
|
candidates = []
|
|
for sector in sector_pool:
|
|
if sector.adj_outflow() >= 2:
|
|
candidates.append(sector)
|
|
return candidates
|
|
|
|
|
|
def neutralize_the_rest(sector_pool):
|
|
neutral_choices = []
|
|
main_pool = list(sector_pool)
|
|
failed_pool = []
|
|
r_size = 1
|
|
while len(main_pool) > 0 or len(failed_pool) > 0:
|
|
if len(main_pool) <= r_size:
|
|
main_pool.extend(failed_pool)
|
|
failed_pool.clear()
|
|
r_size += 1
|
|
candidate = random.choice(main_pool)
|
|
main_pool.remove(candidate)
|
|
if r_size > len(main_pool):
|
|
raise GenerationException("Cross Dungeon Builder: no more neutral pairings possible")
|
|
combinations = ncr(len(main_pool), r_size)
|
|
itr = 0
|
|
done = False
|
|
while not done:
|
|
ttl_polarity = candidate.polarity()
|
|
choice_set = kth_combination(itr, main_pool, r_size)
|
|
for choice in choice_set:
|
|
ttl_polarity += choice.polarity()
|
|
if ttl_polarity.is_neutral():
|
|
choice_set.append(candidate)
|
|
neutral_choices.append(choice_set)
|
|
main_pool = [x for x in main_pool if x not in choice_set]
|
|
failed_pool = [x for x in failed_pool if x not in choice_set]
|
|
done = True
|
|
else:
|
|
itr += 1
|
|
if itr >= combinations:
|
|
failed_pool.append(candidate)
|
|
done = True
|
|
return neutral_choices
|
|
|
|
|
|
# doesn't force a grouping when all in the found_list comes from the same sector
|
|
def find_forced_groupings(sector_pool, dungeon_map):
|
|
dungeon_hooks = {}
|
|
for name, builder in dungeon_map.items():
|
|
dungeon_hooks[name] = categorize_groupings(builder.sectors)
|
|
groupings = []
|
|
queue = deque(sector_pool)
|
|
skips = set()
|
|
while len(queue) > 0:
|
|
grouping = queue.popleft()
|
|
is_list = isinstance(grouping, List)
|
|
if not is_list and grouping in skips:
|
|
continue
|
|
grouping = grouping if is_list else [grouping]
|
|
hook_categories = categorize_groupings(grouping)
|
|
force_found = False
|
|
for val in Hook:
|
|
if val in hook_categories.keys():
|
|
required_doors, flexible_doors = hook_categories[val]
|
|
if len(required_doors) >= 1:
|
|
opp = opposite_h_type(val)
|
|
found_list = []
|
|
if opp in hook_categories.keys() and len(hook_categories[opp][1]) > 0:
|
|
found_list.extend(hook_categories[opp][1])
|
|
for name, hooks in dungeon_hooks.items():
|
|
if opp in hooks.keys() and len(hooks[opp][1]) > 0:
|
|
found_list.extend(hooks[opp][1])
|
|
other_sectors = [x for x in sector_pool if x not in grouping]
|
|
other_sector_cats = categorize_groupings(other_sectors)
|
|
if opp in other_sector_cats.keys() and len(other_sector_cats[opp][1]) > 0:
|
|
found_list.extend(other_sector_cats[opp][1])
|
|
if len(required_doors) == len(found_list):
|
|
forced_sectors = []
|
|
for sec in other_sectors:
|
|
cats = categorize_groupings([sec])
|
|
if opp in cats.keys() and len(cats[opp][1]) > 0:
|
|
forced_sectors.append(sec)
|
|
if len(forced_sectors) > 0:
|
|
grouping.extend(forced_sectors)
|
|
skips.update(forced_sectors)
|
|
merge_groups = []
|
|
for group in groupings:
|
|
for sector in group:
|
|
if sector in forced_sectors:
|
|
merge_groups.append(group)
|
|
for merge in merge_groups:
|
|
grouping = list(set(grouping).union(set(merge)))
|
|
groupings.remove(merge)
|
|
queue.append(grouping)
|
|
force_found = True
|
|
elif len(flexible_doors) == 1:
|
|
opp = opposite_h_type(val)
|
|
found_list = []
|
|
if opp in hook_categories.keys() and (len(hook_categories[opp][0]) > 0 or len(hook_categories[opp][1]) > 0):
|
|
found_list.extend(hook_categories[opp][0])
|
|
found_list.extend([x for x in hook_categories[opp][1] if x not in flexible_doors])
|
|
for name, hooks in dungeon_hooks.items():
|
|
if opp in hooks.keys() and (len(hooks[opp][0]) > 0 or len(hooks[opp][1]) > 0):
|
|
found_list.extend(hooks[opp][0])
|
|
found_list.extend(hooks[opp][1])
|
|
other_sectors = [x for x in sector_pool if x not in grouping]
|
|
other_sector_cats = categorize_groupings(other_sectors)
|
|
if opp in other_sector_cats.keys() and (len(other_sector_cats[opp][0]) > 0 or len(other_sector_cats[opp][1]) > 0):
|
|
found_list.extend(other_sector_cats[opp][0])
|
|
found_list.extend(other_sector_cats[opp][1])
|
|
if len(found_list) == 1:
|
|
forced_sectors = []
|
|
for sec in other_sectors:
|
|
cats = categorize_groupings([sec])
|
|
if opp in cats.keys() and (len(cats[opp][0]) > 0 or len(cats[opp][1]) > 0):
|
|
forced_sectors.append(sec)
|
|
if len(forced_sectors) > 0:
|
|
grouping.extend(forced_sectors)
|
|
skips.update(forced_sectors)
|
|
merge_groups = []
|
|
for group in groupings:
|
|
for sector in group:
|
|
if sector in forced_sectors:
|
|
merge_groups.append(group)
|
|
for merge in merge_groups:
|
|
grouping += merge
|
|
groupings.remove(merge)
|
|
queue.append(grouping)
|
|
force_found = True
|
|
if force_found:
|
|
break
|
|
if not force_found:
|
|
groupings.append(grouping)
|
|
return groupings
|
|
|
|
|
|
def categorize_groupings(sectors):
|
|
hook_categories = {}
|
|
for sector in sectors:
|
|
for door in sector.outstanding_doors:
|
|
hook = hook_from_door(door)
|
|
if hook not in hook_categories.keys():
|
|
hook_categories[hook] = ([], [])
|
|
if door.blocked or door.dead:
|
|
hook_categories[hook][0].append(door)
|
|
else:
|
|
hook_categories[hook][1].append(door)
|
|
return hook_categories
|
|
|
|
|
|
def valid_assignment(builder, sector_list, builder_info):
|
|
if not valid_entrance(builder, sector_list, builder_info):
|
|
return False, None
|
|
if not valid_c_switch(builder, sector_list):
|
|
return False, None
|
|
if not valid_polarized_assignment(builder, sector_list):
|
|
return False, None
|
|
resolved, problems, package = check_for_valid_layout(builder, sector_list, builder_info)
|
|
return resolved, package
|
|
|
|
|
|
def valid_entrance(builder, sector_list, builder_info):
|
|
is_dead_end = False
|
|
if len(builder.sectors) == 0:
|
|
is_dead_end = True
|
|
else:
|
|
entrances, splits, c_tuple, world, player = builder_info
|
|
if builder.name not in entrances.keys():
|
|
name_parts = builder.name.rsplit(' ', 1)
|
|
entrance_list = splits[name_parts[0]][name_parts[1]]
|
|
entrances = []
|
|
for sector in builder.sectors:
|
|
if sector.is_entrance_sector():
|
|
sector.region_set()
|
|
entrances.append(sector)
|
|
all_dead = True
|
|
for sector in entrances:
|
|
for region in entrance_list:
|
|
if region in sector.region_set():
|
|
portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region.name == region), None)
|
|
if portal and not portal.deadEnd:
|
|
all_dead = False
|
|
break
|
|
if not all_dead:
|
|
break
|
|
is_dead_end = all_dead
|
|
return len(sector_list) == 0 if is_dead_end else True
|
|
|
|
|
|
def valid_c_switch(builder, sector_list):
|
|
if builder.c_switch_present:
|
|
return True
|
|
for sector in sector_list:
|
|
if sector.c_switch:
|
|
return True
|
|
if builder.c_switch_required:
|
|
return False
|
|
for sector in sector_list:
|
|
if sector.blue_barrier:
|
|
return False
|
|
return True
|
|
|
|
|
|
def valid_connected_assignment(builder, sector_list):
|
|
full_list = sector_list + builder.sectors
|
|
if len(full_list) == 1 and sum_magnitude(full_list) == [0, 0, 0]:
|
|
return True
|
|
for sector in full_list:
|
|
if sector.is_entrance_sector():
|
|
continue
|
|
others = [x for x in full_list if x != sector]
|
|
other_mag = sum_magnitude(others)
|
|
sector_mag = sector.magnitude()
|
|
hookable = False
|
|
for i in range(len(sector_mag)):
|
|
if sector_mag[i] > 0 and other_mag[i] > 0:
|
|
hookable = True
|
|
if not hookable:
|
|
return False
|
|
return True
|
|
|
|
|
|
def valid_branch_assignment(builder, sector_list):
|
|
if not valid_connected_assignment(builder, sector_list):
|
|
return False
|
|
return valid_branch_only(builder, sector_list)
|
|
|
|
|
|
def valid_branch_only(builder, sector_list):
|
|
forced_loops = calc_forced_loops(builder.sectors + sector_list)
|
|
ttl_deads = 0
|
|
ttl_branches = 0
|
|
for s in sector_list:
|
|
# calc_sector_balance(sector) # do I want to check lack here? see weed_candidates
|
|
ttl_deads += s.dead_ends()
|
|
ttl_branches += s.branches()
|
|
return builder.dead_ends + ttl_deads + forced_loops * 2 <= builder.branches + ttl_branches + builder.allowance
|
|
|
|
|
|
def valid_polarized_assignment(builder, sector_list):
|
|
if not valid_branch_assignment(builder, sector_list):
|
|
return False
|
|
return (sum_polarity(sector_list) + sum_polarity(builder.sectors)).is_neutral()
|
|
|
|
|
|
def assign_the_rest(dungeon_map, neutral_sectors, global_pole, builder_info):
|
|
comb_w_replace = len(dungeon_map) ** len(neutral_sectors)
|
|
combinations = None
|
|
if comb_w_replace <= 1000:
|
|
combinations = list(itertools.product(dungeon_map.keys(), repeat=len(neutral_sectors)))
|
|
random.shuffle(combinations)
|
|
tries = 0
|
|
while len(neutral_sectors) > 0:
|
|
if tries > 1000 or (combinations and tries >= len(combinations)):
|
|
raise GenerationException('No valid assignment found for "neutral" sectors. Ref: %s' % next(iter(dungeon_map.keys())))
|
|
# sector_list = list(neutral_sectors)
|
|
if combinations:
|
|
choices = combinations[tries]
|
|
else:
|
|
choices = random.choices(list(dungeon_map.keys()), k=len(neutral_sectors))
|
|
neutral_sector_list = list(neutral_sectors)
|
|
chosen_sectors = defaultdict(list)
|
|
for i, choice in enumerate(choices):
|
|
chosen_sectors[choice].append(neutral_sector_list[i])
|
|
all_valid, package_map = True, {}
|
|
for name, sector_list in chosen_sectors.items():
|
|
flag, package = valid_assignment(dungeon_map[name], sector_list, builder_info)
|
|
if not flag:
|
|
all_valid = False
|
|
break
|
|
if package:
|
|
package_map[dungeon_map[name]] = package
|
|
if all_valid:
|
|
for name, sector_list in chosen_sectors.items():
|
|
builder = dungeon_map[name]
|
|
for sector in sector_list:
|
|
assign_sector(sector, builder, neutral_sectors, global_pole)
|
|
if builder in package_map:
|
|
builder.throne_door, builder.throne_sector, builder.chosen_lobby = package_map[builder]
|
|
tries += 1
|
|
|
|
|
|
def split_dungeon_builder(builder, split_list, builder_info):
|
|
ents, splits, c_tuple, world, player = builder_info
|
|
if builder.split_dungeon_map and len(builder.exception_list) == 0:
|
|
for name, proposal in builder.valid_proposal.items():
|
|
builder.split_dungeon_map[name].valid_proposal = proposal
|
|
if builder.name == 'Hyrule Castle':
|
|
builder.chosen_lobby.outstanding_doors.remove(builder.throne_door)
|
|
builder.throne_sector.outstanding_doors.remove(world.get_door('Hyrule Castle Throne Room N', player))
|
|
return builder.split_dungeon_map # we made this earlier in gen, just use it
|
|
|
|
attempts, comb_w_replace, merge_attempt, merge_limit = 0, None, 0, len(split_list) - 1
|
|
while attempts < 5: # does not solve coin flips 3% of the time
|
|
try:
|
|
candidate_sectors = dict.fromkeys(builder.sectors)
|
|
if builder.name == 'Hyrule Castle':
|
|
throne_sector = find_sector('Hyrule Castle Throne Room', candidate_sectors)
|
|
chosen_lobbies = {r_name for x in split_list.values() for r_name in x}
|
|
choices = {}
|
|
for sector in candidate_sectors:
|
|
if sector.adj_outflow() > 1 and sector != throne_sector:
|
|
for door in sector.outstanding_doors:
|
|
if door.direction == Direction.South and door.entrance.parent_region not in chosen_lobbies:
|
|
choices[door] = sector
|
|
chosen_door = random.choice(list(choices.keys()))
|
|
split_list['Sewers'].append(chosen_door.entrance.parent_region.name)
|
|
choices[chosen_door].outstanding_doors.remove(chosen_door)
|
|
builder.throne_door = chosen_door
|
|
builder.throne_sector = throne_sector
|
|
builder.chosen_lobby = choices[chosen_door]
|
|
throne_sector.outstanding_doors.remove(world.get_door('Hyrule Castle Throne Room N', player))
|
|
|
|
global_pole = GlobalPolarity(candidate_sectors)
|
|
dungeon_map, sub_builder, merge_keys = {}, None, []
|
|
if merge_attempt > 0:
|
|
candidates = []
|
|
for name, split_entrances in split_list.items():
|
|
if len(split_entrances) > 1:
|
|
candidates.append(name)
|
|
continue
|
|
elif len(split_entrances) <= 0:
|
|
continue
|
|
r_name = split_entrances[0]
|
|
p = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region.name == r_name), None)
|
|
if p and not p.deadEnd:
|
|
candidates.append(name)
|
|
merge_keys = random.sample(candidates, merge_attempt+1) if len(candidates) >= merge_attempt+1 else []
|
|
for name, split_entrances in split_list.items():
|
|
key = builder.name + ' ' + name
|
|
if merge_keys and name in merge_keys:
|
|
other_keys = [builder.name + ' ' + x for x in merge_keys if x != name]
|
|
other_key = next((x for x in other_keys if x in dungeon_map), None)
|
|
if other_key:
|
|
key = other_key
|
|
sub_builder = dungeon_map[other_key]
|
|
sub_builder.all_entrances.extend(split_entrances)
|
|
if key not in dungeon_map:
|
|
dungeon_map[key] = sub_builder = DungeonBuilder(key)
|
|
sub_builder.split_flag = True
|
|
sub_builder.all_entrances = list(split_entrances)
|
|
for r_name in split_entrances:
|
|
assign_sector(find_sector(r_name, candidate_sectors), sub_builder, candidate_sectors, global_pole)
|
|
if builder.name == 'Hyrule Castle':
|
|
assign_sector(find_sector('Hyrule Castle Throne Room', candidate_sectors),
|
|
dungeon_map['Hyrule Castle Dungeon'], candidate_sectors, global_pole)
|
|
assign_sector(find_sector('Hyrule Dungeon Cellblock', candidate_sectors),
|
|
dungeon_map['Hyrule Castle Dungeon'], candidate_sectors, global_pole)
|
|
dungeon_map['Hyrule Castle Dungeon'].throne_door = world.get_door('Hyrule Castle Throne Room N', player)
|
|
dungeon_map['Hyrule Castle Sewers'].sewers_access = builder.throne_door
|
|
if len(candidate_sectors) == 0:
|
|
return dungeon_map
|
|
comb_w_replace = len(dungeon_map) ** len(candidate_sectors)
|
|
return balance_split(candidate_sectors, dungeon_map, global_pole, builder_info)
|
|
except (GenerationException, NeutralizingException):
|
|
if comb_w_replace and comb_w_replace <= 10000 and not builder.throne_door:
|
|
attempts += 5 # all the combinations were tried already, no use repeating
|
|
else:
|
|
attempts += 1
|
|
if builder.throne_door:
|
|
previous = find_sector(builder.throne_door.entrance.parent_region.name, builder.sectors)
|
|
previous.outstanding_doors.append(builder.throne_door)
|
|
builder.throne_sector.outstanding_doors.append(world.get_door('Hyrule Castle Throne Room N', player))
|
|
split_list['Sewers'].remove(builder.throne_door.entrance.parent_region.name)
|
|
builder.throne_door = None
|
|
if attempts >= 5 and merge_attempt < merge_limit and builder.name != 'Hyrule Castle':
|
|
merge_attempt, attempts = merge_attempt + 1, 0
|
|
|
|
raise GenerationException('Unable to resolve in 5 attempts')
|
|
|
|
|
|
def balance_split(candidate_sectors, dungeon_map, global_pole, builder_info):
|
|
dungeon_entrances, split_dungeon_entrances, connections_tuple, world, player = builder_info
|
|
for name, builder in dungeon_map.items():
|
|
calc_allowance_and_dead_ends(builder, connections_tuple, world, player)
|
|
comb_w_replace = len(dungeon_map) ** len(candidate_sectors)
|
|
if comb_w_replace <= 10000:
|
|
combinations = list(itertools.product(dungeon_map.keys(), repeat=len(candidate_sectors)))
|
|
random.shuffle(combinations)
|
|
tries = 0
|
|
while tries < len(combinations):
|
|
choices = combinations[tries]
|
|
main_sector_list = list(candidate_sectors)
|
|
chosen_sectors = defaultdict(list)
|
|
for i, choice in enumerate(choices):
|
|
chosen_sectors[choice].append(main_sector_list[i])
|
|
all_valid, package_map = True, {}
|
|
for name, builder in dungeon_map.items():
|
|
flag, package = valid_assignment(builder, chosen_sectors[name], builder_info)
|
|
if not flag:
|
|
all_valid = False
|
|
break
|
|
if package:
|
|
package_map[builder] = package
|
|
if all_valid:
|
|
for name, sector_list in chosen_sectors.items():
|
|
builder = dungeon_map[name]
|
|
for sector in sector_list:
|
|
assign_sector(sector, builder, candidate_sectors, global_pole)
|
|
if builder in package_map:
|
|
builder.throne_door, builder.throne_sector, builder.chosen_lobby = package_map[builder]
|
|
return dungeon_map
|
|
tries += 1
|
|
raise GenerationException('Split Dungeon Builder: Impossible dungeon. Ref %s' % next(iter(dungeon_map.keys())))
|
|
|
|
# categorize sectors
|
|
check_for_forced_dead_ends(dungeon_map, candidate_sectors, global_pole)
|
|
check_for_forced_assignments(dungeon_map, candidate_sectors, global_pole)
|
|
check_for_forced_crystal(dungeon_map, candidate_sectors, global_pole)
|
|
crystal_switches, crystal_barriers, neutral_sectors, polarized_sectors = categorize_sectors(candidate_sectors)
|
|
leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole)
|
|
ensure_crystal_switches_reachable(dungeon_map, leftover, polarized_sectors, crystal_barriers, global_pole)
|
|
for sector in leftover:
|
|
if sector.polarity().is_neutral():
|
|
neutral_sectors[sector] = None
|
|
else:
|
|
polarized_sectors[sector] = None
|
|
# blue barriers
|
|
assign_crystal_barrier_sectors(dungeon_map, crystal_barriers, global_pole)
|
|
# polarity:
|
|
assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, builder_info)
|
|
# the rest
|
|
assign_the_rest(dungeon_map, neutral_sectors, global_pole, builder_info)
|
|
return dungeon_map
|
|
|
|
|
|
def check_for_forced_dead_ends(dungeon_map, candidate_sectors, global_pole):
|
|
dead_end_sectors = [x for x in candidate_sectors if x.branching_factor() <= 1]
|
|
other_sectors = [x for x in candidate_sectors if x not in dead_end_sectors]
|
|
for name, builder in dungeon_map.items():
|
|
other_sectors += builder.sectors
|
|
other_magnitude = sum_hook_magnitude(other_sectors)
|
|
dead_cnt = [0] * len(Hook)
|
|
for sector in dead_end_sectors:
|
|
hook_mag = sector.hook_magnitude()
|
|
for hook in Hook:
|
|
if hook_mag[hook.value] != 0:
|
|
dead_cnt[hook.value] += 1
|
|
for hook in Hook:
|
|
opp = opposite_h_type(hook).value
|
|
if dead_cnt[hook.value] > other_magnitude[opp]:
|
|
raise GenerationException('Impossible to satisfy all these dead ends')
|
|
elif dead_cnt[hook.value] == other_magnitude[opp]:
|
|
candidates = [x for x in dead_end_sectors if x.hook_magnitude()[hook.value] > 0]
|
|
for sector in other_sectors:
|
|
if sector.hook_magnitude()[opp] > 0 and sector.is_entrance_sector() and sector.branching_factor() == 2:
|
|
builder = None
|
|
for b in dungeon_map.values():
|
|
if sector in b.sectors:
|
|
builder = b
|
|
break
|
|
valid, candidate_sector = False, None
|
|
while not valid:
|
|
if len(candidates) == 0:
|
|
raise GenerationException('Split Dungeon Builder: Bad dead end %s' % builder.name)
|
|
candidate_sector = random.choice(candidates)
|
|
candidates.remove(candidate_sector)
|
|
valid = global_pole.is_valid_choice(dungeon_map, builder, [candidate_sector]) and check_crystal(candidate_sector, sector)
|
|
assign_sector(candidate_sector, builder, candidate_sectors, global_pole)
|
|
builder.c_locked = True
|
|
|
|
|
|
def check_crystal(dead_end, entrance):
|
|
if dead_end.blue_barrier and not entrance.c_switch and not dead_end.c_switch:
|
|
return False
|
|
if entrance.blue_barrier and not entrance.c_switch and not dead_end.c_switch:
|
|
return False
|
|
return True
|
|
|
|
|
|
def check_for_forced_assignments(dungeon_map, candidate_sectors, global_pole):
|
|
done = False
|
|
while not done:
|
|
done = True
|
|
magnitude = sum_hook_magnitude(candidate_sectors)
|
|
dungeon_hooks = {}
|
|
for name, builder in dungeon_map.items():
|
|
dungeon_hooks[name] = sum_hook_magnitude(builder.sectors)
|
|
for val in Hook:
|
|
if magnitude[val.value] == 1:
|
|
forced_sector = None
|
|
for sec in candidate_sectors:
|
|
if sec.hook_magnitude()[val.value] > 0:
|
|
forced_sector = sec
|
|
break
|
|
opp = opposite_h_type(val).value
|
|
other_sectors = [x for x in candidate_sectors if x != forced_sector]
|
|
if sum_hook_magnitude(other_sectors)[opp] == 0:
|
|
found_hooks = []
|
|
for name, hooks in dungeon_hooks.items():
|
|
if hooks[opp] > 0 and not dungeon_map[name].c_locked:
|
|
found_hooks.append(name)
|
|
if len(found_hooks) == 1:
|
|
done = False
|
|
assign_sector(forced_sector, dungeon_map[found_hooks[0]], candidate_sectors, global_pole)
|
|
|
|
|
|
def check_for_forced_crystal(dungeon_map, candidate_sectors, global_pole):
|
|
for name, builder in dungeon_map.items():
|
|
if check_for_forced_crystal_single(builder, candidate_sectors):
|
|
builder.c_switch_required = True
|
|
|
|
|
|
def check_for_forced_crystal_single(builder, candidate_sectors):
|
|
builder_doors = defaultdict(dict)
|
|
for sector in builder.sectors:
|
|
for door in sector.outstanding_doors:
|
|
builder_doors[hook_from_door(door)][door] = sector
|
|
if len(builder_doors) == 0:
|
|
return False
|
|
candidate_doors = defaultdict(dict)
|
|
for sector in candidate_sectors:
|
|
for door in sector.outstanding_doors:
|
|
candidate_doors[hook_from_door(door)][door] = sector
|
|
for hook in builder_doors.keys():
|
|
for door in builder_doors[hook].keys():
|
|
opp = opposite_h_type(hook)
|
|
if opp in builder_doors.keys():
|
|
for d, sector in builder_doors[opp].items():
|
|
if d != door and (not sector.blue_barrier or sector.c_switch):
|
|
return False
|
|
for d, sector in candidate_doors[opp].items():
|
|
if not sector.blue_barrier or sector.c_switch:
|
|
return False
|
|
return True
|
|
|
|
|
|
def categorize_sectors(candidate_sectors):
|
|
crystal_switches = {}
|
|
crystal_barriers = {}
|
|
polarized_sectors = {}
|
|
neutral_sectors = {}
|
|
for sector in candidate_sectors:
|
|
if sector.c_switch:
|
|
crystal_switches[sector] = None
|
|
elif sector.blue_barrier:
|
|
crystal_barriers[sector] = None
|
|
elif sector.polarity().is_neutral():
|
|
neutral_sectors[sector] = None
|
|
else:
|
|
polarized_sectors[sector] = None
|
|
return crystal_switches, crystal_barriers, neutral_sectors, polarized_sectors
|
|
|
|
|
|
class NeutralizingException(Exception):
|
|
pass
|
|
|
|
|
|
class GenerationException(Exception):
|
|
pass
|
|
|
|
|
|
class DoorEquation:
|
|
|
|
def __init__(self, door):
|
|
self.door = door
|
|
self.cost = None, None
|
|
self.benefit = defaultdict(list)
|
|
self.required = False
|
|
self.access_id = None
|
|
self.c_switch = False
|
|
self.crystal_blocked = {}
|
|
self.entrance_flag = False
|
|
|
|
def copy(self):
|
|
eq = DoorEquation(self.door)
|
|
eq.cost = self.cost
|
|
for key, doors in self.benefit.items():
|
|
eq.benefit[key] = doors.copy()
|
|
eq.required = self.required
|
|
eq.c_switch = self.c_switch
|
|
eq.crystal_blocked = self.crystal_blocked.copy()
|
|
return eq
|
|
|
|
def total_cost(self):
|
|
return 0 if self.cost[0] is None else 1
|
|
|
|
def gross(self, current_access):
|
|
key, cost_door = self.cost
|
|
if key is None:
|
|
# todo: could just be Orange as well (multiple entrance case)
|
|
crystal_access = current_access.access_door[None]
|
|
else:
|
|
crystal_access = None
|
|
for match_door, crystal in current_access.outstanding_doors.items():
|
|
if hook_from_door(match_door) == key:
|
|
if crystal_access is None or current_access._better_crystal(crystal_access, crystal):
|
|
crystal_access = crystal
|
|
ttl = 0
|
|
for key, door_list in self.benefit.items():
|
|
for door in door_list:
|
|
if door in current_access.outstanding_doors.keys() or door in current_access.proposed_connections.keys():
|
|
continue
|
|
if door in self.crystal_blocked.keys() and not self.c_switch:
|
|
if crystal_access == CrystalBarrier.Either or crystal_access == self.crystal_blocked[door]:
|
|
ttl += 1
|
|
else:
|
|
ttl += 1
|
|
return ttl
|
|
|
|
def profit(self, current_access):
|
|
return self.gross(current_access) - self.total_cost()
|
|
|
|
def neutral(self):
|
|
key, door = self.cost
|
|
if key is not None and len(self.benefit[key]) <= 0:
|
|
return False
|
|
return True
|
|
|
|
def neutral_profit(self):
|
|
key, door = self.cost
|
|
if key is not None:
|
|
if len(self.benefit[key]) < 1:
|
|
return False
|
|
if len(self.benefit[key]) > 1:
|
|
return True
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
def can_cover_cost(self, current_access):
|
|
key, door = self.cost
|
|
if key is not None and current_access[key] < 1:
|
|
return False
|
|
return True
|
|
|
|
|
|
class DungeonAccess:
|
|
|
|
def __init__(self):
|
|
self.access = defaultdict(int)
|
|
self.door_access = {} # door -> crystal
|
|
self.door_sector_map = {} # door -> original sector
|
|
self.outstanding_doors = {}
|
|
self.blocked_doors = {}
|
|
self.door_access[None] = CrystalBarrier.Orange
|
|
self.proposed_connections = {}
|
|
self.reached_doors = set()
|
|
|
|
def can_cover_equation(self, equation):
|
|
key, door = equation.cost
|
|
if key is None:
|
|
return True
|
|
return self.access[key] >= 1
|
|
|
|
def can_pay(self, key):
|
|
if key is None:
|
|
return True
|
|
return self.access[key] >= 1
|
|
|
|
def adjust_for_equation(self, equation, sector):
|
|
if equation.cost[0] is None:
|
|
original_crystal = self.door_access[None]
|
|
for key, door_list in equation.benefit.items():
|
|
self.access[key] += len(door_list)
|
|
for door in door_list:
|
|
# I can't think of an entrance sector that forces blue
|
|
crystal_state = CrystalBarrier.Either if equation.c_switch else original_crystal
|
|
if crystal_state == CrystalBarrier.Either:
|
|
self.door_access[None] = CrystalBarrier.Either
|
|
self.door_access[door] = crystal_state
|
|
self.door_sector_map[door] = sector
|
|
self.outstanding_doors[door] = crystal_state
|
|
self.reached_doors.add(door)
|
|
else:
|
|
key, door = equation.cost
|
|
self.access[key] -= 1
|
|
# find the a matching connection
|
|
best_door, best_crystal = None, None
|
|
for match_door, crystal in self.outstanding_doors.items():
|
|
if hook_from_door(match_door) == key:
|
|
if best_door is None or self._better_crystal(best_crystal, crystal):
|
|
best_door = match_door
|
|
best_crystal = crystal
|
|
if best_door is None:
|
|
raise Exception('Something went terribly wrong I think')
|
|
# for match_door, crystal in self.blocked_doors.items():
|
|
# if hook_from_door(match_door) == key:
|
|
# if best_door is None or self._better_crystal(best_crystal, crystal):
|
|
# best_door = match_door
|
|
# best_crystal = crystal
|
|
self.door_sector_map[door] = sector
|
|
self.door_access[door] = best_crystal
|
|
self.reached_doors.add(door)
|
|
self.proposed_connections[door] = best_door
|
|
self.proposed_connections[best_door] = door
|
|
if best_door in self.outstanding_doors.keys():
|
|
del self.outstanding_doors[best_door]
|
|
elif best_door in self.blocked_doors.keys():
|
|
del self.blocked_doors[best_door]
|
|
self.reached_doors.add(best_door)
|
|
|
|
# todo: backpropagate crystal access
|
|
if equation.c_switch or best_crystal == CrystalBarrier.Either:
|
|
# if not equation.door.blocked:
|
|
self.door_access[door] = CrystalBarrier.Either
|
|
self.door_access[best_door] = CrystalBarrier.Either
|
|
queue = deque([best_door, door])
|
|
visited = set()
|
|
while len(queue) > 0:
|
|
next_door = queue.popleft()
|
|
visited.add(next_door)
|
|
curr_sector = self.door_sector_map[next_door]
|
|
next_eq = None
|
|
for eq in curr_sector.equations:
|
|
if eq.door == next_door:
|
|
next_eq = eq
|
|
break
|
|
if next_eq.entrance_flag:
|
|
crystal_state = self.door_access[next_door]
|
|
self.door_access[None] = crystal_state
|
|
for eq in curr_sector.equations:
|
|
cand_door = eq.door
|
|
crystal_state = self.door_access[None]
|
|
if cand_door in next_eq.crystal_blocked.keys():
|
|
crystal_state = next_eq.crystal_blocked[cand_door]
|
|
if cand_door not in visited:
|
|
self.door_access[cand_door] = crystal_state
|
|
if not cand_door.blocked:
|
|
if cand_door in self.outstanding_doors.keys():
|
|
self.outstanding_doors[cand_door] = crystal_state
|
|
if cand_door in self.proposed_connections.keys():
|
|
partner_door = self.proposed_connections[cand_door]
|
|
self.door_access[partner_door] = crystal_state
|
|
if partner_door in self.outstanding_doors.keys():
|
|
self.outstanding_doors[partner_door] = crystal_state
|
|
if partner_door not in visited:
|
|
queue.append(partner_door)
|
|
else:
|
|
for key, door_list in next_eq.benefit.items():
|
|
for cand_door in door_list:
|
|
crystal_state = self.door_access[next_door]
|
|
if cand_door in next_eq.crystal_blocked.keys():
|
|
crystal_state = next_eq.crystal_blocked[cand_door]
|
|
if cand_door in self.blocked_doors.keys():
|
|
needed_crystal = self.blocked_doors[cand_door]
|
|
if meets_crystal_requirment(crystal_state, needed_crystal):
|
|
del self.blocked_doors[cand_door]
|
|
if cand_door != door:
|
|
self.access[key] += 1
|
|
self.outstanding_doors[cand_door] = crystal_state
|
|
self.door_access[cand_door] = crystal_state
|
|
self.reached_doors.add(cand_door)
|
|
if cand_door not in visited:
|
|
self.door_access[cand_door] = crystal_state
|
|
if not cand_door.blocked:
|
|
if cand_door in self.outstanding_doors.keys():
|
|
self.outstanding_doors[cand_door] = crystal_state
|
|
if cand_door in self.proposed_connections.keys():
|
|
partner_door = self.proposed_connections[cand_door]
|
|
self.door_access[partner_door] = crystal_state
|
|
if partner_door in self.outstanding_doors.keys():
|
|
self.outstanding_doors[partner_door] = crystal_state
|
|
queue.append(cand_door)
|
|
queue.append(partner_door)
|
|
|
|
for key, door_list in equation.benefit.items():
|
|
for door in door_list:
|
|
crystal_access = self.door_access[best_door]
|
|
can_access = True
|
|
if door in equation.crystal_blocked.keys():
|
|
if crystal_access == CrystalBarrier.Either or crystal_access == equation.crystal_blocked[door]:
|
|
crystal_access = equation.crystal_blocked[door]
|
|
else:
|
|
self.blocked_doors[door] = equation.crystal_blocked[door]
|
|
can_access = False
|
|
self.door_sector_map[door] = sector
|
|
if can_access and door not in self.reached_doors:
|
|
self.access[key] += 1
|
|
self.door_access[door] = crystal_access
|
|
self.outstanding_doors[door] = crystal_access
|
|
self.reached_doors.add(door)
|
|
|
|
def _better_crystal(self, current_champ, contender):
|
|
if current_champ == CrystalBarrier.Either:
|
|
return False
|
|
elif contender == CrystalBarrier.Either:
|
|
return True
|
|
elif current_champ == CrystalBarrier.Blue:
|
|
return False
|
|
elif contender == CrystalBarrier.Blue:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
def identify_branching_issues(dungeon_map, builder_info):
|
|
unconnected_builders = {}
|
|
for name, builder in dungeon_map.items():
|
|
resolved, unreached_doors, package = check_for_valid_layout(builder, [], builder_info)
|
|
if not resolved:
|
|
unconnected_builders[name] = builder
|
|
for hook, door_list in unreached_doors.items():
|
|
builder.unfulfilled[hook] += len(door_list)
|
|
elif package:
|
|
builder.throne_door, builder.throne_sector, builder.chosen_lobby = package
|
|
return unconnected_builders
|
|
|
|
|
|
def check_for_valid_layout(builder, sector_list, builder_info):
|
|
dungeon_entrances, split_dungeon_entrances, c_tuple, world, player = builder_info
|
|
if builder.name in split_dungeon_entrances.keys():
|
|
try:
|
|
temp_builder = DungeonBuilder(builder.name)
|
|
for s in sector_list + builder.sectors:
|
|
assign_sector_helper(s, temp_builder)
|
|
split_list = split_dungeon_entrances[builder.name]
|
|
builder.split_dungeon_map = split_dungeon_builder(temp_builder, split_list, builder_info)
|
|
builder.valid_proposal = {}
|
|
possible_regions = set()
|
|
for portal in world.dungeon_portals[player]:
|
|
if not portal.destination and portal.name in dungeon_portals[builder.name]:
|
|
possible_regions.add(portal.door.entrance.parent_region.name)
|
|
if builder.name in dungeon_drops.keys() and (builder.name != 'Hyrule Castle'
|
|
or world.mode[player] != 'standard'):
|
|
possible_regions.update(dungeon_drops[builder.name])
|
|
independents = find_independent_entrances(possible_regions, world, player)
|
|
for name, split_build in builder.split_dungeon_map.items():
|
|
name_bits = name.split(" ")
|
|
orig_name = " ".join(name_bits[:-1])
|
|
entrance_regions = split_dungeon_entrances[orig_name][name_bits[-1]]
|
|
# todo: this is hardcoded information for random entrances
|
|
for sector in split_build.sectors:
|
|
match_set = set(sector.region_set()).intersection(possible_regions)
|
|
if len(match_set) > 0:
|
|
for r_name in match_set:
|
|
if r_name not in entrance_regions:
|
|
entrance_regions.append(r_name)
|
|
# entrance_regions = [x for x in entrance_regions if x not in split_check_entrance_invalid]
|
|
split = any(x for x in independents if x not in entrance_regions)
|
|
proposal = generate_dungeon_find_proposal(split_build, entrance_regions, split, world, player)
|
|
# record split proposals
|
|
builder.valid_proposal[name] = proposal
|
|
package = None
|
|
if temp_builder.name == 'Hyrule Castle':
|
|
temp_builder.chosen_lobby.outstanding_doors.append(temp_builder.throne_door)
|
|
temp_builder.throne_sector.outstanding_doors.append(world.get_door('Hyrule Castle Throne Room N', player))
|
|
package = temp_builder.throne_door, temp_builder.throne_sector, temp_builder.chosen_lobby
|
|
split_list['Sewers'].remove(temp_builder.throne_door.entrance.parent_region.name)
|
|
builder.exception_list = list(sector_list)
|
|
return True, {}, package
|
|
except (GenerationException, NeutralizingException, OtherGenException) as e:
|
|
logging.getLogger('').debug(f'Bailing on this layout for {builder.name}', exc_info=1)
|
|
builder.split_dungeon_map = None
|
|
builder.valid_proposal = None
|
|
if temp_builder.name == 'Hyrule Castle' and temp_builder.throne_door:
|
|
temp_builder.chosen_lobby.outstanding_doors.append(temp_builder.throne_door)
|
|
temp_builder.throne_sector.outstanding_doors.append(world.get_door('Hyrule Castle Throne Room N', player))
|
|
old_entrance = temp_builder.throne_door.entrance.parent_region.name
|
|
split_dungeon_entrances[builder.name]['Sewers'].remove(old_entrance)
|
|
unreached_doors = resolve_equations(builder, sector_list)
|
|
return False, unreached_doors, None
|
|
else:
|
|
unreached_doors = resolve_equations(builder, sector_list)
|
|
return len(unreached_doors) == 0, unreached_doors, None
|
|
|
|
|
|
def find_independent_entrances(entrance_regions, world, player):
|
|
independents = set()
|
|
for region in entrance_regions:
|
|
portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region.name == region), None)
|
|
if portal:
|
|
if portal.destination:
|
|
continue
|
|
elif len(entrance_regions) > 1:
|
|
p_region = portal.door.entrance.connected_region
|
|
access_region = next(x.parent_region for x in p_region.entrances
|
|
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld])
|
|
if access_region.name in world.inaccessible_regions[player]:
|
|
continue
|
|
else:
|
|
r = world.get_region(region, player)
|
|
access_region = next(x.parent_region for x in r.entrances
|
|
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]
|
|
or x.parent_region.name == 'Sewer Drop')
|
|
if access_region.name == 'Sewer Drop':
|
|
access_region = next(x.parent_region for x in access_region.entrances)
|
|
if access_region.name in world.inaccessible_regions[player]:
|
|
continue
|
|
independents.add(region)
|
|
return independents
|
|
|
|
|
|
def resolve_equations(builder, sector_list):
|
|
unreached_doors = defaultdict(list)
|
|
equations = {x: y for x, y in copy_door_equations(builder, sector_list).items() if len(y) > 0}
|
|
current_access = {}
|
|
sector_split = {} # those sectors that belong to a certain sector
|
|
if builder.name in split_region_starts.keys():
|
|
for name, region_list in split_region_starts[builder.name].items():
|
|
current_access[name] = DungeonAccess()
|
|
for r_name in region_list:
|
|
sector = find_sector(r_name, builder.sectors)
|
|
sector_split[sector] = name
|
|
else:
|
|
current_access[builder.name] = DungeonAccess()
|
|
|
|
# resolve all that provide more access
|
|
free_sector, eq_list, free_eq = find_free_equation(equations)
|
|
while free_eq is not None:
|
|
if free_sector in sector_split.keys():
|
|
access_id = sector_split[free_sector]
|
|
access = current_access[access_id]
|
|
else:
|
|
access_id = next(iter(current_access.keys()))
|
|
access = current_access[access_id]
|
|
resolve_equation(free_eq, eq_list, free_sector, access_id, access, equations)
|
|
free_sector, eq_list, free_eq = find_free_equation(equations)
|
|
while len(equations) > 0:
|
|
valid_access = next_access(current_access)
|
|
eq, eq_list, sector, access, access_id = None, None, None, None, None
|
|
if len(valid_access) == 1:
|
|
access_id, access = valid_access[0]
|
|
eq, eq_list, sector = find_priority_equation(equations, access_id, access)
|
|
elif len(valid_access) > 1:
|
|
access_id, access = valid_access[0]
|
|
eq, eq_list, sector = find_greedy_equation(equations, access_id, access, sector_split)
|
|
if eq:
|
|
resolve_equation(eq, eq_list, sector, access_id, access, equations)
|
|
else:
|
|
for sector, eq_list in equations.items():
|
|
for eq in eq_list:
|
|
unreached_doors[hook_from_door(eq.door)].append(eq.door)
|
|
return unreached_doors
|
|
valid_access = next_access(current_access)
|
|
for access_id, dungeon_access in valid_access:
|
|
access = dungeon_access.access
|
|
access[Hook.Stairs] = access[Hook.Stairs] % 2
|
|
ns_leftover = min(access[Hook.North], access[Hook.South])
|
|
access[Hook.North] -= ns_leftover
|
|
access[Hook.South] -= ns_leftover
|
|
ew_leftover = min(access[Hook.West], access[Hook.East])
|
|
access[Hook.East] -= ew_leftover
|
|
access[Hook.West] -= ew_leftover
|
|
if sum(access.values()) > 0:
|
|
for hook, num in access.items():
|
|
for i in range(num):
|
|
unreached_doors[hook].append('placeholder')
|
|
return unreached_doors
|
|
|
|
|
|
def next_access(current_access):
|
|
valid_ones = [(x, y) for x, y in current_access.items() if sum(y.access.values()) > 0]
|
|
valid_ones.sort(key=lambda x: sum(x[1].access.values()))
|
|
return valid_ones
|
|
|
|
|
|
# an equations with no change to access (check)
|
|
# the highest benefit equations, that can be paid for (check)
|
|
# 0-benefit required transforms
|
|
# 0-benefit transforms (how to pick between these?)
|
|
# negative benefit transforms (dead end)
|
|
def find_priority_equation(equations, access_id, current_access):
|
|
flex = calc_flex(equations, current_access)
|
|
required = calc_required(equations, current_access)
|
|
wanted_candidates = []
|
|
best_profit = None
|
|
all_candidates = []
|
|
local_profit_map = {}
|
|
|
|
for sector, eq_list in equations.items():
|
|
eq_list.sort(key=lambda eq: eq.profit(current_access), reverse=True)
|
|
best_local_profit = None
|
|
for eq in eq_list:
|
|
profit = eq.profit(current_access)
|
|
if current_access.can_cover_equation(eq) and (eq.access_id is None or eq.access_id == access_id):
|
|
# if eq.neutral_profit() or eq.neutral():
|
|
# return eq, eq_list, sector # don't need to compare - just use it now
|
|
if best_local_profit is None or profit > best_local_profit:
|
|
best_local_profit = profit
|
|
all_candidates.append((eq, eq_list, sector))
|
|
elif (best_profit is None or profit >= best_profit) and profit > 0:
|
|
if best_profit is None or profit > best_profit:
|
|
wanted_candidates = [eq]
|
|
best_profit = profit
|
|
else:
|
|
wanted_candidates.append(eq)
|
|
local_profit_map[sector] = best_local_profit
|
|
filtered_candidates = filter_requirements(all_candidates, equations, required, current_access)
|
|
filtered_candidates = [x for x in filtered_candidates if x[0].gross(current_access) > 0]
|
|
if len(filtered_candidates) == 0:
|
|
filtered_candidates = all_candidates # probably bad things
|
|
if len(filtered_candidates) == 0:
|
|
return None, None, None # can't pay for anything
|
|
if len(filtered_candidates) == 1:
|
|
return filtered_candidates[0]
|
|
|
|
neutral_candidates = [x for x in filtered_candidates if (x[0].neutral_profit() or x[0].neutral()) and x[0].profit(current_access) == local_profit_map[x[2]]]
|
|
if len(neutral_candidates) == 0:
|
|
neutral_candidates = filtered_candidates
|
|
if len(neutral_candidates) == 1:
|
|
return neutral_candidates[0]
|
|
|
|
filtered_candidates = filter_requirements(neutral_candidates, equations, required, current_access)
|
|
if len(filtered_candidates) == 0:
|
|
filtered_candidates = neutral_candidates
|
|
if len(filtered_candidates) == 1:
|
|
return filtered_candidates[0]
|
|
|
|
triplet_candidates = []
|
|
best_profit = None
|
|
for eq, eq_list, sector in filtered_candidates:
|
|
profit = eq.profit(current_access)
|
|
if best_profit is None or profit >= best_profit:
|
|
if best_profit is None or profit > best_profit:
|
|
triplet_candidates = [(eq, eq_list, sector)]
|
|
best_profit = profit
|
|
else:
|
|
triplet_candidates.append((eq, eq_list, sector))
|
|
|
|
filtered_candidates = filter_requirements(triplet_candidates, equations, required, current_access)
|
|
if len(filtered_candidates) == 0:
|
|
filtered_candidates = triplet_candidates
|
|
if len(filtered_candidates) == 1:
|
|
return filtered_candidates[0]
|
|
|
|
required_candidates = [x for x in filtered_candidates if x[0].required]
|
|
if len(required_candidates) == 0:
|
|
required_candidates = filtered_candidates
|
|
if len(required_candidates) == 1:
|
|
return required_candidates[0]
|
|
|
|
c_switch_candidates = [x for x in required_candidates if x[0].c_switch]
|
|
if len(c_switch_candidates) == 0:
|
|
c_switch_candidates = required_candidates
|
|
if len(c_switch_candidates) == 1:
|
|
return c_switch_candidates[0]
|
|
|
|
loop_candidates = find_enabling_switch_connections(current_access)
|
|
if len(loop_candidates) >= 1:
|
|
return loop_candidates[0] # just pick one
|
|
|
|
flexible_candidates = [x for x in c_switch_candidates if x[0].can_cover_cost(flex)]
|
|
if len(flexible_candidates) == 0:
|
|
flexible_candidates = c_switch_candidates
|
|
if len(flexible_candidates) == 1:
|
|
return flexible_candidates[0]
|
|
|
|
good_local_candidates = [x for x in flexible_candidates if local_profit_map[x[2]] == x[0].profit(current_access)]
|
|
if len(good_local_candidates) == 0:
|
|
good_local_candidates = flexible_candidates
|
|
if len(good_local_candidates) == 1:
|
|
return good_local_candidates[0]
|
|
|
|
leads_to_profit = [x for x in good_local_candidates if can_enable_wanted(x[0], wanted_candidates)]
|
|
if len(leads_to_profit) == 0:
|
|
leads_to_profit = good_local_candidates
|
|
if len(leads_to_profit) == 1:
|
|
return leads_to_profit[0]
|
|
|
|
cost_point = {x[0]: find_cost_point(x, current_access) for x in leads_to_profit}
|
|
best_point = max(cost_point.values())
|
|
cost_point_candidates = [x for x in leads_to_profit if cost_point[x[0]] == best_point]
|
|
if len(cost_point_candidates) == 0:
|
|
cost_point_candidates = leads_to_profit
|
|
return cost_point_candidates[0] # just pick one I guess
|
|
|
|
|
|
def find_enabling_switch_connections(current_access):
|
|
triad_list = []
|
|
# probably should check for loop/branches in builder at some stage
|
|
# - but this could indicate that a loop or branch is necessary
|
|
for cand_door, crystal in current_access.outstanding_doors.items():
|
|
for blocked_door, req_crystal in current_access.blocked_doors.items():
|
|
if hook_from_door(cand_door) == hanger_from_door(blocked_door):
|
|
if crystal == CrystalBarrier.Either or crystal == req_crystal:
|
|
sector, equation = current_access.door_sector_map[blocked_door], None
|
|
for eq in sector.equations:
|
|
if eq.door == blocked_door:
|
|
equation = eq.copy()
|
|
break
|
|
if equation:
|
|
triad_list.append((equation, [equation], sector))
|
|
return triad_list
|
|
|
|
|
|
def find_cost_point(eq_triplet, access):
|
|
cost_point = 0
|
|
key, cost_door = eq_triplet[0].cost
|
|
if cost_door is not None:
|
|
cost_point += access.access[key] - 1
|
|
return cost_point
|
|
|
|
|
|
def find_greedy_equation(equations, access_id, current_access, sector_split):
|
|
all_candidates = []
|
|
for sector, eq_list in equations.items():
|
|
if sector not in sector_split.keys() or sector_split[sector] == access_id:
|
|
eq_list.sort(key=lambda eq: eq.profit(current_access), reverse=True)
|
|
for eq in eq_list:
|
|
if current_access.can_cover_equation(eq) and (eq.access_id is None or eq.access_id == access_id):
|
|
all_candidates.append((eq, eq_list, sector))
|
|
if len(all_candidates) == 0:
|
|
return None, None, None # can't pay for anything
|
|
if len(all_candidates) == 1:
|
|
return all_candidates[0]
|
|
filtered_candidates = [x for x in all_candidates if x[0].profit(current_access) + 2 >= len(x[2].outstanding_doors)]
|
|
if len(filtered_candidates) == 0:
|
|
filtered_candidates = all_candidates # terrible! ugly dead ends
|
|
if len(filtered_candidates) == 1:
|
|
return filtered_candidates[0]
|
|
|
|
triplet_candidates = []
|
|
worst_profit = None
|
|
for eq, eq_list, sector in filtered_candidates:
|
|
profit = eq.profit(current_access)
|
|
if worst_profit is None or profit <= worst_profit:
|
|
if worst_profit is None or profit < worst_profit:
|
|
triplet_candidates = [(eq, eq_list, sector)]
|
|
worst_profit = profit
|
|
else:
|
|
triplet_candidates.append((eq, eq_list, sector))
|
|
if len(triplet_candidates) == 0:
|
|
triplet_candidates = filtered_candidates # probably bad things
|
|
return triplet_candidates[0] # just pick one?
|
|
|
|
|
|
def calc_required(equations, current_access):
|
|
ttl = sum(current_access.access.values())
|
|
local_profit_map = {}
|
|
for sector, eq_list in equations.items():
|
|
best_local_profit = None
|
|
for eq in eq_list:
|
|
profit = eq.profit(current_access)
|
|
if best_local_profit is None or profit > best_local_profit:
|
|
best_local_profit = profit
|
|
local_profit_map[sector] = best_local_profit
|
|
ttl += best_local_profit
|
|
if ttl == 0:
|
|
new_lists = {}
|
|
for sector, eq_list in equations.items():
|
|
if len(eq_list) > 1:
|
|
rem_list = []
|
|
for eq in eq_list:
|
|
if eq.profit(current_access) < local_profit_map[sector]:
|
|
rem_list.append(eq)
|
|
if len(rem_list) > 0:
|
|
new_lists[sector] = [x for x in eq_list if x not in rem_list]
|
|
for sector, eq_list in new_lists.items():
|
|
if len(eq_list) <= 1:
|
|
for eq in eq_list:
|
|
eq.required = True
|
|
equations[sector] = eq_list
|
|
required_costs = defaultdict(int)
|
|
required_benefits = defaultdict(int)
|
|
for sector, eq_list in equations.items():
|
|
for eq in eq_list:
|
|
if eq.required:
|
|
key, door = eq.cost
|
|
required_costs[key] += 1
|
|
for key, door_list in eq.benefit.items():
|
|
required_benefits[key] += len(door_list)
|
|
return required_costs, required_benefits
|
|
|
|
|
|
def calc_flex(equations, current_access):
|
|
flex_spending = defaultdict(int)
|
|
required_costs = defaultdict(int)
|
|
for sector, eq_list in equations.items():
|
|
for eq in eq_list:
|
|
if eq.required:
|
|
key, door = eq.cost
|
|
required_costs[key] += 1
|
|
for key in Hook:
|
|
flex_spending[key] = max(0, current_access.access[key]-required_costs[key])
|
|
return flex_spending
|
|
|
|
|
|
def filter_requirements(triplet_candidates, equations, required, current_access):
|
|
r_costs, r_exits = required
|
|
valid_candidates = []
|
|
for cand, cand_list, cand_sector in triplet_candidates:
|
|
valid = True
|
|
if not cand.required and not cand.c_switch:
|
|
potential_benefit = defaultdict(int)
|
|
benefit_counted = set()
|
|
potential_costs = defaultdict(int)
|
|
for h_type, benefit in current_access.access.items():
|
|
cur_cost = 1 if cand.cost[0] is not None else 0
|
|
if benefit - cur_cost > 0:
|
|
potential_benefit[h_type] += benefit - cur_cost
|
|
for h_type, benefit_list in cand.benefit.items():
|
|
potential_benefit[h_type] += len(benefit_list)
|
|
for sector, eq_list in equations.items():
|
|
if sector == cand_sector:
|
|
affected_doors = [d for x in cand.benefit.values() for d in x] + [cand.cost[1]]
|
|
adj_list = [x for x in eq_list if x.door not in affected_doors]
|
|
else:
|
|
adj_list = eq_list
|
|
for eq in adj_list:
|
|
for h_type, benefit_list in eq.benefit.items():
|
|
total_benefit = set(benefit_list) - benefit_counted
|
|
potential_benefit[h_type] += len(total_benefit)
|
|
benefit_counted.update(benefit_list)
|
|
h_type, cost_door = eq.cost
|
|
potential_costs[h_type] += 1
|
|
for h_type, requirement in r_costs.items():
|
|
if requirement > 0 and potential_benefit[h_type] < requirement:
|
|
valid = False
|
|
break
|
|
if valid:
|
|
for h_type, requirement in r_exits.items():
|
|
if requirement > 0 and potential_costs[h_type] < requirement:
|
|
valid = False
|
|
break
|
|
if valid:
|
|
valid_candidates.append((cand, cand_list, cand_sector))
|
|
return valid_candidates
|
|
|
|
|
|
def can_enable_wanted(test_eq, wanted_candidates):
|
|
for wanted in wanted_candidates:
|
|
covered = True
|
|
key, cost_door = wanted.cost
|
|
if len(test_eq.benefit[key]) < 1:
|
|
covered = False
|
|
if covered:
|
|
return True
|
|
return False
|
|
|
|
|
|
def resolve_equation(equation, eq_list, sector, access_id, current_access, equations):
|
|
if not current_access.can_pay(equation.cost[0]):
|
|
raise GenerationException('Cannot pay for this connection')
|
|
current_access.adjust_for_equation(equation, sector)
|
|
eq_list.remove(equation)
|
|
reached_doors = set(current_access.reached_doors)
|
|
reached_doors.update(current_access.blocked_doors.keys())
|
|
for r_eq in list(eq_list):
|
|
all_benefits_met = r_eq.door in reached_doors
|
|
for key in Hook:
|
|
fringe_list = [x for x in r_eq.benefit[key] if x not in reached_doors]
|
|
r_eq.benefit[key] = fringe_list
|
|
if len(fringe_list) > 0:
|
|
all_benefits_met = False
|
|
if all_benefits_met:
|
|
eq_list.remove(r_eq)
|
|
if len(eq_list) == 0 and sector in equations.keys():
|
|
del equations[sector]
|
|
else:
|
|
for eq in eq_list:
|
|
eq.access_id = access_id
|
|
|
|
|
|
def find_free_equation(equations):
|
|
for sector, eq_list in equations.items():
|
|
for eq in eq_list:
|
|
if eq.total_cost() <= 0:
|
|
return sector, eq_list, eq
|
|
return None, None, None
|
|
|
|
|
|
def copy_door_equations(builder, sector_list):
|
|
equations = {}
|
|
for sector in builder.sectors + sector_list:
|
|
sector.equations = calc_sector_equations(sector, builder.sewers_access)
|
|
curr_list = equations[sector] = []
|
|
for equation in sector.equations:
|
|
curr_list.append(equation.copy())
|
|
return equations
|
|
|
|
|
|
def calc_sector_equations(sector, sewers_flag=False):
|
|
equations = []
|
|
is_entrance = (sector.is_entrance_sector() and not sector.destination_entrance) or sewers_flag
|
|
if is_entrance:
|
|
flagged_equations = []
|
|
for door in sector.outstanding_doors:
|
|
equation, flag = calc_door_equation(door, sector, True, sewers_flag)
|
|
if flag:
|
|
flagged_equations.append(equation)
|
|
equations.append(equation)
|
|
for flagged_equation in flagged_equations:
|
|
for equation in equations:
|
|
for key, door_list in equation.benefit.items():
|
|
if flagged_equation.door in door_list and flagged_equation != equation:
|
|
door_list.remove(flagged_equation.door)
|
|
else:
|
|
for door in sector.outstanding_doors:
|
|
equation, flag = calc_door_equation(door, sector, False)
|
|
equations.append(equation)
|
|
return equations
|
|
|
|
|
|
def calc_door_equation(door, sector, look_for_entrance, sewers_flag=None):
|
|
if look_for_entrance and not door.blocked:
|
|
flag = sector.is_entrance_sector() or sewers_flag
|
|
if flag:
|
|
eq = DoorEquation(door)
|
|
eq.benefit[hook_from_door(door)].append(door)
|
|
eq.required = True
|
|
eq.c_switch = sector.c_switch # Big change - not true for mire fishbone, need to verify for others
|
|
# exceptions for long entrances ???
|
|
# if door.name in ['PoD Dark Alley']:
|
|
eq.entrance_flag = True
|
|
return eq, flag
|
|
eq = DoorEquation(door)
|
|
eq.required = door.blocked or door.dead
|
|
eq.cost = (hanger_from_door(door), door)
|
|
eq.entrance_flag = sector.is_entrance_sector()
|
|
if not door.stonewall:
|
|
start_region = door.entrance.parent_region
|
|
visited = {(start_region, CrystalBarrier.Null)}
|
|
queue = deque([(start_region, CrystalBarrier.Null)])
|
|
found_events = set()
|
|
event_doors = set()
|
|
while len(queue) > 0:
|
|
region, crystal_barrier = queue.popleft()
|
|
if region.crystal_switch and crystal_barrier == CrystalBarrier.Null:
|
|
eq.c_switch = True
|
|
crystal_barrier = CrystalBarrier.Either
|
|
# todo: backtracking from double switch with orange on--
|
|
for loc in region.locations:
|
|
if loc.name in dungeon_events:
|
|
found_events.add(loc.name)
|
|
for d in event_doors:
|
|
if loc.name == d.req_event:
|
|
connect = d.entrance.connected_region
|
|
if connect is not None and connect.type == RegionType.Dungeon and valid_crystal(d, crystal_barrier):
|
|
cb_flag = crystal_barrier if d.crystal == CrystalBarrier.Null else d.crystal
|
|
cb_flag = CrystalBarrier.Null if cb_flag == CrystalBarrier.Either else cb_flag
|
|
if (connect, cb_flag) not in visited:
|
|
visited.add((connect, cb_flag))
|
|
queue.append((connect, cb_flag))
|
|
for ext in region.exits:
|
|
d = ext.door
|
|
if d is not None:
|
|
if d.controller is not None:
|
|
d = d.controller
|
|
if d is not door and d in sector.outstanding_doors and not d.blocked:
|
|
eq_list = eq.benefit[hook_from_door(d)]
|
|
if d not in eq_list:
|
|
eq_list.append(d)
|
|
crystal_barrier = crystal_barrier if d.crystal == CrystalBarrier.Null else d.crystal
|
|
if crystal_barrier != CrystalBarrier.Null:
|
|
if d in eq.crystal_blocked.keys() and eq.crystal_blocked[d] != crystal_barrier:
|
|
del eq.crystal_blocked[d]
|
|
else:
|
|
eq.crystal_blocked[d] = crystal_barrier
|
|
elif d.crystal == CrystalBarrier.Null:
|
|
if d in eq.crystal_blocked.keys() and eq.crystal_blocked[d] != crystal_barrier:
|
|
del eq.crystal_blocked[d]
|
|
if d.req_event is not None and d.req_event not in found_events:
|
|
event_doors.add(d)
|
|
else:
|
|
connect = ext.connected_region if ext.door.controller is None else d.entrance.parent_region
|
|
if connect is not None and connect.type == RegionType.Dungeon and valid_crystal(d, crystal_barrier):
|
|
cb_flag = crystal_barrier if d.crystal == CrystalBarrier.Null else d.crystal
|
|
cb_flag = CrystalBarrier.Null if cb_flag == CrystalBarrier.Either else cb_flag
|
|
if (connect, cb_flag) not in visited:
|
|
visited.add((connect, cb_flag))
|
|
queue.append((connect, cb_flag))
|
|
if len(eq.benefit) == 0:
|
|
eq.required = True
|
|
return eq, False
|
|
|
|
|
|
def meets_crystal_requirment(current_crystal, requirement):
|
|
if current_crystal == CrystalBarrier.Either:
|
|
return True
|
|
return current_crystal == requirement
|
|
|
|
|
|
def valid_crystal(door, current_crystal):
|
|
if door.crystal in [CrystalBarrier.Null, CrystalBarrier.Either]:
|
|
return True
|
|
if current_crystal in [CrystalBarrier.Either, CrystalBarrier.Null]:
|
|
return True
|
|
return door.crystal == current_crystal
|
|
|
|
|
|
# common functions - todo: move to a common place
|
|
def kth_combination(k, l, r):
|
|
if r == 0:
|
|
return []
|
|
elif len(l) == r:
|
|
return l
|
|
else:
|
|
i = ncr(len(l) - 1, r - 1)
|
|
if k < i:
|
|
return l[0:1] + kth_combination(k, l[1:], r - 1)
|
|
else:
|
|
return kth_combination(k - i, l[1:], r)
|
|
|
|
|
|
def ncr(n, r):
|
|
if r == 0:
|
|
return 1
|
|
r = min(r, n - r)
|
|
numerator = reduce(op.mul, range(n, n - r, -1), 1)
|
|
denominator = reduce(op.mul, range(1, r + 1), 1)
|
|
return int(numerator / denominator)
|
|
|
|
|
|
dungeon_boss_sectors = {
|
|
'Hyrule Castle': [],
|
|
'Eastern Palace': ['Eastern Boss'],
|
|
'Desert Palace': ['Desert Boss'],
|
|
'Tower of Hera': ['Hera Boss'],
|
|
'Agahnims Tower': ['Tower Agahnim 1'],
|
|
'Palace of Darkness': ['PoD Boss'],
|
|
'Swamp Palace': ['Swamp Boss'],
|
|
'Skull Woods': ['Skull Boss'],
|
|
'Thieves Town': ['Thieves Boss'],
|
|
'Ice Palace': ['Ice Boss'],
|
|
'Misery Mire': ['Mire Boss'],
|
|
'Turtle Rock': ['TR Boss'],
|
|
'Ganons Tower': ['GT Agahnim 2']
|
|
}
|
|
|
|
default_dungeon_entrances = {
|
|
'Hyrule Castle': ['Hyrule Castle Lobby', 'Hyrule Castle West Lobby', 'Hyrule Castle East Lobby', 'Sewers Rat Path',
|
|
'Sanctuary'],
|
|
'Eastern Palace': ['Eastern Lobby'],
|
|
'Desert Palace': ['Desert Back Lobby', 'Desert Main Lobby', 'Desert West Lobby', 'Desert East Lobby'],
|
|
'Tower of Hera': ['Hera Lobby'],
|
|
'Agahnims Tower': ['Tower Lobby'],
|
|
'Palace of Darkness': ['PoD Lobby'],
|
|
'Swamp Palace': ['Swamp Lobby'],
|
|
'Skull Woods': ['Skull 1 Lobby', 'Skull Pinball', 'Skull Left Drop', 'Skull Pot Circle', 'Skull 2 East Lobby',
|
|
'Skull 2 West Lobby', 'Skull Back Drop', 'Skull 3 Lobby'],
|
|
'Thieves Town': ['Thieves Lobby'],
|
|
'Ice Palace': ['Ice Lobby'],
|
|
'Misery Mire': ['Mire Lobby'],
|
|
'Turtle Rock': ['TR Main Lobby', 'TR Eye Bridge', 'TR Big Chest Entrance', 'TR Lazy Eyes'],
|
|
'Ganons Tower': ['GT Lobby']
|
|
}
|
|
|
|
drop_entrances = {
|
|
'Hyrule Castle': ['Sewers Rat Path'],
|
|
'Eastern Palace': [],
|
|
'Desert Palace': [],
|
|
'Tower of Hera': [],
|
|
'Agahnims Tower': [],
|
|
'Palace of Darkness': [],
|
|
'Swamp Palace': [],
|
|
'Skull Woods': ['Skull Pinball', 'Skull Left Drop', 'Skull Pot Circle', 'Skull Back Drop'],
|
|
'Thieves Town': [],
|
|
'Ice Palace': [],
|
|
'Misery Mire': [],
|
|
'Turtle Rock': [],
|
|
'Ganons Tower': []
|
|
}
|
|
|
|
|
|
# todo: calculate these for ER - the multi entrance dungeons anyway
|
|
dungeon_dead_end_allowance = {
|
|
'Hyrule Castle': 6,
|
|
'Eastern Palace': 1,
|
|
'Desert Palace': 2,
|
|
'Tower of Hera': 1,
|
|
'Agahnims Tower': 1,
|
|
'Palace of Darkness': 1,
|
|
'Swamp Palace': 1,
|
|
'Skull Woods': 3, # two allowed in skull 1, 1 in skull 3, 0 in skull 2
|
|
'Thieves Town': 1,
|
|
'Ice Palace': 1,
|
|
'Misery Mire': 1,
|
|
'Turtle Rock': 2, # this assumes one overworld connection
|
|
'Ganons Tower': 1,
|
|
'Desert Palace Back': 1,
|
|
'Desert Palace Main': 1,
|
|
'Skull Woods 1': 0,
|
|
'Skull Woods 2': 0,
|
|
'Skull Woods 3': 1,
|
|
}
|
|
|
|
drop_entrances_allowance = [
|
|
'Sewers Rat Path', 'Skull Pinball', 'Skull Left Drop', 'Skull Pot Circle', 'Skull Back Drop'
|
|
]
|
|
|
|
dead_entrances = [
|
|
'TR Big Chest Entrance'
|
|
]
|
|
|
|
split_check_entrance_invalid = [
|
|
'Desert East Lobby', 'Skull 2 West Lobby'
|
|
]
|
|
|
|
dungeon_portals = {
|
|
'Hyrule Castle': ['Hyrule Castle South', 'Hyrule Castle West', 'Hyrule Castle East', 'Sanctuary'],
|
|
'Eastern Palace': ['Eastern'],
|
|
'Desert Palace': ['Desert Back', 'Desert South', 'Desert West', 'Desert East'],
|
|
'Tower of Hera': ['Hera'],
|
|
'Agahnims Tower': ['Agahnims Tower'],
|
|
'Palace of Darkness': ['Palace of Darkness'],
|
|
'Swamp Palace': ['Swamp'],
|
|
'Skull Woods': ['Skull 1', 'Skull 2 East', 'Skull 2 West', 'Skull 3'],
|
|
'Thieves Town': ['Thieves Town'],
|
|
'Ice Palace': ['Ice'],
|
|
'Misery Mire': ['Mire'],
|
|
'Turtle Rock': ['Turtle Rock Main', 'Turtle Rock Lazy Eyes', 'Turtle Rock Chest', 'Turtle Rock Eye Bridge'],
|
|
'Ganons Tower': ['Ganons Tower']
|
|
}
|
|
|
|
dungeon_drops = {
|
|
'Hyrule Castle': ['Sewers Rat Path'],
|
|
'Skull Woods': ['Skull Pot Circle', 'Skull Pinball', 'Skull Left Drop', 'Skull Back Drop'],
|
|
}
|
|
|