Key logic rework for placement rules

--exhaustive per key_counter
--contradictions between rules
Mire Lobby chest fix
This commit is contained in:
aerinon
2020-03-20 09:05:07 -06:00
parent 269de35590
commit a18f5c4d51
7 changed files with 290 additions and 100 deletions

View File

@@ -1050,8 +1050,9 @@ def log_key_logic(d_name, key_logic):
logger.debug('*Rule for %s:', rule.door_reference) logger.debug('*Rule for %s:', rule.door_reference)
if rule.bk_conditional_set: if rule.bk_conditional_set:
logger.debug('**BK Checks %s', ','.join([x.name for x in rule.bk_conditional_set])) logger.debug('**BK Checks %s', ','.join([x.name for x in rule.bk_conditional_set]))
logger.debug('**BK Blocked By Door (%s) : %s', rule.needed_keys_wo_bk, ','.join([x.name for x in rule.check_locations_wo_bk])) logger.debug('**BK Blocked (%s) : %s', rule.needed_keys_wo_bk, ','.join([x.name for x in rule.check_locations_wo_bk]))
logger.debug('**BK Elsewhere (%s) : %s', rule.needed_keys_w_bk, ','.join([x.name for x in rule.check_locations_w_bk])) if rule.needed_keys_w_bk:
logger.debug('**BK Available (%s) : %s', rule.needed_keys_w_bk, ','.join([x.name for x in rule.check_locations_w_bk]))
def build_pair_list(flat_list): def build_pair_list(flat_list):
@@ -1579,10 +1580,12 @@ logical_connections = [
('Ice Big Chest Landing Push Blocks', 'Ice Big Chest View'), ('Ice Big Chest Landing Push Blocks', 'Ice Big Chest View'),
('Mire Lobby Gap', 'Mire Post-Gap'), ('Mire Lobby Gap', 'Mire Post-Gap'),
('Mire Post-Gap Gap', 'Mire Lobby'), ('Mire Post-Gap Gap', 'Mire Lobby'),
('Mire Hub Upper Blue Barrier', 'Mire Hub Top'), ('Mire Hub Upper Blue Barrier', 'Mire Hub Switch'),
('Mire Hub Lower Blue Barrier', 'Mire Hub Right'), ('Mire Hub Lower Blue Barrier', 'Mire Hub Right'),
('Mire Hub Right Blue Barrier', 'Mire Hub'), ('Mire Hub Right Blue Barrier', 'Mire Hub'),
('Mire Hub Top Blue Barrier', 'Mire Hub'), ('Mire Hub Top Blue Barrier', 'Mire Hub Switch'),
('Mire Hub Switch Blue Barrier N', 'Mire Hub Top'),
('Mire Hub Switch Blue Barrier S', 'Mire Hub'),
('Mire Map Spike Side Drop Down', 'Mire Lone Shooter'), ('Mire Map Spike Side Drop Down', 'Mire Lone Shooter'),
('Mire Map Spike Side Blue Barrier', 'Mire Crystal Dead End'), ('Mire Map Spike Side Blue Barrier', 'Mire Crystal Dead End'),
('Mire Map Spot Blue Barrier', 'Mire Crystal Dead End'), ('Mire Map Spot Blue Barrier', 'Mire Crystal Dead End'),

View File

@@ -722,6 +722,8 @@ def create_doors(world, player):
create_door(player, 'Mire Hub Lower Blue Barrier', Lgcl), create_door(player, 'Mire Hub Lower Blue Barrier', Lgcl),
create_door(player, 'Mire Hub Right Blue Barrier', Lgcl), create_door(player, 'Mire Hub Right Blue Barrier', Lgcl),
create_door(player, 'Mire Hub Top Blue Barrier', Lgcl), create_door(player, 'Mire Hub Top Blue Barrier', Lgcl),
create_door(player, 'Mire Hub Switch Blue Barrier N', Lgcl),
create_door(player, 'Mire Hub Switch Blue Barrier S', Lgcl),
create_door(player, 'Mire Hub Right EN', Nrml).dir(Ea, 0xc2, Top, High).small_key().pos(0), create_door(player, 'Mire Hub Right EN', Nrml).dir(Ea, 0xc2, Top, High).small_key().pos(0),
create_door(player, 'Mire Hub Top NW', Nrml).dir(No, 0xc2, Left, High).pos(2), create_door(player, 'Mire Hub Top NW', Nrml).dir(No, 0xc2, Left, High).pos(2),
create_door(player, 'Mire Lone Shooter WS', Nrml).dir(We, 0xc3, Bot, High).pos(6), create_door(player, 'Mire Lone Shooter WS', Nrml).dir(We, 0xc3, Bot, High).pos(6),
@@ -1161,6 +1163,8 @@ def create_doors(world, player):
world.get_door('Mire Hub Lower Blue Barrier', player).barrier(CrystalBarrier.Blue) world.get_door('Mire Hub Lower Blue Barrier', player).barrier(CrystalBarrier.Blue)
world.get_door('Mire Hub Right Blue Barrier', player).barrier(CrystalBarrier.Blue) world.get_door('Mire Hub Right Blue Barrier', player).barrier(CrystalBarrier.Blue)
world.get_door('Mire Hub Top Blue Barrier', player).barrier(CrystalBarrier.Blue) world.get_door('Mire Hub Top Blue Barrier', player).barrier(CrystalBarrier.Blue)
world.get_door('Mire Hub Switch Blue Barrier N', player).barrier(CrystalBarrier.Blue)
world.get_door('Mire Hub Switch Blue Barrier S', player).barrier(CrystalBarrier.Blue)
world.get_door('Mire Map Spike Side Blue Barrier', player).barrier(CrystalBarrier.Blue) world.get_door('Mire Map Spike Side Blue Barrier', player).barrier(CrystalBarrier.Blue)
world.get_door('Mire Map Spot Blue Barrier', player).barrier(CrystalBarrier.Blue) world.get_door('Mire Map Spot Blue Barrier', player).barrier(CrystalBarrier.Blue)
world.get_door('Mire Crystal Dead End Left Barrier', player).barrier(CrystalBarrier.Blue) world.get_door('Mire Crystal Dead End Left Barrier', player).barrier(CrystalBarrier.Blue)

View File

@@ -255,11 +255,11 @@ ice_regions = [
] ]
mire_regions = [ mire_regions = [
'Mire Lobby', 'Mire Post-Gap', 'Mire 2', 'Mire Hub', 'Mire Hub Right', 'Mire Hub Top', 'Mire Lone Shooter', 'Mire Lobby', 'Mire Post-Gap', 'Mire 2', 'Mire Hub', 'Mire Hub Right', 'Mire Hub Top', 'Mire Hub Switch',
'Mire Failure Bridge', 'Mire Falling Bridge', 'Mire Map Spike Side', 'Mire Map Spot', 'Mire Crystal Dead End', 'Mire Lone Shooter', 'Mire Failure Bridge', 'Mire Falling Bridge', 'Mire Map Spike Side', 'Mire Map Spot',
'Mire Hidden Shooters', 'Mire Hidden Shooters Blocked', 'Mire Cross', 'Mire Minibridge', 'Mire BK Door Room', 'Mire Crystal Dead End', 'Mire Hidden Shooters', 'Mire Hidden Shooters Blocked', 'Mire Cross', 'Mire Minibridge',
'Mire Spikes', 'Mire Ledgehop', 'Mire Bent Bridge', 'Mire Over Bridge', 'Mire Right Bridge', 'Mire Left Bridge', 'Mire BK Door Room', 'Mire Spikes', 'Mire Ledgehop', 'Mire Bent Bridge', 'Mire Over Bridge', 'Mire Right Bridge',
'Mire Fishbone', 'Mire South Fish', 'Mire Spike Barrier', 'Mire Square Rail', 'Mire Lone Warp', 'Mire Left Bridge', 'Mire Fishbone', 'Mire South Fish', 'Mire Spike Barrier', 'Mire Square Rail', 'Mire Lone Warp',
'Mire Wizzrobe Bypass', 'Mire Conveyor Crystal', 'Mire Tile Room', 'Mire Compass Room', 'Mire Compass Chest', 'Mire Wizzrobe Bypass', 'Mire Conveyor Crystal', 'Mire Tile Room', 'Mire Compass Room', 'Mire Compass Chest',
'Mire Neglected Room', 'Mire Chest View', 'Mire Conveyor Barrier', 'Mire BK Chest Ledge', 'Mire Warping Pool', 'Mire Neglected Room', 'Mire Chest View', 'Mire Conveyor Barrier', 'Mire BK Chest Ledge', 'Mire Warping Pool',
'Mire Torches Top', 'Mire Torches Bottom', 'Mire Attic Hint', 'Mire Dark Shooters', 'Mire Key Rupees', 'Mire Torches Top', 'Mire Torches Bottom', 'Mire Attic Hint', 'Mire Dark Shooters', 'Mire Key Rupees',

View File

@@ -234,7 +234,7 @@ def valid_key_placement(item, location, itempool, world):
return True return True
key_logic = world.key_logic[item.player][dungeon.name] key_logic = world.key_logic[item.player][dungeon.name]
unplaced_keys = len([x for x in itempool if x.name == key_logic.small_key_name and x.player == item.player]) unplaced_keys = len([x for x in itempool if x.name == key_logic.small_key_name and x.player == item.player])
return key_logic.check_placement(unplaced_keys) return key_logic.check_placement(unplaced_keys, location if item.bigkey else None)
else: else:
inside_dungeon_item = ((item.smallkey and not world.keyshuffle[item.player]) inside_dungeon_item = ((item.smallkey and not world.keyshuffle[item.player])
or (item.bigkey and not world.bigkeyshuffle[item.player])) or (item.bigkey and not world.bigkeyshuffle[item.player]))

View File

@@ -1,3 +1,4 @@
import itertools
import logging import logging
from collections import defaultdict, deque from collections import defaultdict, deque
@@ -22,6 +23,7 @@ class KeyLayout(object):
self.all_chest_locations = {} self.all_chest_locations = {}
self.big_key_special = False self.big_key_special = False
self.all_locations = set() self.all_locations = set()
self.item_locations = set()
# bk special? # bk special?
# bk required? True if big chests or big doors exists # bk required? True if big chests or big doors exists
@@ -31,6 +33,8 @@ class KeyLayout(object):
self.flat_prop = flatten_pair_list(self.proposal) self.flat_prop = flatten_pair_list(self.proposal)
self.key_logic = KeyLogic(self.sector.name) self.key_logic = KeyLogic(self.sector.name)
self.max_chests = calc_max_chests(builder, self, world, player) self.max_chests = calc_max_chests(builder, self, world, player)
self.all_locations = set()
self.item_locations = set()
class KeyLogic(object): class KeyLogic(object):
@@ -48,10 +52,14 @@ class KeyLogic(object):
self.placement_rules = [] self.placement_rules = []
self.outside_keys = 0 self.outside_keys = 0
def check_placement(self, unplaced_keys): def check_placement(self, unplaced_keys, big_key_loc=None):
for rule in self.placement_rules: for rule in self.placement_rules:
if not rule.is_satisfiable(self.outside_keys, unplaced_keys): if not rule.is_satisfiable(self.outside_keys, unplaced_keys):
return False return False
if big_key_loc:
for rule_a, rule_b in itertools.combinations(self.placement_rules, 2):
if rule_a.contradicts(rule_b, unplaced_keys, big_key_loc):
return False
return True return True
@@ -66,6 +74,7 @@ class DoorRules(object):
# for a place with only 1 free location/key_only_location behind it ... no goals and locations # for a place with only 1 free location/key_only_location behind it ... no goals and locations
self.allow_small = False self.allow_small = False
self.small_location = None self.small_location = None
self.opposite = None
class PlacementRule(object): class PlacementRule(object):
@@ -79,6 +88,35 @@ class PlacementRule(object):
self.check_locations_w_bk = None self.check_locations_w_bk = None
self.check_locations_wo_bk = None self.check_locations_wo_bk = None
def contradicts(self, rule, unplaced_keys, big_key_loc):
bk_blocked = big_key_loc in self.bk_conditional_set if self.bk_conditional_set else False
rule_blocked = big_key_loc in rule.bk_conditional_set if rule.bk_conditional_set else False
check_locations = self.check_locations_wo_bk if bk_blocked else self.check_locations_w_bk
rule_locations = rule.check_locations_wo_bk if rule_blocked else rule.check_locations_w_bk
if check_locations is None or rule_locations is None:
return False
check_locations = check_locations - {big_key_loc}
rule_locations = rule_locations - {big_key_loc}
threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk
rule_threshold = rule.needed_keys_wo_bk if rule_blocked else rule.needed_keys_w_bk
common_locations = rule_locations & check_locations
shared = len(common_locations)
if min(rule_threshold, threshold) - shared > 0:
left = unplaced_keys - shared
check_locations = check_locations - common_locations
check_needed = threshold - shared
if len(check_locations) < check_needed or left < check_needed:
return True
else:
left -= check_needed
rule_locations = rule_locations - common_locations
rule_needed = rule_threshold - shared
if len(rule_locations) < rule_needed or left < rule_needed:
return True
else:
left -= rule_needed
return False
def is_satisfiable(self, outside_keys, unplaced_keys): def is_satisfiable(self, outside_keys, unplaced_keys):
bk_blocked = False bk_blocked = False
if self.bk_conditional_set: if self.bk_conditional_set:
@@ -86,9 +124,11 @@ class PlacementRule(object):
if loc.item and loc.item.bigkey: if loc.item and loc.item.bigkey:
bk_blocked = True bk_blocked = True
break break
check_locations = self.check_locations_wo_bk if bk_blocked else self.check_locations_w_bk
if not bk_blocked and check_locations is None:
return True
available_keys = outside_keys available_keys = outside_keys
empty_chests = 0 empty_chests = 0
check_locations = self.check_locations_wo_bk if bk_blocked else self.check_locations_w_bk
threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk
for loc in check_locations: for loc in check_locations:
if not loc.item: if not loc.item:
@@ -134,9 +174,27 @@ def build_key_layout(builder, start_regions, proposal, world, player):
key_layout.max_drops = count_key_drops(key_layout.sector) key_layout.max_drops = count_key_drops(key_layout.sector)
key_layout.max_chests = calc_max_chests(builder, key_layout, world, player) key_layout.max_chests = calc_max_chests(builder, key_layout, world, player)
key_layout.big_key_special = 'Hyrule Dungeon Cellblock' in key_layout.sector.region_set() key_layout.big_key_special = 'Hyrule Dungeon Cellblock' in key_layout.sector.region_set()
key_layout.all_locations = find_all_locations(key_layout.sector)
return key_layout return key_layout
def count_key_drops(sector):
cnt = 0
for region in sector.regions:
for loc in region.locations:
if loc.event and 'Small Key' in loc.item.name:
cnt += 1
return cnt
def find_all_locations(sector):
all_locations = set()
for region in sector.regions:
for loc in region.locations:
all_locations.add(loc)
return all_locations
def calc_max_chests(builder, key_layout, world, player): def calc_max_chests(builder, key_layout, world, player):
if world.doorShuffle[player] != 'crossed': if world.doorShuffle[player] != 'crossed':
return len(world.get_dungeon(key_layout.sector.name, player).small_keys) return len(world.get_dungeon(key_layout.sector.name, player).small_keys)
@@ -165,17 +223,13 @@ def analyze_dungeon(key_layout, world, player):
raw_avail = chest_keys + len(key_counter.key_only_locations) raw_avail = chest_keys + len(key_counter.key_only_locations)
available = raw_avail - key_counter.used_keys available = raw_avail - key_counter.used_keys
possible_smalls = count_unique_small_doors(key_counter, key_layout.flat_prop) possible_smalls = count_unique_small_doors(key_counter, key_layout.flat_prop)
avail_bigs = exist_relevant_big_doors(key_counter, key_layout) avail_bigs = exist_relevant_big_doors(key_counter, key_layout) or exist_big_chest(key_counter)
non_big_locs = count_locations_big_optional(key_counter.free_locations) non_big_locs = count_locations_big_optional(key_counter.free_locations)
if not key_counter.big_key_opened: if not key_counter.big_key_opened:
if chest_keys == non_big_locs and chest_keys > 0 and available <= possible_smalls and not avail_bigs: if chest_keys == non_big_locs and chest_keys > 0 and available <= possible_smalls and not avail_bigs:
key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations)) key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations))
if not key_counter.big_key_opened and big_chest_in_locations(key_counter.free_locations):
key_logic.sm_restricted.update(find_big_chest_locations(key_counter.free_locations))
# todo: detect forced subsequent keys - see keypuzzles
# try to relax the rules here? - smallest requirement that doesn't force a softlock # try to relax the rules here? - smallest requirement that doesn't force a softlock
child_queue = deque() child_queue = deque()
smallest_rule = None
for child in key_counter.child_doors.keys(): for child in key_counter.child_doors.keys():
if not child.bigKey or not key_layout.big_key_special or key_counter.big_key_opened: if not child.bigKey or not key_layout.big_key_special or key_counter.big_key_opened:
odd_counter = create_odd_key_counter(child, key_counter, key_layout, world, player) odd_counter = create_odd_key_counter(child, key_counter, key_layout, world, player)
@@ -183,34 +237,20 @@ def analyze_dungeon(key_layout, world, player):
child_queue.append((child, odd_counter, empty_flag)) child_queue.append((child, odd_counter, empty_flag))
if child in doors_completed and child in key_logic.door_rules.keys(): if child in doors_completed and child in key_logic.door_rules.keys():
rule = key_logic.door_rules[child] rule = key_logic.door_rules[child]
if smallest_rule is None or rule.small_key_num < smallest_rule:
smallest_rule = rule.small_key_num
while len(child_queue) > 0: while len(child_queue) > 0:
child, odd_counter, empty_flag = child_queue.popleft() child, odd_counter, empty_flag = child_queue.popleft()
if not child.bigKey and child not in doors_completed: if not child.bigKey and child not in doors_completed:
best_counter = find_best_counter(child, odd_counter, key_counter, key_layout, world, player, False, empty_flag) best_counter = find_best_counter(child, odd_counter, key_counter, key_layout, world, player, False, empty_flag)
rule = create_rule(best_counter, key_counter, key_layout, world, player) rule = create_rule(best_counter, key_counter, key_layout, world, player)
# todo: seems to be caused by best_counter not opening the big key door when that's logically required. Re-evaluate usage of this
# if not rule.is_valid:
# logging.getLogger('').warning('Key logic for door %s requires too many chests. Seed may be beatable anyway.', child.name)
if smallest_rule is None or rule.small_key_num < smallest_rule:
smallest_rule = rule.small_key_num
check_for_self_lock_key(rule, child, best_counter, key_layout, world, player) check_for_self_lock_key(rule, child, best_counter, key_layout, world, player)
bk_restricted_rules(rule, child, odd_counter, empty_flag, key_counter, key_layout, world, player) bk_restricted_rules(rule, child, odd_counter, empty_flag, key_counter, key_layout, world, player)
key_logic.door_rules[child.name] = rule key_logic.door_rules[child.name] = rule
create_placement_rule(key_layout, child, odd_counter, key_counter, world, player)
doors_completed.add(child) doors_completed.add(child)
next_counter = find_next_counter(child, key_counter, key_layout) next_counter = find_next_counter(child, key_counter, key_layout)
ctr_id = cid(next_counter, key_layout) ctr_id = cid(next_counter, key_layout)
if ctr_id not in visited_cid: if ctr_id not in visited_cid:
queue.append((child, next_counter)) queue.append((child, next_counter))
visited_cid.add(ctr_id) visited_cid.add(ctr_id)
possible_smalls_collected = len(key_counter.key_only_locations) + non_big_locs
if not key_counter.big_key_opened:
if smallest_rule is not None and smallest_rule >= possible_smalls_collected and not avail_bigs:
key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations))
if not key_counter.big_key_opened and big_chest_in_locations(key_counter.free_locations):
key_logic.sm_restricted.update(find_big_chest_locations(key_counter.free_locations))
check_rules(original_key_counter, key_layout, world, player) check_rules(original_key_counter, key_layout, world, player)
# Flip bk rules if more restrictive, to prevent placing a big key in a softlocking location # Flip bk rules if more restrictive, to prevent placing a big key in a softlocking location
@@ -219,59 +259,52 @@ def analyze_dungeon(key_layout, world, player):
max_counter = find_max_counter(key_layout) max_counter = find_max_counter(key_layout)
rule.alternate_big_key_loc = set(max_counter.free_locations.keys()).difference(rule.alternate_big_key_loc) rule.alternate_big_key_loc = set(max_counter.free_locations.keys()).difference(rule.alternate_big_key_loc)
rule.small_key_num, rule.alternate_small_key = rule.alternate_small_key, rule.small_key_num rule.small_key_num, rule.alternate_small_key = rule.alternate_small_key, rule.small_key_num
create_exhaustive_placement_rules(key_layout, world, player)
set_paired_rules(key_logic, world, player)
def create_placement_rule(key_layout, door, odd_ctr, current_ctr, world, player): def create_exhaustive_placement_rules(key_layout, world, player):
key_logic = key_layout.key_logic key_logic = key_layout.key_logic
worst_ctr = find_worst_counter(door, odd_ctr, current_ctr, key_layout, False) max_ctr = find_max_counter(key_layout)
sm_num = worst_ctr.used_keys + 1 for code, key_counter in key_layout.key_counters.items():
accessible_loc = set() accessible_loc = set()
accessible_loc.update(worst_ctr.free_locations) accessible_loc.update(key_counter.free_locations)
accessible_loc.update(worst_ctr.key_only_locations) accessible_loc.update(key_counter.key_only_locations)
worst_ctr_wo_bk, post_ctr, alt_num = find_worst_counter_wo_bk(sm_num, accessible_loc, door, odd_ctr, current_ctr, key_layout) blocked_loc = key_layout.item_locations.difference(accessible_loc)
blocked_loc = key_layout.all_locations.difference(accessible_loc) valid_rule = True
# min_keys = max(count_unique_sm_doors(key_counter.child_doors), key_counter.used_keys + 1)
if len(blocked_loc) > 0: min_keys = key_counter.used_keys + 1
rule = PlacementRule() if len(blocked_loc) > 0 and len(key_counter.key_only_locations) < min_keys:
rule.door_reference = door rule = PlacementRule()
rule.small_key = key_logic.small_key_name rule.door_reference = code
rule.needed_keys_w_bk = sm_num rule.small_key = key_logic.small_key_name
placement_self_lock_adjustment(rule, key_layout, blocked_loc, worst_ctr, world, player) if key_counter.big_key_opened or not big_key_progress(key_counter):
rule.check_locations_w_bk = accessible_loc # rule.needed_keys_w_bk = key_counter.used_keys + 1
if worst_ctr_wo_bk: rule.needed_keys_w_bk = min_keys
accessible_wo_bk, post_set = set(), set() if key_counter.big_key_opened and rule.needed_keys_w_bk + 1 > len(accessible_loc):
accessible_wo_bk.update(worst_ctr_wo_bk.free_locations) valid_rule = False # indicates that the big key cannot be in the accessible locations
accessible_wo_bk.update(worst_ctr_wo_bk.key_only_locations) key_logic.bk_restricted.update(accessible_loc.difference(max_ctr.key_only_locations))
post_set.update(post_ctr.free_locations) else:
post_set.update(post_ctr.key_only_locations) placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player)
blocked_wo_bk = post_set.difference(accessible_wo_bk) rule.check_locations_w_bk = accessible_loc
if len(blocked_wo_bk) > 0: check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc)
rule.bk_conditional_set = blocked_wo_bk else:
rule.needed_keys_wo_bk = alt_num if big_key_progress(key_counter) and only_sm_doors(key_counter):
# can this self lock a key if bk not avail? I'm thinking no. create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, accessible_loc, min_keys, world, player)
# placement_self_lock_adjustment(rule, key_layout, ???, worst_ctr_wo_bk, world, player) # if key_counter.used_keys + 2 > len(accessible_loc) and big_key_no_progress(key_counter):
rule.check_locations_wo_bk = accessible_wo_bk # key_logic.bk_restricted.update(accessible_loc.difference(max_ctr.key_only_locations))
key_logic.placement_rules.append(rule) rule.bk_conditional_set = blocked_loc
if worst_ctr_wo_bk: # rule.needed_keys_wo_bk = key_counter.used_keys + 1
check_bk_restriction_needed(key_layout, worst_ctr_wo_bk, post_ctr, alt_num) rule.needed_keys_wo_bk = min_keys
rule.check_locations_wo_bk = set(filter_big_chest(accessible_loc))
if valid_rule:
key_logic.placement_rules.append(rule)
refine_placement_rules(key_layout, max_ctr)
def check_bk_restriction_needed(key_layout, worst_ctr_wo_bk, post_ctr, alt_num): def placement_self_lock_adjustment(rule, max_ctr, blocked_loc, ctr, world, player):
avail_keys = len(worst_ctr_wo_bk.key_only_locations)
place_able_keys = min(key_layout.max_chests, len(worst_ctr_wo_bk.free_locations))
if avail_keys + place_able_keys < alt_num:
accessible_wo_bk, post_set = set(), set()
accessible_wo_bk.update(worst_ctr_wo_bk.free_locations)
accessible_wo_bk.update(worst_ctr_wo_bk.key_only_locations)
post_set.update(post_ctr.free_locations)
post_set.update(post_ctr.key_only_locations)
key_layout.key_logic.bk_restricted.update(post_set.difference(accessible_wo_bk))
def placement_self_lock_adjustment(rule, key_layout, blocked_loc, worst_ctr, world, player):
if len(blocked_loc) == 1 and world.accessibility[player] != 'locations': if len(blocked_loc) == 1 and world.accessibility[player] != 'locations':
max_ctr = find_max_counter(key_layout) blocked_others = set(max_ctr.other_locations).difference(set(ctr.other_locations))
blocked_others = set(max_ctr.other_locations).difference(set(worst_ctr.other_locations))
important_found = False important_found = False
for loc in blocked_others: for loc in blocked_others:
if important_location(loc, world, player): if important_location(loc, world, player):
@@ -281,13 +314,115 @@ def placement_self_lock_adjustment(rule, key_layout, blocked_loc, worst_ctr, wor
rule.needed_keys_w_bk -= 1 rule.needed_keys_w_bk -= 1
def count_key_drops(sector): def check_sm_restriction_needed(key_layout, max_ctr, rule, blocked):
cnt = 0 if rule.needed_keys_w_bk == key_layout.max_chests + len(max_ctr.key_only_locations):
for region in sector.regions: key_layout.key_logic.sm_restricted.update(blocked.difference(max_ctr.key_only_locations))
for loc in region.locations: return True
if loc.event and 'Small Key' in loc.item.name: return False
cnt += 1
return cnt
def refine_placement_rules(key_layout, max_ctr):
key_logic = key_layout.key_logic
changed = True
while changed:
changed = False
rules_to_remove = []
for rule in key_logic.placement_rules:
if rule.check_locations_w_bk:
rule.check_locations_w_bk.difference_update(key_logic.sm_restricted)
key_onlys = rule.check_locations_w_bk.intersection(max_ctr.key_only_locations)
if len(key_onlys) > 0:
rule.check_locations_w_bk.difference_update(key_onlys)
rule.needed_keys_w_bk -= len(key_onlys)
if rule.needed_keys_w_bk == 0:
rules_to_remove.append(rule)
if len(rule.check_locations_w_bk) == rule.needed_keys_w_bk + 1:
new_restricted = set(max_ctr.free_locations) - rule.check_locations_w_bk
if len(new_restricted - key_logic.bk_restricted) > 0:
key_logic.bk_restricted.update(new_restricted) # bk must be in one of the check_locations
changed = True
if rule.needed_keys_w_bk > key_layout.max_chests or len(rule.check_locations_w_bk) < rule.needed_keys_w_bk:
logging.getLogger('').warning('Invalid rule - what went wrong here??')
rules_to_remove.append(rule)
changed = True
if rule.bk_conditional_set is not None:
rule.bk_conditional_set.difference_update(key_logic.bk_restricted)
rule.bk_conditional_set.difference_update(max_ctr.key_only_locations)
if len(rule.bk_conditional_set) == 0:
rules_to_remove.append(rule)
if rule.check_locations_wo_bk:
rule.check_locations_wo_bk.difference_update(key_logic.sm_restricted)
key_onlys = rule.check_locations_wo_bk.intersection(max_ctr.key_only_locations)
if len(key_onlys) > 0:
rule.check_locations_wo_bk.difference_update(key_onlys)
rule.needed_keys_wo_bk -= len(key_onlys)
if rule.needed_keys_wo_bk == 0:
rules_to_remove.append(rule)
if len(rule.check_locations_wo_bk) < rule.needed_keys_wo_bk or rule.needed_keys_wo_bk > key_layout.max_chests:
if len(rule.bk_conditional_set) > 0:
key_logic.bk_restricted.update(rule.bk_conditional_set)
rules_to_remove.append(rule)
changed = True # impossible for bk to be here, I think
for rule_a, rule_b in itertools.combinations([x for x in key_logic.placement_rules if x not in rules_to_remove], 2):
if rule_b.bk_conditional_set and rule_a.check_locations_w_bk:
temp = rule_a
rule_a = rule_b
rule_b = temp
if rule_a.bk_conditional_set and rule_b.check_locations_w_bk:
common_needed = min(rule_a.needed_keys_wo_bk, rule_b.needed_keys_w_bk)
if len(rule_b.check_locations_w_bk & rule_a.check_locations_wo_bk) < common_needed:
key_logic.bk_restricted.update(rule_a.bk_conditional_set)
rules_to_remove.append(rule_a)
changed = True
break
equivalent_rules = []
for rule in key_logic.placement_rules:
for rule2 in key_logic.placement_rules:
if rule != rule2:
if rule.check_locations_w_bk and rule2.check_locations_w_bk:
if rule2.check_locations_w_bk == rule.check_locations_w_bk and rule2.needed_keys_w_bk > rule.needed_keys_w_bk:
rules_to_remove.append(rule)
elif rule2.needed_keys_w_bk == rule.needed_keys_w_bk and rule2.check_locations_w_bk < rule.check_locations_w_bk:
rules_to_remove.append(rule)
elif rule2.check_locations_w_bk == rule.check_locations_w_bk and rule2.needed_keys_w_bk == rule.needed_keys_w_bk:
equivalent_rules.append((rule, rule2))
if rule.check_locations_wo_bk and rule2.check_locations_wo_bk and rule.bk_conditional_set == rule2.bk_conditional_set:
if rule2.check_locations_wo_bk == rule.check_locations_wo_bk and rule2.needed_keys_wo_bk > rule.needed_keys_wo_bk:
rules_to_remove.append(rule)
elif rule2.needed_keys_wo_bk == rule.needed_keys_wo_bk and rule2.check_locations_wo_bk < rule.check_locations_wo_bk:
rules_to_remove.append(rule)
elif rule2.check_locations_wo_bk == rule.check_locations_wo_bk and rule2.needed_keys_wo_bk == rule.needed_keys_wo_bk:
equivalent_rules.append((rule, rule2))
if len(rules_to_remove) > 0:
key_logic.placement_rules = [x for x in key_logic.placement_rules if x not in rules_to_remove]
equivalent_rules = [x for x in equivalent_rules if x[0] not in rules_to_remove and x[1] not in rules_to_remove]
if len(equivalent_rules) > 0:
removed_rules = {}
for r1, r2 in equivalent_rules:
if r1 in removed_rules.keys():
r1 = removed_rules[r1]
if r2 in removed_rules.keys():
r2 = removed_rules[r2]
if r1 != r2:
r1.door_reference += ','+r2.door_reference
key_logic.placement_rules.remove(r2)
removed_rules[r2] = r1
def create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, accessible_loc, min_keys, world, player):
key_logic = key_layout.key_logic
rule = PlacementRule()
rule.door_reference = code
rule.small_key = key_logic.small_key_name
rule.needed_keys_w_bk = min_keys
if key_counter.big_key_opened and rule.needed_keys_w_bk + 1 > len(accessible_loc):
# indicates that the big key cannot be in the accessible locations
key_logic.bk_restricted.update(accessible_loc.difference(max_ctr.key_only_locations))
else:
placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player)
rule.check_locations_w_bk = accessible_loc
check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc)
key_logic.placement_rules.append(rule)
def queue_sorter(queue_item): def queue_sorter(queue_item):
@@ -314,8 +449,8 @@ def find_bk_locked_sections(key_layout, world, player):
big_chest_allowed_big_key = world.accessibility[player] != 'locations' big_chest_allowed_big_key = world.accessibility[player] != 'locations'
for counter in key_counters.values(): for counter in key_counters.values():
key_layout.all_chest_locations.update(counter.free_locations) key_layout.all_chest_locations.update(counter.free_locations)
key_layout.all_locations.update(counter.free_locations) key_layout.item_locations.update(counter.free_locations)
key_layout.all_locations.update(counter.key_only_locations) key_layout.item_locations.update(counter.key_only_locations)
if counter.big_key_opened and counter.important_location: if counter.big_key_opened and counter.important_location:
big_chest_allowed_big_key = False big_chest_allowed_big_key = False
if not counter.big_key_opened: if not counter.big_key_opened:
@@ -710,6 +845,16 @@ def count_unique_sm_doors(doors):
return len(unique_d_set) return len(unique_d_set)
def big_key_progress(key_counter):
return not only_sm_doors(key_counter) or exist_big_chest(key_counter)
def only_sm_doors(key_counter):
for door in key_counter.child_doors:
if door.bigKey:
return False
return True
# doesn't count dest doors # doesn't count dest doors
def count_unique_small_doors(key_counter, proposal): def count_unique_small_doors(key_counter, proposal):
cnt = 0 cnt = 0
@@ -738,6 +883,13 @@ def exist_relevant_big_doors(key_counter, key_layout):
return False return False
def exist_big_chest(key_counter):
for loc in key_counter.free_locations:
if '- Big Chest' in loc.name:
return True
return False
def count_locations_big_optional(locations, bk=False): def count_locations_big_optional(locations, bk=False):
cnt = 0 cnt = 0
for loc in locations: for loc in locations:
@@ -989,6 +1141,13 @@ def reduce_rules(small_rules, collected, collected_alt):
rule.small_key_num = collected rule.small_key_num = collected
def set_paired_rules(key_logic, world, player):
for d_name, rule in key_logic.door_rules.items():
door = world.get_door(d_name, player)
if door.dest.name in key_logic.door_rules.keys():
rule.opposite = key_logic.door_rules[door.dest.name]
# Soft lock stuff # Soft lock stuff
def validate_key_layout(key_layout, world, player): def validate_key_layout(key_layout, world, player):
# retro is all good - except for hyrule castle in standard mode # retro is all good - except for hyrule castle in standard mode
@@ -1015,7 +1174,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa
ttl_key_only = count_key_only_locations(state) ttl_key_only = count_key_only_locations(state)
available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_key_only, state, world, player) available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_key_only, state, world, player)
available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player) available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player)
if invalid_self_locking_key(state, prev_state, prev_avail, world, player): if invalid_self_locking_key(key_layout, state, prev_state, prev_avail, world, player):
return False return False
# todo: allow more key shuffles - refine placement rules # todo: allow more key shuffles - refine placement rules
# if (not smalls_avail or available_small_locations == 0) and (state.big_key_opened or num_bigs == 0 or available_big_locations == 0): # if (not smalls_avail or available_small_locations == 0) and (state.big_key_opened or num_bigs == 0 or available_big_locations == 0):
@@ -1054,18 +1213,24 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa
return True return True
def invalid_self_locking_key(state, prev_state, prev_avail, world, player): def invalid_self_locking_key(key_layout, state, prev_state, prev_avail, world, player):
if prev_state is None or state.used_smalls == prev_state.used_smalls: if prev_state is None or state.used_smalls == prev_state.used_smalls:
return False return False
new_locations = set(state.found_locations).difference(set(prev_state.found_locations)) new_bk_doors = set(state.big_doors).difference(set(prev_state.big_doors))
state_copy = state.copy()
while len(new_bk_doors) > 0:
for door in new_bk_doors:
open_a_door(door.door, state_copy, key_layout.flat_prop)
new_bk_doors = set(state_copy.big_doors).difference(set(prev_state.big_doors))
expand_key_state(state_copy, key_layout.flat_prop, world, player)
new_locations = set(state_copy.found_locations).difference(set(prev_state.found_locations))
important_found = False important_found = False
for loc in new_locations: for loc in new_locations:
important_found |= important_location(loc, world, player) important_found |= important_location(loc, world, player)
if not important_found: if not important_found:
return False return False
new_small_doors = set(state.small_doors).difference(set(prev_state.small_doors)) new_small_doors = set(state.small_doors).difference(set(prev_state.small_doors))
new_bk_doors = set(state.big_doors).difference(set(prev_state.big_doors)) if len(new_small_doors) > 0:
if len(new_small_doors) > 0 or len(new_bk_doors) > 0:
return False return False
return prev_avail - 1 == 0 return prev_avail - 1 == 0
@@ -1151,11 +1316,21 @@ def create_key_counter(state, key_layout, world, player):
return key_counter return key_counter
def important_location(loc, world, player): imp_locations = None
important_locations = ['Agahnim 1', 'Agahnim 2', 'Attic Cracked Floor', 'Suspicious Maiden']
def imp_locations_factory(world, player):
global imp_locations
if imp_locations:
return imp_locations
imp_locations = ['Agahnim 1', 'Agahnim 2', 'Attic Cracked Floor', 'Suspicious Maiden']
if world.mode[player] == 'standard' or world.doorShuffle[player] == 'crossed': if world.mode[player] == 'standard' or world.doorShuffle[player] == 'crossed':
important_locations.append('Hyrule Dungeon Cellblock') imp_locations.append('Hyrule Dungeon Cellblock')
return '- Prize' in loc.name or loc.name in important_locations return imp_locations
def important_location(loc, world, player):
return '- Prize' in loc.name or loc.name in imp_locations_factory(world, player)
def create_odd_key_counter(door, parent_counter, key_layout, world, player): def create_odd_key_counter(door, parent_counter, key_layout, world, player):
@@ -1428,7 +1603,7 @@ def validate_key_placement(key_layout, world, player):
max_counter = find_max_counter(key_layout) max_counter = find_max_counter(key_layout)
big_key_outside = dungeon.big_key not in (l.item for l in max_counter.free_locations) big_key_outside = dungeon.big_key not in (l.item for l in max_counter.free_locations)
for counter in key_layout.key_counters.values(): for code, counter in key_layout.key_counters.items():
if len(counter.child_doors) == 0: if len(counter.child_doors) == 0:
continue continue
big_found = any(i.item == dungeon.big_key for i in counter.free_locations if "- Big Chest" not in i.name) or big_key_outside big_found = any(i.item == dungeon.big_key for i in counter.free_locations if "- Big Chest" not in i.name) or big_key_outside
@@ -1445,6 +1620,7 @@ def validate_key_placement(key_layout, world, player):
# missing_key_only = set(max_counter.key_only_locations.keys()).difference(counter.key_only_locations.keys()) # do freestanding keys matter for locations? # missing_key_only = set(max_counter.key_only_locations.keys()).difference(counter.key_only_locations.keys()) # do freestanding keys matter for locations?
if len(missing_items) > 0: # world.accessibility[player]=='locations' and (len(missing_locations)>0 or len(missing_key_only) > 0): if len(missing_items) > 0: # world.accessibility[player]=='locations' and (len(missing_locations)>0 or len(missing_key_only) > 0):
logging.getLogger('').error("Keylock - can't open locations: ") logging.getLogger('').error("Keylock - can't open locations: ")
logging.getLogger('').error("code: " + code)
for i in missing_locations: for i in missing_locations:
logging.getLogger('').error(i) logging.getLogger('').error(i)
return False return False

View File

@@ -540,7 +540,8 @@ def create_dungeon_regions(world, player):
create_dungeon_region(player, 'Mire 2', 'Misery Mire', None, ['Mire 2 Up Stairs', 'Mire 2 NE']), create_dungeon_region(player, 'Mire 2', 'Misery Mire', None, ['Mire 2 Up Stairs', 'Mire 2 NE']),
create_dungeon_region(player, 'Mire Hub', 'Misery Mire', None, ['Mire Hub SE', 'Mire Hub ES', 'Mire Hub E', 'Mire Hub NE', 'Mire Hub WN', 'Mire Hub WS', 'Mire Hub Upper Blue Barrier', 'Mire Hub Lower Blue Barrier']), create_dungeon_region(player, 'Mire Hub', 'Misery Mire', None, ['Mire Hub SE', 'Mire Hub ES', 'Mire Hub E', 'Mire Hub NE', 'Mire Hub WN', 'Mire Hub WS', 'Mire Hub Upper Blue Barrier', 'Mire Hub Lower Blue Barrier']),
create_dungeon_region(player, 'Mire Hub Right', 'Misery Mire', None, ['Mire Hub Right EN', 'Mire Hub Right Blue Barrier']), create_dungeon_region(player, 'Mire Hub Right', 'Misery Mire', None, ['Mire Hub Right EN', 'Mire Hub Right Blue Barrier']),
create_dungeon_region(player, 'Mire Hub Top', 'Misery Mire', ['Misery Mire - Main Lobby'], ['Mire Hub Top NW', 'Mire Hub Top Blue Barrier']), create_dungeon_region(player, 'Mire Hub Top', 'Misery Mire', None, ['Mire Hub Top NW', 'Mire Hub Top Blue Barrier']),
create_dungeon_region(player, 'Mire Hub Switch', 'Misery Mire', ['Misery Mire - Main Lobby'], ['Mire Hub Switch Blue Barrier N', 'Mire Hub Switch Blue Barrier S']),
create_dungeon_region(player, 'Mire Lone Shooter', 'Misery Mire', None, ['Mire Lone Shooter WS', 'Mire Lone Shooter ES']), create_dungeon_region(player, 'Mire Lone Shooter', 'Misery Mire', None, ['Mire Lone Shooter WS', 'Mire Lone Shooter ES']),
create_dungeon_region(player, 'Mire Failure Bridge', 'Misery Mire', None, ['Mire Failure Bridge W', 'Mire Failure Bridge E']), create_dungeon_region(player, 'Mire Failure Bridge', 'Misery Mire', None, ['Mire Failure Bridge W', 'Mire Failure Bridge E']),
create_dungeon_region(player, 'Mire Falling Bridge', 'Misery Mire', ['Misery Mire - Big Chest'], ['Mire Falling Bridge WS', 'Mire Falling Bridge W', 'Mire Falling Bridge WN']), create_dungeon_region(player, 'Mire Falling Bridge', 'Misery Mire', ['Misery Mire - Big Chest'], ['Mire Falling Bridge WS', 'Mire Falling Bridge W', 'Mire Falling Bridge WN']),

View File

@@ -342,6 +342,8 @@ def global_rules(world, player):
set_rule(world.get_entrance('Mire Hub Lower Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Hub', player), player)) set_rule(world.get_entrance('Mire Hub Lower Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Hub', player), player))
set_rule(world.get_entrance('Mire Hub Right Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Hub Right', player), player)) set_rule(world.get_entrance('Mire Hub Right Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Hub Right', player), player))
set_rule(world.get_entrance('Mire Hub Top Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Hub Top', player), player)) set_rule(world.get_entrance('Mire Hub Top Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Hub Top', player), player))
set_rule(world.get_entrance('Mire Hub Switch Blue Barrier N', player), lambda state: state.can_reach_blue(world.get_region('Mire Hub Switch', player), player))
set_rule(world.get_entrance('Mire Hub Switch Blue Barrier S', player), lambda state: state.can_reach_blue(world.get_region('Mire Hub Switch', player), player))
set_rule(world.get_entrance('Mire Map Spike Side Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Map Spike Side', player), player)) set_rule(world.get_entrance('Mire Map Spike Side Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Map Spike Side', player), player))
set_rule(world.get_entrance('Mire Map Spot Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Map Spot', player), player)) set_rule(world.get_entrance('Mire Map Spot Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Map Spot', player), player))
set_rule(world.get_entrance('Mire Crystal Dead End Left Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Crystal Dead End', player), player)) set_rule(world.get_entrance('Mire Crystal Dead End Left Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Crystal Dead End', player), player))
@@ -1516,7 +1518,8 @@ bunny_impassible_doors = {
'Ice Crystal Right Blue Hole', 'Ice Crystal Left Blue Barrier', 'Ice Big Chest Landing Push Blocks', 'Ice Crystal Right Blue Hole', 'Ice Crystal Left Blue Barrier', 'Ice Big Chest Landing Push Blocks',
'Ice Backwards Room Hole', 'Ice Switch Room SE', 'Ice Antechamber NE', 'Ice Antechamber Hole', 'Mire Lobby Gap', 'Ice Backwards Room Hole', 'Ice Switch Room SE', 'Ice Antechamber NE', 'Ice Antechamber Hole', 'Mire Lobby Gap',
'Mire Post-Gap Gap', 'Mire 2 NE', 'Mire Hub Upper Blue Barrier', 'Mire Hub Lower Blue Barrier', 'Mire Post-Gap Gap', 'Mire 2 NE', 'Mire Hub Upper Blue Barrier', 'Mire Hub Lower Blue Barrier',
'Mire Hub Right Blue Barrier', 'Mire Hub Top Blue Barrier', 'Mire Falling Bridge WN', 'Mire Hub Right Blue Barrier', 'Mire Hub Top Blue Barrier', 'Mire Hub Switch Blue Barrier N',
'Mire Hub Switch Blue Barrier S', 'Mire Falling Bridge WN',
'Mire Map Spike Side Blue Barrier', 'Mire Map Spot Blue Barrier', 'Mire Crystal Dead End Left Barrier', 'Mire Map Spike Side Blue Barrier', 'Mire Map Spot Blue Barrier', 'Mire Crystal Dead End Left Barrier',
'Mire Crystal Dead End Right Barrier', 'Mire Cross ES', 'Mire Hidden Shooters Block Path S', 'Mire Crystal Dead End Right Barrier', 'Mire Cross ES', 'Mire Hidden Shooters Block Path S',
'Mire Hidden Shooters Block Path N', 'Mire Left Bridge Hook Path', 'Mire Fishbone Blue Barrier', 'Mire Hidden Shooters Block Path N', 'Mire Left Bridge Hook Path', 'Mire Fishbone Blue Barrier',
@@ -1546,7 +1549,10 @@ def add_key_logic_rules(world, player):
key_logic = world.key_logic[player] key_logic = world.key_logic[player]
for d_name, d_logic in key_logic.items(): for d_name, d_logic in key_logic.items():
for door_name, keys in d_logic.door_rules.items(): for door_name, keys in d_logic.door_rules.items():
add_rule(world.get_entrance(door_name, player), create_advanced_key_rule(d_logic, player, keys)) spot = world.get_entrance(door_name, player)
add_rule(spot, create_advanced_key_rule(d_logic, player, keys))
if keys.opposite:
add_rule(spot, create_advanced_key_rule(d_logic, player, keys.opposite), 'or')
for location in d_logic.bk_restricted: for location in d_logic.bk_restricted:
if location.name not in key_only_locations.keys(): if location.name not in key_only_locations.keys():
forbid_item(location, d_logic.bk_name, player) forbid_item(location, d_logic.bk_name, player)