Lots of cross gen work
This commit is contained in:
@@ -2,15 +2,16 @@ import random
|
||||
import collections
|
||||
import itertools
|
||||
from collections import defaultdict, deque
|
||||
import logging
|
||||
from functools import reduce
|
||||
import logging
|
||||
import math
|
||||
import operator as op
|
||||
from typing import List
|
||||
|
||||
from BaseClasses import DoorType, Direction, CrystalBarrier, RegionType, Polarity, PolSlot, flooded_keys
|
||||
from BaseClasses import Hook, hook_from_door
|
||||
from Regions import key_only_locations, dungeon_events, flooded_keys_reverse
|
||||
from Dungeons import dungeon_regions
|
||||
from Dungeons import dungeon_regions, split_region_starts
|
||||
|
||||
|
||||
class GraphPiece:
|
||||
@@ -100,7 +101,7 @@ def generate_dungeon_main(builder, entrance_region_names, split_dungeon, world,
|
||||
continue
|
||||
prev_choices = choices_master[depth]
|
||||
# make a choice
|
||||
hanger, hook = make_a_choice(dungeon, hangers, hooks, prev_choices)
|
||||
hanger, hook = make_a_choice(dungeon, hangers, hooks, prev_choices, name)
|
||||
if hanger is None:
|
||||
backtrack = True
|
||||
else:
|
||||
@@ -157,9 +158,12 @@ def gen_dungeon_info(name, available_sectors, entrance_regions, proposed_map, va
|
||||
dungeon = {}
|
||||
start = ExplorationState(dungeon=name)
|
||||
start.big_key_special = bk_special
|
||||
|
||||
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,
|
||||
valid_doors, bk_needed, world, player)
|
||||
dungeon['Origin'] = create_graph_piece_from_state(None, original_state, original_state, proposed_map)
|
||||
valid_doors, bk_needed, world, player, exception)
|
||||
dungeon['Origin'] = create_graph_piece_from_state(None, original_state, original_state, proposed_map, exception)
|
||||
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:
|
||||
@@ -177,11 +181,11 @@ def gen_dungeon_info(name, available_sectors, entrance_regions, proposed_map, va
|
||||
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,
|
||||
valid_doors, False, world, player)
|
||||
valid_doors, False, world, player, exception)
|
||||
o_state_cache[door.name] = o_state
|
||||
piece = create_graph_piece_from_state(door, o_state, o_state, proposed_map)
|
||||
piece = create_graph_piece_from_state(door, o_state, o_state, proposed_map, exception)
|
||||
dungeon[door.name] = piece
|
||||
check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, valid_doors, world, player)
|
||||
check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, valid_doors, world, player, exception)
|
||||
|
||||
# catalog hooks: Dict<Hook, List<Door, Crystal, Door>>
|
||||
# and hangers: Dict<Hang, List<Door>>
|
||||
@@ -201,7 +205,7 @@ def gen_dungeon_info(name, available_sectors, entrance_regions, proposed_map, va
|
||||
return dungeon, hangers, avail_hooks
|
||||
|
||||
|
||||
def check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, valid_doors, world, player):
|
||||
def check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, valid_doors, world, player, exception):
|
||||
not_blue = set()
|
||||
not_blue.update(hanger_set)
|
||||
doors_to_check = set()
|
||||
@@ -229,20 +233,22 @@ def check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, valid_do
|
||||
hang_type = hanger_from_door(door) # am I hangable on a hook?
|
||||
hook_type = hook_from_door(door) # am I hookable onto a hanger?
|
||||
if (hang_type in blue_hooks and not door.stonewall) or hook_type in blue_hangers:
|
||||
explore_blue_state(door, dungeon, o_state_cache[door.name], proposed_map, valid_doors, world, player)
|
||||
explore_blue_state(door, dungeon, o_state_cache[door.name], proposed_map, valid_doors,
|
||||
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, valid_doors, world, player):
|
||||
def explore_blue_state(door, dungeon, o_state, proposed_map, valid_doors, 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, valid_doors, False, world, player)
|
||||
dungeon[door.name] = create_graph_piece_from_state(door, o_state, b_state, proposed_map)
|
||||
b_state = extend_reachable_state_improved([parent], blue_start, proposed_map, valid_doors, False,
|
||||
world, player, exception)
|
||||
dungeon[door.name] = create_graph_piece_from_state(door, o_state, b_state, proposed_map, exception)
|
||||
|
||||
|
||||
def make_a_choice(dungeon, hangers, avail_hooks, prev_choices):
|
||||
def make_a_choice(dungeon, hangers, avail_hooks, prev_choices, name):
|
||||
# choose a hanger
|
||||
all_hooks = {}
|
||||
origin = dungeon['Origin']
|
||||
@@ -294,6 +300,8 @@ def make_a_choice(dungeon, hangers, avail_hooks, prev_choices):
|
||||
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
|
||||
|
||||
@@ -463,7 +471,7 @@ def stonewall_valid(stonewall):
|
||||
return True
|
||||
|
||||
|
||||
def create_graph_piece_from_state(door, o_state, b_state, proposed_map):
|
||||
def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exception):
|
||||
# todo: info about dungeon events - not sure about that
|
||||
graph_piece = GraphPiece()
|
||||
all_unattached = {}
|
||||
@@ -485,7 +493,7 @@ def create_graph_piece_from_state(door, o_state, b_state, proposed_map):
|
||||
all_unattached[exp_d.door] = exp_d.crystal
|
||||
h_crystal = door.crystal if door is not None else None
|
||||
for d, crystal in all_unattached.items():
|
||||
if (door is None or d != door) and not d.blocked and d not in proposed_map.keys():
|
||||
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
|
||||
@@ -515,7 +523,7 @@ type_map = {
|
||||
}
|
||||
|
||||
|
||||
def opposite_h_type(h_type):
|
||||
def opposite_h_type(h_type) -> Hook:
|
||||
return type_map[h_type]
|
||||
|
||||
|
||||
@@ -755,9 +763,9 @@ class ExplorationState(object):
|
||||
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):
|
||||
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 self.can_traverse(door):
|
||||
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():
|
||||
@@ -814,9 +822,9 @@ class ExplorationState(object):
|
||||
def visited_at_all(self, region):
|
||||
return region in self.visited_blue or region in self.visited_orange
|
||||
|
||||
def can_traverse(self, door):
|
||||
def can_traverse(self, door, exception=None):
|
||||
if door.blocked:
|
||||
return False
|
||||
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
|
||||
@@ -906,11 +914,11 @@ def extend_reachable_state(search_regions, state, world, player):
|
||||
return local_state
|
||||
|
||||
|
||||
def extend_reachable_state_improved(search_regions, state, proposed_map, valid_doors, isOrigin, world, player):
|
||||
def extend_reachable_state_improved(search_regions, state, proposed_map, valid_doors, isOrigin, 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)
|
||||
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:
|
||||
@@ -927,7 +935,7 @@ def extend_reachable_state_improved(search_regions, state, proposed_map, valid_d
|
||||
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)
|
||||
local_state.add_all_doors_check_proposed(connect_region, proposed_map, valid_doors, flag, world, player, exception)
|
||||
return local_state
|
||||
|
||||
|
||||
@@ -993,6 +1001,7 @@ class DungeonBuilder(object):
|
||||
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)
|
||||
@@ -1037,6 +1046,12 @@ class DungeonBuilder(object):
|
||||
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)
|
||||
@@ -1048,11 +1063,13 @@ def simple_dungeon_builder(name, sector_list):
|
||||
return builder
|
||||
|
||||
|
||||
def create_dungeon_builders(all_sectors, connections_tuple, world, player, dungeon_entrances=None):
|
||||
def create_dungeon_builders(all_sectors, connections_tuple, world, player, 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)
|
||||
candidate_sectors = dict.fromkeys(all_sectors)
|
||||
global_pole = GlobalPolarity(candidate_sectors)
|
||||
@@ -1067,13 +1084,24 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge
|
||||
if key == 'Hyrule Castle' and world.mode[player] == 'standard':
|
||||
for r_name in ['Hyrule Dungeon Cellblock', 'Sanctuary']: # need to deliver zelda
|
||||
assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors, global_pole)
|
||||
entrances_map, potentials, connections = connections_tuple
|
||||
accessible_sectors, reverse_d_map = set(), {}
|
||||
for key in dungeon_entrances.keys():
|
||||
current_dungeon = dungeon_map[key]
|
||||
current_dungeon.all_entrances = dungeon_entrances[key]
|
||||
for r_name in current_dungeon.all_entrances:
|
||||
assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors, global_pole)
|
||||
# categorize sectors
|
||||
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
|
||||
|
||||
# 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)
|
||||
|
||||
@@ -1114,6 +1142,52 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge
|
||||
return dungeon_map
|
||||
|
||||
|
||||
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 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 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
|
||||
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
|
||||
|
||||
|
||||
def calc_allowance_and_dead_ends(builder, connections_tuple):
|
||||
entrances_map, potentials, connections = connections_tuple
|
||||
needed_connections = [x for x in builder.all_entrances if x not in entrances_map[builder.name]]
|
||||
@@ -1183,7 +1257,7 @@ def define_sector_features(sectors):
|
||||
|
||||
|
||||
def assign_sector(sector, dungeon, candidate_sectors, global_pole):
|
||||
if sector is not None:
|
||||
if sector:
|
||||
del candidate_sectors[sector]
|
||||
dungeon.sectors.append(sector)
|
||||
global_pole.consume(sector)
|
||||
@@ -1344,11 +1418,17 @@ def identify_polarity_issues(dungeon_map):
|
||||
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 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
|
||||
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:
|
||||
@@ -1382,7 +1462,8 @@ def identify_simple_branching_issues(dungeon_map):
|
||||
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
|
||||
if builder.dead_ends > builder.branches + builder.allowance:
|
||||
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]
|
||||
@@ -1392,6 +1473,28 @@ def identify_simple_branching_issues(dungeon_map):
|
||||
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_entrance_sector(builder, sector):
|
||||
for entrance in builder.all_entrances:
|
||||
r_set = sector.region_set()
|
||||
@@ -1483,20 +1586,22 @@ def assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger
|
||||
|
||||
# step 4: fix dead ends again
|
||||
neutral_choices: List[List] = neutralize_the_rest(polarized_sectors)
|
||||
problem_builders = identify_branching_issues_2(dungeon_map)
|
||||
problem_builders = identify_branching_issues(dungeon_map)
|
||||
while len(problem_builders) > 0:
|
||||
for name, builder in problem_builders.items():
|
||||
candidates = find_branching_candidates(builder, neutral_choices)
|
||||
# if len(candidates) <= 0:
|
||||
# problem_builders = {}
|
||||
# continue
|
||||
choice = random.choice(candidates)
|
||||
if valid_polarized_assignment(builder, choice):
|
||||
neutral_choices.remove(choice)
|
||||
for sector in choice:
|
||||
assign_sector(sector, builder, polarized_sectors, global_pole)
|
||||
valid, choice = False, None
|
||||
while not valid:
|
||||
if len(candidates) <= 0:
|
||||
raise Exception('Cross Dungeon Builder: Complex branch problems: %s' % name)
|
||||
choice = random.choice(candidates)
|
||||
candidates.remove(choice)
|
||||
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)
|
||||
builder.unfulfilled.clear()
|
||||
problem_builders = identify_branching_issues_2(problem_builders)
|
||||
problem_builders = identify_branching_issues(problem_builders)
|
||||
|
||||
# step 5: assign randomly until gone - must maintain connectedness, neutral polarity, branching, lack, etc.
|
||||
comb_w_replace = len(dungeon_map) ** len(neutral_choices)
|
||||
@@ -1534,8 +1639,9 @@ def polarity_step_3(dungeon_map, polarized_sectors, global_pole, logger):
|
||||
random.shuffle(odd_builders)
|
||||
for builder in odd_builders:
|
||||
while sum_polarity(builder.sectors).charge() % 2 != 0:
|
||||
odd_candidates = find_odd_sectors_ranked_by_charge(builder, polarized_sectors)
|
||||
sub_candidates, valid, best_charge, candidate = [], False, min(list(odd_candidates.keys())), None
|
||||
grouped_choices: List[List] = find_forced_groupings(polarized_sectors, dungeon_map)
|
||||
odd_candidates = find_odd_sectors_ranked_by_charge(builder, grouped_choices)
|
||||
sub_candidates, valid, best_charge, candidate_list = [], False, min(list(odd_candidates.keys())), None
|
||||
while not valid:
|
||||
if len(sub_candidates) == 0:
|
||||
if len(odd_candidates) == 0:
|
||||
@@ -1543,10 +1649,11 @@ def polarity_step_3(dungeon_map, polarized_sectors, global_pole, logger):
|
||||
while best_charge not in odd_candidates.keys():
|
||||
best_charge += 2
|
||||
sub_candidates = odd_candidates.pop(best_charge)
|
||||
candidate = random.choice(sub_candidates)
|
||||
sub_candidates.remove(candidate)
|
||||
valid = global_pole.is_valid_choice(dungeon_map, builder, [candidate])
|
||||
assign_sector(candidate, builder, polarized_sectors, global_pole)
|
||||
candidate_list = random.choice(sub_candidates)
|
||||
sub_candidates.remove(candidate_list)
|
||||
valid = global_pole.is_valid_choice(dungeon_map, builder, candidate_list) and valid_branch_only(builder, candidate_list)
|
||||
for candidate in candidate_list:
|
||||
assign_sector(candidate, builder, polarized_sectors, global_pole)
|
||||
|
||||
# step 3b: neutralize all builders
|
||||
builder_order = list(dungeon_map.values())
|
||||
@@ -1697,7 +1804,7 @@ def find_connection_candidates(mag_needed, sector_pool):
|
||||
def find_simple_branching_candidates(builder, sector_pool):
|
||||
candidates = defaultdict(list)
|
||||
charges = defaultdict(list)
|
||||
outflow_needed = builder.dead_ends > builder.branches + builder.allowance
|
||||
outflow_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:
|
||||
@@ -1709,7 +1816,9 @@ def find_simple_branching_candidates(builder, sector_pool):
|
||||
lack = builder.conn_balance[hook] + sector.conn_balance[hook]
|
||||
if lack < 0:
|
||||
ttl_lack += -lack
|
||||
if ttl_lack < original_lack or original_lack >= 0:
|
||||
forced_loops = calc_forced_loops(builder.sectors + [sector])
|
||||
valid_branches = builder.dead_ends + forced_loops * 2 + sector.dead_ends() <= builder.branches + builder.allowance + sector.branches()
|
||||
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:
|
||||
@@ -1731,12 +1840,12 @@ def calc_sector_balance(sector): # todo: move to base class?
|
||||
sector.conn_balance[hanger_from_door(door)] += 1
|
||||
|
||||
|
||||
def find_odd_sectors_ranked_by_charge(builder, polarized_sectors):
|
||||
def find_odd_sectors_ranked_by_charge(builder, grouped_candidates):
|
||||
polarity = builder.polarity()
|
||||
candidates = defaultdict(list)
|
||||
for candidate in [x for x in polarized_sectors if x.polarity().charge() % 2 != 0]:
|
||||
p_charge = (polarity + candidate.polarity()).charge()
|
||||
candidates[p_charge].append(candidate)
|
||||
for candidate_list in [x for x in grouped_candidates if sum_polarity(x).charge() % 2 != 0]:
|
||||
p_charge = (polarity + sum_polarity(candidate_list)).charge()
|
||||
candidates[p_charge].append(candidate_list)
|
||||
return candidates
|
||||
|
||||
|
||||
@@ -1802,7 +1911,8 @@ def weed_candidates(builder, candidates, best_charge):
|
||||
ttl_balance += lack
|
||||
if lack < 0:
|
||||
ttl_lack += -lack
|
||||
if ttl_balance >= 0 and builder.dead_ends + ttl_deads <= builder.branches + ttl_branches + builder.allowance:
|
||||
forced_loops = calc_forced_loops(builder.sectors + 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]
|
||||
@@ -1833,10 +1943,8 @@ def find_branching_candidates(builder, neutral_choices):
|
||||
for door in sector.outstanding_doors:
|
||||
if builder.unfulfilled[hanger_from_door(door)] > 0:
|
||||
door_match = True
|
||||
if door_match and flow_match:
|
||||
if (door_match and flow_match) or len(resolve_equations(builder, choice)) == 0:
|
||||
candidates.append(choice)
|
||||
if len(candidates) == 0:
|
||||
raise Exception('Cross Dungeon Builder: No more branching candidates! %s' % builder.name)
|
||||
return candidates
|
||||
|
||||
|
||||
@@ -1876,6 +1984,50 @@ def neutralize_the_rest(sector_pool):
|
||||
return neutral_choices
|
||||
|
||||
|
||||
def find_forced_groupings(sector_pool, dungeon_map):
|
||||
dungeon_hooks = {}
|
||||
for name, builder in dungeon_map.items():
|
||||
dungeon_hooks[name] = sum_hook_magnitude(builder.sectors)
|
||||
groupings = []
|
||||
queue = deque(sector_pool)
|
||||
skips = set()
|
||||
while len(queue) > 0:
|
||||
grouping = queue.pop()
|
||||
is_list = isinstance(grouping, List)
|
||||
if not is_list and grouping in skips:
|
||||
continue
|
||||
grouping = grouping if is_list else [grouping]
|
||||
hook_mag = sum_hook_magnitude(grouping)
|
||||
force_found = False
|
||||
for val in Hook:
|
||||
if hook_mag[val.value] == 1:
|
||||
opp = opposite_h_type(val).value
|
||||
num_found = hook_mag[opp]
|
||||
for name, hooks in dungeon_hooks.items():
|
||||
if hooks[opp] > 0:
|
||||
num_found += hooks[opp]
|
||||
other_sectors = [x for x in sector_pool if x not in grouping]
|
||||
other_sector_mag = sum_hook_magnitude(other_sectors)
|
||||
if other_sector_mag[opp] > 0:
|
||||
num_found += other_sector_mag[opp]
|
||||
if num_found == 1:
|
||||
forced_sector = None
|
||||
for sec in other_sectors:
|
||||
if sec.hook_magnitude()[opp] > 0:
|
||||
forced_sector = sec
|
||||
break
|
||||
if forced_sector:
|
||||
grouping.append(forced_sector)
|
||||
skips.add(forced_sector)
|
||||
queue.append(grouping)
|
||||
force_found = True
|
||||
if force_found:
|
||||
break
|
||||
if not force_found:
|
||||
groupings.append(grouping)
|
||||
return groupings
|
||||
|
||||
|
||||
def valid_assignment(builder, sector_list):
|
||||
if not valid_polarized_assignment(builder, sector_list):
|
||||
return False
|
||||
@@ -1897,9 +2049,26 @@ def valid_connected_assignment(builder, sector_list):
|
||||
return True
|
||||
|
||||
|
||||
def valid_polarized_assignment(builder, sector_list):
|
||||
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()
|
||||
|
||||
|
||||
@@ -1909,7 +2078,7 @@ def assign_the_rest(dungeon_map, neutral_sectors, global_pole):
|
||||
choices = random.choices(list(dungeon_map.keys()), k=len(sector_list))
|
||||
for i, choice in enumerate(choices):
|
||||
builder = dungeon_map[choice]
|
||||
if valid_polarized_assignment(builder, [sector_list[i]]):
|
||||
if valid_assignment(builder, [sector_list[i]]):
|
||||
assign_sector(sector_list[i], builder, neutral_sectors, global_pole)
|
||||
|
||||
|
||||
@@ -1964,13 +2133,13 @@ def check_for_forced_dead_ends(dungeon_map, candidate_sectors, global_pole):
|
||||
if hook_mag[hook.value] != 0:
|
||||
dead_cnt[hook.value] += 1
|
||||
for hook in Hook:
|
||||
opp = opposite_h_type(hook)
|
||||
if dead_cnt[hook.value] > other_magnitude[opp.value]:
|
||||
opp = opposite_h_type(hook).value
|
||||
if dead_cnt[hook.value] > other_magnitude[opp]:
|
||||
raise Exception('Impossible to satisfy all these dead ends')
|
||||
elif dead_cnt[hook.value] == other_magnitude[opp.value]:
|
||||
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.value] > 0 and sector.is_entrance_sector() and sector.branching_factor() == 2:
|
||||
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:
|
||||
@@ -1998,9 +2167,9 @@ def check_for_forced_assignments(dungeon_map, candidate_sectors, global_pole):
|
||||
for val in Hook:
|
||||
if magnitude[val.value] == 1:
|
||||
found_hooks = []
|
||||
opp = opposite_h_type(val)
|
||||
opp = opposite_h_type(val).value
|
||||
for name, hooks in dungeon_hooks.items():
|
||||
if hooks[opp.value] > 0 and not dungeon_map[name].c_locked:
|
||||
if hooks[opp] > 0 and not dungeon_map[name].c_locked:
|
||||
found_hooks.append(name)
|
||||
if len(found_hooks) == 1:
|
||||
done = False
|
||||
@@ -2048,6 +2217,7 @@ class DoorEquation:
|
||||
self.cost = defaultdict(list)
|
||||
self.benefit = defaultdict(list)
|
||||
self.required = False
|
||||
self.access_id = None
|
||||
|
||||
def copy(self):
|
||||
eq = DoorEquation(self.door)
|
||||
@@ -2076,6 +2246,15 @@ class DoorEquation:
|
||||
return False
|
||||
return True
|
||||
|
||||
def neutral_profit(self):
|
||||
better_found = False
|
||||
for key in Hook:
|
||||
if len(self.cost[key]) > len(self.benefit[key]):
|
||||
return False
|
||||
if len(self.cost[key]) < len(self.benefit[key]):
|
||||
better_found = True
|
||||
return better_found
|
||||
|
||||
def can_cover_cost(self, current_access):
|
||||
for key, door_list in self.cost.items():
|
||||
if len(door_list) > current_access[key]:
|
||||
@@ -2083,7 +2262,7 @@ class DoorEquation:
|
||||
return True
|
||||
|
||||
|
||||
def identify_branching_issues_2(dungeon_map):
|
||||
def identify_branching_issues(dungeon_map):
|
||||
unconnected_builders = {}
|
||||
for name, builder in dungeon_map.items():
|
||||
unreached_doors = resolve_equations(builder, [])
|
||||
@@ -2097,17 +2276,40 @@ def identify_branching_issues_2(dungeon_map):
|
||||
def resolve_equations(builder, sector_list):
|
||||
unreached_doors = defaultdict(list)
|
||||
equations = copy_door_equations(builder, sector_list)
|
||||
current_access = defaultdict(int)
|
||||
reached_doors = set()
|
||||
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] = defaultdict(int)
|
||||
for r_name in region_list:
|
||||
sector = find_sector(r_name, builder.sectors)
|
||||
sector_split[sector] = name
|
||||
else:
|
||||
current_access[builder.name] = defaultdict(int)
|
||||
|
||||
# resolve all that provide more access
|
||||
free_sector, eq_list, free_eq = find_free_equation(equations)
|
||||
while free_eq is not None:
|
||||
resolve_equation(free_eq, eq_list, free_sector, current_access, reached_doors, equations)
|
||||
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, reached_doors, equations)
|
||||
free_sector, eq_list, free_eq = find_free_equation(equations)
|
||||
while len(equations) > 0:
|
||||
eq, eq_list, sector = find_priority_equation(equations, current_access)
|
||||
if eq is not None:
|
||||
resolve_equation(eq, eq_list, sector, current_access, reached_doors, equations)
|
||||
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)
|
||||
if eq:
|
||||
resolve_equation(eq, eq_list, sector, access_id, access, reached_doors, equations)
|
||||
else:
|
||||
for sector, eq_list in equations.items():
|
||||
for eq in eq_list:
|
||||
@@ -2116,39 +2318,65 @@ def resolve_equations(builder, sector_list):
|
||||
return unreached_doors
|
||||
|
||||
|
||||
def next_access(current_access):
|
||||
valid_ones = [(x, y) for x, y in current_access.items() if sum(y.values()) > 0]
|
||||
valid_ones.sort(key=lambda x: sum(x[1].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, current_access):
|
||||
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
|
||||
triplet_candidates = []
|
||||
all_candidates = []
|
||||
local_profit_map = {}
|
||||
|
||||
for sector, eq_list in equations.items():
|
||||
eq_list.sort(key=lambda eq: eq.profit(), reverse=True)
|
||||
best_local_profit = None
|
||||
for eq in eq_list:
|
||||
profit = eq.profit()
|
||||
if best_local_profit is None or profit > best_local_profit:
|
||||
best_local_profit = profit
|
||||
if eq.can_cover_cost(current_access):
|
||||
if eq.neutral():
|
||||
return eq, eq_list, sector # don't need to compare
|
||||
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))
|
||||
if eq.can_cover_cost(current_access) 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:
|
||||
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)
|
||||
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]
|
||||
|
||||
triplet_candidates = []
|
||||
best_profit = None
|
||||
for eq, eq_list, sector in filtered_candidates:
|
||||
profit = eq.profit()
|
||||
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) == 0:
|
||||
return None, None, None # can't pay for anything
|
||||
if len(filtered_candidates) == 1:
|
||||
return filtered_candidates[0]
|
||||
|
||||
@@ -2167,7 +2395,45 @@ def find_priority_equation(equations, current_access):
|
||||
good_local_candidates = [x for x in flexible_candidates if local_profit_map[x[2]] == x[0].profit()]
|
||||
if len(good_local_candidates) == 0:
|
||||
good_local_candidates = flexible_candidates
|
||||
return good_local_candidates[0] # just pick one I guess
|
||||
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
|
||||
return leads_to_profit[0] # just pick one I guess
|
||||
|
||||
|
||||
def find_greedy_equation(equations, access_id, current_access):
|
||||
all_candidates = []
|
||||
for sector, eq_list in equations.items():
|
||||
eq_list.sort(key=lambda eq: eq.profit(), reverse=True)
|
||||
for eq in eq_list:
|
||||
if eq.can_cover_cost(current_access) 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() + 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()
|
||||
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):
|
||||
@@ -2262,7 +2528,19 @@ def filter_requirements(triplet_candidates, equations, required, current_access)
|
||||
return valid_candidates
|
||||
|
||||
|
||||
def resolve_equation(equation, eq_list, sector, current_access, reached_doors, equations):
|
||||
def can_enable_wanted(test_eq, wanted_candidates):
|
||||
for wanted in wanted_candidates:
|
||||
covered = True
|
||||
for key, door_list in wanted.cost.items():
|
||||
if len(test_eq.benefit[key]) < len(door_list):
|
||||
covered = False
|
||||
break
|
||||
if covered:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def resolve_equation(equation, eq_list, sector, access_id, current_access, reached_doors, equations):
|
||||
for key, door_list in equation.cost.items():
|
||||
if current_access[key] - len(door_list) < 0:
|
||||
raise Exception('Cannot pay for this connection')
|
||||
@@ -2286,6 +2564,9 @@ def resolve_equation(equation, eq_list, sector, current_access, reached_doors, e
|
||||
eq_list.remove(r_eq)
|
||||
if len(eq_list) == 0:
|
||||
del equations[sector]
|
||||
else:
|
||||
for eq in eq_list:
|
||||
eq.access_id = access_id
|
||||
|
||||
|
||||
def find_free_equation(equations):
|
||||
@@ -2310,7 +2591,7 @@ def copy_door_equations(builder, sector_list):
|
||||
|
||||
def calc_sector_equations(sector, builder):
|
||||
equations = []
|
||||
is_entrance = is_entrance_sector(builder, sector)
|
||||
is_entrance = is_entrance_sector(builder, sector) and not sector.destination_entrance
|
||||
if is_entrance:
|
||||
flagged_equations = []
|
||||
for door in sector.outstanding_doors:
|
||||
@@ -2370,7 +2651,7 @@ def calc_door_equation(door, sector, look_for_entrance):
|
||||
if d.req_event is not None and d.req_event not in found_events:
|
||||
event_doors.add(d)
|
||||
else:
|
||||
connect = ext.connected_region
|
||||
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 connect not in visited:
|
||||
visited.add(connect)
|
||||
queue.append(connect)
|
||||
|
||||
Reference in New Issue
Block a user