Merge branch 'DoorDev' into EdgeWork

This commit is contained in:
aerinon
2020-02-21 16:26:50 -07:00
13 changed files with 336 additions and 25 deletions

View File

@@ -2,7 +2,7 @@ import copy
from enum import Enum, unique, Flag from enum import Enum, unique, Flag
import logging import logging
import json import json
from collections import OrderedDict, deque from collections import OrderedDict, deque, defaultdict
from EntranceShuffle import door_addresses from EntranceShuffle import door_addresses
from _vendor.collections_extended import bag from _vendor.collections_extended import bag
@@ -70,6 +70,7 @@ class World(object):
self.inaccessible_regions = {} self.inaccessible_regions = {}
self.key_logic = {} self.key_logic = {}
self.pool_adjustment = {} self.pool_adjustment = {}
self.key_layout = defaultdict(dict)
for player in range(1, players + 1): for player in range(1, players + 1):
def set_player_attr(attr, val): def set_player_attr(attr, val):

View File

@@ -163,6 +163,7 @@ def vanilla_key_logic(world, player):
world.key_logic[player] = {} world.key_logic[player] = {}
analyze_dungeon(key_layout, world, player) analyze_dungeon(key_layout, world, player)
world.key_logic[player][builder.name] = key_layout.key_logic world.key_logic[player][builder.name] = key_layout.key_logic
log_key_logic(builder.name, key_layout.key_logic)
last_key = None last_key = None
if world.shuffle[player] == 'vanilla': if world.shuffle[player] == 'vanilla':
validate_vanilla_key_logic(world, player) validate_vanilla_key_logic(world, player)
@@ -784,6 +785,8 @@ def assign_cross_keys(dungeon_builders, world, player):
# Last Step: Adjust Small Key Dungeon Pool # Last Step: Adjust Small Key Dungeon Pool
if not world.retro[player]: if not world.retro[player]:
for name, builder in dungeon_builders.items(): for name, builder in dungeon_builders.items():
reassign_key_doors(builder, world, player)
log_key_logic(builder.name, world.key_logic[player][builder.name])
actual_chest_keys = max(builder.key_doors_num - builder.key_drop_cnt, 0) actual_chest_keys = max(builder.key_doors_num - builder.key_drop_cnt, 0)
dungeon = world.get_dungeon(name, player) dungeon = world.get_dungeon(name, player)
if actual_chest_keys == 0: if actual_chest_keys == 0:
@@ -864,8 +867,11 @@ def combine_layouts(recombinant_builders, dungeon_builders, entrances_map):
if recombine.master_sector is None: if recombine.master_sector is None:
recombine.master_sector = builder.master_sector recombine.master_sector = builder.master_sector
recombine.master_sector.name = recombine.name recombine.master_sector.name = recombine.name
recombine.pre_open_stonewall = builder.pre_open_stonewall
else: else:
recombine.master_sector.regions.extend(builder.master_sector.regions) recombine.master_sector.regions.extend(builder.master_sector.regions)
if builder.pre_open_stonewall:
recombine.pre_open_stonewall = builder.pre_open_stonewall
recombine.layout_starts = list(entrances_map[recombine.name]) recombine.layout_starts = list(entrances_map[recombine.name])
dungeon_builders[recombine.name] = recombine dungeon_builders[recombine.name] = recombine
@@ -898,9 +904,11 @@ def shuffle_key_doors(builder, world, player):
builder.key_doors_num = num_key_doors builder.key_doors_num = num_key_doors
find_small_key_door_candidates(builder, start_regions, world, player) find_small_key_door_candidates(builder, start_regions, world, player)
find_valid_combination(builder, start_regions, world, player) find_valid_combination(builder, start_regions, world, player)
reassign_key_doors(builder, world, player)
log_key_logic(builder.name, world.key_logic[player][builder.name])
def find_current_key_doors(builder, world, player): def find_current_key_doors(builder):
current_doors = [] current_doors = []
for region in builder.master_sector.regions: for region in builder.master_sector.regions:
for ext in region.exits: for ext in region.exits:
@@ -987,9 +995,9 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True
if player not in world.key_logic.keys(): if player not in world.key_logic.keys():
world.key_logic[player] = {} world.key_logic[player] = {}
analyze_dungeon(key_layout, world, player) analyze_dungeon(key_layout, world, player)
reassign_key_doors(builder, proposal, world, player) builder.key_door_proposal = proposal
log_key_logic(builder.name, key_layout.key_logic)
world.key_logic[player][builder.name] = key_layout.key_logic world.key_logic[player][builder.name] = key_layout.key_logic
world.key_layout[player][builder.name] = key_layout
return True return True
@@ -1011,6 +1019,13 @@ def log_key_logic(d_name, key_logic):
if rule.alternate_small_key is not None: if rule.alternate_small_key is not None:
for loc in rule.alternate_big_key_loc: for loc in rule.alternate_big_key_loc:
logger.debug('---BK Loc %s', loc.name) logger.debug('---BK Loc %s', loc.name)
logger.debug('Placement rules for %s', d_name)
for rule in key_logic.placement_rules:
logger.debug('*Rule for %s:', rule.door_reference)
if 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 Elsewhere (%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):
@@ -1045,7 +1060,9 @@ def find_key_door_candidates(region, checked, world, player):
while len(queue) > 0: while len(queue) > 0:
current, last_door, last_region = queue.pop() current, last_door, last_region = queue.pop()
for ext in current.exits: for ext in current.exits:
d = world.check_for_door(ext.name, player) d = ext.door
if d and d.controller:
d = d.controller
if d is not None and not d.blocked and d.dest is not last_door and d.dest is not last_region and d not in checked_doors: if d is not None and not d.blocked and d.dest is not last_door and d.dest is not last_region and d not in checked_doors:
valid = False valid = False
if 0 <= d.doorListPos < 4 and d.type in [DoorType.Interior, DoorType.Normal, DoorType.SpiralStairs]: if 0 <= d.doorListPos < 4 and d.type in [DoorType.Interior, DoorType.Normal, DoorType.SpiralStairs]:
@@ -1064,9 +1081,11 @@ def find_key_door_candidates(region, checked, world, player):
okay_normals = [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, okay_normals = [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable,
DoorKind.Dashable, DoorKind.DungeonChanger] DoorKind.Dashable, DoorKind.DungeonChanger]
valid = kind in okay_normals and kind_b in okay_normals valid = kind in okay_normals and kind_b in okay_normals
if valid and 0 <= d2.doorListPos < 4:
candidates.append(d2)
else: else:
valid = True valid = True
if valid: if valid and d not in candidates:
candidates.append(d) candidates.append(d)
if ext.connected_region.type != RegionType.Dungeon or ext.connected_region.dungeon == dungeon: if ext.connected_region.type != RegionType.Dungeon or ext.connected_region.dungeon == dungeon:
queue.append((ext.connected_region, d, current)) queue.append((ext.connected_region, d, current))
@@ -1097,10 +1116,12 @@ def ncr(n, r):
return numerator / denominator return numerator / denominator
def reassign_key_doors(builder, proposal, world, player): def reassign_key_doors(builder, world, player):
logger = logging.getLogger('') logger = logging.getLogger('')
logger.debug('Key doors for %s', builder.name)
proposal = builder.key_door_proposal
flat_proposal = flatten_pair_list(proposal) flat_proposal = flatten_pair_list(proposal)
queue = deque(find_current_key_doors(builder, world, player)) queue = deque(find_current_key_doors(builder))
while len(queue) > 0: while len(queue) > 0:
d = queue.pop() d = queue.pop()
if d.type is DoorType.SpiralStairs and d not in proposal: if d.type is DoorType.SpiralStairs and d not in proposal:

View File

@@ -451,9 +451,12 @@ def stonewall_valid(stonewall):
if bad_door.blocked: if bad_door.blocked:
return True # great we're done with this one return True # great we're done with this one
loop_region = stonewall.entrance.parent_region loop_region = stonewall.entrance.parent_region
start_region = bad_door.entrance.parent_region start_regions = [bad_door.entrance.parent_region]
queue = deque([start_region]) if bad_door.dependents:
visited = {start_region} for dep in bad_door.dependents:
start_regions.append(dep.entrance.parent_region)
queue = deque(start_regions)
visited = set(start_regions)
while len(queue) > 0: while len(queue) > 0:
region = queue.popleft() region = queue.popleft()
if region == loop_region: if region == loop_region:
@@ -1053,6 +1056,7 @@ class DungeonBuilder(object):
self.key_doors_num = None self.key_doors_num = None
self.combo_size = None self.combo_size = None
self.flex = 0 self.flex = 0
self.key_door_proposal = None
if name in dungeon_dead_end_allowance.keys(): if name in dungeon_dead_end_allowance.keys():
self.allowance = dungeon_dead_end_allowance[name] self.allowance = dungeon_dead_end_allowance[name]

View File

@@ -184,6 +184,7 @@ def parse_arguments(argv, no_defaults=False):
base game. base game.
''') ''')
parser.add_argument('--experimental', default=defval(False), help='Enable experimental features', action='store_true') parser.add_argument('--experimental', default=defval(False), help='Enable experimental features', action='store_true')
parser.add_argument('--dungeon_counters', default=defval(False), help='Enable dungeon chest counters', action='store_true')
parser.add_argument('--crystals_ganon', default=defval('7'), const='7', nargs='?', choices=['random', '0', '1', '2', '3', '4', '5', '6', '7'], parser.add_argument('--crystals_ganon', default=defval('7'), const='7', nargs='?', choices=['random', '0', '1', '2', '3', '4', '5', '6', '7'],
help='''\ help='''\
How many crystals are needed to defeat ganon. Any other How many crystals are needed to defeat ganon. Any other
@@ -302,7 +303,7 @@ def parse_arguments(argv, no_defaults=False):
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality',
'shuffle', 'door_shuffle', 'crystals_ganon', 'crystals_gt', 'openpyramid', 'shuffle', 'door_shuffle', 'crystals_ganon', 'crystals_gt', 'openpyramid',
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters',
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots', 'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots',
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep', 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep',
'remote_items']: 'remote_items']:

34
Fill.py
View File

@@ -201,7 +201,8 @@ def fill_restrictive(world, base_state, locations, itempool, single_player_place
else: else:
test_state = maximum_exploration_state test_state = maximum_exploration_state
if (not single_player_placement or location.player == item_to_place.player)\ if (not single_player_placement or location.player == item_to_place.player)\
and location.can_fill(test_state, item_to_place, perform_access_check): and location.can_fill(test_state, item_to_place, perform_access_check)\
and valid_key_placement(item_to_place, location, itempool, world):
spot_to_fill = location spot_to_fill = location
break break
elif item_to_place.smallkey or item_to_place.bigkey: elif item_to_place.smallkey or item_to_place.bigkey:
@@ -217,11 +218,42 @@ def fill_restrictive(world, base_state, locations, itempool, single_player_place
raise FillError('No more spots to place %s' % item_to_place) raise FillError('No more spots to place %s' % item_to_place)
world.push_item(spot_to_fill, item_to_place, False) world.push_item(spot_to_fill, item_to_place, False)
track_outside_keys(item_to_place, spot_to_fill, world)
locations.remove(spot_to_fill) locations.remove(spot_to_fill)
spot_to_fill.event = True spot_to_fill.event = True
itempool.extend(unplaced_items) itempool.extend(unplaced_items)
def valid_key_placement(item, location, itempool, world):
if (not item.smallkey and not item.bigkey) or item.player != location.player or world.retro[item.player]:
return True
dungeon = location.parent_region.dungeon
if dungeon:
if dungeon.name not in item.name and (dungeon.name != 'Hyrule Castle' or 'Escape' not in item.name):
return True
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])
return key_logic.check_placement(unplaced_keys)
else:
inside_dungeon_item = ((item.smallkey and not world.keyshuffle[item.player])
or (item.bigkey and not world.bigkeyshuffle[item.player]))
return not inside_dungeon_item
def track_outside_keys(item, location, world):
if not item.smallkey:
return
item_dungeon = item.name.split('(')[1][:-1]
if item_dungeon == 'Escape':
item_dungeon = 'Hyrule Castle'
if location.player == item.player:
loc_dungeon = location.parent_region.dungeon
if loc_dungeon and loc_dungeon.name == item_dungeon:
return # this is an inside key
world.key_logic[item.player][item_dungeon].outside_keys += 1
def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None): def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None):
# If not passed in, then get a shuffled list of locations to fill in # If not passed in, then get a shuffled list of locations to fill in
if not fill_locations: if not fill_locations:

4
Gui.py
View File

@@ -87,6 +87,8 @@ def guiMain(args=None):
experimentCheckbutton = Checkbutton(checkBoxFrame, text="Enable Experimental Features", variable=experimentVar) experimentCheckbutton = Checkbutton(checkBoxFrame, text="Enable Experimental Features", variable=experimentVar)
customVar = IntVar() customVar = IntVar()
customCheckbutton = Checkbutton(checkBoxFrame, text="Use custom item pool", variable=customVar) customCheckbutton = Checkbutton(checkBoxFrame, text="Use custom item pool", variable=customVar)
dungeonCounterVar = IntVar()
dungeonCounterbutton = Checkbutton(checkBoxFrame, text="Enable dungeon chest counters", variable=dungeonCounterVar)
createSpoilerCheckbutton.pack(expand=True, anchor=W) createSpoilerCheckbutton.pack(expand=True, anchor=W)
suppressRomCheckbutton.pack(expand=True, anchor=W) suppressRomCheckbutton.pack(expand=True, anchor=W)
@@ -102,6 +104,7 @@ def guiMain(args=None):
hintsCheckbutton.pack(expand=True, anchor=W) hintsCheckbutton.pack(expand=True, anchor=W)
experimentCheckbutton.pack(expand=True, anchor=W) experimentCheckbutton.pack(expand=True, anchor=W)
customCheckbutton.pack(expand=True, anchor=W) customCheckbutton.pack(expand=True, anchor=W)
dungeonCounterbutton.pack(expand=True, anchor=W)
romOptionsFrame = LabelFrame(rightHalfFrame, text="Rom options") romOptionsFrame = LabelFrame(rightHalfFrame, text="Rom options")
romOptionsFrame.columnconfigure(0, weight=1) romOptionsFrame.columnconfigure(0, weight=1)
@@ -472,6 +475,7 @@ def guiMain(args=None):
int(arrow1Var.get()), int(arrow10Var.get()), int(bomb1Var.get()), int(bomb3Var.get()), int(rupee1Var.get()), int(rupee5Var.get()), int(rupee20Var.get()), int(rupee50Var.get()), int(rupee100Var.get()), int(arrow1Var.get()), int(arrow10Var.get()), int(bomb1Var.get()), int(bomb3Var.get()), int(rupee1Var.get()), int(rupee5Var.get()), int(rupee20Var.get()), int(rupee50Var.get()), int(rupee100Var.get()),
int(rupee300Var.get()), int(rupoorVar.get()), int(blueclockVar.get()), int(greenclockVar.get()), int(redclockVar.get()), int(progbowVar.get()), int(bomb10Var.get()), int(triforcepieceVar.get()), int(rupee300Var.get()), int(rupoorVar.get()), int(blueclockVar.get()), int(greenclockVar.get()), int(redclockVar.get()), int(progbowVar.get()), int(bomb10Var.get()), int(triforcepieceVar.get()),
int(triforcecountVar.get()), int(triforceVar.get()), int(rupoorcostVar.get()), int(universalkeyVar.get())] int(triforcecountVar.get()), int(triforceVar.get()), int(rupoorcostVar.get()), int(universalkeyVar.get())]
guiargs.dungeon_counters = bool(dungeonCounterVar.get())
guiargs.rom = romVar.get() guiargs.rom = romVar.get()
guiargs.sprite = sprite guiargs.sprite = sprite
guiargs.outputpath = args.outputpath if args else None guiargs.outputpath = args.outputpath if args else None

View File

@@ -21,6 +21,7 @@ class KeyLayout(object):
self.max_drops = None self.max_drops = None
self.all_chest_locations = {} self.all_chest_locations = {}
self.big_key_special = False self.big_key_special = False
self.all_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
@@ -44,6 +45,14 @@ class KeyLogic(object):
self.bk_chests = set() self.bk_chests = set()
self.logic_min = {} self.logic_min = {}
self.logic_max = {} self.logic_max = {}
self.placement_rules = []
self.outside_keys = 0
def check_placement(self, unplaced_keys):
for rule in self.placement_rules:
if not rule.is_satisfiable(self.outside_keys, unplaced_keys):
return False
return True
class DoorRules(object): class DoorRules(object):
@@ -59,6 +68,38 @@ class DoorRules(object):
self.small_location = None self.small_location = None
class PlacementRule(object):
def __init__(self):
self.door_reference = None
self.small_key = None
self.bk_conditional_set = None # the location that means
self.needed_keys_w_bk = None
self.needed_keys_wo_bk = None
self.check_locations_w_bk = None
self.check_locations_wo_bk = None
def is_satisfiable(self, outside_keys, unplaced_keys):
bk_blocked = False
if self.bk_conditional_set:
for loc in self.bk_conditional_set:
if loc.item and loc.item.bigkey:
bk_blocked = True
break
available_keys = outside_keys
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
for loc in check_locations:
if not loc.item:
empty_chests += 1
elif loc.item and loc.item.name == self.small_key:
available_keys += 1
place_able_keys = min(empty_chests, unplaced_keys)
available_keys += place_able_keys
return available_keys >= threshold
class KeyCounter(object): class KeyCounter(object):
def __init__(self, max_chests): def __init__(self, max_chests):
@@ -108,6 +149,8 @@ def analyze_dungeon(key_layout, world, player):
find_bk_locked_sections(key_layout, world) find_bk_locked_sections(key_layout, world)
key_logic.bk_chests.update(find_big_chest_locations(key_layout.all_chest_locations)) key_logic.bk_chests.update(find_big_chest_locations(key_layout.all_chest_locations))
if world.retro[player] and world.mode[player] != 'standard':
return
original_key_counter = find_counter({}, False, key_layout) original_key_counter = find_counter({}, False, key_layout)
queue = deque([(None, original_key_counter)]) queue = deque([(None, original_key_counter)])
@@ -147,13 +190,15 @@ def analyze_dungeon(key_layout, world, player):
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)
if not rule.is_valid: # todo: seems to be caused by best_counter not opening the big key door when that's logically required. Re-evaluate usage of this
logging.getLogger('').warning('Key logic for door %s requires too many chests. Seed may be beatable anyway.', child.name) # 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: if smallest_rule is None or rule.small_key_num < smallest_rule:
smallest_rule = rule.small_key_num 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)
@@ -176,6 +221,65 @@ def analyze_dungeon(key_layout, world, player):
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
def create_placement_rule(key_layout, door, odd_ctr, current_ctr, world, player):
key_logic = key_layout.key_logic
worst_ctr = find_worst_counter(door, odd_ctr, current_ctr, key_layout, False)
sm_num = worst_ctr.used_keys + 1
accessible_loc = set()
accessible_loc.update(worst_ctr.free_locations)
accessible_loc.update(worst_ctr.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.all_locations.difference(accessible_loc)
if len(blocked_loc) > 0:
rule = PlacementRule()
rule.door_reference = door
rule.small_key = key_logic.small_key_name
rule.needed_keys_w_bk = sm_num
placement_self_lock_adjustment(rule, key_layout, blocked_loc, worst_ctr, world, player)
rule.check_locations_w_bk = accessible_loc
if worst_ctr_wo_bk:
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)
blocked_wo_bk = post_set.difference(accessible_wo_bk)
if len(blocked_wo_bk) > 0:
rule.bk_conditional_set = blocked_wo_bk
rule.needed_keys_wo_bk = alt_num
# can this self lock a key if bk not avail? I'm thinking no.
# placement_self_lock_adjustment(rule, key_layout, ???, worst_ctr_wo_bk, world, player)
rule.check_locations_wo_bk = accessible_wo_bk
key_logic.placement_rules.append(rule)
if worst_ctr_wo_bk:
check_bk_restriction_needed(key_layout, worst_ctr_wo_bk, post_ctr, alt_num)
def check_bk_restriction_needed(key_layout, worst_ctr_wo_bk, post_ctr, alt_num):
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':
max_ctr = find_max_counter(key_layout)
blocked_others = set(max_ctr.other_locations).difference(set(worst_ctr.other_locations))
important_found = False
for loc in blocked_others:
if important_location(loc, world, player):
important_found = True
break
if not important_found:
rule.needed_keys_w_bk -= 1
def count_key_drops(sector): def count_key_drops(sector):
cnt = 0 cnt = 0
@@ -210,6 +314,8 @@ def find_bk_locked_sections(key_layout, world):
big_chest_allowed_big_key = world.accessibility != 'locations' big_chest_allowed_big_key = world.accessibility != '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.all_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:
@@ -240,6 +346,28 @@ def relative_empty_counter(odd_counter, key_counter):
return True return True
def relative_empty_counter_2(odd_counter, key_counter):
if len(set(odd_counter.key_only_locations).difference(key_counter.key_only_locations)) > 0:
return False
if len(set(odd_counter.free_locations).difference(key_counter.free_locations)) > 0:
return False
for child in odd_counter.child_doors:
if unique_child_door_2(child, key_counter):
return False
return True
def progressive_ctr(new_counter, last_counter):
if len(set(new_counter.key_only_locations).difference(last_counter.key_only_locations)) > 0:
return True
if len(set(new_counter.free_locations).difference(last_counter.free_locations)) > 0:
return True
for child in new_counter.child_doors:
if unique_child_door_2(child, last_counter):
return True
return False
def unique_child_door(child, key_counter): def unique_child_door(child, key_counter):
if child in key_counter.child_doors or child.dest in key_counter.child_doors: if child in key_counter.child_doors or child.dest in key_counter.child_doors:
return False return False
@@ -250,6 +378,14 @@ def unique_child_door(child, key_counter):
return True return True
def unique_child_door_2(child, key_counter):
if child in key_counter.child_doors or child.dest in key_counter.child_doors:
return False
if child in key_counter.open_doors or child.dest in key_counter.child_doors:
return False
return True
def find_best_counter(door, odd_counter, key_counter, key_layout, world, player, skip_bk, empty_flag): # try to waste as many keys as possible? def find_best_counter(door, odd_counter, key_counter, key_layout, world, player, skip_bk, empty_flag): # try to waste as many keys as possible?
ignored_doors = {door, door.dest} if door is not None else {} ignored_doors = {door, door.dest} if door is not None else {}
finished = False finished = False
@@ -279,7 +415,34 @@ def find_best_counter(door, odd_counter, key_counter, key_layout, world, player,
return last_counter return last_counter
def find_potential_open_doors(key_counter, ignored_doors, key_layout, skip_bk): def find_worst_counter(door, odd_counter, key_counter, key_layout, skip_bk): # try to waste as many keys as possible?
ignored_doors = {door, door.dest} if door is not None else {}
finished = False
opened_doors = dict(key_counter.open_doors)
bk_opened = key_counter.big_key_opened
# new_counter = key_counter
last_counter = key_counter
while not finished:
door_set = find_potential_open_doors(last_counter, ignored_doors, key_layout, skip_bk, 0)
if door_set is None or len(door_set) == 0:
finished = True
continue
for new_door in door_set:
proposed_doors = {**opened_doors, **dict.fromkeys([new_door, new_door.dest])}
bk_open = bk_opened or new_door.bigKey
new_counter = find_counter(proposed_doors, bk_open, key_layout)
bk_open = new_counter.big_key_opened
if not new_door.bigKey and progressive_ctr(new_counter, last_counter) and relative_empty_counter_2(odd_counter, new_counter):
ignored_doors.add(new_door)
else:
last_counter = new_counter
opened_doors = proposed_doors
bk_opened = bk_open
# this means the new_door invalidates the door / leads to the same stuff
return last_counter
def find_potential_open_doors(key_counter, ignored_doors, key_layout, skip_bk, reserve=1):
small_doors = [] small_doors = []
big_doors = [] big_doors = []
for other in key_counter.child_doors: for other in key_counter.child_doors:
@@ -292,7 +455,7 @@ def find_potential_open_doors(key_counter, ignored_doors, key_layout, skip_bk):
if key_layout.big_key_special: if key_layout.big_key_special:
big_key_available = key_counter.big_key_opened big_key_available = key_counter.big_key_opened
else: else:
big_key_available = len(key_counter.free_locations) - key_counter.used_smalls_loc(1) > 0 big_key_available = len(key_counter.free_locations) - key_counter.used_smalls_loc(reserve) > 0
if len(small_doors) == 0 and (not skip_bk and (len(big_doors) == 0 or not big_key_available)): if len(small_doors) == 0 and (not skip_bk and (len(big_doors) == 0 or not big_key_available)):
return None return None
return small_doors + big_doors return small_doors + big_doors
@@ -366,7 +529,7 @@ def create_rule(key_counter, prev_counter, key_layout, world, player):
def check_for_self_lock_key(rule, door, parent_counter, key_layout, world, player): def check_for_self_lock_key(rule, door, parent_counter, key_layout, world, player):
if world.accessibility != 'locations': if world.accessibility[player] != 'locations':
counter = find_inverted_counter(door, parent_counter, key_layout, world, player) counter = find_inverted_counter(door, parent_counter, key_layout, world, player)
if not self_lock_possible(counter): if not self_lock_possible(counter):
return return
@@ -488,6 +651,27 @@ def bk_restricted_rules(rule, door, odd_counter, empty_flag, key_counter, key_la
# key_layout.key_logic.bk_restricted.update(unique_loc) # key_layout.key_logic.bk_restricted.update(unique_loc)
def find_worst_counter_wo_bk(small_key_num, accessible_set, door, odd_ctr, key_counter, key_layout):
if key_counter.big_key_opened:
return None, None, None
worst_counter = find_worst_counter(door, odd_ctr, key_counter, key_layout, True)
bk_rule_num = worst_counter.used_keys + 1
bk_access_set = set()
bk_access_set.update(worst_counter.free_locations)
bk_access_set.update(worst_counter.key_only_locations)
if bk_rule_num == small_key_num and len(bk_access_set ^ accessible_set) == 0:
return None, None, None
door_open = find_next_counter(door, worst_counter, key_layout)
ignored_doors = dict_intersection(worst_counter.child_doors, door_open.child_doors)
dest_ignored = []
for door in ignored_doors.keys():
if door.dest not in ignored_doors:
dest_ignored.append(door.dest)
ignored_doors = {**ignored_doors, **dict.fromkeys(dest_ignored)}
post_counter = open_some_counter(door_open, key_layout, ignored_doors.keys())
return worst_counter, post_counter, bk_rule_num
def open_a_door(door, child_state, flat_proposal): def open_a_door(door, child_state, flat_proposal):
if door.bigKey: if door.bigKey:
child_state.big_key_opened = True child_state.big_key_opened = True
@@ -832,6 +1016,8 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa
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(state, prev_state, prev_avail, world, player):
return False return False
# 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 not enough_small_locations(state, available_small_locations)) and (state.big_key_opened or num_bigs == 0 or available_big_locations == 0): if (not smalls_avail or not enough_small_locations(state, available_small_locations)) and (state.big_key_opened or num_bigs == 0 or available_big_locations == 0):
return False return False
else: else:
@@ -1233,3 +1419,43 @@ def val_rule(rule, skn, allow=False, loc=None, askn=None, setCheck=None):
assert len(setCheck) == len(rule.alternate_big_key_loc) assert len(setCheck) == len(rule.alternate_big_key_loc)
for loc in rule.alternate_big_key_loc: for loc in rule.alternate_big_key_loc:
assert loc.name in setCheck assert loc.name in setCheck
# Soft lock stuff
def validate_key_placement(key_layout, world, player):
if world.retro[player] or world.accessibility[player] == 'none':
return True # Can't keylock in retro. Expected if beatable only.
max_counter = find_max_counter(key_layout)
keys_outside = 0
big_key_outside = False
dungeon = world.get_dungeon(key_layout.sector.name, player)
smallkey_name = 'Small Key (%s)' % (key_layout.sector.name if key_layout.sector.name != 'Hyrule Castle' else 'Escape')
if world.keyshuffle[player]:
keys_outside = key_layout.max_chests - sum(1 for i in max_counter.free_locations if i.item is not None and i.item.name == smallkey_name and i.item.player == player)
if world.bigkeyshuffle[player]:
max_counter = find_max_counter(key_layout)
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():
if len(counter.child_doors) == 0:
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
if counter.big_key_opened and not big_found:
continue # Can't get to this state
found_locations = set(i for i in counter.free_locations if big_found or "- Big Chest" not in i.name)
found_keys = sum(1 for i in found_locations if i.item is not None and i.item.name == smallkey_name and i.item.player == player) + \
len(counter.key_only_locations) + keys_outside
can_progress = (not counter.big_key_opened and big_found and any(d.bigKey for d in counter.child_doors)) or \
found_keys > counter.used_keys and any(not d.bigKey for d in counter.child_doors)
if not can_progress:
missing_locations = set(max_counter.free_locations.keys()).difference(found_locations)
missing_items = [l for l in missing_locations if l.item is None or (l.item.name != smallkey_name and l.item != dungeon.big_key) or "- Boss" in l.name]
#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):
logging.getLogger('').error("Keylock - can't open locations: ")
for i in missing_locations:
logging.getLogger('').error(i)
return False
return True

View File

@@ -10,6 +10,7 @@ import zlib
from BaseClasses import World, CollectionState, Item, Region, Location, Shop from BaseClasses import World, CollectionState, Item, Region, Location, Shop
from Items import ItemFactory from Items import ItemFactory
from KeyDoorShuffle import validate_key_placement
from Regions import create_regions, create_shops, mark_light_world_regions, create_dungeon_regions from Regions import create_regions, create_shops, mark_light_world_regions, create_dungeon_regions
from InvertedRegions import create_inverted_regions, mark_dark_world_regions from InvertedRegions import create_inverted_regions, mark_dark_world_regions
from EntranceShuffle import link_entrances, link_inverted_entrances from EntranceShuffle import link_entrances, link_inverted_entrances
@@ -23,7 +24,7 @@ from Fill import distribute_items_cutoff, distribute_items_staleness, distribute
from ItemList import generate_itempool, difficulties, fill_prizes from ItemList import generate_itempool, difficulties, fill_prizes
from Utils import output_path, parse_player_names from Utils import output_path, parse_player_names
__version__ = '0.0.14pre' __version__ = '0.0.17pre'
def main(args, seed=None): def main(args, seed=None):
@@ -56,6 +57,7 @@ def main(args, seed=None):
world.enemy_health = args.enemy_health.copy() world.enemy_health = args.enemy_health.copy()
world.enemy_damage = args.enemy_damage.copy() world.enemy_damage = args.enemy_damage.copy()
world.beemizer = args.beemizer.copy() world.beemizer = args.beemizer.copy()
world.dungeon_counters = args.dungeon_counters.copy()
world.experimental = args.experimental.copy() world.experimental = args.experimental.copy()
world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)} world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)}
@@ -133,6 +135,11 @@ def main(args, seed=None):
else: else:
fill_dungeons(world) fill_dungeons(world)
for player in range(1, world.players+1):
for key_layout in world.key_layout[player].values():
if not validate_key_placement(key_layout, world, player):
raise RuntimeError("Keylock detected: %s (Player %d)" % (key_layout.sector.name, player))
logger.info('Fill the world.') logger.info('Fill the world.')
if args.algorithm == 'flood': if args.algorithm == 'flood':

6
Rom.py
View File

@@ -22,7 +22,7 @@ from EntranceShuffle import door_addresses, exit_ids
JAP10HASH = '03a63945398191337e896e5771f77173' JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = '818b2c659a610cb4112804cf612ffd37' RANDOMIZERBASEHASH = '5e01caffabb4509a0987ef2f2f0bcd56'
class JsonRom(object): class JsonRom(object):
@@ -618,6 +618,8 @@ def patch_rom(world, rom, player, team, enemized):
if builder.pre_open_stonewall.name == 'Desert Wall Slide NW': if builder.pre_open_stonewall.name == 'Desert Wall Slide NW':
dr_flags |= DROptions.Open_Desert_Wall dr_flags |= DROptions.Open_Desert_Wall
rom.write_byte(0x139006, dr_flags.value) rom.write_byte(0x139006, dr_flags.value)
if dr_flags & DROptions.Town_Portal and world.mode[player] == 'inverted':
rom.write_byte(0x139008, 1)
# fix skull woods exit, if not fixed during exit patching # fix skull woods exit, if not fixed during exit patching
if world.fix_skullwoods_exit[player] and world.shuffle[player] == 'vanilla': if world.fix_skullwoods_exit[player] and world.shuffle[player] == 'vanilla':
@@ -1157,6 +1159,8 @@ def patch_rom(world, rom, player, team, enemized):
# compasses showing dungeon count # compasses showing dungeon count
if world.clock_mode != 'off': if world.clock_mode != 'off':
rom.write_byte(0x18003C, 0x00) # Currently must be off if timer is on, because they use same HUD location rom.write_byte(0x18003C, 0x00) # Currently must be off if timer is on, because they use same HUD location
elif world.dungeon_counters[player]:
rom.write_byte(0x18003C, 0x02) # show always
elif world.compassshuffle[player] or world.doorShuffle[player] != 'vanilla': elif world.compassshuffle[player] or world.doorShuffle[player] != 'vanilla':
rom.write_byte(0x18003C, 0x01) # show on pickup rom.write_byte(0x18003C, 0x01) # show on pickup
else: else:

View File

@@ -31,6 +31,8 @@ DRMode:
dw 0 dw 0
DRFlags: DRFlags:
dw 0 dw 0
DRScroll:
db 0
; Vert 0,6,0 Horz 2,0,8 ; Vert 0,6,0 Horz 2,0,8
org $279010 org $279010

View File

@@ -72,6 +72,13 @@ jsl GtBossHeartCheckOverride : bcs .not_in_ganons_tower
nop : stz $0dd0, X : rts nop : stz $0dd0, X : rts
.not_in_ganons_tower .not_in_ganons_tower
org $2081f2
jsl MirrorCheckOverride2
org $20825c
jsl MirrorCheckOverride2
; These two, if enabled together, have implications for vanilla BK doors in IP/Hera/Mire ; These two, if enabled together, have implications for vanilla BK doors in IP/Hera/Mire
; IPBJ is common enough to consider not doing this. Mire is not a concern for vanilla - maybe glitched modes ; IPBJ is common enough to consider not doing this. Mire is not a concern for vanilla - maybe glitched modes
; Hera BK door back can be seen with Pot clipping - likely useful for no logic seeds ; Hera BK door back can be seen with Pot clipping - likely useful for no logic seeds

View File

@@ -43,9 +43,11 @@ OnFileLoadOverride:
+ rtl + rtl
MirrorCheckOverride: MirrorCheckOverride:
lda $8A : and #$40 ; what I wrote over
beq +
lda DRFlags : and #$02 : beq ++ lda DRFlags : and #$02 : beq ++
lda $7ef353 : cmp #$01 : beq + lda $7ef353 : cmp #$01 : beq +
++ lda #$01 ++ lda $8A : and #$40 ; what I wrote over
+ rtl rtl
+ lda DRScroll : rtl
MirrorCheckOverride2:
lda $7ef353 : and #$02 : rtl

File diff suppressed because one or more lines are too long