More trap doors (mostly interior)

DungeonGen refinements
--More Big Key door considerations
--Backtracks earlier when hook candidates comes up empty
Minor work on key shuffling - lots of bad rules still
Playthrough gen doesn't flood swamp keys now
This commit is contained in:
aerinon
2019-12-03 15:50:15 -07:00
parent 43ba391df1
commit 9dfd93adbc
5 changed files with 104 additions and 22 deletions

View File

@@ -12,7 +12,7 @@ from Dungeons import dungeon_regions, region_starts, split_region_starts, dungeo
from Dungeons import drop_entrances
from RoomData import DoorKind, PairedDoor
from DungeonGenerator import ExplorationState, convert_regions, generate_dungeon
from KeyDoorShuffle import analyze_dungeon, validate_vanilla_key_logic
from KeyDoorShuffle import analyze_dungeon, validate_vanilla_key_logic, validate_key_layout_ex
def link_doors(world, player):

View File

@@ -299,7 +299,7 @@ def create_doors(world, player):
create_door(player, 'Tower Catwalk North Stairs', StrS).dir(No, 0x40, Left, High),
create_door(player, 'Tower Antechamber South Stairs', StrS).dir(So, 0x30, Left, High),
create_door(player, 'Tower Antechamber NW', Intr).dir(No, 0x30, Left, High).pos(1),
create_door(player, 'Tower Altar SW', Intr).dir(So, 0x30, Left, High).pos(1),
create_door(player, 'Tower Altar SW', Intr).dir(So, 0x30, Left, High).no_exit().pos(1),
create_door(player, 'Tower Altar NW', Nrml).dir(No, 0x30, Left, High).pos(0),
create_door(player, 'Tower Agahnim 1 SW', Nrml).dir(So, 0x20, Left, High).no_exit().trap(0x4).pos(0),
@@ -481,7 +481,7 @@ def create_doors(world, player):
create_door(player, 'Swamp I S', Intr).dir(So, 0x16, Mid, High).pos(0),
create_door(player, 'Swamp T SW', Intr).dir(So, 0x16, Left, High).small_key().pos(1),
create_door(player, 'Swamp T NW', Nrml).dir(No, 0x16, Left, High).pos(3),
create_door(player, 'Swamp Boss SW', Nrml).dir(So, 0x06, Left, High).trap(0x4).pos(0),
create_door(player, 'Swamp Boss SW', Nrml).dir(So, 0x06, Left, High).no_exit().trap(0x4).pos(0),
create_door(player, 'Skull 1 Lobby WS', Nrml).dir(We, 0x58, Bot, High).small_key().pos(1),
create_door(player, 'Skull 1 Lobby ES', Intr).dir(Ea, 0x58, Bot, High).pos(5),
@@ -643,7 +643,7 @@ def create_doors(world, player):
create_door(player, 'Ice Firebar ES', Intr).dir(Ea, 0x5e, Bot, High).pos(3),
create_door(player, 'Ice Firebar Down Ladder', Lddr),
create_door(player, 'Ice Spike Cross NE', Intr).dir(No, 0x5e, Right, High).pos(1),
create_door(player, 'Ice Falling Square SE', Intr).dir(So, 0x5e, Right, High).pos(1),
create_door(player, 'Ice Falling Square SE', Intr).dir(So, 0x5e, Right, High).no_exit().pos(1),
create_door(player, 'Ice Falling Square Hole', Hole),
create_door(player, 'Ice Spike Room WS', Nrml).dir(We, 0x5f, Bot, High).small_key().pos(0),
create_door(player, 'Ice Spike Room Down Stairs', Sprl).dir(Dn, 0x5f, 3, HTH).ss(Z, 0x11, 0x48, True, True),
@@ -980,7 +980,7 @@ def create_doors(world, player):
create_door(player, 'GT Ice Armos NE', Intr).dir(No, 0x1c, Right, High).pos(0),
create_door(player, 'GT Big Key Room SE', Intr).dir(So, 0x1c, Right, High).pos(0),
create_door(player, 'GT Ice Armos WS', Intr).dir(We, 0x1c, Bot, High).pos(1),
create_door(player, 'GT Four Torches ES', Intr).dir(Ea, 0x1c, Bot, High).pos(1),
create_door(player, 'GT Four Torches ES', Intr).dir(Ea, 0x1c, Bot, High).no_exit().pos(1),
create_door(player, 'GT Four Torches NW', Intr).dir(No, 0x1c, Left, High).pos(2),
create_door(player, 'GT Fairy Abyss SW', Intr).dir(So, 0x1c, Left, High).pos(2),
create_door(player, 'GT Four Torches Up Stairs', Sprl).dir(Up, 0x1c, 0, HTH).ss(Z, 0x1b, 0x2c, True, True),
@@ -1012,7 +1012,7 @@ def create_doors(world, player):
create_door(player, 'GT Beam Dash WS', Intr).dir(We, 0x6c, Bot, High).pos(0),
create_door(player, 'GT Lanmolas 2 ES', Intr).dir(Ea, 0x6c, Bot, High).pos(0),
create_door(player, 'GT Lanmolas 2 NW', Intr).dir(No, 0x6c, Left, High).pos(1),
create_door(player, 'GT Quad Pot SW', Intr).dir(So, 0x6c, Left, High).pos(1),
create_door(player, 'GT Quad Pot SW', Intr).dir(So, 0x6c, Left, High).no_exit().pos(1),
create_door(player, 'GT Quad Pot Up Stairs', Sprl).dir(Up, 0x6c, 0, HTH).ss(A, 0x1b, 0x6c, True, True),
create_door(player, 'GT Wizzrobes 1 Down Stairs', Sprl).dir(Dn, 0xa5, 0, HTH).ss(A, 0x12, 0x80, True, True),
create_door(player, 'GT Wizzrobes 1 SW', Intr).dir(So, 0xa5, Left, High).pos(2),

View File

@@ -52,7 +52,7 @@ def generate_dungeon(available_sectors, entrance_region_names, split_dungeon, wo
if itr > 5000:
raise Exception('Generation taking too long. Ref %s' % entrance_region_names[0])
if depth not in dungeon_cache.keys():
dungeon, hangers, hooks = gen_dungeon_info(available_sectors, entrance_regions, proposed_map, doors_to_connect, world, player)
dungeon, hangers, hooks = gen_dungeon_info(available_sectors, entrance_regions, proposed_map, doors_to_connect, bk_needed, world, player)
dungeon_cache[depth] = dungeon, hangers, hooks
valid = check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions, bk_needed)
else:
@@ -109,10 +109,10 @@ def determine_if_bk_needed(sector, split_dungeon, world, player):
return False
def gen_dungeon_info(available_sectors, entrance_regions, proposed_map, valid_doors, world, player):
def gen_dungeon_info(available_sectors, entrance_regions, proposed_map, valid_doors, bk_needed, world, player):
# step 1 create dungeon: Dict<DoorName|Origin, GraphPiece>
dungeon = {}
original_state = extend_reachable_state_improved(entrance_regions, ExplorationState(), proposed_map, valid_doors, world, player)
original_state = extend_reachable_state_improved(entrance_regions, ExplorationState(), proposed_map, valid_doors, bk_needed, world, player)
dungeon['Origin'] = create_graph_piece_from_state(None, original_state, original_state, proposed_map)
doors_to_connect = set()
hanger_set = set()
@@ -123,7 +123,7 @@ def gen_dungeon_info(available_sectors, entrance_regions, proposed_map, valid_do
if not door.stonewall and door not in proposed_map.keys():
hanger_set.add(door)
parent = parent_region(door, world, player).parent_region
o_state = extend_reachable_state_improved([parent], ExplorationState(), proposed_map, valid_doors, world, player)
o_state = extend_reachable_state_improved([parent], ExplorationState(), proposed_map, valid_doors, False, world, player)
o_state_cache[door.name] = o_state
piece = create_graph_piece_from_state(door, o_state, o_state, proposed_map)
dungeon[door.name] = piece
@@ -183,7 +183,7 @@ def check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, valid_do
def explore_blue_state(door, dungeon, o_state, proposed_map, valid_doors, world, player):
parent = parent_region(door, world, player).parent_region
blue_start = ExplorationState(CrystalBarrier.Blue)
b_state = extend_reachable_state_improved([parent], blue_start, proposed_map, valid_doors, world, player)
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)
@@ -234,6 +234,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))
else:
return None, None
return next_hanger, hook
@@ -252,7 +254,7 @@ def check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_reg
return False
# origin has no more hooks, but not all doors have been proposed
possible_bks = len(dungeon['Origin'].possible_bk_locations)
true_origin_hooks = [x for x in dungeon['Origin'].hooks.keys() if not x.bigKey or possible_bks > 0]
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
for key in hangers.keys():
@@ -553,7 +555,7 @@ class ExplorationState(object):
return ret
def next_avail_door(self):
self.avail_doors.sort(key=lambda x: 0 if x.flag else 1)
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
@@ -784,13 +786,16 @@ def extend_reachable_state(search_regions, state, world, player):
return local_state
def extend_reachable_state_improved(search_regions, state, proposed_map, valid_doors, world, player):
def extend_reachable_state_improved(search_regions, state, proposed_map, valid_doors, isOrigin, world, player):
local_state = state.copy()
for region in search_regions:
local_state.visit_region(region)
local_state.add_all_doors_check_proposed(region, proposed_map, valid_doors, False, world, player)
while len(local_state.avail_doors) > 0:
explorable_door = local_state.next_avail_door()
if explorable_door.door.bigKey:
if isOrigin and local_state.count_locations_exclude_specials() == 0:
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:

View File

@@ -138,7 +138,8 @@ def analyze_dungeon(key_layout, world, player):
key_sphere, key_counter = queue.popleft()
chest_keys = available_chest_small_keys(key_counter, False, world)
# chest_keys_bk = available_chest_small_keys(key_counter, True, world)
available = chest_keys + len(key_counter.key_only_locations) - key_counter.used_keys
raw_avail = chest_keys + len(key_counter.key_only_locations)
available = raw_avail - key_counter.used_keys
possible_smalls = count_unique_small_doors(key_counter, key_layout.flat_prop)
if not key_counter.big_key_opened:
if chest_keys == count_locations_big_optional(key_counter.free_locations) and available <= possible_smalls:
@@ -167,10 +168,10 @@ def analyze_dungeon(key_layout, world, player):
true_min = minimal_keys
last_small_child = len([x for x in childqueue if not x[0].bigKey]) == 0
force_min = not minimal_satisfied and last_small_child
rule = create_rule(expanded_counter, key_layout, true_min, force_min, world)
rule = create_rule(expanded_counter, key_layout, true_min, force_min, raw_avail, world)
minimal_satisfied = minimal_satisfied or rule.small_key_num <= minimal_keys
check_for_self_lock_key(rule, next_sphere, key_layout, world)
bk_restricted_rules(rule, next_sphere, key_counter, key_layout, true_min, force_min, world)
bk_restricted_rules(rule, next_sphere, key_counter, key_layout, true_min, force_min, raw_avail, world)
key_logic.door_rules[child.name] = rule
doors_completed.add(next_sphere.access_door)
next_counter = increment_key_counter(child, next_sphere, key_counter, key_layout.flat_prop)
@@ -298,10 +299,12 @@ def expand_counter_to_last_door(door, key_counter, key_layout, ignored_doors):
return new_counter
def create_rule(key_counter, key_layout, minimal_keys, force_min, world):
def create_rule(key_counter, key_layout, minimal_keys, force_min, prev_avail, world):
chest_keys = available_chest_small_keys(key_counter, key_counter.big_key_opened, world)
available = chest_keys + len(key_counter.key_only_locations) - key_counter.used_keys
raw_avail = chest_keys + len(key_counter.key_only_locations)
available = raw_avail - key_counter.used_keys
possible_smalls = count_unique_small_doors(key_counter, key_layout.flat_prop)
# key_gain = max(raw_avail - prev_avail, 0)
required_keys = min(available, possible_smalls) + key_counter.used_keys
if not force_min or required_keys <= minimal_keys:
return DoorRules(required_keys)
@@ -347,11 +350,11 @@ def available_chest_small_keys(key_counter, bk, world):
return key_counter.max_chests
def bk_restricted_rules(rule, sphere, key_counter, key_layout, minimal_keys, force_min, world):
def bk_restricted_rules(rule, sphere, key_counter, key_layout, minimal_keys, force_min, prev_avail, world):
if sphere.bk_locked:
return
expanded_counter = expand_counter_no_big_doors(sphere.access_door, key_counter, key_layout, set())
bk_number = create_rule(expanded_counter, key_layout, minimal_keys, force_min, world).small_key_num
bk_number = create_rule(expanded_counter, key_layout, minimal_keys, force_min, prev_avail, world).small_key_num
if bk_number == rule.small_key_num:
return
post_counter = KeyCounter(key_layout.max_chests)
@@ -491,6 +494,15 @@ def unique_doors(doors):
return unique_d_set
# does not allow dest doors
def count_unique_doors(doors):
unique_d_set = set()
for d in doors:
if d not in unique_d_set and d.dest not in unique_d_set:
unique_d_set.add(d)
return len(unique_d_set)
# doesn't count dest doors
def count_unique_small_doors(key_counter, proposal):
cnt = 0
@@ -545,6 +557,71 @@ def flatten_pair_list(paired_list):
# Soft lock stuff
def validate_key_layout_ex(key_layout, world, player):
key_layout = KeyLayout(key_layout.sector, key_layout.start_regions, key_layout.proposal)
key_layout.flat_prop = flatten_pair_list(key_layout.proposal)
key_layout.max_chests = len(world.get_dungeon(key_layout.sector.name, player).small_keys)
counters = create_key_counters(key_layout, world, player)
pass
def create_key_counters(key_layout, world, player):
key_counters = {}
flat_proposal = key_layout.flat_prop
state = ExplorationState()
state.key_locations = len(world.get_dungeon(key_layout.sector.name, player).small_keys)
state.big_key_special = world.get_region('Hyrule Dungeon Cellblock', player) in key_layout.sector.regions
for region in key_layout.start_regions:
state.visit_region(region, key_checks=True)
state.add_all_doors_check_keys(region, flat_proposal, world, player)
expand_key_state(state, flat_proposal, world, player)
code = state_id(state, key_layout.flat_prop)
key_counters[code] = create_key_counter_x(state, key_layout.max_chests, world, player)
queue = collections.deque([(key_counters[code], state)])
while len(queue) > 0:
next_key_sphere, parent_state = queue.popleft()
for door in next_key_sphere.child_doors:
child_state = parent_state.copy()
# open the door
open_a_door(door, child_state, flat_proposal)
expand_key_state(child_state, flat_proposal, world, player)
code = state_id(child_state, key_layout.flat_prop)
if code not in key_counters.keys():
child_kr = create_key_counter_x(child_state, key_layout.max_chests, world, player)
key_counters[code] = child_kr
queue.append((child_kr, child_state))
return key_counters
def create_key_counter_x(state, max_chests, world, player):
key_sphere = KeyCounter(max_chests)
key_sphere.child_doors.update(unique_doors(state.small_doors+state.big_doors))
for loc in state.found_locations:
if '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']:
key_sphere.important_location = True
# todo: zelda's cell is special in standard, and probably crossed too
elif loc.name in ['Attic Cracked Floor', 'Suspicious Maiden']:
key_sphere.important_location = True
elif loc.event and 'Small Key' in loc.item.name:
key_sphere.key_only_locations.add(loc)
elif loc.name not in dungeon_events:
key_sphere.free_locations.add(loc)
key_sphere.open_doors.update(state.opened_doors)
key_sphere.used_keys = count_unique_doors(state.opened_doors)
if state.big_key_special:
key_sphere.big_key_opened = state.visited(world.get_region('Hyrule Dungeon Cellblock', player))
else:
key_sphere.big_key_opened = state.big_key_opened
return key_sphere
def state_id(state, flat_proposal):
s_id = '1' if state.big_key_opened else '0'
for d in flat_proposal:
s_id += '1' if d in state.opened_doors else '0'
return s_id
class SoftLockException(Exception):
pass

View File

@@ -381,7 +381,7 @@ def create_playthrough(world):
if not world.keysanity:
state.sweep_for_events(key_only=True)
sphere = list(filter(state.can_reach, required_locations))
sphere = list(filter(lambda loc: state.can_reach(loc) and state.not_flooding_a_key(world, loc), required_locations))
for location in sphere:
required_locations.remove(location)