Merged in DR v1.2.0.8
This commit is contained in:
+51
-13
@@ -35,6 +35,8 @@ class World(object):
|
||||
self.doorShuffle = doorShuffle.copy()
|
||||
self.intensity = {}
|
||||
self.door_type_mode = {}
|
||||
self.trap_door_mode = {}
|
||||
self.key_logic_algorithm = {}
|
||||
self.logic = logic.copy()
|
||||
self.mode = mode.copy()
|
||||
self.swords = swords.copy()
|
||||
@@ -162,6 +164,8 @@ class World(object):
|
||||
set_player_attr('pot_pool', {})
|
||||
set_player_attr('decoupledoors', False)
|
||||
set_player_attr('door_type_mode', 'original')
|
||||
set_player_attr('trap_door_mode', 'optional')
|
||||
set_player_attr('key_logic_algorithm', 'default')
|
||||
|
||||
set_player_attr('shopsanity', False)
|
||||
set_player_attr('mixed_travel', 'prevent')
|
||||
@@ -585,6 +589,7 @@ class CollectionState(object):
|
||||
self.world = parent
|
||||
if not skip_init:
|
||||
self.prog_items = Counter()
|
||||
self.forced_keys = Counter()
|
||||
self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)}
|
||||
self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)}
|
||||
self.events = []
|
||||
@@ -617,12 +622,13 @@ class CollectionState(object):
|
||||
queue = deque(self.blocked_connections[player].items())
|
||||
|
||||
self.traverse_world(queue, rrp, bc, player)
|
||||
unresolved_events = [x for y in self.reachable_regions[player] for x in y.locations
|
||||
if x.event and x.item and (x.item.smallkey or x.item.bigkey or x.item.advancement)
|
||||
and x not in self.locations_checked and x.can_reach(self)]
|
||||
unresolved_events = self._do_not_flood_the_keys(unresolved_events)
|
||||
if len(unresolved_events) == 0:
|
||||
self.check_key_doors_in_dungeons(rrp, player)
|
||||
if self.world.key_logic_algorithm[player] == 'default':
|
||||
unresolved_events = [x for y in self.reachable_regions[player] for x in y.locations
|
||||
if x.event and x.item and (x.item.smallkey or x.item.bigkey or x.item.advancement)
|
||||
and x not in self.locations_checked and x.can_reach(self)]
|
||||
unresolved_events = self._do_not_flood_the_keys(unresolved_events)
|
||||
if len(unresolved_events) == 0:
|
||||
self.check_key_doors_in_dungeons(rrp, player)
|
||||
|
||||
def traverse_world(self, queue, rrp, bc, player):
|
||||
# run BFS on all connections, and keep track of those blocked by missing items
|
||||
@@ -706,6 +712,7 @@ class CollectionState(object):
|
||||
|
||||
def check_key_doors_in_dungeons(self, rrp, player):
|
||||
for dungeon_name, checklist in self.dungeons_to_check[player].items():
|
||||
# todo: optimization idea - abort exploration if there are unresolved events now
|
||||
if self.apply_dungeon_exploration(rrp, player, dungeon_name, checklist):
|
||||
continue
|
||||
init_door_candidates = self.should_explore_child_state(self, dungeon_name, player)
|
||||
@@ -905,6 +912,7 @@ class CollectionState(object):
|
||||
def copy(self):
|
||||
ret = CollectionState(self.world, skip_init=True)
|
||||
ret.prog_items = self.prog_items.copy()
|
||||
ret.forced_keys = self.forced_keys.copy()
|
||||
ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in range(1, self.world.players + 1)}
|
||||
ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in range(1, self.world.players + 1)}
|
||||
ret.events = copy.copy(self.events)
|
||||
@@ -1112,6 +1120,14 @@ class CollectionState(object):
|
||||
return (item, player) in self.prog_items
|
||||
return self.prog_items[item, player] >= count
|
||||
|
||||
def has_sm_key_strict(self, item, player, count=1):
|
||||
if self.world.keyshuffle[player] == 'universal':
|
||||
if self.world.mode[player] == 'standard' and self.world.doorShuffle[player] == 'vanilla' and item == 'Small Key (Escape)':
|
||||
return True # Cannot access the shop until escape is finished. This is safe because the key is manually placed in make_custom_item_pool
|
||||
return self.can_buy_unlimited('Small Key (Universal)', player)
|
||||
obtained = self.prog_items[item, player] - self.forced_keys[item, player]
|
||||
return obtained >= count
|
||||
|
||||
def can_buy_unlimited(self, item, player):
|
||||
for shop in self.world.shops[player]:
|
||||
if shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self):
|
||||
@@ -1341,6 +1357,8 @@ class CollectionState(object):
|
||||
def collect(self, item, event=False, location=None):
|
||||
if location:
|
||||
self.locations_checked.add(location)
|
||||
if item and item.smallkey and location.forced_item is not None:
|
||||
self.forced_keys[item.name, item.player] += 1
|
||||
if not item:
|
||||
return
|
||||
changed = False
|
||||
@@ -1533,10 +1551,10 @@ class Region(object):
|
||||
or (item.bigkey and not self.world.bigkeyshuffle[item.player])
|
||||
or (item.map and not self.world.mapshuffle[item.player])
|
||||
or (item.compass and not self.world.compassshuffle[item.player]))
|
||||
sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Escape)'
|
||||
if sewer_hack or inside_dungeon_item:
|
||||
# not all small keys to escape must be in escape
|
||||
# sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Escape)'
|
||||
if inside_dungeon_item:
|
||||
return self.dungeon and self.dungeon.is_dungeon_item(item) and item.player == self.player
|
||||
|
||||
return True
|
||||
|
||||
def can_cause_bunny(self, player):
|
||||
@@ -2168,6 +2186,9 @@ class Door(object):
|
||||
return world.get_room(self.roomIndex, self.player).kind(self)
|
||||
return None
|
||||
|
||||
def dungeon_name(self):
|
||||
return self.entrance.parent_region.dungeon.name if self.entrance.parent_region.dungeon else 'Cave'
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, self.__class__) and self.name == other.name
|
||||
|
||||
@@ -2846,6 +2867,8 @@ class Spoiler(object):
|
||||
'door_shuffle': self.world.doorShuffle,
|
||||
'intensity': self.world.intensity,
|
||||
'door_type_mode': self.world.door_type_mode,
|
||||
'trap_door_mode': self.world.trap_door_mode,
|
||||
'key_logic': self.world.key_logic_algorithm,
|
||||
'decoupledoors': self.world.decoupledoors,
|
||||
'dungeon_counters': self.world.dungeon_counters,
|
||||
'item_pool': self.world.difficulty,
|
||||
@@ -3070,6 +3093,8 @@ class Spoiler(object):
|
||||
if self.metadata['door_shuffle'][player] != 'vanilla':
|
||||
outfile.write('Intensity:'.ljust(line_width) + '%s\n' % self.metadata['intensity'][player])
|
||||
outfile.write('Door Type Mode:'.ljust(line_width) + '%s\n' % self.metadata['door_type_mode'][player])
|
||||
outfile.write('Trap Door Mode:'.ljust(line_width) + '%s\n' % self.metadata['trap_door_mode'][player])
|
||||
outfile.write('Key Logic Algorithm:'.ljust(line_width) + '%s\n' % self.metadata['key_logic'][player])
|
||||
outfile.write('Decouple Doors:'.ljust(line_width) + '%s\n' % yn(self.metadata['decoupledoors'][player]))
|
||||
outfile.write('Experimental:'.ljust(line_width) + '%s\n' % yn(self.metadata['experimental'][player]))
|
||||
outfile.write('Dungeon Counters:'.ljust(line_width) + '%s\n' % self.metadata['dungeon_counters'][player])
|
||||
@@ -3408,15 +3433,19 @@ orcrossed_mode = {"none": 0, "polar": 1, "grouped": 2, "limited": 3, "chaos": 4}
|
||||
# byte 12: KMB? FF?? (keep similar, mixed/tile flip, bonk drops, flute spots)
|
||||
flutespot_mode = {"vanilla": 0, "balanced": 1, "random": 2}
|
||||
|
||||
# byte 13: FBBB, TTSS (flute_mode, bow_mode, take_any, small_key_mode)
|
||||
# byte 13: FBBB TTSS (flute_mode, bow_mode, take_any, small_key_mode)
|
||||
flute_mode = {'normal': 0, 'active': 1}
|
||||
keyshuffle_mode = {'none': 0, 'wild': 1, 'universal': 2} # reserved 8 modes?
|
||||
take_any_mode = {'none': 0, 'random': 1, 'fixed': 2}
|
||||
bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silvers': 3}
|
||||
|
||||
# additions
|
||||
# psuedoboots does not effect code
|
||||
# sfx_shuffle and other adjust items does not effect settings code
|
||||
# byte 14: POOT TKKK (pseudoboots, overworld_map, trap_door_mode, key_logic_algo)
|
||||
overworld_map_mode = {'default': 0, 'compass': 1, 'map': 2}
|
||||
trap_door_mode = {'vanilla': 0, 'optional': 1, 'boss': 2, 'oneway': 3}
|
||||
key_logic_algo = {'default': 0, 'partial': 1, 'strict': 2}
|
||||
|
||||
# sfx_shuffle and other adjust items does not affect settings code
|
||||
|
||||
# Bump this when making changes that are not backwards compatible (nearly all of them)
|
||||
settings_version = 1
|
||||
@@ -3466,7 +3495,10 @@ class Settings(object):
|
||||
| (0x20 if w.shuffle_bonk_drops[p] else 0) | (flutespot_mode[w.owFluteShuffle[p]] << 4),
|
||||
|
||||
(flute_mode[w.flute_mode[p]] << 7 | bow_mode[w.bow_mode[p]] << 4
|
||||
| take_any_mode[w.take_any[p]] << 2 | keyshuffle_mode[w.keyshuffle[p]])
|
||||
| take_any_mode[w.take_any[p]] << 2 | keyshuffle_mode[w.keyshuffle[p]]),
|
||||
|
||||
((0x80 if w.pseudoboots[p] else 0) | overworld_map_mode[w.overworld_map[p]] << 6
|
||||
| trap_door_mode[w.trap_door_mode[p]] << 4 | key_logic_algo[w.key_logic_algorithm[p]]),
|
||||
])
|
||||
return base64.b64encode(code, "+-".encode()).decode()
|
||||
|
||||
@@ -3547,6 +3579,12 @@ class Settings(object):
|
||||
args.take_any[p] = r(take_any_mode)[(settings[13] & 0xC) >> 2]
|
||||
args.keyshuffle[p] = r(keyshuffle_mode)[settings[13] & 0x3]
|
||||
|
||||
if len(settings) > 14:
|
||||
args.pseudoboots[p] = True if settings[14] & 0x80 else False
|
||||
args.overworld_map[p] = r(overworld_map_mode)[(settings[14] & 0x60) >> 6]
|
||||
args.trap_door_mode[p] = r(trap_door_mode)[(settings[14] & 0x14) >> 4]
|
||||
args.key_logic_algorithm[p] = r(key_logic_algo)[settings[14] & 0x07]
|
||||
|
||||
|
||||
class KeyRuleType(FastEnum):
|
||||
WorstCase = 0
|
||||
|
||||
@@ -141,7 +141,8 @@ def parse_cli(argv, no_defaults=False):
|
||||
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor',
|
||||
'heartbeep', 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', 'keydropshuffle',
|
||||
'mixed_travel', 'standardize_palettes', 'code', 'reduce_flashing', 'shuffle_sfx',
|
||||
'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode', 'bonk_drops']:
|
||||
'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode',
|
||||
'bonk_drops', 'trap_door_mode', 'key_logic_algorithm']:
|
||||
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
|
||||
if player == 1:
|
||||
setattr(ret, name, {1: value})
|
||||
@@ -223,6 +224,8 @@ def parse_settings():
|
||||
"door_shuffle": "vanilla",
|
||||
"intensity": 3,
|
||||
"door_type_mode": "original",
|
||||
"trap_door_mode": "optional",
|
||||
"key_logic_algorithm": "default",
|
||||
"decoupledoors": False,
|
||||
"experimental": False,
|
||||
"dungeon_counters": "default",
|
||||
|
||||
+127
-92
@@ -226,22 +226,38 @@ def vanilla_key_logic(world, player):
|
||||
|
||||
add_inaccessible_doors(world, player)
|
||||
entrances_map, potentials, connections = determine_entrance_list(world, player)
|
||||
for builder in builders:
|
||||
enabled_entrances = world.enabled_entrances[player] = {}
|
||||
builder_queue = deque(builders)
|
||||
last_key, loops = None, 0
|
||||
while len(builder_queue) > 0:
|
||||
builder = builder_queue.popleft()
|
||||
origin_list = entrances_map[builder.name]
|
||||
start_regions = convert_regions(origin_list, world, player)
|
||||
doors = convert_key_doors(default_small_key_doors[builder.name], world, player)
|
||||
key_layout = build_key_layout(builder, start_regions, doors, {}, world, player)
|
||||
valid = validate_key_layout(key_layout, world, player)
|
||||
if not valid:
|
||||
logging.getLogger('').info('Vanilla key layout not valid %s', builder.name)
|
||||
builder.key_door_proposal = doors
|
||||
if player not in world.key_logic.keys():
|
||||
world.key_logic[player] = {}
|
||||
analyze_dungeon(key_layout, world, player)
|
||||
world.key_logic[player][builder.name] = key_layout.key_logic
|
||||
world.key_layout[player][builder.name] = key_layout
|
||||
log_key_logic(builder.name, key_layout.key_logic)
|
||||
# if world.shuffle[player] == 'vanilla' and world.owShuffle[player] == 'vanilla' and world.owCrossed[player] == 'none' and not world.owMixed[player] and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]:
|
||||
find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, builder.name)
|
||||
if len(origin_list) <= 0:
|
||||
if last_key == builder.name or loops > 1000:
|
||||
origin_name = (world.get_region(origin_list[0], player).entrances[0].parent_region.name
|
||||
if len(origin_list) > 0 else 'no origin')
|
||||
raise GenerationException(f'Infinite loop detected for "{builder.name}" located at {origin_name}')
|
||||
builder_queue.append(builder)
|
||||
last_key = builder.name
|
||||
loops += 1
|
||||
else:
|
||||
find_new_entrances(builder.master_sector, entrances_map, connections, potentials,
|
||||
enabled_entrances, world, player)
|
||||
start_regions = convert_regions(origin_list, world, player)
|
||||
doors = convert_key_doors(default_small_key_doors[builder.name], world, player)
|
||||
key_layout = build_key_layout(builder, start_regions, doors, {}, world, player)
|
||||
valid = validate_key_layout(key_layout, world, player)
|
||||
if not valid:
|
||||
logging.getLogger('').info('Vanilla key layout not valid %s', builder.name)
|
||||
builder.key_door_proposal = doors
|
||||
if player not in world.key_logic.keys():
|
||||
world.key_logic[player] = {}
|
||||
analyze_dungeon(key_layout, world, player)
|
||||
world.key_logic[player][builder.name] = key_layout.key_logic
|
||||
world.key_layout[player][builder.name] = key_layout
|
||||
log_key_logic(builder.name, key_layout.key_logic)
|
||||
# if world.shuffle[player] == 'vanilla' and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]:
|
||||
# validate_vanilla_key_logic(world, player)
|
||||
|
||||
|
||||
@@ -1745,12 +1761,12 @@ class DoorTypePool:
|
||||
self.tricky += counts[6]
|
||||
|
||||
def chaos_shuffle(self, counts):
|
||||
weights = [1, 2, 4, 3, 2, 1]
|
||||
weights = [1, 2, 4, 3, 2]
|
||||
return [random.choices(self.get_choices(counts[i]), weights=weights)[0] for i, c in enumerate(counts)]
|
||||
|
||||
@staticmethod
|
||||
def get_choices(number):
|
||||
return [max(number+i, 0) for i in range(-1, 5)]
|
||||
return [max(number+i, 0) for i in range(-1, 4)]
|
||||
|
||||
|
||||
class BuilderDoorCandidates:
|
||||
@@ -1782,58 +1798,68 @@ def shuffle_door_types(door_type_pools, paths, world, player):
|
||||
def shuffle_trap_doors(door_type_pools, paths, start_regions_map, world, player):
|
||||
used_doors = set()
|
||||
for pool, door_type_pool in door_type_pools:
|
||||
ttl = 0
|
||||
suggestion_map, trap_map, flex_map = {}, {}, {}
|
||||
remaining = door_type_pool.traps
|
||||
if player in world.custom_door_types:
|
||||
custom_trap_doors = world.custom_door_types[player]['Trap Door']
|
||||
else:
|
||||
custom_trap_doors = defaultdict(list)
|
||||
if world.trap_door_mode[player] != 'oneway':
|
||||
ttl = 0
|
||||
suggestion_map, trap_map, flex_map = {}, {}, {}
|
||||
remaining = door_type_pool.traps
|
||||
if player in world.custom_door_types and 'Trap Door' in world.custom_door_types[player]:
|
||||
custom_trap_doors = world.custom_door_types[player]['Trap Door']
|
||||
else:
|
||||
custom_trap_doors = defaultdict(list)
|
||||
|
||||
for dungeon in pool:
|
||||
builder = world.dungeon_layouts[player][dungeon]
|
||||
find_trappable_candidates(builder, world, player)
|
||||
if custom_trap_doors[dungeon]:
|
||||
builder.candidates.trap = filter_key_door_pool(builder.candidates.trap, custom_trap_doors[dungeon])
|
||||
remaining -= len(custom_trap_doors[dungeon])
|
||||
ttl += len(builder.candidates.trap)
|
||||
if ttl == 0:
|
||||
continue
|
||||
for dungeon in pool:
|
||||
builder = world.dungeon_layouts[player][dungeon]
|
||||
proportion = len(builder.candidates.trap)
|
||||
calc = int(round(proportion * door_type_pool.traps/ttl))
|
||||
suggested = min(proportion, calc)
|
||||
remaining -= suggested
|
||||
suggestion_map[dungeon] = suggested
|
||||
flex_map[dungeon] = (proportion - suggested) if suggested < proportion else 0
|
||||
for dungeon in pool:
|
||||
builder = world.dungeon_layouts[player][dungeon]
|
||||
valid_traps, trap_number = find_valid_trap_combination(builder, suggestion_map[dungeon],
|
||||
start_regions_map[dungeon], paths, world, player,
|
||||
drop=True)
|
||||
trap_map[dungeon] = valid_traps
|
||||
if trap_number < suggestion_map[dungeon]:
|
||||
flex_map[dungeon] = 0
|
||||
remaining += suggestion_map[dungeon] - trap_number
|
||||
suggestion_map[dungeon] = trap_number
|
||||
builder_order = [x for x in pool if flex_map[x] > 0]
|
||||
random.shuffle(builder_order)
|
||||
queue = deque(builder_order)
|
||||
while len(queue) > 0 and remaining > 0:
|
||||
dungeon = queue.popleft()
|
||||
builder = world.dungeon_layouts[player][dungeon]
|
||||
increased = suggestion_map[dungeon] + 1
|
||||
valid_traps, trap_number = find_valid_trap_combination(builder, increased, start_regions_map[dungeon],
|
||||
paths, world, player)
|
||||
if valid_traps:
|
||||
for dungeon in pool:
|
||||
builder = world.dungeon_layouts[player][dungeon]
|
||||
if 'Mire Warping Pool' in builder.master_sector.region_set():
|
||||
custom_trap_doors[dungeon].append(world.get_door('Mire Warping Pool ES', player))
|
||||
world.custom_door_types[player]['Trap Door'] = custom_trap_doors
|
||||
find_trappable_candidates(builder, world, player)
|
||||
if custom_trap_doors[dungeon]:
|
||||
builder.candidates.trap = filter_key_door_pool(builder.candidates.trap, custom_trap_doors[dungeon])
|
||||
remaining -= len(custom_trap_doors[dungeon])
|
||||
ttl += len(builder.candidates.trap)
|
||||
if ttl == 0:
|
||||
continue
|
||||
for dungeon in pool:
|
||||
builder = world.dungeon_layouts[player][dungeon]
|
||||
proportion = len(builder.candidates.trap)
|
||||
calc = int(round(proportion * door_type_pool.traps/ttl))
|
||||
suggested = min(proportion, calc)
|
||||
remaining -= suggested
|
||||
suggestion_map[dungeon] = suggested
|
||||
flex_map[dungeon] = (proportion - suggested) if suggested < proportion else 0
|
||||
for dungeon in pool:
|
||||
builder = world.dungeon_layouts[player][dungeon]
|
||||
valid_traps, trap_number = find_valid_trap_combination(builder, suggestion_map[dungeon],
|
||||
start_regions_map[dungeon], paths, world, player,
|
||||
drop=True)
|
||||
trap_map[dungeon] = valid_traps
|
||||
remaining -= 1
|
||||
suggestion_map[dungeon] = increased
|
||||
flex_map[dungeon] -= 1
|
||||
if flex_map[dungeon] > 0:
|
||||
queue.append(dungeon)
|
||||
# time to re-assign
|
||||
if trap_number < suggestion_map[dungeon]:
|
||||
flex_map[dungeon] = 0
|
||||
remaining += suggestion_map[dungeon] - trap_number
|
||||
suggestion_map[dungeon] = trap_number
|
||||
builder_order = [x for x in pool if flex_map[x] > 0]
|
||||
random.shuffle(builder_order)
|
||||
queue = deque(builder_order)
|
||||
while len(queue) > 0 and remaining > 0:
|
||||
dungeon = queue.popleft()
|
||||
builder = world.dungeon_layouts[player][dungeon]
|
||||
increased = suggestion_map[dungeon] + 1
|
||||
valid_traps, trap_number = find_valid_trap_combination(builder, increased, start_regions_map[dungeon],
|
||||
paths, world, player)
|
||||
if valid_traps:
|
||||
trap_map[dungeon] = valid_traps
|
||||
remaining -= 1
|
||||
suggestion_map[dungeon] = increased
|
||||
flex_map[dungeon] -= 1
|
||||
if flex_map[dungeon] > 0:
|
||||
queue.append(dungeon)
|
||||
# time to re-assign
|
||||
else:
|
||||
trap_map = {dungeon: [] for dungeon in pool}
|
||||
for dungeon in pool:
|
||||
builder = world.dungeon_layouts[player][dungeon]
|
||||
if 'Mire Warping Pool' in builder.master_sector.region_set():
|
||||
trap_map[dungeon].append(world.get_door('Mire Warping Pool ES', player))
|
||||
reassign_trap_doors(trap_map, world, player)
|
||||
for name, traps in trap_map.items():
|
||||
used_doors.update(traps)
|
||||
@@ -1845,7 +1871,7 @@ def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world,
|
||||
ttl = 0
|
||||
suggestion_map, bk_map, flex_map = {}, {}, {}
|
||||
remaining = door_type_pool.bigs
|
||||
if player in world.custom_door_types:
|
||||
if player in world.custom_door_types and 'Big Key Door' in world.custom_door_types[player]:
|
||||
custom_bk_doors = world.custom_door_types[player]['Big Key Door']
|
||||
else:
|
||||
custom_bk_doors = defaultdict(list)
|
||||
@@ -1907,7 +1933,7 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl
|
||||
suggestion_map, small_map, flex_map = {}, {}, {}
|
||||
remaining = door_type_pool.smalls
|
||||
total_keys = remaining
|
||||
if player in world.custom_door_types:
|
||||
if player in world.custom_door_types and 'Key Door' in world.custom_door_types[player]:
|
||||
custom_key_doors = world.custom_door_types[player]['Key Door']
|
||||
else:
|
||||
custom_key_doors = defaultdict(list)
|
||||
@@ -2007,7 +2033,7 @@ def shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, worl
|
||||
remaining_bomb = door_type_pool.bombable
|
||||
remaining_dash = door_type_pool.dashable
|
||||
|
||||
if player in world.custom_door_types:
|
||||
if player in world.custom_door_types and 'Bomb Door' in world.custom_door_types[player]:
|
||||
custom_bomb_doors = world.custom_door_types[player]['Bomb Door']
|
||||
custom_dash_doors = world.custom_door_types[player]['Dash Door']
|
||||
else:
|
||||
@@ -2139,14 +2165,14 @@ def find_trappable_candidates(builder, world, player):
|
||||
for ext in world.get_region(r, player).exits:
|
||||
if ext.door:
|
||||
d = ext.door
|
||||
if d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name:
|
||||
if d.blocked and d.trapFlag != 0 and exclude_boss_traps(d):
|
||||
builder.candidates.trap.append(d)
|
||||
|
||||
|
||||
def find_valid_trap_combination(builder, suggested, start_regions, paths, world, player, drop=True):
|
||||
trap_door_pool = builder.candidates.trap
|
||||
trap_doors_needed = suggested
|
||||
if player in world.custom_door_types:
|
||||
if player in world.custom_door_types and 'Trap Door' in world.custom_door_types[player]:
|
||||
custom_trap_doors = world.custom_door_types[player]['Trap Door'][builder.name]
|
||||
else:
|
||||
custom_trap_doors = []
|
||||
@@ -2282,7 +2308,7 @@ def reassign_trap_doors(trap_map, world, player):
|
||||
logger = logging.getLogger('')
|
||||
for name, traps in trap_map.items():
|
||||
builder = world.dungeon_layouts[player][name]
|
||||
queue = deque(find_current_trap_doors(builder))
|
||||
queue = deque(find_current_trap_doors(builder, world, player))
|
||||
while len(queue) > 0:
|
||||
d = queue.pop()
|
||||
if d.type is DoorType.Interior and d not in traps:
|
||||
@@ -2301,16 +2327,21 @@ def reassign_trap_doors(trap_map, world, player):
|
||||
d.blocked = False
|
||||
for d in traps:
|
||||
change_door_to_trap(d, world, player)
|
||||
world.spoiler.set_door_type(d.name, 'Trap Door', player)
|
||||
logger.debug('Trap Door: %s', d.name)
|
||||
world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', 'Trap Door', player)
|
||||
logger.debug(f'Trap Door: {d.name} ({d.dungeon_name()})')
|
||||
|
||||
|
||||
def find_current_trap_doors(builder):
|
||||
def exclude_boss_traps(d):
|
||||
return ' Boss ' not in d.name and ' Agahnim ' not in d.name and d.name not in ['Skull Spike Corner SW']
|
||||
|
||||
|
||||
def find_current_trap_doors(builder, world, player):
|
||||
checker = exclude_boss_traps if world.trap_door_mode[player] in ['vanilla', 'optional'] else (lambda x: True)
|
||||
current_doors = []
|
||||
for region in builder.master_sector.regions:
|
||||
for ext in region.exits:
|
||||
d = ext.door
|
||||
if d and d.blocked and d.trapFlag != 0: # could exclude removing boss doors here
|
||||
if d and d.blocked and d.trapFlag != 0 and checker(d):
|
||||
current_doors.append(d)
|
||||
return current_doors
|
||||
|
||||
@@ -2425,7 +2456,7 @@ def find_big_key_door_candidates(region, checked, used, world, player):
|
||||
def find_valid_bk_combination(builder, suggested, start_regions, world, player, drop=True):
|
||||
bk_door_pool = builder.candidates.big
|
||||
bk_doors_needed = suggested
|
||||
if player in world.custom_door_types:
|
||||
if player in world.custom_door_types and 'Big Key Door' in world.custom_door_types[player]:
|
||||
custom_bk_doors = world.custom_door_types[player]['Big Key Door'][builder.name]
|
||||
else:
|
||||
custom_bk_doors = []
|
||||
@@ -2494,12 +2525,14 @@ def reassign_big_key_doors(bk_map, world, player):
|
||||
if d1.type is DoorType.Interior:
|
||||
change_door_to_big_key(d1, world, player)
|
||||
d2.bigKey = True # ensure flag is set
|
||||
if d2.smallKey:
|
||||
d2.smallKey = False
|
||||
else:
|
||||
world.paired_doors[player].append(PairedDoor(d1.name, d2.name))
|
||||
change_door_to_big_key(d1, world, player)
|
||||
change_door_to_big_key(d2, world, player)
|
||||
world.spoiler.set_door_type(d1.name+' <-> '+d2.name, 'Big Key Door', player)
|
||||
logger.debug(f'Big Key Door: {d1.name} <-> {d2.name}')
|
||||
world.spoiler.set_door_type(f'{d1.name} <-> {d2.name} ({d1.dungeon_name()})', 'Big Key Door', player)
|
||||
logger.debug(f'Big Key Door: {d1.name} <-> {d2.name} ({d1.dungeon_name()})')
|
||||
else:
|
||||
d = obj
|
||||
if d.type is DoorType.Interior:
|
||||
@@ -2516,12 +2549,14 @@ def reassign_big_key_doors(bk_map, world, player):
|
||||
if stateful_door(d.dest, dest_room.kind(d.dest)):
|
||||
change_door_to_big_key(d.dest, world, player)
|
||||
add_pair(d, d.dest, world, player)
|
||||
world.spoiler.set_door_type(d.name, 'Big Key Door', player)
|
||||
logger.debug(f'Big Key Door: {d.name}')
|
||||
world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', 'Big Key Door', player)
|
||||
logger.debug(f'Big Key Door: {d.name} ({d.dungeon_name()})')
|
||||
|
||||
|
||||
def change_door_to_big_key(d, world, player):
|
||||
d.bigKey = True
|
||||
if d.smallKey:
|
||||
d.smallKey = False
|
||||
room = world.get_room(d.roomIndex, player)
|
||||
if room.doorList[d.doorListPos][1] != DoorKind.BigKey:
|
||||
verify_door_list_pos(d, room, world, player)
|
||||
@@ -2565,7 +2600,7 @@ def find_valid_combination(builder, target, start_regions, world, player, drop_k
|
||||
logger = logging.getLogger('')
|
||||
key_door_pool = list(builder.candidates.small)
|
||||
key_doors_needed = target
|
||||
if player in world.custom_door_types:
|
||||
if player in world.custom_door_types and 'Key Door' in world.custom_door_types[player]:
|
||||
custom_key_doors = world.custom_door_types[player]['Key Door'][builder.name]
|
||||
else:
|
||||
custom_key_doors = []
|
||||
@@ -2693,7 +2728,7 @@ def find_valid_bd_combination(builder, suggested, world, player):
|
||||
bd_door_pool = builder.candidates.bomb_dash
|
||||
bomb_doors_needed, dash_doors_needed = suggested
|
||||
ttl_needed = bomb_doors_needed + dash_doors_needed
|
||||
if player in world.custom_door_types:
|
||||
if player in world.custom_door_types and 'Bomb Door' in world.custom_door_types[player]:
|
||||
custom_bomb_doors = world.custom_door_types[player]['Bomb Door'][builder.name]
|
||||
custom_dash_doors = world.custom_door_types[player]['Dash Door'][builder.name]
|
||||
else:
|
||||
@@ -2769,7 +2804,7 @@ def do_bombable_dashable(proposal, kind, world, player):
|
||||
change_door_to_kind(d1, kind, world, player)
|
||||
change_door_to_kind(d2, kind, world, player)
|
||||
spoiler_type = 'Bomb Door' if kind == DoorKind.Bombable else 'Dash Door'
|
||||
world.spoiler.set_door_type(d1.name+' <-> '+d2.name, spoiler_type, player)
|
||||
world.spoiler.set_door_type(f'{d1.name} <-> {d2.name} ({d1.dungeon_name()})', spoiler_type, player)
|
||||
else:
|
||||
d = obj
|
||||
if d.type is DoorType.Interior:
|
||||
@@ -2783,7 +2818,7 @@ def do_bombable_dashable(proposal, kind, world, player):
|
||||
change_door_to_kind(d.dest, kind, world, player)
|
||||
add_pair(d, d.dest, world, player)
|
||||
spoiler_type = 'Bomb Door' if kind == DoorKind.Bombable else 'Dash Door'
|
||||
world.spoiler.set_door_type(d.name, spoiler_type, player)
|
||||
world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', spoiler_type, player)
|
||||
|
||||
|
||||
def find_current_bd_doors(builder, world):
|
||||
@@ -2986,8 +3021,8 @@ def reassign_key_doors(small_map, world, player):
|
||||
world.paired_doors[player].append(PairedDoor(d1.name, d2.name))
|
||||
change_door_to_small_key(d1, world, player)
|
||||
change_door_to_small_key(d2, world, player)
|
||||
world.spoiler.set_door_type(d1.name+' <-> '+d2.name, 'Key Door', player)
|
||||
logger.debug('Key Door: %s', d1.name+' <-> '+d2.name)
|
||||
world.spoiler.set_door_type(f'{d1.name} <-> {d2.name} ({d1.dungeon_name()})', 'Key Door', player)
|
||||
logger.debug(f'Key Door: {d1.name} <-> {d2.name} ({d1.dungeon_name()})')
|
||||
else:
|
||||
d = obj
|
||||
if d.type is DoorType.Interior:
|
||||
@@ -3003,8 +3038,8 @@ def reassign_key_doors(small_map, world, player):
|
||||
if stateful_door(d.dest, dest_room.kind(d.dest)):
|
||||
change_door_to_small_key(d.dest, world, player)
|
||||
add_pair(d, d.dest, world, player)
|
||||
world.spoiler.set_door_type(d.name, 'Key Door', player)
|
||||
logger.debug('Key Door: %s', d.name)
|
||||
world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', 'Key Door', player)
|
||||
logger.debug(f'Key Door: {d.name} ({d.dungeon_name()})')
|
||||
|
||||
|
||||
def change_door_to_small_key(d, world, player):
|
||||
@@ -3194,7 +3229,7 @@ def change_pair_type(door, new_type, world, player):
|
||||
room_b.change(door.dest.doorListPos, new_type)
|
||||
add_pair(door, door.dest, world, player)
|
||||
spoiler_type = 'Bomb Door' if new_type == DoorKind.Bombable else 'Dash Door'
|
||||
world.spoiler.set_door_type(door.name + ' <-> ' + door.dest.name, spoiler_type, player)
|
||||
world.spoiler.set_door_type(f'{door.name} <-> {door.dest.name} ({door.dungeon_name()})', spoiler_type, player)
|
||||
|
||||
|
||||
def remove_pair_type_if_present(door, world, player):
|
||||
@@ -4550,7 +4585,7 @@ door_type_counts = {
|
||||
'Swamp Palace': (6, 0, 0, 2, 0, 0, 0),
|
||||
'Palace of Darkness': (6, 1, 1, 3, 2, 0, 0),
|
||||
'Misery Mire': (6, 3, 5, 2, 0, 0, 0),
|
||||
'Skull Woods': (5, 0, 2, 2, 0, 1, 0),
|
||||
'Skull Woods': (5, 0, 1, 2, 0, 1, 0),
|
||||
'Ice Palace': (6, 1, 3, 0, 0, 0, 0),
|
||||
'Tower of Hera': (1, 1, 0, 0, 0, 0, 0),
|
||||
'Thieves Town': (3, 1, 2, 1, 1, 0, 0),
|
||||
|
||||
+13
-2
@@ -772,6 +772,13 @@ def connect_simple_door(exit_door, region):
|
||||
|
||||
|
||||
special_big_key_doors = ['Hyrule Dungeon Cellblock Door', "Thieves Blind's Cell Door"]
|
||||
std_special_big_key_doors = ['Hyrule Castle Throne Room Tapestry'] + special_big_key_doors
|
||||
|
||||
|
||||
def get_special_big_key_doors(world, player):
|
||||
if world.mode[player] == 'standard':
|
||||
return std_special_big_key_doors
|
||||
return special_big_key_doors
|
||||
|
||||
|
||||
class ExplorationState(object):
|
||||
@@ -1002,7 +1009,8 @@ class ExplorationState(object):
|
||||
if self.can_traverse(door):
|
||||
if door.controller:
|
||||
door = door.controller
|
||||
if (door in big_key_door_proposal or door.name in special_big_key_doors) and not self.big_key_opened:
|
||||
if (door in big_key_door_proposal
|
||||
or door.name in get_special_big_key_doors(world, player)) and not self.big_key_opened:
|
||||
if not self.in_door_list(door, self.big_doors):
|
||||
self.append_door_to_list(door, self.big_doors)
|
||||
elif door.req_event is not None and door.req_event not in self.events:
|
||||
@@ -3509,6 +3517,8 @@ def identify_branching_issues(dungeon_map, builder_info):
|
||||
unconnected_builders[name] = builder
|
||||
for hook, door_list in unreached_doors.items():
|
||||
builder.unfulfilled[hook] += len(door_list)
|
||||
elif package:
|
||||
builder.throne_door, builder.throne_sector, builder.chosen_lobby = package
|
||||
return unconnected_builders
|
||||
|
||||
|
||||
@@ -3554,7 +3564,8 @@ def check_for_valid_layout(builder, sector_list, builder_info):
|
||||
split_list['Sewers'].remove(temp_builder.throne_door.entrance.parent_region.name)
|
||||
builder.exception_list = list(sector_list)
|
||||
return True, {}, package
|
||||
except (GenerationException, NeutralizingException, OtherGenException):
|
||||
except (GenerationException, NeutralizingException, OtherGenException) as e:
|
||||
logging.getLogger('').info(f'Bailing on this layout for {builder.name}', exc_info=1)
|
||||
builder.split_dungeon_map = None
|
||||
builder.valid_proposal = None
|
||||
if temp_builder.name == 'Hyrule Castle' and temp_builder.throne_door:
|
||||
|
||||
@@ -105,6 +105,8 @@ def fill_restrictive(world, base_state, locations, itempool, key_pool=None, sing
|
||||
spot_to_fill = None
|
||||
|
||||
item_locations = filter_locations(item_to_place, locations, world, vanilla)
|
||||
verify(item_to_place, item_locations, maximum_exploration_state, single_player_placement,
|
||||
perform_access_check, key_pool, world)
|
||||
for location in item_locations:
|
||||
spot_to_fill = verify_spot_to_fill(location, item_to_place, maximum_exploration_state,
|
||||
single_player_placement, perform_access_check, key_pool, world)
|
||||
@@ -128,9 +130,6 @@ def fill_restrictive(world, base_state, locations, itempool, key_pool=None, sing
|
||||
raise FillError('No more spots to place %s' % item_to_place)
|
||||
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
if item_to_place.smallkey:
|
||||
with suppress(ValueError):
|
||||
key_pool.remove(item_to_place)
|
||||
track_outside_keys(item_to_place, spot_to_fill, world)
|
||||
track_dungeon_items(item_to_place, spot_to_fill, world)
|
||||
locations.remove(spot_to_fill)
|
||||
@@ -143,21 +142,29 @@ def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_pl
|
||||
key_pool, world):
|
||||
if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there
|
||||
location.item = item_to_place
|
||||
location.event = True
|
||||
if item_to_place.smallkey:
|
||||
with suppress(ValueError):
|
||||
key_pool.remove(item_to_place)
|
||||
test_state = max_exp_state.copy()
|
||||
test_state.stale[item_to_place.player] = True
|
||||
else:
|
||||
test_state = max_exp_state
|
||||
if not single_player_placement or location.player == item_to_place.player:
|
||||
test_state.sweep_for_events()
|
||||
if location.can_fill(test_state, item_to_place, perform_access_check):
|
||||
if valid_key_placement(item_to_place, location, key_pool, world):
|
||||
if valid_key_placement(item_to_place, location, key_pool, test_state, world):
|
||||
if item_to_place.crystal or valid_dungeon_placement(item_to_place, location, world):
|
||||
return location
|
||||
if item_to_place.smallkey or item_to_place.bigkey:
|
||||
location.item = None
|
||||
location.event = False
|
||||
if item_to_place.smallkey:
|
||||
key_pool.append(item_to_place)
|
||||
return None
|
||||
|
||||
|
||||
def valid_key_placement(item, location, key_pool, world):
|
||||
def valid_key_placement(item, location, key_pool, collection_state, world):
|
||||
if not valid_reserved_placement(item, location, world):
|
||||
return False
|
||||
if ((not item.smallkey and not item.bigkey) or item.player != location.player
|
||||
@@ -174,7 +181,15 @@ def valid_key_placement(item, location, key_pool, world):
|
||||
prize_loc = world.get_location(key_logic.prize_location, location.player)
|
||||
cr_count = world.crystals_needed_for_gt[location.player]
|
||||
wild_keys = world.keyshuffle[item.player] != 'none'
|
||||
return key_logic.check_placement(unplaced_keys, wild_keys, location if item.bigkey else None, prize_loc, cr_count)
|
||||
if wild_keys:
|
||||
reached_keys = {x for x in collection_state.locations_checked
|
||||
if x.item and x.item.name == key_logic.small_key_name and x.item.player == item.player}
|
||||
else:
|
||||
reached_keys = set() # will be calculated using key logic in a moment
|
||||
self_locking_keys = sum(1 for d, rule in key_logic.door_rules.items() if rule.allow_small
|
||||
and rule.small_location.item and rule.small_location.item.name == key_logic.small_key_name)
|
||||
return key_logic.check_placement(unplaced_keys, wild_keys, reached_keys, self_locking_keys,
|
||||
location if item.bigkey else None, prize_loc, cr_count)
|
||||
else:
|
||||
return not item.is_inside_dungeon_item(world)
|
||||
|
||||
@@ -205,6 +220,7 @@ def track_outside_keys(item, location, world):
|
||||
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
|
||||
world.key_logic[item.player][item_dungeon].outside_keys_locations.add(location)
|
||||
|
||||
|
||||
def track_dungeon_items(item, location, world):
|
||||
@@ -345,7 +361,9 @@ def find_spot_for_item(item_to_place, locations, world, base_state, pool,
|
||||
test_state = maximum_exploration_state
|
||||
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 valid_key_placement(item_to_place, location, pool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world):
|
||||
and valid_key_placement(item_to_place, location,
|
||||
pool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool,
|
||||
test_state, world):
|
||||
return location
|
||||
if item_to_place.smallkey or item_to_place.bigkey:
|
||||
location.item = old_item
|
||||
@@ -386,11 +404,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
|
||||
random.shuffle(fill_locations)
|
||||
|
||||
random.shuffle(world.itempool)
|
||||
if world.item_pool_config.preferred:
|
||||
pref = list(world.item_pool_config.preferred.keys())
|
||||
pref_len = len(pref)
|
||||
world.itempool.sort(key=lambda i: pref_len - pref.index((i.name, i.player))
|
||||
if (i.name, i.player) in world.item_pool_config.preferred else 0)
|
||||
config_sort(world)
|
||||
progitempool = [item for item in world.itempool if item.advancement]
|
||||
prioitempool = [item for item in world.itempool if not item.advancement and item.priority]
|
||||
restitempool = [item for item in world.itempool if not item.advancement and not item.priority]
|
||||
@@ -430,10 +444,16 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
|
||||
random.shuffle(fill_locations)
|
||||
fill_locations.reverse()
|
||||
|
||||
# Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots
|
||||
# todo: crossed
|
||||
progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)'
|
||||
and world.keyshuffle[item.player] != 'none' and world.mode[item.player] == 'standard' else 0)
|
||||
# Make sure the escape keys ire placed first in standard to prevent running out of spots
|
||||
def std_item_sort(item):
|
||||
if world.mode[item.player] == 'standard':
|
||||
if item.name == 'Small Key (Escape)':
|
||||
return 1
|
||||
if item.name == 'Big Key (Escape)':
|
||||
return 2
|
||||
return 0
|
||||
|
||||
progitempool.sort(key=std_item_sort)
|
||||
key_pool = [x for x in progitempool if x.smallkey]
|
||||
|
||||
# sort maps and compasses to the back -- this may not be viable in equitable & ambrosia
|
||||
@@ -482,6 +502,20 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
|
||||
ensure_good_pots(world)
|
||||
|
||||
|
||||
def config_sort(world):
|
||||
if world.item_pool_config.verify:
|
||||
config_sort_helper(world, world.item_pool_config.verify)
|
||||
elif world.item_pool_config.preferred:
|
||||
config_sort_helper(world, world.item_pool_config.preferred)
|
||||
|
||||
|
||||
def config_sort_helper(world, sort_dict):
|
||||
pref = list(sort_dict.keys())
|
||||
pref_len = len(pref)
|
||||
world.itempool.sort(key=lambda i: pref_len - pref.index((i.name, i.player))
|
||||
if (i.name, i.player) in sort_dict else 0)
|
||||
|
||||
|
||||
def calc_trash_locations(world, player):
|
||||
total_count, gt_count = 0, 0
|
||||
for loc in world.get_locations():
|
||||
@@ -656,6 +690,40 @@ def sell_keys(world, player):
|
||||
world.itempool.remove(universal_key)
|
||||
|
||||
|
||||
def verify(item_to_place, item_locations, state, spp, pac, key_pool, world):
|
||||
if world.item_pool_config.verify:
|
||||
logger = logging.getLogger('')
|
||||
item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name
|
||||
item_player = item_to_place.player
|
||||
config = world.item_pool_config
|
||||
if (item_name, item_player) in config.verify:
|
||||
tests = config.verify[(item_name, item_player)]
|
||||
issues = []
|
||||
for location in item_locations:
|
||||
if location.name in tests:
|
||||
expected = tests[location.name]
|
||||
spot = verify_spot_to_fill(location, item_to_place, state, spp, pac, key_pool, world)
|
||||
if spot and (item_to_place.smallkey or item_to_place.bigkey):
|
||||
location.item = None
|
||||
location.event = False
|
||||
if item_to_place.smallkey:
|
||||
key_pool.append(item_to_place)
|
||||
if (expected and spot) or (not expected and spot is None):
|
||||
logger.debug(f'Placing {item_name} ({item_player}) at {location.name} was {expected}')
|
||||
config.verify_count += 1
|
||||
if config.verify_count >= config.verify_target:
|
||||
exit()
|
||||
else:
|
||||
issues.append((item_name, item_player, location.name, expected))
|
||||
if len(issues) > 0:
|
||||
for name, player, loc, expected in issues:
|
||||
if expected:
|
||||
logger.error(f'Could not place {name} ({player}) at {loc}')
|
||||
else:
|
||||
logger.error(f'{name} ({player}) should not be allowed at {loc}')
|
||||
raise Exception(f'Test failed placing {name}')
|
||||
|
||||
|
||||
def balance_multiworld_progression(world):
|
||||
state = CollectionState(world)
|
||||
checked_locations = set()
|
||||
|
||||
@@ -1589,6 +1589,13 @@ def fill_specific_items(world):
|
||||
item_player = player if len(item_parts) < 2 else int(item_parts[1])
|
||||
item_name = item_parts[0]
|
||||
world.item_pool_config.preferred[(item_name, item_player)] = placement['locations']
|
||||
elif placement['type'] == 'Verification':
|
||||
item = placement['item']
|
||||
item_parts = item.split('#')
|
||||
item_player = player if len(item_parts) < 2 else int(item_parts[1])
|
||||
item_name = item_parts[0]
|
||||
world.item_pool_config.verify[(item_name, item_player)] = placement['locations']
|
||||
world.item_pool_config.verify_target += len(placement['locations'])
|
||||
|
||||
|
||||
def get_item_and_event_flag(item, world, player, dungeon_pool, prize_set, prize_pool):
|
||||
|
||||
+31
-17
@@ -5,7 +5,7 @@ from collections import defaultdict, deque
|
||||
from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType
|
||||
from Regions import dungeon_events
|
||||
from Dungeons import dungeon_keys, dungeon_bigs, dungeon_table
|
||||
from DungeonGenerator import ExplorationState, special_big_key_doors, count_locations_exclude_big_chest, prize_or_event
|
||||
from DungeonGenerator import ExplorationState, get_special_big_key_doors, count_locations_exclude_big_chest, prize_or_event
|
||||
from DungeonGenerator import reserved_location, blind_boss_unavail
|
||||
|
||||
|
||||
@@ -59,13 +59,16 @@ class KeyLogic(object):
|
||||
self.placement_rules = []
|
||||
self.location_rules = {}
|
||||
self.outside_keys = 0
|
||||
self.outside_keys_locations = set()
|
||||
self.dungeon = dungeon_name
|
||||
self.sm_doors = {}
|
||||
self.prize_location = None
|
||||
|
||||
def check_placement(self, unplaced_keys, wild_keys, big_key_loc=None, prize_loc=None, cr_count=7):
|
||||
def check_placement(self, unplaced_keys, wild_keys, reached_keys, self_locking_keys,
|
||||
big_key_loc=None, prize_loc=None, cr_count=7):
|
||||
for rule in self.placement_rules:
|
||||
if not rule.is_satisfiable(self.outside_keys, wild_keys, unplaced_keys, big_key_loc, prize_loc, cr_count):
|
||||
if not rule.is_satisfiable(self.outside_keys_locations, wild_keys, reached_keys, self_locking_keys,
|
||||
unplaced_keys, big_key_loc, prize_loc, cr_count):
|
||||
return False
|
||||
if big_key_loc:
|
||||
for rule_a, rule_b in itertools.combinations(self.placement_rules, 2):
|
||||
@@ -159,7 +162,8 @@ class PlacementRule(object):
|
||||
left -= rule_needed
|
||||
return False
|
||||
|
||||
def is_satisfiable(self, outside_keys, wild_keys, unplaced_keys, big_key_loc, prize_location, cr_count):
|
||||
def is_satisfiable(self, outside_keys_locations, wild_keys, reached_keys, self_locking_keys, unplaced_keys,
|
||||
big_key_loc, prize_location, cr_count):
|
||||
if self.prize_relevance and prize_location:
|
||||
if self.prize_relevance == 'BigBomb':
|
||||
if prize_location.item.name not in ['Crystal 5', 'Crystal 6']:
|
||||
@@ -186,10 +190,11 @@ class PlacementRule(object):
|
||||
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 = len(outside_keys_locations)
|
||||
# todo: sometimes we need an extra empty chest to accomodate the big key too
|
||||
# dungeon bias seed 563518200 for example
|
||||
threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk
|
||||
threshold -= self_locking_keys
|
||||
if not wild_keys:
|
||||
empty_chests = 0
|
||||
for loc in check_locations:
|
||||
@@ -200,7 +205,8 @@ class PlacementRule(object):
|
||||
place_able_keys = min(empty_chests, unplaced_keys)
|
||||
available_keys += place_able_keys
|
||||
else:
|
||||
available_keys += unplaced_keys
|
||||
available_keys += len(reached_keys.difference(outside_keys_locations)) # already placed small keys
|
||||
available_keys += unplaced_keys # small keys not yet placed
|
||||
return available_keys >= threshold
|
||||
|
||||
|
||||
@@ -1002,7 +1008,7 @@ def find_worst_counter_wo_bk(small_key_num, accessible_set, door, odd_ctr, key_c
|
||||
|
||||
|
||||
def open_a_door(door, child_state, flat_proposal, world, player):
|
||||
if door.bigKey or door.name in special_big_key_doors:
|
||||
if door.bigKey or door.name in get_special_big_key_doors(world, player):
|
||||
child_state.big_key_opened = True
|
||||
child_state.avail_doors.extend(child_state.big_doors)
|
||||
child_state.opened_doors.extend(set([d.door for d in child_state.big_doors]))
|
||||
@@ -1485,7 +1491,8 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa
|
||||
else:
|
||||
ttl_locations = count_locations_exclude_big_chest(state.found_locations, world, player)
|
||||
ttl_small_key_only = count_small_key_only_locations(state)
|
||||
available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player)
|
||||
available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state,
|
||||
key_layout, world, player)
|
||||
available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player)
|
||||
if invalid_self_locking_key(key_layout, state, prev_state, prev_avail, world, player):
|
||||
return False
|
||||
@@ -1615,18 +1622,24 @@ def determine_prize_lock(key_layout, world, player):
|
||||
key_layout.prize_can_lock = True
|
||||
|
||||
|
||||
def cnt_avail_small_locations(free_locations, key_only, state, world, player):
|
||||
if world.keyshuffle[player] == 'none':
|
||||
def cnt_avail_small_locations(free_locations, key_only, state, key_layout, world, player):
|
||||
std_flag = world.mode[player] == 'standard' and key_layout.sector.name == 'Hyrule Castle'
|
||||
if world.keyshuffle[player] == 'none' or std_flag:
|
||||
bk_adj = 1 if state.big_key_opened and not state.big_key_special else 0
|
||||
avail_chest_keys = min(free_locations - bk_adj, state.key_locations - key_only)
|
||||
# this is the secret passage, could expand to Uncle/Links House with appropriate logic
|
||||
std_adj = 1 if std_flag and world.keyshuffle[player] != 'none' else 0
|
||||
avail_chest_keys = min(free_locations + std_adj - bk_adj, state.key_locations - key_only)
|
||||
return max(0, avail_chest_keys + key_only - state.used_smalls)
|
||||
return state.key_locations - state.used_smalls
|
||||
|
||||
|
||||
def cnt_avail_small_locations_by_ctr(free_locations, counter, layout, world, player):
|
||||
if world.keyshuffle[player] == 'none':
|
||||
std_flag = world.mode[player] == 'standard' and layout.sector.name == 'Hyrule Castle'
|
||||
if world.keyshuffle[player] == 'none' or std_flag:
|
||||
bk_adj = 1 if counter.big_key_opened and not layout.big_key_special else 0
|
||||
avail_chest_keys = min(free_locations - bk_adj, layout.max_chests)
|
||||
# this is the secret passage, could expand to Uncle/Links House with appropriate logic
|
||||
std_adj = 1 if std_flag and world.keyshuffle[player] != 'none' else 0
|
||||
avail_chest_keys = min(free_locations + std_adj - bk_adj, layout.max_chests)
|
||||
return max(0, avail_chest_keys + len(counter.key_only_locations) - counter.used_keys)
|
||||
return layout.max_chests + len(counter.key_only_locations) - counter.used_keys
|
||||
|
||||
@@ -1683,10 +1696,10 @@ def create_key_counters(key_layout, world, player):
|
||||
if door.dest in flat_proposal and door.type != DoorType.SpiralStairs:
|
||||
key_layout.found_doors.add(door.dest)
|
||||
child_state = parent_state.copy()
|
||||
if door.bigKey or door.name in special_big_key_doors:
|
||||
if door.bigKey or door.name in get_special_big_key_doors(world, player):
|
||||
key_layout.key_logic.bk_doors.add(door)
|
||||
# open the door, if possible
|
||||
if can_open_door(door, child_state, world, player):
|
||||
if can_open_door(door, child_state, key_layout, world, player):
|
||||
open_a_door(door, child_state, flat_proposal, world, player)
|
||||
expand_key_state(child_state, flat_proposal, world, player)
|
||||
code = state_id(child_state, key_layout.flat_prop)
|
||||
@@ -1707,14 +1720,15 @@ def find_outside_connection(region):
|
||||
return None, None
|
||||
|
||||
|
||||
def can_open_door(door, state, world, player):
|
||||
def can_open_door(door, state, key_layout, world, player):
|
||||
if state.big_key_opened:
|
||||
ttl_locations = count_free_locations(state, world, player)
|
||||
else:
|
||||
ttl_locations = count_locations_exclude_big_chest(state.found_locations, world, player)
|
||||
if door.smallKey:
|
||||
ttl_small_key_only = count_small_key_only_locations(state)
|
||||
available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player)
|
||||
available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state,
|
||||
key_layout, world, player)
|
||||
return available_small_locations > 0
|
||||
elif door.bigKey:
|
||||
available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player)
|
||||
|
||||
@@ -35,7 +35,7 @@ from source.overworld.EntranceShuffle2 import link_entrances_new
|
||||
from source.tools.BPS import create_bps_from_data
|
||||
from source.classes.CustomSettings import CustomSettings
|
||||
|
||||
__version__ = '1.2.0.7-u'
|
||||
__version__ = '1.2.0.8-u'
|
||||
|
||||
from source.classes.BabelFish import BabelFish
|
||||
|
||||
@@ -116,6 +116,8 @@ def main(args, seed=None, fish=None):
|
||||
world.beemizer = args.beemizer.copy()
|
||||
world.intensity = {player: random.randint(1, 3) if args.intensity[player] == 'random' else int(args.intensity[player]) for player in range(1, world.players + 1)}
|
||||
world.door_type_mode = args.door_type_mode.copy()
|
||||
world.trap_door_mode = args.trap_door_mode.copy()
|
||||
world.key_logic_algorithm = args.key_logic_algorithm.copy()
|
||||
world.decoupledoors = args.decoupledoors.copy()
|
||||
world.experimental = args.experimental.copy()
|
||||
world.dungeon_counters = args.dungeon_counters.copy()
|
||||
|
||||
@@ -108,6 +108,15 @@ These are now independent of retro mode and have three options: None, Random, an
|
||||
* Bonk Fairy (Dark)
|
||||
|
||||
# Bug Fixes and Notes
|
||||
* 1.2.0.8-u
|
||||
* New Features: trap_door_mode and key_logic_algorithm
|
||||
* Change S&Q in door shuffle + standard during escape to spawn as Uncle
|
||||
* Fix for vanilla doors + certain ER modes
|
||||
* Fix for unintentional decoupled door in standard
|
||||
* Fix a problem with BK doors being one-sided
|
||||
* Change to how wilds keys are placed in standard, better randomization
|
||||
* Removed a Triforce text
|
||||
* Fix for Desert Tiles 1 key door
|
||||
* 1.2.0.7-u
|
||||
* Fix for some misery mire key logic
|
||||
* Minor standard generation fix
|
||||
|
||||
@@ -38,7 +38,7 @@ from source.dungeon.RoomList import Room0127
|
||||
|
||||
|
||||
JAP10HASH = '03a63945398191337e896e5771f77173'
|
||||
RANDOMIZERBASEHASH = '45aa34e724750862234f9a4b09caa3d6'
|
||||
RANDOMIZERBASEHASH = '1694ba41bf6ab7086f05e914e8d08433'
|
||||
|
||||
|
||||
class JsonRom(object):
|
||||
|
||||
+1
-1
@@ -103,7 +103,7 @@ def create_rooms(world, player):
|
||||
Room(player, 0x60, 0x51309).door(Position.NorthE2, DoorKind.NormalLow2).door(Position.East2, DoorKind.NormalLow2).door(Position.East2, DoorKind.ToggleFlag).door(Position.EastN, DoorKind.Normal).door(Position.SouthE, DoorKind.Normal).door(Position.SouthE, DoorKind.IncognitoEntrance),
|
||||
Room(player, 0x61, 0x51454).door(Position.West2, DoorKind.NormalLow).door(Position.West2, DoorKind.ToggleFlag).door(Position.East2, DoorKind.NormalLow).door(Position.East2, DoorKind.ToggleFlag).door(Position.South2, DoorKind.NormalLow).door(Position.South2, DoorKind.IncognitoEntrance).door(Position.WestN, DoorKind.Normal),
|
||||
Room(player, 0x62, 0x51577).door(Position.West2, DoorKind.NormalLow2).door(Position.West2, DoorKind.ToggleFlag).door(Position.NorthW2, DoorKind.NormalLow2).door(Position.North, DoorKind.Normal).door(Position.SouthW, DoorKind.Normal).door(Position.SouthW, DoorKind.IncognitoEntrance),
|
||||
Room(player, 0x63, 0xf88ed).door(Position.NorthE, DoorKind.StairKey).door(Position.InteriorW, DoorKind.TrapTriggerable).door(Position.SouthW, DoorKind.DungeonEntrance), # looked like a huge typo - I had to guess on StairKey
|
||||
Room(player, 0x63, 0xf88ed).door(Position.NorthW, DoorKind.StairKey).door(Position.InteriorW, DoorKind.TrapTriggerable).door(Position.SouthW, DoorKind.DungeonEntrance), # looked like a huge typo - I had to guess on StairKey
|
||||
Room(player, 0x64, 0xfda53).door(Position.InteriorS, DoorKind.Trap2),
|
||||
Room(player, 0x65, 0xfdac5).door(Position.InteriorS, DoorKind.Normal),
|
||||
Room(player, 0x66, 0xfa01b).door(Position.InteriorE2, DoorKind.Waterfall).door(Position.SouthW2, DoorKind.NormalLow2).door(Position.SouthW2, DoorKind.ToggleFlag).door(Position.InteriorW2, DoorKind.NormalLow2),
|
||||
|
||||
@@ -1459,7 +1459,7 @@ def standard_rules(world, player):
|
||||
# zelda should be saved before agahnim is in play
|
||||
add_rule(world.get_location('Agahnim 1', player), lambda state: state.has('Zelda Delivered', player))
|
||||
|
||||
# too restrictive for crossed?
|
||||
# uncle can't have keys generally because unplaced items aren't used here
|
||||
def uncle_item_rule(item):
|
||||
copy_state = CollectionState(world)
|
||||
copy_state.collect(item)
|
||||
@@ -1817,13 +1817,16 @@ bunny_impassible_doors = {
|
||||
|
||||
def add_key_logic_rules(world, player):
|
||||
key_logic = world.key_logic[player]
|
||||
eval_func = eval_small_key_door
|
||||
if world.key_logic_algorithm[player] == 'strict' and world.keyshuffle[player] == 'wild':
|
||||
eval_func = eval_small_key_door_strict
|
||||
for d_name, d_logic in key_logic.items():
|
||||
for door_name, rule in d_logic.door_rules.items():
|
||||
door_entrance = world.get_entrance(door_name, player)
|
||||
add_rule(door_entrance, eval_small_key_door(door_name, d_name, player))
|
||||
add_rule(door_entrance, eval_func(door_name, d_name, player))
|
||||
if door_entrance.door.dependents:
|
||||
for dep in door_entrance.door.dependents:
|
||||
add_rule(dep.entrance, eval_small_key_door(door_name, d_name, player))
|
||||
add_rule(dep.entrance, eval_func(door_name, d_name, player))
|
||||
for location in d_logic.bk_restricted:
|
||||
if not location.forced_item:
|
||||
forbid_item(location, d_logic.bk_name, player)
|
||||
@@ -1870,10 +1873,24 @@ def eval_small_key_door_main(state, door_name, dungeon, player):
|
||||
return door_openable
|
||||
|
||||
|
||||
def eval_small_key_door_strict_main(state, door_name, dungeon, player):
|
||||
if state.is_door_open(door_name, player):
|
||||
return True
|
||||
key_layout = state.world.key_layout[player][dungeon]
|
||||
number = key_layout.max_chests
|
||||
if number <= 0:
|
||||
return True
|
||||
return state.has_sm_key_strict(key_layout.key_logic.small_key_name, player, number)
|
||||
|
||||
|
||||
def eval_small_key_door(door_name, dungeon, player):
|
||||
return lambda state: eval_small_key_door_main(state, door_name, dungeon, player)
|
||||
|
||||
|
||||
def eval_small_key_door_strict(door_name, dungeon, player):
|
||||
return lambda state: eval_small_key_door_strict_main(state, door_name, dungeon, player)
|
||||
|
||||
|
||||
def allow_big_key_in_big_chest(bk_name, player):
|
||||
return lambda state, item: item.name == bk_name and item.player == player
|
||||
|
||||
|
||||
@@ -91,7 +91,6 @@ Triforce_texts = [
|
||||
'Who stole the fourth triangle?',
|
||||
'Trifource?\nMore Like Tritrice, am I right?'
|
||||
'\n Well Done!',
|
||||
'You just wasted 2 hours of your life.',
|
||||
'This was meant to be a trapezoid',
|
||||
# these ones are from web randomizer
|
||||
"\n G G",
|
||||
|
||||
Binary file not shown.
@@ -41,6 +41,14 @@
|
||||
big: 2
|
||||
all: 1
|
||||
chaos: 1
|
||||
trap_door_mode:
|
||||
vanilla: 1
|
||||
boss: 0
|
||||
oneway: 0
|
||||
key_logic_algorithm:
|
||||
default: 1
|
||||
partial: 0
|
||||
strict: 0
|
||||
decoupledoors: off
|
||||
dropshuffle:
|
||||
on: 1
|
||||
|
||||
+19
-1
@@ -7,12 +7,30 @@ algorithm:
|
||||
district: 1
|
||||
door_shuffle:
|
||||
vanilla: 1
|
||||
basic: 2
|
||||
basic: 1
|
||||
partitioned: 1
|
||||
crossed: 3 # crossed yield more errors so is preferred
|
||||
intensity:
|
||||
1: 1
|
||||
2: 1
|
||||
3: 2 # intensity 3 usually yield more errors
|
||||
door_type_mode:
|
||||
original: 2
|
||||
big: 2
|
||||
all: 1
|
||||
chaos: 1
|
||||
trap_door_mode:
|
||||
vanilla: 3 # more errors
|
||||
optional: 1
|
||||
boss: 1
|
||||
oneway: 1
|
||||
key_logic_algorithm:
|
||||
default: 1
|
||||
partial: 0
|
||||
strict: 0
|
||||
decoupledoors:
|
||||
off: 9 # more strict
|
||||
on: 1
|
||||
dropshuffle:
|
||||
on: 1
|
||||
off: 1
|
||||
|
||||
@@ -239,6 +239,21 @@
|
||||
"chaos"
|
||||
]
|
||||
},
|
||||
"trap_door_mode": {
|
||||
"choices": [
|
||||
"vanilla",
|
||||
"optional",
|
||||
"boss",
|
||||
"oneway"
|
||||
]
|
||||
},
|
||||
"key_logic_algorithm": {
|
||||
"choices": [
|
||||
"default",
|
||||
"partial",
|
||||
"strict"
|
||||
]
|
||||
},
|
||||
"decoupledoors": {
|
||||
"action": "store_true",
|
||||
"type": "bool"
|
||||
|
||||
@@ -284,6 +284,19 @@
|
||||
"all: Adds traps doors (and any future supported door types)",
|
||||
"chaos: Increases the number of door types in all dungeon pools"
|
||||
],
|
||||
"trap_door_mode" : [
|
||||
"Trap Door Removal (default: %(default)s)",
|
||||
"vanilla: No trap door removal",
|
||||
"optional: Trap doors removed if blocking",
|
||||
"boss: Also remove boss traps",
|
||||
"oneway: Remove all annoying trap doors"
|
||||
],
|
||||
"key_logic_algorithm": [
|
||||
"Key Logic Algorithm (default: %(default)s)",
|
||||
"default: Balance between safety and randomization",
|
||||
"partial: Partial protection when using certain minor glitches",
|
||||
"strict: Ensure small keys are available"
|
||||
],
|
||||
"decoupledoors" : [ "Door entrances and exits are decoupled" ],
|
||||
"experimental": [ "Enable experimental features. (default: %(default)s)" ],
|
||||
"dungeon_counters": [ "Enable dungeon chest counters. (default: %(default)s)" ],
|
||||
|
||||
@@ -91,6 +91,17 @@
|
||||
"randomizer.dungeon.door_type_mode.all": "Adds Trap Doors",
|
||||
"randomizer.dungeon.door_type_mode.chaos": "Increases all door types",
|
||||
|
||||
"randomizer.dungeon.trap_door_mode": "Trap Door Removal",
|
||||
"randomizer.dungeon.trap_door_mode.vanilla": "No Removal",
|
||||
"randomizer.dungeon.trap_door_mode.optional": "Removed If Blocking Path",
|
||||
"randomizer.dungeon.trap_door_mode.boss": "Also Remove Boss Traps",
|
||||
"randomizer.dungeon.trap_door_mode.oneway": "Remove All Annoying Traps",
|
||||
|
||||
"randomizer.dungeon.key_logic_algorithm": "Key Logic Algorithm",
|
||||
"randomizer.dungeon.key_logic_algorithm.default": "Default",
|
||||
"randomizer.dungeon.key_logic_algorithm.partial": "Partial Protection",
|
||||
"randomizer.dungeon.key_logic_algorithm.strict": "Strict",
|
||||
|
||||
"randomizer.dungeon.experimental": "Enable Experimental Features",
|
||||
|
||||
"randomizer.dungeon.dungeon_counters": "Dungeon Chest Counters",
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
},
|
||||
"door_type_mode": {
|
||||
"type": "selectbox",
|
||||
"default": "basic",
|
||||
"default": "original",
|
||||
"options": [
|
||||
"original",
|
||||
"big",
|
||||
@@ -44,6 +44,28 @@
|
||||
"width": 45
|
||||
}
|
||||
},
|
||||
"trap_door_mode": {
|
||||
"type": "selectbox",
|
||||
"default": "optional",
|
||||
"options": [
|
||||
"vanilla",
|
||||
"optional",
|
||||
"boss",
|
||||
"oneway"
|
||||
],
|
||||
"config": {
|
||||
"width": 30
|
||||
}
|
||||
},
|
||||
"key_logic_algorithm": {
|
||||
"type": "selectbox",
|
||||
"default": "default",
|
||||
"options": [
|
||||
"default",
|
||||
"partial",
|
||||
"strict"
|
||||
]
|
||||
},
|
||||
"decoupledoors": { "type": "checkbox" },
|
||||
"keydropshuffle": { "type": "checkbox" },
|
||||
"pottery": {
|
||||
|
||||
@@ -109,6 +109,8 @@ class CustomSettings(object):
|
||||
args.standardize_palettes[p])
|
||||
args.intensity[p] = get_setting(settings['intensity'], args.intensity[p])
|
||||
args.door_type_mode[p] = get_setting(settings['door_type_mode'], args.door_type_mode[p])
|
||||
args.trap_door_mode[p] = get_setting(settings['trap_door_mode'], args.trap_door_mode[p])
|
||||
args.key_logic_algorithm[p] = get_setting(settings['key_logic_algorithm'], args.key_logic_algorithm[p])
|
||||
args.decoupledoors[p] = get_setting(settings['decoupledoors'], args.decoupledoors[p])
|
||||
args.dungeon_counters[p] = get_setting(settings['dungeon_counters'], args.dungeon_counters[p])
|
||||
args.crystals_gt[p] = get_setting(settings['crystals_gt'], args.crystals_gt[p])
|
||||
@@ -219,6 +221,8 @@ class CustomSettings(object):
|
||||
settings_dict[p]['door_shuffle'] = world.doorShuffle[p]
|
||||
settings_dict[p]['intensity'] = world.intensity[p]
|
||||
settings_dict[p]['door_type_mode'] = world.door_type_mode[p]
|
||||
settings_dict[p]['trap_door_mode'] = world.trap_door_mode[p]
|
||||
settings_dict[p]['key_logic_algorithm'] = world.key_logic_algorithm[p]
|
||||
settings_dict[p]['decoupledoors'] = world.decoupledoors[p]
|
||||
settings_dict[p]['logic'] = world.logic[p]
|
||||
settings_dict[p]['mode'] = world.mode[p]
|
||||
|
||||
@@ -109,6 +109,8 @@ SETTINGSTOPROCESS = {
|
||||
"dungeondoorshuffle": "door_shuffle",
|
||||
"dungeonintensity": "intensity",
|
||||
"door_type_mode": "door_type_mode",
|
||||
"trap_door_mode": "trap_door_mode",
|
||||
"key_logic_algorithm": "key_logic_algorithm",
|
||||
"decoupledoors": "decoupledoors",
|
||||
"keydropshuffle": "keydropshuffle",
|
||||
"dropshuffle": "dropshuffle",
|
||||
|
||||
@@ -320,7 +320,10 @@ def determine_paths_for_dungeon(world, player, all_regions, name):
|
||||
paths.append('Hyrule Dungeon Cellblock')
|
||||
paths.append(('Hyrule Dungeon Cellblock', 'Hyrule Castle Throne Room'))
|
||||
entrance = next(x for x in world.dungeon_portals[player] if x.name == 'Hyrule Castle South')
|
||||
# todo: in non-er, we can use the other portals too
|
||||
paths.append(('Hyrule Dungeon Cellblock', entrance.door.entrance.parent_region.name))
|
||||
paths.append(('Hyrule Castle Throne Room', [entrance.door.entrance.parent_region.name,
|
||||
'Hyrule Dungeon Cellblock']))
|
||||
if world.doorShuffle[player] in ['basic'] and name == 'Thieves Town':
|
||||
paths.append('Thieves Attic Window')
|
||||
elif 'Thieves Attic Window' in all_r_names:
|
||||
@@ -434,6 +437,13 @@ def connect_simple_door(exit_door, region):
|
||||
|
||||
|
||||
special_big_key_doors = ['Hyrule Dungeon Cellblock Door', "Thieves Blind's Cell Door"]
|
||||
std_special_big_key_doors = ['Hyrule Castle Throne Room Tapestry'] + special_big_key_doors
|
||||
|
||||
|
||||
def get_special_big_key_doors(world, player):
|
||||
if world.mode[player] == 'standard':
|
||||
return std_special_big_key_doors
|
||||
return special_big_key_doors
|
||||
|
||||
|
||||
class ExplorationState(object):
|
||||
@@ -621,6 +631,7 @@ class ExplorationState(object):
|
||||
elif not self.in_door_list(door, self.avail_doors):
|
||||
self.append_door_to_list(door, self.avail_doors, flag)
|
||||
|
||||
# same as above but traps are ignored, and flag is not used
|
||||
def add_all_doors_check_proposed_2(self, region, proposed_map, valid_doors, world, player):
|
||||
for door in get_doors(world, region, player):
|
||||
if door in proposed_map and door.name in valid_doors:
|
||||
@@ -641,6 +652,27 @@ class ExplorationState(object):
|
||||
elif not self.in_door_list(door, self.avail_doors):
|
||||
self.append_door_to_list(door, self.avail_doors)
|
||||
|
||||
# same as above but traps are checked for
|
||||
def add_all_doors_check_proposed_3(self, region, proposed_map, valid_doors, world, player):
|
||||
for door in get_doors(world, region, player):
|
||||
if door in proposed_map and door.name in valid_doors:
|
||||
self.visited_doors.add(door)
|
||||
if self.can_traverse(door):
|
||||
if door.controller is not None:
|
||||
door = door.controller
|
||||
if door.dest is None and door not in proposed_map.keys() and door.name in valid_doors:
|
||||
if not self.in_door_list_ic(door, self.unattached_doors):
|
||||
self.append_door_to_list(door, self.unattached_doors)
|
||||
else:
|
||||
other = self.find_door_in_list(door, self.unattached_doors)
|
||||
if self.crystal != other.crystal:
|
||||
other.crystal = CrystalBarrier.Either
|
||||
elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door,
|
||||
self.event_doors):
|
||||
self.append_door_to_list(door, self.event_doors)
|
||||
elif not self.in_door_list(door, self.avail_doors):
|
||||
self.append_door_to_list(door, self.avail_doors)
|
||||
|
||||
def add_all_doors_check_proposed_traps(self, region, proposed_traps, world, player):
|
||||
for door in get_doors(world, region, player):
|
||||
if self.can_traverse_ignore_traps(door) and door not in proposed_traps:
|
||||
@@ -674,7 +706,7 @@ class ExplorationState(object):
|
||||
if door in key_door_proposal and door not in self.opened_doors:
|
||||
if not self.in_door_list(door, self.small_doors):
|
||||
self.append_door_to_list(door, self.small_doors)
|
||||
elif (door.bigKey or door.name in special_big_key_doors) and not self.big_key_opened:
|
||||
elif (door.bigKey or door.name in get_special_big_key_doors(world, player)) and not self.big_key_opened:
|
||||
if not self.in_door_list(door, self.big_doors):
|
||||
self.append_door_to_list(door, self.big_doors)
|
||||
elif door.req_event is not None and door.req_event not in self.events:
|
||||
@@ -827,7 +859,10 @@ def extend_reachable_state_lenient(search_regions, state, proposed_map, all_regi
|
||||
local_state = state.copy()
|
||||
for region in search_regions:
|
||||
local_state.visit_region(region)
|
||||
local_state.add_all_doors_check_proposed_2(region, proposed_map, valid_doors, world, player)
|
||||
if world.trap_door_mode[player] == 'vanilla':
|
||||
local_state.add_all_doors_check_proposed_3(region, proposed_map, valid_doors, world, player)
|
||||
else:
|
||||
local_state.add_all_doors_check_proposed_2(region, proposed_map, valid_doors, world, player)
|
||||
while len(local_state.avail_doors) > 0:
|
||||
explorable_door = local_state.next_avail_door()
|
||||
if explorable_door.door in proposed_map:
|
||||
@@ -838,7 +873,10 @@ def extend_reachable_state_lenient(search_regions, state, proposed_map, all_regi
|
||||
if (valid_region_to_explore_in_regions(connect_region, all_regions, world, player)
|
||||
and not local_state.visited(connect_region)):
|
||||
local_state.visit_region(connect_region)
|
||||
local_state.add_all_doors_check_proposed_2(connect_region, proposed_map, valid_doors, world, player)
|
||||
if world.trap_door_mode[player] == 'vanilla':
|
||||
local_state.add_all_doors_check_proposed_3(connect_region, proposed_map, valid_doors, world, player)
|
||||
else:
|
||||
local_state.add_all_doors_check_proposed_2(connect_region, proposed_map, valid_doors, world, player)
|
||||
return local_state
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@ class ItemPoolConfig(object):
|
||||
self.reserved_locations = defaultdict(set)
|
||||
self.restricted = {}
|
||||
self.preferred = {}
|
||||
self.verify = {}
|
||||
self.verify_count = 0
|
||||
self.verify_target = 0
|
||||
|
||||
self.recorded_choices = []
|
||||
|
||||
@@ -435,6 +438,9 @@ def filter_locations(item_to_place, locations, world, vanilla_skip=False, potion
|
||||
if (item_name, item_to_place.player) in config.preferred:
|
||||
locs = config.preferred[(item_name, item_to_place.player)]
|
||||
return sorted(locations, key=lambda l: 0 if l.name in locs else 1)
|
||||
if (item_name, item_to_place.player) in config.verify:
|
||||
locs = config.verify[(item_name, item_to_place.player)].keys()
|
||||
return sorted(locations, key=lambda l: 0 if l.name in locs else 1)
|
||||
return locations
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import importlib.util
|
||||
import webbrowser
|
||||
from tkinter import Tk, Label, Button, Frame
|
||||
|
||||
|
||||
def check_requirements(console=False):
|
||||
@@ -26,6 +24,9 @@ def check_requirements(console=False):
|
||||
logger.error('See the step about "Installing Platform-specific dependencies":')
|
||||
logger.error('https://github.com/aerinon/ALttPDoorRandomizer/blob/DoorDev/docs/BUILDING.md')
|
||||
else:
|
||||
import webbrowser
|
||||
from tkinter import Tk, Label, Button, Frame
|
||||
|
||||
master = Tk()
|
||||
master.title('Error')
|
||||
frame = Frame(master)
|
||||
|
||||
@@ -304,7 +304,8 @@ def do_main_shuffle(entrances, exits, avail, mode_def):
|
||||
unused_entrances = set()
|
||||
if not cross_world:
|
||||
lw_entrances, dw_entrances = [], []
|
||||
for x in rem_entrances:
|
||||
left = sorted(rem_entrances)
|
||||
for x in left:
|
||||
if bonk_fairy_exception(x):
|
||||
lw_entrances.append(x) if x in LW_Entrances else dw_entrances.append(x)
|
||||
do_same_world_connectors(lw_entrances, dw_entrances, multi_exit_caves, avail)
|
||||
|
||||
@@ -95,6 +95,8 @@ def roll_settings(weights):
|
||||
ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla'
|
||||
ret.intensity = get_choice('intensity')
|
||||
ret.door_type_mode = get_choice('door_type_mode')
|
||||
ret.trap_door_mode = get_choice('trap_door_mode')
|
||||
ret.key_logic_algorithm = get_choice('key_logic_algorithm')
|
||||
ret.decoupledoors = get_choice('decoupledoors') == 'on'
|
||||
ret.experimental = get_choice('experimental') == 'on'
|
||||
ret.collection_rate = get_choice('collection_rate') == 'on'
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import fnmatch
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import multiprocessing
|
||||
import concurrent.futures
|
||||
import argparse
|
||||
from collections import OrderedDict
|
||||
|
||||
cpu_threads = multiprocessing.cpu_count()
|
||||
py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
||||
|
||||
|
||||
def main(args=None):
|
||||
successes = []
|
||||
errors = []
|
||||
task_mapping = []
|
||||
tests = OrderedDict()
|
||||
|
||||
successes.append(f"Testing DR (NewTestSuite)")
|
||||
print(successes[0])
|
||||
|
||||
# max_attempts = args.count
|
||||
pool = concurrent.futures.ThreadPoolExecutor(max_workers=cpu_threads)
|
||||
dead_or_alive = 0
|
||||
alive = 0
|
||||
|
||||
def test(test_name: str, command: str, test_file: str):
|
||||
tests[test_name] = [command]
|
||||
|
||||
base_command = f"python3.8 DungeonRandomizer.py --suppress_rom --suppress_spoiler"
|
||||
|
||||
def gen_seed():
|
||||
task_command = base_command + " " + command
|
||||
return subprocess.run(task_command, capture_output=True, shell=True, text=True)
|
||||
|
||||
task = pool.submit(gen_seed)
|
||||
task.success = False
|
||||
task.name = test_name
|
||||
task.test_file = test_file
|
||||
task.cmd = base_command + " " + command
|
||||
task_mapping.append(task)
|
||||
|
||||
for test_suite, test_files in args.test_suite.items():
|
||||
for test_file in test_files:
|
||||
test(test_suite, f'--customizer {os.path.join(test_suite, test_file)}', test_file)
|
||||
|
||||
from tqdm import tqdm
|
||||
with tqdm(concurrent.futures.as_completed(task_mapping),
|
||||
total=len(task_mapping), unit="seed(s)",
|
||||
desc=f"Success rate: 0.00%") as progressbar:
|
||||
for task in progressbar:
|
||||
dead_or_alive += 1
|
||||
try:
|
||||
result = task.result()
|
||||
if result.returncode:
|
||||
errors.append([task.name + ' ' + task.test_file, task.cmd, result.stderr])
|
||||
else:
|
||||
alive += 1
|
||||
task.success = True
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
progressbar.set_description(f"Success rate: {(alive/dead_or_alive)*100:.2f}% - {task.name}")
|
||||
|
||||
def get_results(testname: str):
|
||||
result = ""
|
||||
dead_or_alive = [task.success for task in task_mapping if task.name == testname]
|
||||
alive = [x for x in dead_or_alive if x]
|
||||
success = f"{testname} Rate: {(len(alive) / len(dead_or_alive)) * 100:.2f}%"
|
||||
successes.append(success)
|
||||
print(success)
|
||||
result += f"{(len(alive)/len(dead_or_alive))*100:.2f}%\t"
|
||||
return result.strip()
|
||||
|
||||
results = []
|
||||
for t in tests.keys():
|
||||
results.append(get_results(t))
|
||||
|
||||
for result in results:
|
||||
print(result)
|
||||
successes.append(result)
|
||||
|
||||
return successes, errors
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
successes = []
|
||||
|
||||
parser = argparse.ArgumentParser(add_help=False)
|
||||
# parser.add_argument('--count', default=0, type=lambda value: max(int(value), 0))
|
||||
parser.add_argument('--cpu_threads', default=cpu_threads, type=lambda value: max(int(value), 1))
|
||||
parser.add_argument('--help', default=False, action='store_true')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.help:
|
||||
parser.print_help()
|
||||
exit(0)
|
||||
|
||||
cpu_threads = args.cpu_threads
|
||||
|
||||
test_suites = {}
|
||||
# not sure if it supports subdirectories properly yet
|
||||
for root, dirnames, filenames in os.walk('test/suite'):
|
||||
test_suites[root] = fnmatch.filter(filenames, '*.yaml')
|
||||
|
||||
args = argparse.Namespace()
|
||||
args.test_suite = test_suites
|
||||
s, errors = main(args=args)
|
||||
if successes:
|
||||
successes += [""] * 2
|
||||
successes += s
|
||||
print()
|
||||
|
||||
if errors:
|
||||
with open(f"new-test-suite-errors.txt", 'w') as stream:
|
||||
for error in errors:
|
||||
stream.write(error[0] + "\n")
|
||||
stream.write(error[1] + "\n")
|
||||
stream.write(error[2] + "\n\n")
|
||||
|
||||
with open("new-test-suite-success.txt", "w") as stream:
|
||||
stream.write(str.join("\n", successes))
|
||||
|
||||
input("Press enter to continue")
|
||||
@@ -0,0 +1,44 @@
|
||||
# Possible improvements: account for items that are possibly in logic
|
||||
# Example: Mire Big Key in harmless means all 6 mire smalls required for fire-locked side,
|
||||
# if you have access to harmless via:
|
||||
# 2 pod smalls + bow, hammer or 3 pod small
|
||||
meta:
|
||||
players: 1
|
||||
settings:
|
||||
1:
|
||||
key_logic_algorithm: default
|
||||
keysanity: True
|
||||
crystals_needed_for_gt: 0 # to skip trash fill
|
||||
placements:
|
||||
1:
|
||||
Hobo: Big Key (Misery Mire)
|
||||
Waterfall Fairy - Left: Small Key (Misery Mire)
|
||||
Waterfall Fairy - Right: Small Key (Misery Mire)
|
||||
Palace of Darkness - Big Chest: Hammer
|
||||
advanced_placements:
|
||||
1:
|
||||
# Contrast with partial_2
|
||||
- type: Verification
|
||||
item: Flippers
|
||||
locations:
|
||||
Misery Mire - Map Chest: True
|
||||
Misery Mire - Main Lobby: True
|
||||
Misery Mire - Bridge Chest: True
|
||||
Misery Mire - Spike Chest: True
|
||||
Misery Mire - Compass Chest: False
|
||||
Misery Mire - Big Key Chest: False
|
||||
Misery Mire - Boss: False
|
||||
# Contrast with partial_3
|
||||
- type: Verification
|
||||
item: Big Key (Ganons Tower)
|
||||
locations:
|
||||
Ganons Tower - Big Key Chest: True
|
||||
Ganons Tower - Big Key Room - Left: True
|
||||
Ganons Tower - Big Key Room - Right: True
|
||||
Ganons Tower - Bob's Chest: True
|
||||
# Normal logic doesn't allow this placement
|
||||
# unless hammer is placed before it - no algorithm does this in non-keysanity, but possible in keysanity
|
||||
- type: Verification
|
||||
item: Small Key (Palace of Darkness)
|
||||
locations:
|
||||
Palace of Darkness - Dark Maze - Bottom: True
|
||||
@@ -0,0 +1,24 @@
|
||||
# Even though Lamp is Flipper-locked, this logic considers that a key could be wasted in the dark in mire
|
||||
# Only fire locked mire is off limits
|
||||
meta:
|
||||
players: 1
|
||||
settings:
|
||||
1:
|
||||
key_logic_algorithm: partial
|
||||
keysanity: true
|
||||
placements:
|
||||
1:
|
||||
Hobo: Lamp
|
||||
Waterfall Fairy - Left: Small Key (Misery Mire)
|
||||
advanced_placements:
|
||||
1:
|
||||
- type: Verification
|
||||
item: Flippers
|
||||
locations:
|
||||
Misery Mire - Map Chest: True
|
||||
Misery Mire - Main Lobby: True
|
||||
Misery Mire - Bridge Chest: True
|
||||
Misery Mire - Spike Chest: True
|
||||
Misery Mire - Compass Chest: False
|
||||
Misery Mire - Big Key Chest: False
|
||||
Misery Mire - Boss: False
|
||||
@@ -0,0 +1,26 @@
|
||||
# For contrast with default logic
|
||||
# This logic is not yet smart enough to allow the crystal blocked chests with two keys (Spike Pot and one other)
|
||||
meta:
|
||||
players: 1
|
||||
settings:
|
||||
1:
|
||||
key_logic_algorithm: partial
|
||||
keysanity: True
|
||||
placements:
|
||||
1:
|
||||
Hobo: Lamp
|
||||
Waterfall Fairy - Left: Small Key (Misery Mire)
|
||||
Waterfall Fairy - Right: Small Key (Misery Mire)
|
||||
Swamp Palace - Entrance: Big Key (Misery Mire)
|
||||
advanced_placements:
|
||||
1:
|
||||
- type: Verification
|
||||
item: Flippers
|
||||
locations:
|
||||
Misery Mire - Map Chest: False
|
||||
Misery Mire - Main Lobby: False
|
||||
Misery Mire - Bridge Chest: True
|
||||
Misery Mire - Spike Chest: True
|
||||
Misery Mire - Compass Chest: False
|
||||
Misery Mire - Big Key Chest: False
|
||||
Misery Mire - Boss: False
|
||||
@@ -0,0 +1,160 @@
|
||||
# For contrast with default logic
|
||||
# Examples of valid big key placement that doesn't work with pure worst case scenarios
|
||||
# Basically chests that are obtainable two ways+ of spending keys
|
||||
# (Possible fix: access to the extra door grants access to the mini helma key too)
|
||||
meta:
|
||||
players: 1
|
||||
settings:
|
||||
1:
|
||||
key_logic_algorithm: partial
|
||||
crystals_needed_for_gt: 0 # to skip trash fill
|
||||
advanced_placements:
|
||||
1:
|
||||
- type: Verification
|
||||
item: Big Key (Desert Palace)
|
||||
locations:
|
||||
Desert Palace - Big Chest: False
|
||||
Desert Palace - Big Key Chest: True
|
||||
Desert Palace - Boss: False
|
||||
Desert Palace - Compass Chest: True
|
||||
Desert Palace - Map Chest: True
|
||||
Desert Palace - Torch: True
|
||||
- type: Verification
|
||||
item: Big Key (Eastern Palace)
|
||||
locations:
|
||||
Eastern Palace - Big Chest: False
|
||||
Eastern Palace - Big Key Chest: True
|
||||
Eastern Palace - Boss: False
|
||||
Eastern Palace - Cannonball Chest: True
|
||||
Eastern Palace - Compass Chest: True
|
||||
Eastern Palace - Map Chest: True
|
||||
- type: Verification
|
||||
item: Big Key (Ganons Tower)
|
||||
locations:
|
||||
# These four require not wasting keys upstairs because the big key is down here
|
||||
Ganons Tower - Big Key Chest: False
|
||||
Ganons Tower - Big Key Room - Left: False
|
||||
Ganons Tower - Big Key Room - Right: False
|
||||
Ganons Tower - Bob's Chest: False
|
||||
# These are normal
|
||||
Ganons Tower - Big Chest: False
|
||||
Ganons Tower - Bob's Torch: True
|
||||
Ganons Tower - Compass Room - Bottom Left: True
|
||||
Ganons Tower - Compass Room - Bottom Right: True
|
||||
Ganons Tower - Compass Room - Top Left: True
|
||||
Ganons Tower - Compass Room - Top Right: True
|
||||
Ganons Tower - DMs Room - Bottom Left: True
|
||||
Ganons Tower - DMs Room - Bottom Right: True
|
||||
Ganons Tower - DMs Room - Top Left: True
|
||||
Ganons Tower - DMs Room - Top Right: True
|
||||
Ganons Tower - Firesnake Room: True
|
||||
Ganons Tower - Hope Room - Left: True
|
||||
Ganons Tower - Hope Room - Right: True
|
||||
Ganons Tower - Map Chest: True
|
||||
Ganons Tower - Mini Helmasaur Room - Left: False
|
||||
Ganons Tower - Mini Helmasaur Room - Right: False
|
||||
Ganons Tower - Pre-Moldorm Chest: False
|
||||
Ganons Tower - Randomizer Room - Bottom Left: True
|
||||
Ganons Tower - Randomizer Room - Bottom Right: True
|
||||
Ganons Tower - Randomizer Room - Top Left: True
|
||||
Ganons Tower - Randomizer Room - Top Right: True
|
||||
Ganons Tower - Tile Room: True
|
||||
Ganons Tower - Validation Chest: False
|
||||
- type: Verification
|
||||
item: Big Key (Ice Palace)
|
||||
locations:
|
||||
Ice Palace - Big Chest: False
|
||||
Ice Palace - Big Key Chest: True
|
||||
Ice Palace - Boss: False
|
||||
Ice Palace - Compass Chest: True
|
||||
Ice Palace - Freezor Chest: True
|
||||
Ice Palace - Iced T Room: True
|
||||
Ice Palace - Map Chest: True
|
||||
Ice Palace - Spike Room: True
|
||||
- type: Verification
|
||||
item: Big Key (Misery Mire)
|
||||
locations:
|
||||
Misery Mire - Big Chest: False
|
||||
Misery Mire - Big Key Chest: True
|
||||
Misery Mire - Boss: False
|
||||
Misery Mire - Bridge Chest: True
|
||||
Misery Mire - Compass Chest: True
|
||||
Misery Mire - Main Lobby: True
|
||||
Misery Mire - Map Chest: True
|
||||
Misery Mire - Spike Chest: True
|
||||
- type: Verification
|
||||
item: Big Key (Palace of Darkness)
|
||||
locations:
|
||||
Palace of Darkness - Big Chest: False
|
||||
Palace of Darkness - Big Key Chest: True
|
||||
Palace of Darkness - Boss: False
|
||||
Palace of Darkness - Compass Chest: True
|
||||
Palace of Darkness - Dark Basement - Left: True
|
||||
Palace of Darkness - Dark Basement - Right: True
|
||||
Palace of Darkness - Dark Maze - Bottom: True
|
||||
Palace of Darkness - Dark Maze - Top: True
|
||||
Palace of Darkness - Harmless Hellway: True
|
||||
Palace of Darkness - Map Chest: True
|
||||
Palace of Darkness - Shooter Room: True
|
||||
Palace of Darkness - Stalfos Basement: True
|
||||
Palace of Darkness - The Arena - Bridge: True
|
||||
Palace of Darkness - The Arena - Ledge: True
|
||||
- type: Verification
|
||||
item: Big Key (Skull Woods)
|
||||
locations:
|
||||
Skull Woods - Big Chest: True
|
||||
Skull Woods - Big Key Chest: True
|
||||
Skull Woods - Boss: True
|
||||
Skull Woods - Bridge Room: True
|
||||
Skull Woods - Compass Chest: True
|
||||
Skull Woods - Map Chest: True
|
||||
Skull Woods - Pinball Room: True
|
||||
Skull Woods - Pot Prison: True
|
||||
- type: Verification
|
||||
item: Big Key (Swamp Palace)
|
||||
locations:
|
||||
Swamp Palace - Big Chest: True
|
||||
Swamp Palace - Big Key Chest: True
|
||||
Swamp Palace - Boss: True
|
||||
Swamp Palace - Compass Chest: True
|
||||
Swamp Palace - Entrance: False
|
||||
Swamp Palace - Flooded Room - Left: True
|
||||
Swamp Palace - Flooded Room - Right: True
|
||||
Swamp Palace - Map Chest: True
|
||||
Swamp Palace - Waterfall Room: True
|
||||
Swamp Palace - West Chest: True
|
||||
- type: Verification
|
||||
item: Big Key (Thieves Town)
|
||||
locations:
|
||||
Thieves' Town - Ambush Chest: True
|
||||
Thieves' Town - Attic: False
|
||||
Thieves' Town - Big Chest: False
|
||||
Thieves' Town - Big Key Chest: True
|
||||
Thieves' Town - Blind's Cell: False
|
||||
Thieves' Town - Boss: False
|
||||
Thieves' Town - Compass Chest: True
|
||||
Thieves' Town - Map Chest: True
|
||||
- type: Verification
|
||||
item: Big Key (Tower of Hera)
|
||||
locations:
|
||||
Tower of Hera - Basement Cage: True
|
||||
Tower of Hera - Big Chest: False
|
||||
Tower of Hera - Big Key Chest: True
|
||||
Tower of Hera - Boss: False
|
||||
Tower of Hera - Compass Chest: False
|
||||
Tower of Hera - Map Chest: True
|
||||
- type: Verification
|
||||
item: Big Key (Turtle Rock)
|
||||
locations:
|
||||
Turtle Rock - Big Chest: False
|
||||
Turtle Rock - Big Key Chest: True
|
||||
Turtle Rock - Boss: False
|
||||
Turtle Rock - Chain Chomps: True
|
||||
Turtle Rock - Compass Chest: True
|
||||
Turtle Rock - Crystaroller Room: False
|
||||
Turtle Rock - Eye Bridge - Bottom Left: False
|
||||
Turtle Rock - Eye Bridge - Bottom Right: False
|
||||
Turtle Rock - Eye Bridge - Top Left: False
|
||||
Turtle Rock - Eye Bridge - Top Right: False
|
||||
Turtle Rock - Roller Room - Left: True
|
||||
Turtle Rock - Roller Room - Right: True
|
||||
@@ -0,0 +1,22 @@
|
||||
meta:
|
||||
players: 1
|
||||
settings:
|
||||
1:
|
||||
key_logic_algorithm: strict
|
||||
keysanity: true
|
||||
placements:
|
||||
1:
|
||||
Hobo: Big Key (Misery Mire)
|
||||
Waterfall Fairy - Left: Small Key (Misery Mire)
|
||||
advanced_placements:
|
||||
1:
|
||||
- type: Verification
|
||||
item: Flippers
|
||||
locations:
|
||||
Misery Mire - Map Chest: False
|
||||
Misery Mire - Main Lobby: False
|
||||
Misery Mire - Bridge Chest: True
|
||||
Misery Mire - Spike Chest: True
|
||||
Misery Mire - Compass Chest: False
|
||||
Misery Mire - Big Key Chest: False
|
||||
Misery Mire - Boss: False
|
||||
Reference in New Issue
Block a user