Merge branch 'DoorDevUnstable' into DoorDev

# Conflicts:
#	Main.py
#	RELEASENOTES.md
#	Rom.py
#	data/base2current.bps
This commit is contained in:
aerinon
2022-01-13 15:23:49 -07:00
43 changed files with 2132 additions and 526 deletions
+1
View File
@@ -36,6 +36,7 @@ def main():
parser.add_argument('--ow_palettes', default='default', choices=['default', 'random', 'blackout'])
parser.add_argument('--uw_palettes', default='default', choices=['default', 'random', 'blackout'])
parser.add_argument('--reduce_flashing', help='Reduce some in-game flashing.', action='store_true')
parser.add_argument('--shuffle_sfx', help='Shuffles sound sfx', action='store_true')
parser.add_argument('--sprite', help='''\
Path to a sprite sheet to use for Link. Needs to be in
binary format and have a length of 0x7000 (28672) bytes,
+1 -1
View File
@@ -25,7 +25,7 @@ def adjust(args):
args.sprite = None
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic,
args.sprite, args.ow_palettes, args.uw_palettes, args.reduce_flashing)
args.sprite, args.ow_palettes, args.uw_palettes, args.reduce_flashing, args.shuffle_sfx)
output_path.cached_path = args.outputpath
rom.write_to_file(output_path('%s.sfc' % outfilebase))
+484 -61
View File
@@ -77,6 +77,7 @@ class World(object):
self._room_cache = {}
self.dungeon_layouts = {}
self.inaccessible_regions = {}
self.enabled_entrances = {}
self.key_logic = {}
self.pool_adjustment = {}
self.key_layout = defaultdict(dict)
@@ -96,6 +97,7 @@ class World(object):
set_player_attr('player_names', [])
set_player_attr('remote_items', False)
set_player_attr('required_medallions', ['Ether', 'Quake'])
set_player_attr('bottle_refills', ['Bottle (Green Potion)', 'Bottle (Green Potion)'])
set_player_attr('swamp_patch_required', False)
set_player_attr('powder_patch_required', False)
set_player_attr('ganon_at_pyramid', True)
@@ -114,6 +116,7 @@ class World(object):
set_player_attr('compassshuffle', False)
set_player_attr('keyshuffle', False)
set_player_attr('bigkeyshuffle', False)
set_player_attr('bombbag', False)
set_player_attr('difficulty_requirements', None)
set_player_attr('boss_shuffle', 'none')
set_player_attr('enemy_shuffle', 'none')
@@ -139,6 +142,9 @@ class World(object):
set_player_attr('standardize_palettes', 'standardize')
set_player_attr('force_fix', {'gt': False, 'sw': False, 'pod': False, 'tr': False})
set_player_attr('exp_cache', defaultdict(dict))
set_player_attr('enabled_entrances', {})
def get_name_string_for_object(self, obj):
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})'
@@ -393,6 +399,10 @@ class World(object):
def clear_location_cache(self):
self._cached_locations = None
def clear_exp_cache(self):
for p in range(1, self.players + 1):
self.exp_cache[p].clear()
def get_unfilled_locations(self, player=None):
return [location for location in self.get_locations() if (player is None or location.player == player) and location.item is None]
@@ -425,8 +435,10 @@ class World(object):
else:
return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1)))
def can_beat_game(self, starting_state=None):
def can_beat_game(self, starting_state=None, log_error=False):
if starting_state:
if self.has_beaten_game(starting_state):
return True
state = starting_state.copy()
else:
state = CollectionState(self)
@@ -445,6 +457,9 @@ class World(object):
if not sphere:
# ran out of places and did not finish yet, quit
if log_error:
missing_locations = ", ".join([x.name for x in prog_locations])
logging.getLogger('').error(f'Cannot reach the following locations: {missing_locations}')
return False
for location in sphere:
@@ -459,17 +474,25 @@ class World(object):
class CollectionState(object):
def __init__(self, parent):
self.prog_items = Counter()
def __init__(self, parent, skip_init=False):
self.world = parent
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 = []
self.path = {}
self.locations_checked = set()
self.stale = {player: True for player in range(1, parent.players + 1)}
for item in parent.precollected_items:
self.collect(item, True)
if not skip_init:
self.prog_items = 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 = []
self.path = {}
self.locations_checked = set()
self.stale = {player: True for player in range(1, parent.players + 1)}
for item in parent.precollected_items:
self.collect(item, True)
# reached vs. opened in the counter
self.door_counter = {player: (Counter(), Counter()) for player in range(1, parent.players + 1)}
self.reached_doors = {player: set() for player in range(1, parent.players + 1)}
self.opened_doors = {player: set() for player in range(1, parent.players + 1)}
self.dungeons_to_check = {player: defaultdict(dict) for player in range(1, parent.players + 1)}
self.dungeon_limits = None
# self.trace = None
def update_reachable_regions(self, player):
self.stale[player] = False
@@ -480,68 +503,386 @@ class CollectionState(object):
start = self.world.get_region('Menu', player)
if not start in rrp:
rrp[start] = CrystalBarrier.Orange
for exit in start.exits:
bc[exit] = CrystalBarrier.Orange
for conn in start.exits:
bc[conn] = CrystalBarrier.Orange
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)
def traverse_world(self, queue, rrp, bc, player):
# run BFS on all connections, and keep track of those blocked by missing items
while True:
try:
connection, crystal_state = queue.popleft()
new_region = connection.connected_region
if new_region is None or new_region in rrp and (new_region.type != RegionType.Dungeon or (rrp[new_region] & crystal_state) == crystal_state):
while len(queue) > 0:
connection, crystal_state = queue.popleft()
new_region = connection.connected_region
if not self.should_visit(new_region, rrp, crystal_state, player):
if not new_region or not self.dungeon_limits or self.possibly_connected_to_dungeon(new_region, player):
bc.pop(connection, None)
elif connection.can_reach(self):
if new_region.type == RegionType.Dungeon:
new_crystal_state = crystal_state
for exit in new_region.exits:
door = exit.door
if door is not None and door.crystal == CrystalBarrier.Either and door.entrance.can_reach(self):
new_crystal_state = CrystalBarrier.Either
break
if new_region in rrp:
new_crystal_state |= rrp[new_region]
elif connection.can_reach(self):
bc.pop(connection, None)
if new_region.type == RegionType.Dungeon:
new_crystal_state = crystal_state
if new_region in rrp:
new_crystal_state |= rrp[new_region]
rrp[new_region] = new_crystal_state
for exit in new_region.exits:
door = exit.door
if door is not None and not door.blocked:
rrp[new_region] = new_crystal_state
for conn in new_region.exits:
door = conn.door
if door is not None and not door.blocked:
if self.valid_crystal(door, new_crystal_state):
door_crystal_state = door.crystal if door.crystal else new_crystal_state
bc[exit] = door_crystal_state
queue.append((exit, door_crystal_state))
elif door is None:
queue.append((exit, new_crystal_state))
else:
new_crystal_state = CrystalBarrier.Orange
rrp[new_region] = new_crystal_state
bc.pop(connection, None)
for exit in new_region.exits:
bc[exit] = new_crystal_state
queue.append((exit, new_crystal_state))
bc[conn] = door_crystal_state
queue.append((conn, door_crystal_state))
elif door is None:
# note: no door in dungeon indicates what exactly? (always traversable)?
queue.append((conn, new_crystal_state))
else:
new_crystal_state = CrystalBarrier.Orange
rrp[new_region] = new_crystal_state
for conn in new_region.exits:
bc[conn] = new_crystal_state
queue.append((conn, new_crystal_state))
self.path[new_region] = (new_region.name, self.path.get(connection, None))
self.path[new_region] = (new_region.name, self.path.get(connection, None))
# Retry connections if the new region can unblock them
if new_region.name in indirect_connections:
new_entrance = self.world.get_entrance(indirect_connections[new_region.name], player)
if new_entrance in bc and new_entrance not in queue and new_entrance.parent_region in rrp:
queue.append((new_entrance, rrp[new_entrance.parent_region]))
except IndexError:
break
# Retry connections if the new region can unblock them
if new_region.name in indirect_connections:
new_entrance = self.world.get_entrance(indirect_connections[new_region.name], player)
if new_entrance in bc and new_entrance.parent_region in rrp:
new_crystal_state = rrp[new_entrance.parent_region]
if (new_entrance, new_crystal_state) not in queue:
queue.append((new_entrance, new_crystal_state))
# else those connections that are not accessible yet
if self.is_small_door(connection):
door = connection.door if connection.door.smallKey else connection.door.controller
dungeon_name = connection.parent_region.dungeon.name
key_logic = self.world.key_logic[player][dungeon_name]
if door.name not in self.reached_doors[player]:
self.door_counter[player][0][dungeon_name] += 1
self.reached_doors[player].add(door.name)
if key_logic.sm_doors[door]:
self.reached_doors[player].add(key_logic.sm_doors[door].name)
if not connection.can_reach(self):
checklist_key = 'Universal' if self.world.retro[player] else dungeon_name
checklist = self.dungeons_to_check[player][checklist_key]
checklist[connection.name] = (connection, crystal_state)
elif door.name not in self.opened_doors[player]:
opened_doors = self.opened_doors[player]
door = connection.door if connection.door.smallKey else connection.door.controller
if door.name not in opened_doors:
self.door_counter[player][1][dungeon_name] += 1
opened_doors.add(door.name)
key_logic = self.world.key_logic[player][dungeon_name]
if key_logic.sm_doors[door]:
opened_doors.add(key_logic.sm_doors[door].name)
def should_visit(self, new_region, rrp, crystal_state, player):
if not new_region:
return False
if self.dungeon_limits and not self.possibly_connected_to_dungeon(new_region, player):
return False
if new_region not in rrp:
return True
if new_region.type != RegionType.Dungeon:
return False
return (rrp[new_region] & crystal_state) != crystal_state
def possibly_connected_to_dungeon(self, new_region, player):
if new_region.dungeon:
return new_region.dungeon.name in self.dungeon_limits
else:
return new_region.name in self.world.inaccessible_regions[player]
@staticmethod
def valid_crystal(door, new_crystal_state):
return (not door.crystal or door.crystal == CrystalBarrier.Either or new_crystal_state == CrystalBarrier.Either
or new_crystal_state == door.crystal)
def check_key_doors_in_dungeons(self, rrp, player):
for dungeon_name, checklist in self.dungeons_to_check[player].items():
if self.apply_dungeon_exploration(rrp, player, dungeon_name, checklist):
continue
init_door_candidates = self.should_explore_child_state(self, dungeon_name, player)
key_total = self.prog_items[(dungeon_keys[dungeon_name], player)] # todo: universal
remaining_keys = key_total - self.door_counter[player][1][dungeon_name]
if not init_door_candidates or remaining_keys == 0:
continue
dungeon_doors = {x.name for x in self.world.key_logic[player][dungeon_name].sm_doors.keys()}
def valid_d_door(x):
return x in dungeon_doors
child_states = deque()
child_states.append(self)
visited_opened_doors = set()
visited_opened_doors.add(frozenset(self.opened_doors[player]))
terminal_states, common_regions, common_bc, common_doors = [], {}, {}, set()
while len(child_states) > 0:
next_child = child_states.popleft()
door_candidates = CollectionState.should_explore_child_state(next_child, dungeon_name, player)
child_checklist = next_child.dungeons_to_check[player][dungeon_name]
if door_candidates:
for chosen_door in door_candidates:
child_state = next_child.copy()
child_queue = deque()
child_state.door_counter[player][1][dungeon_name] += 1
if isinstance(chosen_door, tuple):
child_state.opened_doors[player].add(chosen_door[0])
child_state.opened_doors[player].add(chosen_door[1])
if chosen_door[0] in child_checklist:
child_queue.append(child_checklist[chosen_door[0]])
if chosen_door[1] in child_checklist:
child_queue.append(child_checklist[chosen_door[1]])
else:
child_state.opened_doors[player].add(chosen_door)
if chosen_door in child_checklist:
child_queue.append(child_checklist[chosen_door])
if child_state.opened_doors[player] not in visited_opened_doors:
done = False
while not done:
rrp_ = child_state.reachable_regions[player]
bc_ = child_state.blocked_connections[player]
child_state.set_dungeon_limits(player, dungeon_name)
child_queue.extend([(x, y) for x, y in bc_.items()
if child_state.possibly_connected_to_dungeon(x.parent_region,
player)])
child_state.traverse_world(child_queue, rrp_, bc_, player)
new_events = child_state.sweep_for_events_once(player)
child_state.stale[player] = False
if new_events:
for conn in bc_:
if conn.parent_region.dungeon and conn.parent_region.dungeon.name == dungeon_name:
child_queue.append((conn, bc_[conn]))
done = not new_events
if child_state.opened_doors[player] not in visited_opened_doors:
visited_opened_doors.add(frozenset(child_state.opened_doors[player]))
child_states.append(child_state)
else:
terminal_states.append(next_child)
common_regions, common_bc, common_doors, first = {}, {}, set(), True
bc = self.blocked_connections[player]
for term_state in terminal_states:
t_rrp = term_state.reachable_regions[player]
t_bc = term_state.blocked_connections[player]
if first:
first = False
common_regions = {x: y for x, y in t_rrp.items() if x not in rrp or y != rrp[x]}
common_bc = {x: y for x, y in t_bc.items() if x not in bc}
common_doors = {x for x in term_state.opened_doors[player] - self.opened_doors[player]
if valid_d_door(x)}
else:
cm_rrp = {x: y for x, y in t_rrp.items() if x not in rrp or y != rrp[x]}
common_regions = {k: self.comb_crys(v, cm_rrp[k]) for k, v in common_regions.items()
if k in cm_rrp and self.crys_agree(v, cm_rrp[k])}
common_bc.update({x: y for x, y in t_bc.items() if x not in bc and x not in common_bc})
common_doors &= {x for x in term_state.opened_doors[player] - self.opened_doors[player]
if valid_d_door(x)}
terminal_queue = deque()
for door in common_doors:
pair = self.find_door_pair(player, dungeon_name, door)
if door not in self.reached_doors[player]:
self.door_counter[player][0][dungeon_name] += 1
self.reached_doors[player].add(door)
if pair not in self.reached_doors[player]:
self.reached_doors[player].add(pair)
self.opened_doors[player].add(door)
if door in checklist:
terminal_queue.append(checklist[door])
if pair not in self.opened_doors[player]:
self.door_counter[player][1][dungeon_name] += 1
self.set_dungeon_limits(player, dungeon_name)
rrp_ = self.reachable_regions[player]
bc_ = self.blocked_connections[player]
for block, crystal in bc_.items():
if (block, crystal) not in terminal_queue and self.possibly_connected_to_dungeon(block.connected_region, player):
terminal_queue.append((block, crystal))
self.traverse_world(terminal_queue, rrp_, bc_, player)
self.dungeon_limits = None
rrp = self.reachable_regions[player]
missing_regions = {x: y for x, y in common_regions.items() if x not in rrp}
paths = {}
for k in missing_regions:
rrp[k] = missing_regions[k]
possible_path = terminal_states[0].path[k]
self.path[k] = paths[k] = possible_path
missing_bc = {}
for blocked, crystal in common_bc.items():
if (blocked not in bc and blocked.parent_region in rrp
and self.should_visit(blocked.connected_region, rrp, crystal, player)):
missing_bc[blocked] = crystal
for k in missing_bc:
bc[k] = missing_bc[k]
self.record_dungeon_exploration(player, dungeon_name, checklist,
common_doors, missing_regions, missing_bc, paths)
checklist.clear()
@staticmethod
def comb_crys(a, b):
return a if a == b or a != CrystalBarrier.Either else b
@staticmethod
def crys_agree(a, b):
return a == b or a == CrystalBarrier.Either or b == CrystalBarrier.Either
def find_door_pair(self, player, dungeon_name, name):
for door in self.world.key_logic[player][dungeon_name].sm_doors.keys():
if door.name == name:
paired_door = self.world.key_logic[player][dungeon_name].sm_doors[door]
return paired_door.name if paired_door else None
return None
def set_dungeon_limits(self, player, dungeon_name):
if self.world.retro[player] and self.world.mode[player] == 'standard':
self.dungeon_limits = ['Hyrule Castle', 'Agahnims Tower']
else:
self.dungeon_limits = [dungeon_name]
@staticmethod
def should_explore_child_state(state, dungeon_name, player):
small_key_name = dungeon_keys[dungeon_name]
key_total = state.prog_items[(small_key_name, player)]
remaining_keys = key_total - state.door_counter[player][1][dungeon_name]
unopened_doors = state.door_counter[player][0][dungeon_name] - state.door_counter[player][1][dungeon_name]
if remaining_keys > 0 and unopened_doors > 0:
key_logic = state.world.key_logic[player][dungeon_name]
door_candidates, skip = [], set()
for door, paired in key_logic.sm_doors.items():
if door.name in state.reached_doors[player] and door.name not in state.opened_doors[player]:
if door.name not in skip:
if paired:
door_candidates.append((door.name, paired.name))
skip.add(paired.name)
else:
door_candidates.append(door.name)
return door_candidates
return None
@staticmethod
def print_rrp(rrp):
logger = logging.getLogger('')
logger.debug('RRP Checking')
for region, packet in rrp.items():
new_crystal_state, logic, path = packet
logger.debug(f'\nRegion: {region.name} (CS: {str(new_crystal_state)})')
for i in range(0, len(logic)):
logger.debug(f'{logic[i]}')
logger.debug(f'{",".join(str(x) for x in path[i])}')
def copy(self):
ret = CollectionState(self.world)
ret = CollectionState(self.world, skip_init=True)
ret.prog_items = self.prog_items.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)
ret.path = copy.copy(self.path)
ret.locations_checked = copy.copy(self.locations_checked)
ret.stale = {player: self.stale[player] for player in range(1, self.world.players + 1)}
ret.door_counter = {player: (copy.copy(self.door_counter[player][0]), copy.copy(self.door_counter[player][1]))
for player in range(1, self.world.players + 1)}
ret.reached_doors = {player: copy.copy(self.reached_doors[player]) for player in range(1, self.world.players + 1)}
ret.opened_doors = {player: copy.copy(self.opened_doors[player]) for player in range(1, self.world.players + 1)}
ret.dungeons_to_check = {
player: defaultdict(dict, {name: copy.copy(checklist)
for name, checklist in self.dungeons_to_check[player].items()})
for player in range(1, self.world.players + 1)}
return ret
def apply_dungeon_exploration(self, rrp, player, dungeon_name, checklist):
bc = self.blocked_connections[player]
ec = self.world.exp_cache[player]
prog_set = self.reduce_prog_items(player, dungeon_name)
exp_key = (prog_set, frozenset(checklist))
if dungeon_name in ec and exp_key in ec[dungeon_name]:
# apply
common_doors, missing_regions, missing_bc, paths = ec[dungeon_name][exp_key]
terminal_queue = deque()
for door in common_doors:
pair = self.find_door_pair(player, dungeon_name, door)
if door not in self.reached_doors[player]:
self.door_counter[player][0][dungeon_name] += 1
self.reached_doors[player].add(door)
if pair not in self.reached_doors[player]:
self.reached_doors[player].add(pair)
self.opened_doors[player].add(door)
if door in checklist:
terminal_queue.append(checklist[door])
if pair not in self.opened_doors[player]:
self.door_counter[player][1][dungeon_name] += 1
self.set_dungeon_limits(player, dungeon_name)
rrp_ = self.reachable_regions[player]
bc_ = self.blocked_connections[player]
for block, crystal in bc_.items():
if (block, crystal) not in terminal_queue and self.possibly_connected_to_dungeon(block.connected_region, player):
terminal_queue.append((block, crystal))
self.traverse_world(terminal_queue, rrp_, bc_, player)
self.dungeon_limits = None
for k in missing_regions:
rrp[k] = missing_regions[k]
for r, path in paths.items():
self.path[r] = path
for k in missing_bc:
bc[k] = missing_bc[k]
return True
return False
def record_dungeon_exploration(self, player, dungeon_name, checklist,
common_doors, missing_regions, missing_bc, paths):
ec = self.world.exp_cache[player]
prog_set = self.reduce_prog_items(player, dungeon_name)
exp_key = (prog_set, frozenset(checklist))
ec[dungeon_name][exp_key] = (common_doors, missing_regions, missing_bc, paths)
def reduce_prog_items(self, player, dungeon_name):
# todo: possibly could include an analysis of dungeon items req. like Hammer, Hookshot, etc
# cross dungeon requirements may be necessary for keysanity - which invalidates the above
# todo: universal smalls where needed
life_count, bottle_count = 0, 0
reduced = Counter()
for item, cnt in self.prog_items.items():
item_name, item_player = item
if item_player == player and self.check_if_progressive(item_name):
if item_name.startswith('Bottle'): # I think magic requirements can require multiple bottles
bottle_count += cnt
elif item_name in ['Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart']:
if 'Container' in item_name:
life_count += 1
elif 'Piece of Heart' == item_name:
life_count += .25
else:
reduced[item] = cnt
if bottle_count > 0:
reduced[('Bottle', player)] = 1
if life_count >= 1:
reduced[('Heart Container', player)] = 1
return frozenset(reduced.items())
@staticmethod
def check_if_progressive(item_name):
return (item_name in
['Bow', 'Progressive Bow', 'Progressive Bow (Alt)', 'Book of Mudora', 'Hammer', 'Hookshot',
'Magic Mirror', 'Ocarina', 'Pegasus Boots', 'Power Glove', 'Cape', 'Mushroom', 'Shovel',
'Lamp', 'Magic Powder', 'Moon Pearl', 'Cane of Somaria', 'Fire Rod', 'Flippers', 'Ice Rod',
'Titans Mitts', 'Bombos', 'Ether', 'Quake', 'Master Sword', 'Tempered Sword', 'Fighter Sword',
'Golden Sword', 'Progressive Sword', 'Progressive Glove', 'Silver Arrows', 'Green Pendant',
'Blue Pendant', 'Red Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5',
'Crystal 6', 'Crystal 7', 'Blue Boomerang', 'Red Boomerang', 'Blue Shield', 'Red Shield',
'Mirror Shield', 'Progressive Shield', 'Bug Catching Net', 'Cane of Byrna',
'Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart', 'Magic Upgrade (1/2)',
'Magic Upgrade (1/4)']
or item_name.startswith(('Bottle', 'Small Key', 'Big Key')))
def can_reach(self, spot, resolution_hint=None, player=None):
try:
spot_type = spot.spot_type
@@ -557,6 +898,16 @@ class CollectionState(object):
return spot.can_reach(self)
def sweep_for_events_once(self, player):
locations = self.world.get_filled_locations(player)
checked_locations = set([l for l in locations if l in self.locations_checked])
reachable_events = [location for location in locations if location.event and location.can_reach(self)]
reachable_events = self._do_not_flood_the_keys(reachable_events)
for event in reachable_events:
if event not in checked_locations:
self.events.append((event.name, event.player))
self.collect(event.item, True, event)
return len(reachable_events) > len(checked_locations)
def sweep_for_events(self, key_only=False, locations=None):
# this may need improvement
@@ -604,6 +955,18 @@ class CollectionState(object):
or not self.location_can_be_flooded(flood_location))
return True
@staticmethod
def is_small_door(connection):
return connection and connection.door and (connection.door.smallKey or
CollectionState.is_controlled_by_small(connection))
@staticmethod
def is_controlled_by_small(connection):
return connection.door.controller and connection.door.controller.smallKey
def is_door_open(self, door_name, player):
return door_name in self.opened_doors[player]
@staticmethod
def location_can_be_flooded(location):
return location.parent_region.name in ['Swamp Trench 1 Alcove', 'Swamp Trench 2 Alcove']
@@ -686,8 +1049,7 @@ class CollectionState(object):
# In the future, this can be used to check if the player starts without bombs
def can_use_bombs(self, player):
StartingBombs = True
return StartingBombs or self.has('Bomb Upgrade (+10)', player)
return (not self.world.bombbag[player] or self.has('Bomb Upgrade (+10)', player))
def can_hit_crystal(self, player):
return (self.can_use_bombs(player)
@@ -719,6 +1081,7 @@ class CollectionState(object):
def can_get_good_bee(self, player):
cave = self.world.get_region('Good Bee Cave', player)
return (
self.can_use_bombs(player) and
self.has_bottle(player) and
self.has('Bug Catching Net', player) and
(self.has_Boots(player) or (self.has_sword(player) and self.has('Quake', player))) and
@@ -807,6 +1170,8 @@ class CollectionState(object):
def collect(self, item, event=False, location=None):
if location:
self.locations_checked.add(location)
if not item:
return
changed = False
if item.name.startswith('Progressive '):
if 'Sword' in item.name:
@@ -1282,6 +1647,7 @@ class Door(object):
self.bk_shuffle_req = False
self.standard_restricted = False # flag if portal is not allowed in HC in standard
self.lw_restricted = False # flag if portal is not allowed in DW
self.rupee_bow_restricted = False # flag if portal is not allowed in HC in standard+rupee_bow
# self.incognitoPos = -1
# self.sectorLink = False
@@ -1483,6 +1849,7 @@ class Sector(object):
self.entrance_sector = None
self.destination_entrance = False
self.equations = None
self.item_logic = set()
def region_set(self):
if self.r_name_set is None:
@@ -1735,9 +2102,7 @@ class Location(object):
return self.always_allow(state, item) or (self.parent_region.can_fill(item) and self.item_rule(item) and (not check_access or self.can_reach(state)))
def can_reach(self, state):
if self.parent_region.can_reach(state) and self.access_rule(state):
return True
return False
return self.parent_region.can_reach(state) and self.access_rule(state)
def forced_big_key(self):
if self.forced_item and self.forced_item.bigkey and self.player == self.forced_item.player:
@@ -1764,6 +2129,12 @@ class Location(object):
world = self.parent_region.world if self.parent_region and self.parent_region.world else None
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
def __eq__(self, other):
return self.name == other.name and self.player == other.player
def __hash__(self):
return hash((self.name, self.player))
class Item(object):
@@ -1807,6 +2178,15 @@ class Item(object):
def compass(self):
return self.type == 'Compass'
@property
def dungeon(self):
if not self.smallkey and not self.bigkey and not self.map and not self.compass:
return None
item_dungeon = self.name.split('(')[1][:-1]
if item_dungeon == 'Escape':
item_dungeon = 'Hyrule Castle'
return item_dungeon
def __str__(self):
return str(self.__unicode__())
@@ -1893,6 +2273,7 @@ class Spoiler(object):
self.doorTypes = {}
self.lobbies = {}
self.medallions = {}
self.bottles = {}
self.playthrough = {}
self.unreachables = []
self.startinventory = []
@@ -1936,6 +2317,15 @@ class Spoiler(object):
self.medallions[f'Misery Mire ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][0]
self.medallions[f'Turtle Rock ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][1]
self.bottles = OrderedDict()
if self.world.players == 1:
self.bottles['Waterfall Bottle'] = self.world.bottle_refills[1][0]
self.bottles['Pyramid Bottle'] = self.world.bottle_refills[1][1]
else:
for player in range(1, self.world.players + 1):
self.bottles[f'Waterfall Bottle ({self.world.get_player_names(player)})'] = self.world.bottle_refills[player][0]
self.bottles[f'Pyramid Bottle ({self.world.get_player_names(player)})'] = self.world.bottle_refills[player][1]
self.startinventory = list(map(str, self.world.precollected_items))
self.locations = OrderedDict()
@@ -2013,9 +2403,11 @@ class Spoiler(object):
'logic': self.world.logic,
'mode': self.world.mode,
'retro': self.world.retro,
'bombbag': self.world.bombbag,
'weapons': self.world.swords,
'goal': self.world.goal,
'shuffle': self.world.shuffle,
'shufflelinks': self.world.shufflelinks,
'door_shuffle': self.world.doorShuffle,
'intensity': self.world.intensity,
'item_pool': self.world.difficulty,
@@ -2054,6 +2446,7 @@ class Spoiler(object):
out.update(self.locations)
out['Starting Inventory'] = self.startinventory
out['Special'] = self.medallions
out['Bottles'] = self.bottles
if self.hashes:
out['Hashes'] = {f"{self.world.player_names[player][team]} (Team {team+1})": hash for (player, team), hash in self.hashes.items()}
if self.shops:
@@ -2090,6 +2483,7 @@ class Spoiler(object):
outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player])
outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player])
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player])
outfile.write(f"Link's House Shuffled: {'Yes' if self.metadata['shufflelinks'][player] else 'No'}\n")
outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player])
outfile.write('Intensity: %s\n' % self.metadata['intensity'][player])
addition = ' (Random)' if self.world.crystals_gt_orig[player] == 'random' else ''
@@ -2111,6 +2505,7 @@ class Spoiler(object):
outfile.write('Experimental: %s\n' % ('Yes' if self.metadata['experimental'][player] else 'No'))
outfile.write('Key Drops shuffled: %s\n' % ('Yes' if self.metadata['keydropshuffle'][player] else 'No'))
outfile.write(f"Shopsanity: {'Yes' if self.metadata['shopsanity'][player] else 'No'}\n")
outfile.write('Bombbag: %s\n' % ('Yes' if self.metadata['bombbag'][player] else 'No'))
if self.doors:
outfile.write('\n\nDoors:\n\n')
outfile.write('\n'.join(
@@ -2138,6 +2533,9 @@ class Spoiler(object):
outfile.write('\n\nMedallions:\n')
for dungeon, medallion in self.medallions.items():
outfile.write(f'\n{dungeon}: {medallion} Medallion')
outfile.write('\n\nBottle Refills:\n')
for fairy, bottle in self.bottles.items():
outfile.write(f'\n{fairy}: {bottle}')
if self.startinventory:
outfile.write('\n\nStarting Inventory:\n\n')
outfile.write('\n'.join(self.startinventory))
@@ -2154,7 +2552,7 @@ class Spoiler(object):
for player in range(1, self.world.players + 1):
if self.world.boss_shuffle[player] != 'none':
bossmap = self.bosses[player] if self.world.players > 1 else self.bosses
bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses
outfile.write(f'\n\nBosses ({self.world.get_player_names(player)}):\n\n')
outfile.write('\n'.join([f'{x}: {y}' for x, y in bossmap.items() if y not in ['Agahnim', 'Agahnim 2', 'Ganon']]))
@@ -2197,6 +2595,22 @@ dungeon_names = [
'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace', 'Misery Mire', 'Turtle Rock', 'Ganons Tower'
]
dungeon_keys = {
'Hyrule Castle': 'Small Key (Escape)',
'Eastern Palace': 'Small Key (Eastern Palace)',
'Desert Palace': 'Small Key (Desert Palace)',
'Tower of Hera': 'Small Key (Tower of Hera)',
'Agahnims Tower': 'Small Key (Agahnims Tower)',
'Palace of Darkness': 'Small Key (Palace of Darkness)',
'Swamp Palace': 'Small Key (Swamp Palace)',
'Skull Woods': 'Small Key (Skull Woods)',
'Thieves Town': 'Small Key (Thieves Town)',
'Ice Palace': 'Small Key (Ice Palace)',
'Misery Mire': 'Small Key (Misery Mire)',
'Turtle Rock': 'Small Key (Turtle Rock)',
'Ganons Tower': 'Small Key (Ganons Tower)',
'Universal': 'Small Key (Universal)'
}
class PotItem(FastEnum):
Nothing = 0x0
@@ -2272,7 +2686,7 @@ access_mode = {"items": 0, "locations": 1, "none": 2}
boss_mode = {"none": 0, "simple": 1, "full": 2, "random": 3, "chaos": 3}
enemy_mode = {"none": 0, "shuffled": 1, "random": 2, "chaos": 2, "legacy": 3}
# byte 7: HHHD DP?? (enemy_health, enemy_dmg, potshuffle, ?)
# byte 7: HHHD DPBS (enemy_health, enemy_dmg, potshuffle, bomb logic, shuffle links)
e_health = {"default": 0, "easy": 1, "normal": 2, "hard": 3, "expert": 4}
e_dmg = {"default": 0, "shuffled": 1, "random": 2}
@@ -2303,7 +2717,8 @@ class Settings(object):
| (0x20 if w.mapshuffle[p] else 0) | (0x10 if w.compassshuffle[p] else 0)
| (boss_mode[w.boss_shuffle[p]] << 2) | (enemy_mode[w.enemy_shuffle[p]]),
(e_health[w.enemy_health[p]] << 5) | (e_dmg[w.enemy_damage[p]] << 3) | (0x4 if w.potshuffle[p] else 0)])
(e_health[w.enemy_health[p]] << 5) | (e_dmg[w.enemy_damage[p]] << 3) | (0x4 if w.potshuffle[p] else 0)
| (0x2 if w.bombbag[p] else 0) | (1 if w.shufflelinks[p] else 0)])
return base64.b64encode(code, "+-".encode()).decode()
@staticmethod
@@ -2347,3 +2762,11 @@ class Settings(object):
args.enemy_health[p] = r(e_health)[(settings[7] & 0xE0) >> 5]
args.enemy_damage[p] = r(e_dmg)[(settings[7] & 0x18) >> 3]
args.shufflepots[p] = True if settings[7] & 0x4 else False
args.bombbag[p] = True if settings[7] & 0x2 else False
args.shufflelinks[p] = True if settings[7] & 0x1 else False
class KeyRuleType(FastEnum):
WorstCase = 0
AllowSmall = 1
Lock = 2
+8 -8
View File
@@ -32,7 +32,7 @@ def LanmolasDefeatRule(state, player):
state.has('Fire Rod', player) or
state.has('Ice Rod', player) or
state.has('Cane of Somaria', player) or
state.has('Cane of Byrna', player) or
(state.has('Cane of Byrna', player) and state.can_use_bombs(player)) or
state.can_shoot_arrows(player))
def MoldormDefeatRule(state, player):
@@ -51,7 +51,7 @@ def ArrghusDefeatRule(state, player):
return True
return ((state.has('Fire Rod', player) and (state.can_shoot_arrows(player) or state.can_extend_magic(player, 12))) or #assuming mostly gitting two puff with one shot
(state.has('Ice Rod', player) and (state.can_shoot_arrows(player) or state.can_extend_magic(player, 16))))
(state.has('Ice Rod', player) and state.can_use_bombs(player) and (state.can_shoot_arrows(player) or state.can_extend_magic(player, 16))))
def MothulaDefeatRule(state, player):
@@ -92,7 +92,7 @@ def KholdstareDefeatRule(state, player):
)
def VitreousDefeatRule(state, player):
return state.can_shoot_arrows(player) or state.has_blunt_weapon(player)
return (state.can_shoot_arrows(player) and state.can_use_bombs(player)) or state.has_blunt_weapon(player)
def TrinexxDefeatRule(state, player):
if not (state.has('Fire Rod', player) and state.has('Ice Rod', player)):
@@ -176,16 +176,16 @@ def place_bosses(world, player):
if world.boss_shuffle[player] == "simple": # vanilla bosses shuffled
bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm']
else: # all bosses present, the three duplicates chosen at random
bosses = all_bosses + [random.choice(placeable_bosses) for _ in range(3)]
else: # all bosses present, the three duplicates chosen at random
bosses = all_bosses + random.sample(placeable_bosses, 3)
logging.getLogger('').debug('Bosses chosen %s', bosses)
random.shuffle(bosses)
for [loc, level] in boss_locations:
loc_text = loc + (' ('+level+')' if level else '')
boss = next((b for b in bosses if can_place_boss(world, player, b, loc, level)), None)
if not boss:
try:
boss = random.choice([b for b in bosses if can_place_boss(world, player, b, loc, level)])
except IndexError:
raise FillError('Could not place boss for location %s' % loc_text)
bosses.remove(boss)
+4 -1
View File
@@ -96,13 +96,14 @@ def parse_cli(argv, no_defaults=False):
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality',
'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid',
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
'bombbag',
'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max',
'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'pseudoboots',
'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters',
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots',
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep',
'remote_items', 'shopsanity', 'keydropshuffle', 'mixed_travel', 'standardize_palettes', 'code',
'reduce_flashing']:
'reduce_flashing', 'shuffle_sfx']:
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
if player == 1:
setattr(ret, name, {1: value})
@@ -126,6 +127,7 @@ def parse_settings():
settings = {
"lang": "en",
"retro": False,
"bombbag": False,
"mode": "open",
"logic": "noglitches",
"goal": "ganon",
@@ -190,6 +192,7 @@ def parse_settings():
"ow_palettes": "default",
"uw_palettes": "default",
"reduce_flashing": False,
"shuffle_sfx": False,
# Spoiler defaults to TRUE
# Playthrough defaults to TRUE
+79 -50
View File
@@ -1,22 +1,21 @@
import RaceRandom as random
from collections import defaultdict, deque
import logging
import operator as op
import time
from enum import unique, Flag
from typing import DefaultDict, Dict, List
from functools import reduce
from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo
from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo, dungeon_keys
from Doors import reset_portals
from Dungeons import dungeon_regions, region_starts, standard_starts, split_region_starts
from Dungeons import dungeon_bigs, dungeon_keys, dungeon_hints
from Dungeons import dungeon_bigs, dungeon_hints
from Items import ItemFactory
from RoomData import DoorKind, PairedDoor, reset_rooms
from DungeonGenerator import ExplorationState, convert_regions, generate_dungeon, pre_validate, determine_required_paths, drop_entrances
from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances
from DungeonGenerator import dungeon_portals, dungeon_drops, GenerationException
from KeyDoorShuffle import analyze_dungeon, validate_vanilla_key_logic, build_key_layout, validate_key_layout
from KeyDoorShuffle import analyze_dungeon, build_key_layout, validate_key_layout, determine_prize_lock
from Utils import ncr, kth_combination
def link_doors(world, player):
@@ -102,6 +101,8 @@ def link_doors_main(world, player):
connect_portal(portal, world, player)
if not world.doorShuffle[player] == 'vanilla':
fix_big_key_doors_with_ugly_smalls(world, player)
else:
unmark_ugly_smalls(world, player)
if world.doorShuffle[player] == 'vanilla':
for entrance, ext in open_edges:
connect_two_way(world, entrance, ext, player)
@@ -214,8 +215,8 @@ def vanilla_key_logic(world, player):
analyze_dungeon(key_layout, world, player)
world.key_logic[player][builder.name] = key_layout.key_logic
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)
# 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)
# some useful functions
@@ -317,6 +318,13 @@ def connect_one_way(world, entrancename, exitname, player):
y.dest = x
def unmark_ugly_smalls(world, player):
for d in ['Eastern Hint Tile Blocked Path SE', 'Eastern Darkness S', 'Thieves Hallway SE', 'Mire Left Bridge S',
'TR Lava Escape SE', 'GT Hidden Spikes SE']:
door = world.get_door(d, player)
door.smallKey = False
def fix_big_key_doors_with_ugly_smalls(world, player):
remove_ugly_small_key_doors(world, player)
unpair_big_key_doors(world, player)
@@ -360,7 +368,8 @@ def choose_portals(world, player):
if world.doorShuffle[player] in ['basic', 'crossed']:
cross_flag = world.doorShuffle[player] == 'crossed'
bk_shuffle = world.bigkeyshuffle[player]
# key drops allow the big key in the right place in Desert Tiles 2
bk_shuffle = world.bigkeyshuffle[player] or world.keydropshuffle[player]
std_flag = world.mode[player] == 'standard'
# roast incognito doors
world.get_room(0x60, player).delete(5)
@@ -407,6 +416,7 @@ def choose_portals(world, player):
for dungeon, info in shuffled_info:
outstanding_portals = list(dungeon_portals[dungeon])
hc_flag = std_flag and dungeon == 'Hyrule Castle'
rupee_bow_flag = hc_flag and world.retro[player] # rupee bow
if hc_flag:
sanc = world.get_portal('Sanctuary', player)
sanc.destination = True
@@ -416,14 +426,14 @@ def choose_portals(world, player):
info.required_passage = {x: y for x, y in info.required_passage.items() if len(y) > 0}
for target_region, possible_portals in info.required_passage.items():
candidates = find_portal_candidates(master_door_list, dungeon, need_passage=True, crossed=cross_flag,
bk_shuffle=bk_shuffle)
bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag)
choice, portal = assign_portal(candidates, possible_portals, world, player)
portal.destination = True
clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals)
dead_end_choices = info.total - 1 - len(portal_assignment[dungeon])
for i in range(0, dead_end_choices):
candidates = find_portal_candidates(master_door_list, dungeon, dead_end_allowed=True,
crossed=cross_flag, bk_shuffle=bk_shuffle)
crossed=cross_flag, bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag)
possible_portals = outstanding_portals if not info.sole_entrance else [x for x in outstanding_portals if x != info.sole_entrance]
choice, portal = assign_portal(candidates, possible_portals, world, player)
if choice.deadEnd:
@@ -435,7 +445,7 @@ def choose_portals(world, player):
the_rest = info.total - len(portal_assignment[dungeon])
for i in range(0, the_rest):
candidates = find_portal_candidates(master_door_list, dungeon, crossed=cross_flag,
bk_shuffle=bk_shuffle, standard=hc_flag)
bk_shuffle=bk_shuffle, standard=hc_flag, rupee_bow=rupee_bow_flag)
choice, portal = assign_portal(candidates, outstanding_portals, world, player)
clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals)
@@ -503,6 +513,9 @@ def analyze_portals(world, player):
raise Exception('please inspect this case')
if len(reachable_portals) == 1:
info.sole_entrance = reachable_portals[0]
if world.intensity[player] < 2 and world.doorShuffle[player] == 'basic' and dungeon == 'Desert Palace':
if len(inaccessible_portals) == 1 and inaccessible_portals[0] == 'Desert Back':
info.required_passage.clear() # can't make a passage at this intensity level, something else must exit
info_map[dungeon] = info
for dungeon, info in info_map.items():
@@ -553,7 +566,7 @@ def disconnect_portal(portal, world, player):
def find_portal_candidates(door_list, dungeon, need_passage=False, dead_end_allowed=False, crossed=False,
bk_shuffle=False, standard=False):
bk_shuffle=False, standard=False, rupee_bow=False):
ret = [x for x in door_list if bk_shuffle or not x.bk_shuffle_req]
if crossed:
ret = [x for x in ret if not x.dungeonLink or x.dungeonLink == dungeon or x.dungeonLink.startswith('link')]
@@ -565,6 +578,8 @@ def find_portal_candidates(door_list, dungeon, need_passage=False, dead_end_allo
ret = [x for x in ret if not x.deadEnd]
if standard:
ret = [x for x in ret if not x.standard_restricted]
if rupee_bow:
ret = [x for x in ret if not x.rupee_bow_restricted]
return ret
@@ -742,7 +757,7 @@ def handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map,
def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player):
entrances_map, potentials, connections = connections_tuple
enabled_entrances = {}
enabled_entrances = world.enabled_entrances[player] = {}
sector_queue = deque(dungeon_builders.values())
last_key, loops = None, 0
logging.getLogger('').info(world.fish.translate("cli", "cli", "generating.dungeon"))
@@ -1071,6 +1086,7 @@ def assign_cross_keys(dungeon_builders, world, player):
# Step 3: Initial valid combination find - reduce flex if needed
for name, builder in dungeon_builders.items():
suggested = builder.key_doors_num - builder.key_drop_cnt
builder.total_keys = builder.key_doors_num
find_valid_combination(builder, start_regions_map[name], world, player)
actual_chest_keys = builder.key_doors_num - builder.key_drop_cnt
if actual_chest_keys < suggested:
@@ -1087,6 +1103,7 @@ def assign_cross_keys(dungeon_builders, world, player):
name = builder.name
logger.debug('Cross Dungeon: Increasing key count by 1 for %s', name)
builder.key_doors_num += 1
builder.total_keys = builder.key_doors_num
result = find_valid_combination(builder, start_regions_map[name], world, player, drop_keys=False)
if result:
remaining -= 1
@@ -1102,9 +1119,9 @@ def assign_cross_keys(dungeon_builders, world, player):
logger.debug('Cross Dungeon: Keys unable to assign in pool %s', remaining)
# Last Step: Adjust Small Key Dungeon Pool
if not world.retro[player]:
for name, builder in dungeon_builders.items():
reassign_key_doors(builder, world, player)
for name, builder in dungeon_builders.items():
reassign_key_doors(builder, world, player)
if not world.retro[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)
dungeon = world.get_dungeon(name, player)
@@ -1345,10 +1362,8 @@ def combine_layouts(recombinant_builders, dungeon_builders, entrances_map):
if recombine.master_sector is None:
recombine.master_sector = builder.master_sector
recombine.master_sector.name = recombine.name
recombine.pre_open_stonewalls = builder.pre_open_stonewalls
else:
recombine.master_sector.regions.extend(builder.master_sector.regions)
recombine.pre_open_stonewalls.update(builder.pre_open_stonewalls)
recombine.layout_starts = list(entrances_map[recombine.name])
dungeon_builders[recombine.name] = recombine
@@ -1380,7 +1395,7 @@ def shuffle_key_doors(builder, world, player):
skips.append(world.get_door(dp.door_a, player))
break
num_key_doors += 1
builder.key_doors_num = num_key_doors
builder.key_doors_num = builder.total_keys = num_key_doors
find_small_key_door_candidates(builder, start_regions, world, player)
find_valid_combination(builder, start_regions, world, player)
reassign_key_doors(builder, world, player)
@@ -1443,7 +1458,16 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True
random.shuffle(sample_list)
proposal = kth_combination(sample_list[itr], builder.candidates, builder.key_doors_num)
# eliminate start region if portal marked as destination
excluded = {}
for region in start_regions:
portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None)
if portal and portal.destination:
excluded[region] = None
start_regions = [x for x in start_regions if x not in excluded.keys()]
key_layout = build_key_layout(builder, start_regions, proposal, world, player)
determine_prize_lock(key_layout, world, player)
while not validate_key_layout(key_layout, world, player):
itr += 1
stop_early = False
@@ -1561,7 +1585,7 @@ def find_key_door_candidates(region, checked, world, player):
if d2.type == DoorType.Normal:
room_b = world.get_room(d2.roomIndex, player)
pos_b, kind_b = room_b.doorList[d2.doorListPos]
valid = kind in okay_normals and kind_b in okay_normals
valid = kind in okay_normals and kind_b in okay_normals and valid_key_door_pair(d, d2)
else:
valid = kind in okay_normals
if valid and 0 <= d2.doorListPos < 4:
@@ -1578,26 +1602,10 @@ def find_key_door_candidates(region, checked, world, player):
return candidates, checked_doors
def kth_combination(k, l, r):
if r == 0:
return []
elif len(l) == r:
return l
else:
i = ncr(len(l)-1, r-1)
if k < i:
return l[0:1] + kth_combination(k, l[1:], r-1)
else:
return kth_combination(k-i, l[1:], r)
def ncr(n, r):
if r == 0:
return 1
r = min(r, n-r)
numerator = reduce(op.mul, range(n, n-r, -1), 1)
denominator = reduce(op.mul, range(1, r+1), 1)
return numerator / denominator
def valid_key_door_pair(door1, door2):
if door1.roomIndex != door2.roomIndex:
return True
return len(door1.entrance.parent_region.exits) <= 1 or len(door2.entrance.parent_region.exits) <= 1
def reassign_key_doors(builder, world, player):
@@ -1828,6 +1836,10 @@ def find_inaccessible_regions(world, player):
if connect.type is not RegionType.Dungeon or connect.name.endswith(' Portal'):
queue.append(connect)
world.inaccessible_regions[player].extend([r.name for r in all_regions.difference(visited_regions) if valid_inaccessible_region(r)])
if world.mode[player] == 'inverted':
ledge = world.get_region('Hyrule Castle Ledge', player)
if any(x for x in ledge.exits if x.connected_region.name == 'Agahnims Tower Portal'):
world.inaccessible_regions[player].append('Hyrule Castle Ledge')
logger = logging.getLogger('')
logger.debug('Inaccessible Regions:')
for r in world.inaccessible_regions[player]:
@@ -1912,14 +1924,18 @@ def check_required_paths(paths, world, player):
if dungeon_name in world.dungeon_layouts[player].keys():
builder = world.dungeon_layouts[player][dungeon_name]
if len(paths[dungeon_name]) > 0:
states_to_explore = defaultdict(list)
states_to_explore = {}
for path in paths[dungeon_name]:
if type(path) is tuple:
states_to_explore[tuple([path[0]])] = path[1]
states_to_explore[tuple([path[0]])] = (path[1], 'any')
else:
states_to_explore[tuple(builder.path_entrances)].append(path)
common_starts = tuple(builder.path_entrances)
if common_starts not in states_to_explore:
states_to_explore[common_starts] = ([], 'all')
states_to_explore[common_starts][0].append(path)
cached_initial_state = None
for start_regs, dest_regs in states_to_explore.items():
for start_regs, info in states_to_explore.items():
dest_regs, path_type = info
if type(dest_regs) is not list:
dest_regs = [dest_regs]
check_paths = convert_regions(dest_regs, world, player)
@@ -1936,11 +1952,17 @@ def check_required_paths(paths, world, player):
cached_initial_state = state
else:
state = cached_initial_state
valid, bad_region = check_if_regions_visited(state, check_paths)
if path_type == 'any':
valid, bad_region = check_if_any_regions_visited(state, check_paths)
else:
valid, bad_region = check_if_all_regions_visited(state, check_paths)
if not valid:
if check_for_pinball_fix(state, bad_region, world, player):
explore_state(state, world, player)
valid, bad_region = check_if_regions_visited(state, check_paths)
if path_type == 'any':
valid, bad_region = check_if_any_regions_visited(state, check_paths)
else:
valid, bad_region = check_if_all_regions_visited(state, check_paths)
if not valid:
raise Exception('%s cannot reach %s' % (dungeon_name, bad_region.name))
@@ -1980,7 +2002,7 @@ def explore_state_not_inaccessible(state, world, player):
state.add_all_doors_check_unattached(connect_region, world, player)
def check_if_regions_visited(state, check_paths):
def check_if_any_regions_visited(state, check_paths):
valid = False
breaking_region = None
for region_target in check_paths:
@@ -1992,6 +2014,13 @@ def check_if_regions_visited(state, check_paths):
return valid, breaking_region
def check_if_all_regions_visited(state, check_paths):
for region_target in check_paths:
if not state.visited_at_all(region_target):
return False, region_target
return True, None
def check_for_pinball_fix(state, bad_region, world, player):
pinball_region = world.get_region('Skull Pinball', player)
# todo: lobby shuffle
@@ -2015,10 +2044,10 @@ class DROptions(Flag):
Town_Portal = 0x02 # If on, Players will start with mirror scroll
Map_Info = 0x04
Debug = 0x08
Rails = 0x10 # If on, draws rails
Fix_EG = 0x10 # used to be Rails = 0x10 # Unused bit now
OriginalPalettes = 0x20
Open_PoD_Wall = 0x40 # If on, pre opens the PoD wall, no bow required
Open_Desert_Wall = 0x80 # If on, pre opens the desert wall, no fire required
# Open_PoD_Wall = 0x40 # No longer pre-opening pod wall - unused
# Open_Desert_Wall = 0x80 # No longer pre-opening desert wall - unused
Hide_Total = 0x100
DarkWorld_Spawns = 0x200
+5 -2
View File
@@ -1490,8 +1490,11 @@ def create_doors(world, player):
world.get_door('GT Petting Zoo SE', player).dead_end()
world.get_door('GT DMs Room SW', player).dead_end()
world.get_door("GT Bob\'s Room SE", player).passage = False
world.get_door('Desert Tiles 2 SE', player).bk_shuffle_req = True # key-drop note (todo)
world.get_door('Swamp Lobby S', player).standard_restricted = True # key-drop note (todo)
world.get_door('Desert Tiles 2 SE', player).bk_shuffle_req = True # key-drop note: allows this to be a portal
world.get_door('Swamp Lobby S', player).standard_restricted = True
world.get_door('PoD Mimics 2 SW', player).rupee_bow_restricted = True # bow statue
# enemizer logic could get rid of the following restriction
world.get_door('PoD Pit Room S', player).rupee_bow_restricted = True # so mimics 1 shouldn't be required
# can't unlink from boss right now
world.get_door('Hera Lobby S', player).dungeonLink = 'Tower of Hera'
+108 -69
View File
@@ -39,14 +39,15 @@ def pre_validate(builder, entrance_region_names, split_dungeon, world, player):
proposed_map = {}
doors_to_connect = {}
all_regions = set()
bk_needed = False
bk_special = False
for sector in builder.sectors:
for door in sector.outstanding_doors:
doors_to_connect[door.name] = door
all_regions.update(sector.regions)
bk_needed = bk_needed or determine_if_bk_needed(sector, split_dungeon, world, player)
bk_special = bk_special or check_for_special(sector)
bk_special |= check_for_special(sector.regions)
bk_needed = False
for sector in builder.sectors:
bk_needed |= determine_if_bk_needed(sector, split_dungeon, bk_special, world, player)
paths = determine_paths_for_dungeon(world, player, all_regions, builder.name)
dungeon, hangers, hooks = gen_dungeon_info(builder.name, builder.sectors, entrance_regions, all_regions,
proposed_map, doors_to_connect, bk_needed, bk_special, world, player)
@@ -55,24 +56,6 @@ def pre_validate(builder, entrance_region_names, split_dungeon, world, player):
def generate_dungeon(builder, entrance_region_names, split_dungeon, world, player):
stonewalls = check_for_stonewalls(builder)
sector = generate_dungeon_main(builder, entrance_region_names, split_dungeon, world, player)
for stonewall in stonewalls:
if not stonewall_valid(stonewall):
builder.pre_open_stonewalls.add(stonewall)
return sector
def check_for_stonewalls(builder):
stonewalls = set()
for sector in builder.sectors:
for door in sector.outstanding_doors:
if door.stonewall:
stonewalls.add(door)
return stonewalls
def generate_dungeon_main(builder, entrance_region_names, split_dungeon, world, player):
if builder.valid_proposal: # we made this earlier in gen, just use it
proposed_map = builder.valid_proposal
else:
@@ -101,19 +84,37 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon
excluded = {}
for region in entrance_regions:
portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None)
if portal and portal.destination:
excluded[region] = None
if portal:
if portal.destination:
excluded[region] = None
elif len(entrance_regions) > 1:
p_region = portal.door.entrance.connected_region
access_region = next(x.parent_region for x in p_region.entrances
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld])
if (access_region.name in world.inaccessible_regions[player] and
region.name not in world.enabled_entrances[player]):
excluded[region] = None
elif len(region.entrances) == 1: # for holes
access_region = next(x.parent_region for x in region.entrances
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]
or x.parent_region.name == 'Sewer Drop')
if access_region.name == 'Sewer Drop':
access_region = next(x.parent_region for x in access_region.entrances)
if (access_region.name in world.inaccessible_regions[player] and
region.name not in world.enabled_entrances[player]):
excluded[region] = None
entrance_regions = [x for x in entrance_regions if x not in excluded.keys()]
doors_to_connect = {}
all_regions = set()
bk_needed = False
bk_special = False
for sector in builder.sectors:
for door in sector.outstanding_doors:
doors_to_connect[door.name] = door
all_regions.update(sector.regions)
bk_needed = bk_needed or determine_if_bk_needed(sector, split_dungeon, world, player)
bk_special = bk_special or check_for_special(sector)
bk_special |= check_for_special(sector.regions)
bk_needed = False
for sector in builder.sectors:
bk_needed |= determine_if_bk_needed(sector, split_dungeon, bk_special, world, player)
proposed_map = {}
choices_master = [[]]
depth = 0
@@ -187,8 +188,8 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon
return proposed_map
def determine_if_bk_needed(sector, split_dungeon, world, player):
if not split_dungeon:
def determine_if_bk_needed(sector, split_dungeon, bk_special, world, player):
if not split_dungeon or bk_special:
for region in sector.regions:
for ext in region.exits:
door = world.check_for_door(ext.name, player)
@@ -197,8 +198,8 @@ def determine_if_bk_needed(sector, split_dungeon, world, player):
return False
def check_for_special(sector):
for region in sector.regions:
def check_for_special(regions):
for region in regions:
for loc in region.locations:
if loc.forced_big_key():
return True
@@ -417,6 +418,8 @@ def check_valid(name, dungeon, hangers, hooks, proposed_map, doors_to_connect, a
# origin has no more hooks, but not all doors have been proposed
if not world.bigkeyshuffle[player]:
possible_bks = len(dungeon['Origin'].possible_bk_locations)
if bk_special and check_for_special(dungeon['Origin'].visited_regions):
possible_bks = 1
true_origin_hooks = [x for x in dungeon['Origin'].hooks.keys() if not x.bigKey or possible_bks > 0 or not bk_needed]
if len(true_origin_hooks) == 0 and len(proposed_map.keys()) < len(doors_to_connect):
return False
@@ -450,7 +453,8 @@ def check_valid(name, dungeon, hangers, hooks, proposed_map, doors_to_connect, a
bk_possible = not bk_needed or (world.bigkeyshuffle[player] and not bk_special)
for piece in dungeon.values():
all_visited.update(piece.visited_regions)
if not bk_possible and len(piece.possible_bk_locations) > 0:
if ((not bk_possible and len(piece.possible_bk_locations) > 0) or
(bk_special and check_for_special(piece.visited_regions))):
bk_possible = True
if len(all_regions.difference(all_visited)) > 0:
return False
@@ -572,7 +576,8 @@ def determine_paths_for_dungeon(world, player, all_regions, name):
paths.append(boss)
if 'Thieves Boss' in all_r_names:
paths.append('Thieves Boss')
paths.append(('Thieves Blind\'s Cell', 'Thieves Boss'))
if world.get_dungeon("Thieves Town", player).boss.enemizer_name == 'Blind':
paths.append(('Thieves Blind\'s Cell', 'Thieves Boss'))
for drop_check in drop_path_checks:
if drop_check in all_r_names:
paths.append((drop_check, non_hole_portals))
@@ -598,35 +603,6 @@ def winnow_hangers(hangers, hooks):
hangers[hanger].remove(door)
def stonewall_valid(stonewall):
bad_door = stonewall.dest
if bad_door.blocked:
return True # great we're done with this one
loop_region = stonewall.entrance.parent_region
start_regions = [bad_door.entrance.parent_region]
if bad_door.dependents:
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:
region = queue.popleft()
if region == loop_region:
return False # guaranteed loop
possible_entrances = list(region.entrances)
for entrance in possible_entrances:
parent = entrance.parent_region
if parent.type != RegionType.Dungeon:
return False # you can get stuck from an entrance
else:
door = entrance.door
if (door is None or (door != stonewall and not door.blocked)) and parent not in visited:
visited.add(parent)
queue.append(parent)
# we didn't find anything bad
return True
def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exception):
# todo: info about dungeon events - not sure about that
graph_piece = GraphPiece()
@@ -807,6 +783,10 @@ class ExplorationState(object):
self.dungeon = dungeon
self.pinball_used = False
self.prize_door_set = {}
self.prize_doors = []
self.prize_doors_opened = False
def copy(self):
ret = ExplorationState(dungeon=self.dungeon)
ret.unattached_doors = list(self.unattached_doors)
@@ -833,6 +813,10 @@ class ExplorationState(object):
ret.non_door_entrances = list(self.non_door_entrances)
ret.dungeon = self.dungeon
ret.pinball_used = self.pinball_used
ret.prize_door_set = dict(self.prize_door_set)
ret.prize_doors = list(self.prize_doors)
ret.prize_doors_opened = self.prize_doors_opened
return ret
def next_avail_door(self):
@@ -842,6 +826,8 @@ class ExplorationState(object):
return exp_door
def visit_region(self, region, key_region=None, key_checks=False, bk_Flag=False):
if region.type != RegionType.Dungeon:
self.crystal = CrystalBarrier.Orange
if self.crystal == CrystalBarrier.Either:
if region not in self.visited_blue:
self.visited_blue.append(region)
@@ -868,6 +854,8 @@ class ExplorationState(object):
if location.name in flooded_keys_reverse.keys() and self.location_found(
flooded_keys_reverse[location.name]):
self.perform_event(flooded_keys_reverse[location.name], key_region)
if '- Prize' in location.name:
self.prize_received = True
def flooded_key_check(self, location):
if location.name not in flooded_keys.keys():
@@ -1103,9 +1091,9 @@ def valid_region_to_explore_in_regions(region, all_regions, world, player):
def valid_region_to_explore(region, name, world, player):
if region is None:
return False
return (region.type == RegionType.Dungeon and region.dungeon.name in name)\
or region.name in world.inaccessible_regions[player]\
or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard')
return ((region.type == RegionType.Dungeon and region.dungeon and region.dungeon.name in name)
or region.name in world.inaccessible_regions[player]
or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard'))
def get_doors(world, region, player):
@@ -1172,9 +1160,8 @@ class DungeonBuilder(object):
self.path_entrances = None # used for pathing/key doors, I think
self.split_flag = False
self.pre_open_stonewalls = set() # used by stonewall system
self.candidates = None
self.total_keys = None
self.key_doors_num = None
self.combo_size = None
self.flex = 0
@@ -1249,6 +1236,9 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
for r_name in ['Hyrule Dungeon Cellblock', 'Sanctuary']: # need to deliver zelda
assign_sector(find_sector(r_name, candidate_sectors), current_dungeon,
candidate_sectors, global_pole)
if key == 'Thieves Town' and world.get_dungeon("Thieves Town", player).boss.enemizer_name == 'Blind':
assign_sector(find_sector("Thieves Blind's Cell", candidate_sectors), current_dungeon,
candidate_sectors, global_pole)
entrances_map, potentials, connections = connections_tuple
accessible_sectors, reverse_d_map = set(), {}
for key in dungeon_entrances.keys():
@@ -1290,13 +1280,16 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
sanc_builder = random.choice(lw_builders)
assign_sector(sanc, sanc_builder, candidate_sectors, global_pole)
bow_sectors, retro_std_flag = {}, world.retro[player] and world.mode[player] == 'standard'
free_location_sectors = {}
crystal_switches = {}
crystal_barriers = {}
polarized_sectors = {}
neutral_sectors = {}
for sector in candidate_sectors:
if sector.chest_locations > 0:
if retro_std_flag and 'Bow' in sector.item_logic: # these need to be distributed outside of HC
bow_sectors[sector] = None
elif sector.chest_locations > 0:
free_location_sectors[sector] = None
elif sector.c_switch:
crystal_switches[sector] = None
@@ -1306,6 +1299,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
neutral_sectors[sector] = None
else:
polarized_sectors[sector] = None
if bow_sectors:
assign_bow_sectors(dungeon_map, bow_sectors, global_pole)
assign_location_sectors(dungeon_map, free_location_sectors, global_pole)
leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole)
ensure_crystal_switches_reachable(dungeon_map, leftover, polarized_sectors, crystal_barriers, global_pole)
@@ -1471,6 +1466,9 @@ def define_sector_features(sectors):
sector.blue_barrier = True
if door.bigKey:
sector.bk_required = True
if region.name in ['PoD Mimics 2', 'PoD Bow Statue Right', 'PoD Mimics 1', 'GT Mimics 1', 'GT Mimics 2',
'Eastern Single Eyegore', 'Eastern Duo Eyegores']:
sector.item_logic.add('Bow')
def assign_sector(sector, dungeon, candidate_sectors, global_pole):
@@ -1521,6 +1519,19 @@ def find_sector(r_name, sectors):
return None
def assign_bow_sectors(dungeon_map, bow_sectors, global_pole):
sector_list = list(bow_sectors)
random.shuffle(sector_list)
population = []
for name in dungeon_map:
if name != 'Hyrule Castle':
population.append(name)
choices = random.choices(population, k=len(sector_list))
for i, choice in enumerate(choices):
builder = dungeon_map[choice]
assign_sector(sector_list[i], builder, bow_sectors, global_pole)
def assign_location_sectors(dungeon_map, free_location_sectors, global_pole):
valid = False
choices = None
@@ -3274,6 +3285,7 @@ def check_for_valid_layout(builder, sector_list, builder_info):
possible_regions.add(portal.door.entrance.parent_region.name)
if builder.name in dungeon_drops.keys():
possible_regions.update(dungeon_drops[builder.name])
independents = find_independent_entrances(possible_regions, world, player)
for name, split_build in builder.split_dungeon_map.items():
name_bits = name.split(" ")
orig_name = " ".join(name_bits[:-1])
@@ -3286,7 +3298,8 @@ def check_for_valid_layout(builder, sector_list, builder_info):
if r_name not in entrance_regions:
entrance_regions.append(r_name)
# entrance_regions = [x for x in entrance_regions if x not in split_check_entrance_invalid]
proposal = generate_dungeon_find_proposal(split_build, entrance_regions, True, world, player)
split = any(x for x in independents if x not in entrance_regions)
proposal = generate_dungeon_find_proposal(split_build, entrance_regions, split, world, player)
# record split proposals
builder.valid_proposal[name] = proposal
builder.exception_list = list(sector_list)
@@ -3301,6 +3314,32 @@ def check_for_valid_layout(builder, sector_list, builder_info):
return len(unreached_doors) == 0, unreached_doors
def find_independent_entrances(entrance_regions, world, player):
independents = set()
for region in entrance_regions:
portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region.name == region), None)
if portal:
if portal.destination:
continue
elif len(entrance_regions) > 1:
p_region = portal.door.entrance.connected_region
access_region = next(x.parent_region for x in p_region.entrances
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld])
if access_region.name in world.inaccessible_regions[player]:
continue
else:
r = world.get_region(region, player)
access_region = next(x.parent_region for x in r.entrances
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]
or x.parent_region.name == 'Sewer Drop')
if access_region.name == 'Sewer Drop':
access_region = next(x.parent_region for x in access_region.entrances)
if access_region.name in world.inaccessible_regions[player]:
continue
independents.add(region)
return independents
def resolve_equations(builder, sector_list):
unreached_doors = defaultdict(list)
equations = {x: y for x, y in copy_door_equations(builder, sector_list).items() if len(y) > 0}
@@ -3823,7 +3862,7 @@ dungeon_boss_sectors = {
'Palace of Darkness': ['PoD Boss'],
'Swamp Palace': ['Swamp Boss'],
'Skull Woods': ['Skull Boss'],
'Thieves Town': ['Thieves Blind\'s Cell', 'Thieves Boss'],
'Thieves Town': ['Thieves Boss'],
'Ice Palace': ['Ice Boss'],
'Misery Mire': ['Mire Boss'],
'Turtle Rock': ['TR Boss'],
+33
View File
@@ -375,6 +375,38 @@ flexible_starts = {
'Skull Woods': ['Skull Left Drop', 'Skull Pinball']
}
class DungeonInfo:
def __init__(self, free, keys, bk, map, compass, bk_drop, drops, prize=None):
# todo reduce static maps ideas: prize, bk_name, sm_name, cmp_name, map_name):
self.free_items = free
self.key_num = keys
self.bk_present = bk
self.map_present = map
self.compass_present = compass
self.bk_drops = bk_drop
self.key_drops = drops
self.prize = prize
dungeon_table = {
'Hyrule Castle': DungeonInfo(6, 1, False, True, False, True, 3, None),
'Eastern Palace': DungeonInfo(3, 0, True, True, True, False, 2, 'Eastern Palace - Prize'),
'Desert Palace': DungeonInfo(2, 1, True, True, True, False, 3, 'Desert Palace - Prize'),
'Tower of Hera': DungeonInfo(2, 1, True, True, True, False, 0, 'Tower of Hera - Prize'),
'Agahnims Tower': DungeonInfo(0, 2, False, False, False, False, 2, None),
'Palace of Darkness': DungeonInfo(5, 6, True, True, True, False, 0, 'Palace of Darkness - Prize'),
'Swamp Palace': DungeonInfo(6, 1, True, True, True, False, 5, 'Swamp Palace - Prize'),
'Skull Woods': DungeonInfo(2, 3, True, True, True, False, 2, 'Skull Woods - Prize'),
'Thieves Town': DungeonInfo(4, 1, True, True, True, False, 2, "Thieves' Town - Prize"),
'Ice Palace': DungeonInfo(3, 2, True, True, True, False, 4, 'Ice Palace - Prize'),
'Misery Mire': DungeonInfo(2, 3, True, True, True, False, 3, 'Misery Mire - Prize'),
'Turtle Rock': DungeonInfo(5, 4, True, True, True, False, 2, 'Turtle Rock - Prize'),
'Ganons Tower': DungeonInfo(20, 4, True, True, True, False, 4, None),
}
dungeon_keys = {
'Hyrule Castle': 'Small Key (Escape)',
'Eastern Palace': 'Small Key (Eastern Palace)',
@@ -407,6 +439,7 @@ dungeon_bigs = {
'Ganons Tower': 'Big Key (Ganons Tower)'
}
dungeon_hints = {
'Hyrule Castle': 'in Hyrule Castle',
'Eastern Palace': 'in Eastern Palace',
+71 -17
View File
@@ -109,6 +109,7 @@ def link_entrances(world, player):
links_house_doors = [i for i in LW_Single_Cave_Doors if i not in Isolated_LH_Doors_Open]
links_house = random.choice(links_house_doors)
connect_two_way(world, links_house, 'Links House Exit', player)
connect_exit(world, 'Chris Houlihan Room Exit', links_house, player) # should match link's house
if links_house in bomb_shop_doors:
bomb_shop_doors.remove(links_house)
if links_house in blacksmith_doors:
@@ -173,6 +174,7 @@ def link_entrances(world, player):
links_house_doors = [i for i in lw_entrances if i not in Isolated_LH_Doors_Open]
links_house = random.choice(links_house_doors)
connect_two_way(world, links_house, 'Links House Exit', player)
connect_exit(world, 'Chris Houlihan Room Exit', links_house, player) # should match link's house
if links_house in lw_entrances:
lw_entrances.remove(links_house)
@@ -215,6 +217,13 @@ def link_entrances(world, player):
if bomb_shop in dw_entrances:
dw_entrances.remove(bomb_shop)
# standard mode cannot have Bonk Fairy Light be a connector in case of starting boots
# or boots are in links house, etc.
removed = False
if world.mode[player] == 'standard' and 'Bonk Fairy (Light)' in lw_entrances:
lw_entrances.remove('Bonk Fairy (Light)')
removed = True
# place the old man cave's entrance somewhere in the light world
random.shuffle(lw_entrances)
old_man_entrance = lw_entrances.pop()
@@ -226,6 +235,8 @@ def link_entrances(world, player):
# now scramble the rest
connect_caves(world, lw_entrances, dw_entrances, caves, player)
if removed:
lw_entrances.append('Bonk Fairy (Light)')
# scramble holes
scramble_holes(world, player)
@@ -328,6 +339,7 @@ def link_entrances(world, player):
links_house_doors = [i for i in lw_entrances + lw_must_exits if i not in Isolated_LH_Doors_Open]
links_house = random.choice(links_house_doors)
connect_two_way(world, links_house, 'Links House Exit', player)
connect_exit(world, 'Chris Houlihan Room Exit', links_house, player) # should match link's house
if links_house in lw_entrances:
lw_entrances.remove(links_house)
if links_house in lw_must_exits:
@@ -395,14 +407,23 @@ def link_entrances(world, player):
if bomb_shop in dw_entrances:
dw_entrances.remove(bomb_shop)
# standard mode cannot have Bonk Fairy Light be a connector in case of
# starting boots or boots are in links house, etc.
removed = False
if world.mode[player] == 'standard' and 'Bonk Fairy (Light)' in lw_entrances:
lw_entrances.remove('Bonk Fairy (Light)')
removed = True
# place the old man cave's entrance somewhere in the light world
old_man_entrance = lw_entrances.pop()
connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player)
# now scramble the rest
connect_caves(world, lw_entrances, dw_entrances, caves, player)
if removed:
lw_entrances.append('Bonk Fairy (Light)')
# scramble holes
scramble_holes(world, player)
@@ -449,6 +470,7 @@ def link_entrances(world, player):
links_house_doors = [i for i in links_house_doors if i not in exclusions]
links_house = random.choice(list(links_house_doors))
connect_two_way(world, links_house, 'Links House Exit', player)
connect_exit(world, 'Chris Houlihan Room Exit', links_house, player) # should match link's house
if links_house in entrances:
entrances.remove(links_house)
elif links_house in must_exits:
@@ -487,16 +509,24 @@ def link_entrances(world, player):
connect_entrance(world, bomb_shop, 'Big Bomb Shop', player)
entrances.remove(bomb_shop)
# standard mode cannot have Bonk Fairy Light be a connector in case of
# starting boots or boots are in links house, etc.
removed = False
if world.mode[player] == 'standard' and 'Bonk Fairy (Light)' in entrances:
entrances.remove('Bonk Fairy (Light)')
removed = True
# place the old man cave's entrance somewhere
random.shuffle(entrances)
old_man_entrance = entrances.pop()
connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player)
# now scramble the rest
connect_caves(world, entrances, [], caves, player)
if removed:
entrances.append('Bonk Fairy (Light)')
# scramble holes
scramble_holes(world, player)
@@ -911,6 +941,7 @@ def link_entrances(world, player):
links_house_doors = [i for i in links_house_doors if i not in exclusions]
links_house = random.choice(links_house_doors)
connect_two_way(world, links_house, 'Links House Exit', player)
connect_exit(world, 'Chris Houlihan Room Exit', links_house, player) # should match link's house
exit_pool.remove(links_house)
doors.remove(links_house)
@@ -971,6 +1002,13 @@ def link_entrances(world, player):
connect_entrance(world, bomb_shop, 'Big Bomb Shop', player)
doors.remove(bomb_shop)
# standard mode cannot have Bonk Fairy Light be a connector in case of
# starting boots or boots are in links house, etc.
removed = False
if world.mode[player] == 'standard' and 'Bonk Fairy (Light)' in doors:
doors.remove('Bonk Fairy (Light)')
removed = True
# handle remaining caves
for cave in caves:
if isinstance(cave, str):
@@ -980,6 +1018,9 @@ def link_entrances(world, player):
connect_exit(world, exit, exit_pool.pop(), player)
connect_entrance(world, doors.pop(), exit, player)
if removed:
doors.append('Bonk Fairy (Light)')
# place remaining doors
connect_doors(world, doors, door_targets, player)
elif world.shuffle[player] == 'insanity_legacy':
@@ -1265,6 +1306,7 @@ def link_inverted_entrances(world, player):
links_house_doors = [i for i in DW_Single_Cave_Doors if i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors]
links_house = random.choice(links_house_doors)
connect_two_way(world, links_house, 'Inverted Links House Exit', player)
connect_exit(world, 'Chris Houlihan Room Exit', links_house, player) # should match link's house
if links_house in bomb_shop_doors:
bomb_shop_doors.remove(links_house)
if links_house in blacksmith_doors:
@@ -1345,6 +1387,7 @@ def link_inverted_entrances(world, player):
links_house_doors = [i for i in dw_entrances if i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors]
links_house = random.choice(links_house_doors)
connect_two_way(world, links_house, 'Inverted Links House Exit', player)
connect_exit(world, 'Chris Houlihan Room Exit', links_house, player) # should match link's house
if links_house in dw_entrances:
dw_entrances.remove(links_house)
@@ -1474,6 +1517,7 @@ def link_inverted_entrances(world, player):
links_house_doors = [i for i in dw_entrances if i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors]
links_house = random.choice(links_house_doors)
connect_two_way(world, links_house, 'Inverted Links House Exit', player)
connect_exit(world, 'Chris Houlihan Room Exit', links_house, player) # should match link's house
if links_house in dw_entrances:
dw_entrances.remove(links_house)
@@ -1611,6 +1655,7 @@ def link_inverted_entrances(world, player):
links_house_doors = [i for i in entrances + must_exits if i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors]
links_house = random.choice(links_house_doors)
connect_two_way(world, links_house, 'Inverted Links House Exit', player)
connect_exit(world, 'Chris Houlihan Room Exit', links_house, player) # should match link's house
if links_house in entrances:
entrances.remove(links_house)
elif links_house in must_exits:
@@ -1744,6 +1789,7 @@ def link_inverted_entrances(world, player):
links_house_doors = [i for i in doors if i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors]
links_house = random.choice(links_house_doors)
connect_two_way(world, links_house, 'Inverted Links House Exit', player)
connect_exit(world, 'Chris Houlihan Room Exit', links_house, player) # should match link's house
doors.remove(links_house)
exit_pool.remove(links_house)
@@ -2120,10 +2166,11 @@ def connect_doors(world, doors, targets, player):
"""This works inplace"""
random.shuffle(doors)
random.shuffle(targets)
while doors:
door = doors.pop()
target = targets.pop()
placing = min(len(doors), len(targets))
for door, target in zip(doors, targets):
connect_entrance(world, door, target, player)
doors[:] = doors[placing:]
targets[:] = targets[placing:]
def skull_woods_shuffle(world, player):
@@ -2373,7 +2420,7 @@ Cave_Exits_Base = [['Elder House Exit (East)', 'Elder House Exit (West)'],
['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)'],
['Fairy Ascension Cave Exit (Bottom)', 'Fairy Ascension Cave Exit (Top)'],
['Bumper Cave Exit (Top)', 'Bumper Cave Exit (Bottom)'],
['Hookshot Cave Exit (South)', 'Hookshot Cave Exit (North)']]
['Hookshot Cave Back Exit', 'Hookshot Cave Front Exit']]
Cave_Exits_Base += [('Superbunny Cave Exit (Bottom)', 'Superbunny Cave Exit (Top)'),
('Spiral Cave Exit (Top)', 'Spiral Cave Exit')]
@@ -3115,6 +3162,10 @@ mandatory_connections = [('Links House S&Q', 'Links House'),
('Dark Death Mountain Drop (West)', 'Dark Death Mountain (West Bottom)'),
('East Death Mountain (Top) Mirror Spot', 'East Death Mountain (Top)'),
('Superbunny Cave Climb', 'Superbunny Cave (Top)'),
('Hookshot Cave Front to Middle', 'Hookshot Cave (Middle)'),
('Hookshot Cave Middle to Front', 'Hookshot Cave (Front)'),
('Hookshot Cave Middle to Back', 'Hookshot Cave (Back)'),
('Hookshot Cave Back to Middle', 'Hookshot Cave (Middle)'),
('Turtle Rock Teleporter', 'Turtle Rock (Top)'),
('Turtle Rock Drop', 'Dark Death Mountain (Top)'),
('Floating Island Drop', 'Dark Death Mountain (Top)'),
@@ -3233,6 +3284,10 @@ inverted_mandatory_connections = [('Links House S&Q', 'Inverted Links House'),
('Turtle Rock Tail Drop', 'Turtle Rock (Top)'),
('Turtle Rock Drop', 'Dark Death Mountain'),
('Superbunny Cave Climb', 'Superbunny Cave (Top)'),
('Hookshot Cave Front to Middle', 'Hookshot Cave (Middle)'),
('Hookshot Cave Middle to Front', 'Hookshot Cave (Front)'),
('Hookshot Cave Middle to Back', 'Hookshot Cave (Back)'),
('Hookshot Cave Back to Middle', 'Hookshot Cave (Middle)'),
('Desert Ledge Drop', 'Light World'),
('Floating Island Drop', 'Dark Death Mountain'),
('Dark Lake Hylia Central Island Teleporter', 'Lake Hylia Central Island'),
@@ -3284,7 +3339,6 @@ inverted_mandatory_connections = [('Links House S&Q', 'Inverted Links House'),
('EDDM Flute', 'The Sky'),
('Dark Grassy Lawn Flute', 'The Sky'),
('Hammer Peg Area Flute', 'The Sky'),
('Chris Houlihan Room Exit', 'Pyramid Ledge'),
('Bush Covered Lawn Inner Bushes', 'Light World'),
('Bush Covered Lawn Outer Bushes', 'Bush Covered Lawn'),
('Bush Covered Lawn Mirror Spot', 'Dark Grassy Lawn'),
@@ -3428,16 +3482,16 @@ default_connections = [('Links House', 'Links House'),
('Dark Desert Hint', 'Dark Desert Hint'),
('Dark Desert Fairy', 'Dark Desert Healer Fairy'),
('Spike Cave', 'Spike Cave'),
('Hookshot Cave', 'Hookshot Cave'),
('Hookshot Cave', 'Hookshot Cave (Front)'),
('Superbunny Cave (Top)', 'Superbunny Cave (Top)'),
('Cave Shop (Dark Death Mountain)', 'Cave Shop (Dark Death Mountain)'),
('Dark Death Mountain Fairy', 'Dark Death Mountain Healer Fairy'),
('Superbunny Cave (Bottom)', 'Superbunny Cave (Bottom)'),
('Superbunny Cave Exit (Top)', 'Dark Death Mountain (Top)'),
('Superbunny Cave Exit (Bottom)', 'Dark Death Mountain (East Bottom)'),
('Hookshot Cave Exit (South)', 'Dark Death Mountain (Top)'),
('Hookshot Cave Exit (North)', 'Death Mountain Floating Island (Dark World)'),
('Hookshot Cave Back Entrance', 'Hookshot Cave'),
('Hookshot Cave Front Exit', 'Dark Death Mountain (Top)'),
('Hookshot Cave Back Exit', 'Death Mountain Floating Island (Dark World)'),
('Hookshot Cave Back Entrance', 'Hookshot Cave (Back)'),
('Mimic Cave', 'Mimic Cave'),
('Pyramid Hole', 'Pyramid'),
@@ -3562,13 +3616,13 @@ inverted_default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing'
('Dark Desert Hint', 'Dark Desert Hint'),
('Dark Desert Fairy', 'Dark Desert Healer Fairy'),
('Spike Cave', 'Spike Cave'),
('Hookshot Cave', 'Hookshot Cave'),
('Hookshot Cave', 'Hookshot Cave (Front)'),
('Superbunny Cave (Top)', 'Superbunny Cave (Top)'),
('Cave Shop (Dark Death Mountain)', 'Cave Shop (Dark Death Mountain)'),
('Superbunny Cave (Bottom)', 'Superbunny Cave (Bottom)'),
('Superbunny Cave Exit (Bottom)', 'Dark Death Mountain (East Bottom)'),
('Hookshot Cave Exit (North)', 'Death Mountain Floating Island (Dark World)'),
('Hookshot Cave Back Entrance', 'Hookshot Cave'),
('Hookshot Cave Back Exit', 'Death Mountain Floating Island (Dark World)'),
('Hookshot Cave Back Entrance', 'Hookshot Cave (Back)'),
('Mimic Cave', 'Mimic Cave'),
('Inverted Pyramid Hole', 'Pyramid'),
('Inverted Links House', 'Inverted Links House'),
@@ -3589,7 +3643,7 @@ inverted_default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing'
('Death Mountain Return Cave (East)', 'Death Mountain Return Cave'),
('Death Mountain Return Cave Exit (West)', 'Death Mountain'),
('Death Mountain Return Cave Exit (East)', 'Death Mountain'),
('Hookshot Cave Exit (South)', 'Dark Death Mountain'),
('Hookshot Cave Front Exit', 'Dark Death Mountain'),
('Superbunny Cave Exit (Top)', 'Dark Death Mountain'),
('Pyramid Exit', 'Light World'),
('Inverted Pyramid Entrance', 'Bottom of Pyramid')]
@@ -3937,8 +3991,8 @@ exit_ids = {'Links House Exit': (0x01, 0x00),
'Bumper Cave Exit (Bottom)': (0x16, 0x17),
'Superbunny Cave Exit (Top)': (0x14, 0x15),
'Superbunny Cave Exit (Bottom)': (0x13, 0x14),
'Hookshot Cave Exit (South)': (0x3A, 0x3B),
'Hookshot Cave Exit (North)': (0x3B, 0x3C),
'Hookshot Cave Front Exit': (0x3A, 0x3B),
'Hookshot Cave Back Exit': (0x3B, 0x3C),
'Ganons Tower Exit': (0x37, 0x38),
'Inverted Ganons Tower Exit': (0x37, 0x38),
'Pyramid Exit': (0x36, 0x37),
+154 -55
View File
@@ -1,4 +1,6 @@
import RaceRandom as random
import collections
import itertools
import logging
from BaseClasses import CollectionState
@@ -211,7 +213,7 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool =
and valid_key_placement(item_to_place, location, itempool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world):
spot_to_fill = location
break
elif item_to_place.smallkey or item_to_place.bigkey:
if item_to_place.smallkey or item_to_place.bigkey:
location.item = None
if spot_to_fill is None:
@@ -221,7 +223,10 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool =
if world.accessibility[item_to_place.player] != 'none':
logging.getLogger('').warning('Not all items placed. Game beatable anyway. (Could not place %s)' % item_to_place)
continue
raise FillError('No more spots to place %s' % item_to_place)
spot_to_fill = last_ditch_placement(item_to_place, locations, world, maximum_exploration_state,
base_state, itempool, keys_in_itempool, single_player_placement)
if spot_to_fill is None:
raise FillError('No more spots to place %s' % item_to_place)
world.push_item(spot_to_fill, item_to_place, False)
track_outside_keys(item_to_place, spot_to_fill, world)
@@ -240,7 +245,11 @@ def valid_key_placement(item, location, itempool, world):
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 and x.player == item.player])
return key_logic.check_placement(unplaced_keys, location if item.bigkey else None)
prize_loc = None
if key_logic.prize_location:
prize_loc = world.get_location(key_logic.prize_location, location.player)
cr_count = world.crystals_needed_for_gt[location.player]
return key_logic.check_placement(unplaced_keys, location if item.bigkey else None, prize_loc, cr_count)
else:
inside_dungeon_item = ((item.smallkey and not world.keyshuffle[item.player])
or (item.bigkey and not world.bigkeyshuffle[item.player]))
@@ -250,9 +259,7 @@ def valid_key_placement(item, location, itempool, world):
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'
item_dungeon = item.dungeon
if location.player == item.player:
loc_dungeon = location.parent_region.dungeon
if loc_dungeon and loc_dungeon.name == item_dungeon:
@@ -260,6 +267,80 @@ def track_outside_keys(item, location, world):
world.key_logic[item.player][item_dungeon].outside_keys += 1
def last_ditch_placement(item_to_place, locations, world, state, base_state, itempool,
keys_in_itempool=None, single_player_placement=False):
def location_preference(loc):
if not loc.item.advancement:
return 1
if loc.item.type and loc.item.type != 'Sword':
if loc.item.type in ['Map', 'Compass']:
return 2
else:
return 3
return 4
if item_to_place.type == 'Crystal':
possible_swaps = [x for x in state.locations_checked if x.item.type == 'Crystal']
else:
possible_swaps = [x for x in state.locations_checked
if x.item.type not in ['Event', 'Crystal'] and not x.forced_item]
swap_locations = sorted(possible_swaps, key=location_preference)
for location in swap_locations:
old_item = location.item
new_pool = list(itempool) + [old_item]
new_spot = find_spot_for_item(item_to_place, [location], world, base_state, new_pool,
keys_in_itempool, single_player_placement)
if new_spot:
restore_item = new_spot.item
new_spot.item = item_to_place
swap_spot = find_spot_for_item(old_item, locations, world, base_state, itempool,
keys_in_itempool, single_player_placement)
if swap_spot:
logging.getLogger('').debug(f'Swapping {old_item} for {item_to_place}')
world.push_item(swap_spot, old_item, False)
swap_spot.event = True
locations.remove(swap_spot)
locations.append(new_spot)
return new_spot
else:
new_spot.item = restore_item
else:
location.item = old_item
return None
def find_spot_for_item(item_to_place, locations, world, base_state, pool,
keys_in_itempool=None, single_player_placement=False):
def sweep_from_pool():
new_state = base_state.copy()
for item in pool:
new_state.collect(item, True)
new_state.sweep_for_events()
return new_state
for location in locations:
maximum_exploration_state = sweep_from_pool()
perform_access_check = True
old_item = None
if world.accessibility[item_to_place.player] == 'none':
perform_access_check = not world.has_beaten_game(maximum_exploration_state, item_to_place.player) if single_player_placement else not world.has_beaten_game(maximum_exploration_state)
if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there
old_item = location.item
location.item = item_to_place
test_state = maximum_exploration_state.copy()
test_state.stale[item_to_place.player] = True
else:
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):
return location
if item_to_place.smallkey or item_to_place.bigkey:
location.item = old_item
return 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 fill_locations:
@@ -420,9 +501,8 @@ def sell_keys(world, player):
def balance_multiworld_progression(world):
state = CollectionState(world)
checked_locations = []
unchecked_locations = world.get_locations().copy()
random.shuffle(unchecked_locations)
checked_locations = set()
unchecked_locations = set(world.get_locations())
reachable_locations_count = {}
for player in range(1, world.players + 1):
@@ -430,7 +510,7 @@ def balance_multiworld_progression(world):
def get_sphere_locations(sphere_state, locations):
sphere_state.sweep_for_events(key_only=True, locations=locations)
return [loc for loc in locations if sphere_state.can_reach(loc) and sphere_state.not_flooding_a_key(sphere_state.world, loc)]
return {loc for loc in locations if sphere_state.can_reach(loc) and sphere_state.not_flooding_a_key(sphere_state.world, loc)}
while True:
sphere_locations = get_sphere_locations(state, unchecked_locations)
@@ -441,38 +521,42 @@ def balance_multiworld_progression(world):
if checked_locations:
threshold = max(reachable_locations_count.values()) - 20
balancing_players = [player for player, reachables in reachable_locations_count.items() if reachables < threshold]
if balancing_players is not None and len(balancing_players) > 0:
balancing_players = {player for player, reachables in reachable_locations_count.items() if reachables < threshold}
if balancing_players:
balancing_state = state.copy()
balancing_unchecked_locations = unchecked_locations.copy()
balancing_reachables = reachable_locations_count.copy()
balancing_sphere = sphere_locations.copy()
candidate_items = []
candidate_items = collections.defaultdict(set)
while True:
for location in balancing_sphere:
if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
balancing_state.collect(location.item, True, location)
if location.item.player in balancing_players and not location.locked:
candidate_items.append(location)
player = location.item.player
if player in balancing_players and not location.locked and location.player != player:
candidate_items[player].add(location)
balancing_sphere = get_sphere_locations(balancing_state, balancing_unchecked_locations)
for location in balancing_sphere:
balancing_unchecked_locations.remove(location)
balancing_reachables[location.player] += 1
if world.has_beaten_game(balancing_state) or all([reachables >= threshold for reachables in balancing_reachables.values()]):
if world.has_beaten_game(balancing_state) or all(reachables >= threshold for reachables in balancing_reachables.values()):
break
elif not balancing_sphere:
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
unlocked_locations = [l for l in unchecked_locations if l not in balancing_unchecked_locations]
unlocked_locations = collections.defaultdict(set)
for l in unchecked_locations:
if l not in balancing_unchecked_locations:
unlocked_locations[l.player].add(l)
items_to_replace = []
for player in balancing_players:
locations_to_test = [l for l in unlocked_locations if l.player == player]
# only replace items that end up in another player's world
items_to_test = [l for l in candidate_items if l.item.player == player and l.player != player]
locations_to_test = unlocked_locations[player]
items_to_test = candidate_items[player]
while items_to_test:
testing = items_to_test.pop()
reducing_state = state.copy()
for location in [*[l for l in items_to_replace if l.item.player == player], *items_to_test]:
for location in itertools.chain((l for l in items_to_replace if l.item.player == player),
items_to_test):
reducing_state.collect(location.item, True, location)
reducing_state.sweep_for_events(locations=locations_to_test)
@@ -486,33 +570,44 @@ def balance_multiworld_progression(world):
items_to_replace.append(testing)
replaced_items = False
replacement_locations = [l for l in checked_locations if not l.event and not l.locked]
# sort then shuffle to maintain deterministic behaviour,
# while allowing use of set for better algorithm growth behaviour elsewhere
replacement_locations = sorted((l for l in checked_locations if not l.event and not l.locked),
key=lambda loc: (loc.name, loc.player))
random.shuffle(replacement_locations)
items_to_replace.sort(key=lambda item: (item.name, item.player))
random.shuffle(items_to_replace)
while replacement_locations and items_to_replace:
new_location = replacement_locations.pop()
old_location = items_to_replace.pop()
for new_location in replacement_locations:
if (new_location.can_fill(state, old_location.item, False) and
old_location.can_fill(state, new_location.item, False)):
replacement_locations.remove(new_location)
new_location.item, old_location.item = old_location.item, new_location.item
if world.shopsanity[new_location.player]:
check_shop_swap(new_location)
if world.shopsanity[old_location.player]:
check_shop_swap(old_location)
new_location.event, old_location.event = True, False
logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, "
f"displacing {old_location.item} into {old_location}")
state.collect(new_location.item, True, new_location)
replaced_items = True
break
else:
logging.warning(f"Could not Progression Balance {old_location.item}")
while not new_location.can_fill(state, old_location.item, False) or (new_location.item and not old_location.can_fill(state, new_location.item, False)):
replacement_locations.insert(0, new_location)
new_location = replacement_locations.pop()
new_location.item, old_location.item = old_location.item, new_location.item
if world.shopsanity[new_location.player]:
check_shop_swap(new_location)
if world.shopsanity[old_location.player]:
check_shop_swap(old_location)
new_location.event, old_location.event = True, False
state.collect(new_location.item, True, new_location)
replaced_items = True
if replaced_items:
for location in get_sphere_locations(state, [l for l in unlocked_locations if l.player in balancing_players]):
unlocked = {fresh for player in balancing_players for fresh in unlocked_locations[player]}
for location in get_sphere_locations(state, unlocked):
unchecked_locations.remove(location)
reachable_locations_count[location.player] += 1
sphere_locations.append(location)
sphere_locations.add(location)
for location in sphere_locations:
if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
state.collect(location.item, True, location)
checked_locations.extend(sphere_locations)
checked_locations |= sphere_locations
if world.has_beaten_game(state):
break
@@ -553,7 +648,7 @@ def balance_money_progression(world):
base_value = sum(rupee_rooms.values())
available_money = {player: base_value for player in range(1, world.players+1)}
for loc in world.get_locations():
if loc.item.name in rupee_chart:
if loc.item and loc.item.name in rupee_chart:
available_money[loc.item.player] += rupee_chart[loc.item.name]
total_price = {player: 0 for player in range(1, world.players+1)}
@@ -618,7 +713,7 @@ def balance_money_progression(world):
slot = shop_to_location_table[location.parent_region.name].index(location.name)
shop = location.parent_region.shop
shop_item = shop.inventory[slot]
if interesting_item(location, location.item, world, location.item.player):
if location.item and interesting_item(location, location.item, world, location.item.player):
if location.item.name.startswith('Rupee') and loc_player == location.item.player:
if shop_item['price'] < rupee_chart[location.item.name]:
wallet[loc_player] -= shop_item['price'] # will get picked up in the location_free block
@@ -638,20 +733,21 @@ def balance_money_progression(world):
if location_free:
state.collect(location.item, True, location)
unchecked_locations.remove(location)
if location.item.name.startswith('Rupee'):
wallet[location.item.player] += rupee_chart[location.item.name]
if location.item.name != 'Rupees (300)':
if location.item:
if location.item.name.startswith('Rupee'):
wallet[location.item.player] += rupee_chart[location.item.name]
if location.item.name != 'Rupees (300)':
balance_locations[location.item.player].add(location)
if interesting_item(location, location.item, world, location.item.player):
checked_locations.append(location)
elif location.item.name in acceptable_balancers:
balance_locations[location.item.player].add(location)
if interesting_item(location, location.item, world, location.item.player):
checked_locations.append(location)
elif location.item.name in acceptable_balancers:
balance_locations[location.item.player].add(location)
for room, income in rupee_rooms.items():
for player in range(1, world.players+1):
if room not in rooms_visited[player] and world.get_region(room, player) in state.reachable_regions[player]:
wallet[player] += income
rooms_visited[player].add(room)
if checked_locations:
if checked_locations or len(unchecked_locations) == 0:
if world.has_beaten_game(state):
done = True
continue
@@ -661,14 +757,17 @@ def balance_money_progression(world):
solvent = set()
insolvent = set()
for player in range(1, world.players+1):
if wallet[player] >= sphere_costs[player] > 0:
if wallet[player] >= sphere_costs[player] >= 0:
solvent.add(player)
if sphere_costs[player] > 0 and sphere_costs[player] > wallet[player]:
insolvent.add(player)
if len(solvent) == 0:
target_player = min(insolvent, key=lambda p: sphere_costs[p]-wallet[p])
difference = sphere_costs[target_player]-wallet[target_player]
logger.debug(f'Money balancing needed: Player {target_player} short {difference}')
if len([p for p in solvent if len(locked_by_money[p]) > 0]) == 0:
if len(insolvent) > 0:
target_player = min(insolvent, key=lambda p: sphere_costs[p]-wallet[p])
difference = sphere_costs[target_player]-wallet[target_player]
logger.debug(f'Money balancing needed: Player {target_player} short {difference}')
else:
difference = 0
while difference > 0:
swap_targets = [x for x in unchecked_locations if x not in sphere_locations and x.item.name.startswith('Rupees') and x.item.player == target_player]
if len(swap_targets) == 0:
@@ -705,10 +804,10 @@ def balance_money_progression(world):
for player in solvent:
wallet[player] -= sphere_costs[player]
for location in locked_by_money[player]:
if location == 'Kiki':
if isinstance(location, str) and location == 'Kiki':
kiki_paid[player] = True
else:
state.collect(location.item, True, location)
unchecked_locations.remove(location)
if location.item.name.startswith('Rupee'):
if location.item and location.item.name.startswith('Rupee'):
wallet[location.item.player] += rupee_chart[location.item.name]
+9
View File
@@ -24,6 +24,13 @@ from source.classes.BabelFish import BabelFish
from source.classes.Empty import Empty
def check_python_version(fish):
import sys
version = sys.version_info
if version.major < 3 or version.minor < 7:
messagebox.showinfo("Door Shuffle " + ESVersion, fish.translate("cli","cli","old.python.version") % sys.version)
def guiMain(args=None):
# Save settings to file
def save_settings(args):
@@ -188,6 +195,8 @@ def guiMain(args=None):
# load adjust settings into options
loadadjustargs(self, self.settings)
check_python_version(self.fish)
# run main window
mainWindow.mainloop()
+5 -2
View File
@@ -199,8 +199,11 @@ def create_inverted_regions(world, player):
create_cave_region(player, 'Superbunny Cave (Top)', 'a connector', ['Superbunny Cave - Top', 'Superbunny Cave - Bottom'], ['Superbunny Cave Exit (Top)']),
create_cave_region(player, 'Superbunny Cave (Bottom)', 'a connector', None, ['Superbunny Cave Climb', 'Superbunny Cave Exit (Bottom)']),
create_cave_region(player, 'Spike Cave', 'Spike Cave', ['Spike Cave']),
create_cave_region(player, 'Hookshot Cave', 'a connector', ['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left'],
['Hookshot Cave Exit (South)', 'Hookshot Cave Exit (North)']),
create_cave_region(player, 'Hookshot Cave (Front)', 'a connector', ['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left'],
['Hookshot Cave Front to Middle', 'Hookshot Cave Front Exit']),
create_cave_region(player, 'Hookshot Cave (Back)', 'a connector', None, ['Hookshot Cave Back to Middle', 'Hookshot Cave Back Exit']),
create_cave_region(player, 'Hookshot Cave (Middle)', 'a connector', None, ['Hookshot Cave Middle to Back', 'Hookshot Cave Middle to Front']),
create_dw_region(player, 'Death Mountain Floating Island (Dark World)', None, ['Floating Island Drop', 'Hookshot Cave Back Entrance']),
create_cave_region(player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']),
+66 -26
View File
@@ -4,7 +4,6 @@ import math
import RaceRandom as random
from BaseClasses import Region, RegionType, Shop, ShopType, Location, CollectionState
from Bosses import place_bosses
from Dungeons import get_dungeon_item_pool
from EntranceShuffle import connect_entrance
from Regions import shop_to_location_table, retro_shops, shop_table_by_location
@@ -37,7 +36,7 @@ Difficulty = namedtuple('Difficulty',
['baseitems', 'bottles', 'bottle_count', 'same_bottle', 'progressiveshield',
'basicshield', 'progressivearmor', 'basicarmor', 'swordless',
'progressivesword', 'basicsword', 'basicbow', 'timedohko', 'timedother',
'retro',
'retro', 'bombbag',
'extras', 'progressive_sword_limit', 'progressive_shield_limit',
'progressive_armor_limit', 'progressive_bottle_limit',
'progressive_bow_limit', 'heart_piece_limit', 'boss_heart_container_limit'])
@@ -61,6 +60,7 @@ difficulties = {
timedohko = ['Green Clock'] * 25,
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
retro = ['Small Key (Universal)'] * 18 + ['Rupees (20)'] * 10,
bombbag = ['Bomb Upgrade (+10)'] * 2,
extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
progressive_sword_limit = 4,
progressive_shield_limit = 3,
@@ -86,6 +86,7 @@ difficulties = {
timedohko = ['Green Clock'] * 25,
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
retro = ['Small Key (Universal)'] * 13 + ['Rupees (5)'] * 15,
bombbag = ['Bomb Upgrade (+10)'] * 2,
extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
progressive_sword_limit = 3,
progressive_shield_limit = 2,
@@ -111,6 +112,7 @@ difficulties = {
timedohko = ['Green Clock'] * 20 + ['Red Clock'] * 5,
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
retro = ['Small Key (Universal)'] * 13 + ['Rupees (5)'] * 15,
bombbag = ['Bomb Upgrade (+10)'] * 2,
extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
progressive_sword_limit = 2,
progressive_shield_limit = 1,
@@ -251,10 +253,10 @@ def generate_itempool(world, player):
# set up item pool
if world.custom:
(pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.customitemarray)
(pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bombbag[player], world.customitemarray)
world.rupoor_cost = min(world.customitemarray[player]["rupoorcost"], 9999)
else:
(pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) = get_pool_core(world.progressive, world.shuffle[player], world.difficulty[player], world.treasure_hunt_total[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.doorShuffle[player], world.logic[player])
(pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) = get_pool_core(world.progressive, world.shuffle[player], world.difficulty[player], world.treasure_hunt_total[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bombbag[player], world.doorShuffle[player], world.logic[player])
if player in world.pool_adjustment.keys():
amt = world.pool_adjustment[player]
@@ -284,7 +286,7 @@ def generate_itempool(world, player):
if item in ['Hammer', 'Fire Rod', 'Cane of Somaria', 'Cane of Byrna']:
if item not in possible_weapons:
possible_weapons.append(item)
if item in ['Bombs (10)']:
if not world.bombbag[player] and item in ['Bombs (10)']:
if item not in possible_weapons and world.doorShuffle[player] != 'crossed':
possible_weapons.append(item)
starting_weapon = random.choice(possible_weapons)
@@ -315,6 +317,11 @@ def generate_itempool(world, player):
p_item = next(item for item in items if item.name == potion and item.player == player)
p_item.priority = True # don't beemize one of each potion
if world.bombbag[player]:
for item in items:
if item.name == 'Bomb Upgrade (+10)' and item.player == player:
item.advancement = True
world.lamps_needed_for_dark_rooms = lamps_needed_for_dark_rooms
if clock_mode is not None:
@@ -339,11 +346,11 @@ def generate_itempool(world, player):
# rather than making all hearts/heart pieces progression items (which slows down generation considerably)
# We mark one random heart container as an advancement item (or 4 heart pieces in expert mode)
if world.difficulty[player] in ['normal', 'hard'] and not (world.custom and world.customitemarray[player]["heartcontainer"] == 0):
[item for item in items if item.name == 'Boss Heart Container'][0].advancement = True
next(item for item in items if item.name == 'Boss Heart Container').advancement = True
elif world.difficulty[player] in ['expert'] and not (world.custom and world.customitemarray[player]["heartpiece"] < 4):
adv_heart_pieces = [item for item in items if item.name == 'Piece of Heart'][0:4]
for hp in adv_heart_pieces:
hp.advancement = True
adv_heart_pieces = (item for item in items if item.name == 'Piece of Heart')
for i in range(4):
next(adv_heart_pieces).advancement = True
beeweights = {'0': {None: 100},
'1': {None: 75, 'trap': 25},
@@ -363,7 +370,15 @@ def generate_itempool(world, player):
tr_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)]
world.required_medallions[player] = (mm_medallion, tr_medallion)
place_bosses(world, player)
# shuffle bottle refills
if world.difficulty[player] in ['hard', 'expert']:
waterfall_bottle = hardbottles[random.randint(0, 5)]
pyramid_bottle = hardbottles[random.randint(0, 5)]
else:
waterfall_bottle = normalbottles[random.randint(0, 6)]
pyramid_bottle = normalbottles[random.randint(0, 6)]
world.bottle_refills[player] = (waterfall_bottle, pyramid_bottle)
set_up_shops(world, player)
if world.retro[player]:
@@ -520,6 +535,15 @@ def set_up_shops(world, player):
rss.locked = True
cap_shop = world.get_region('Capacity Upgrade', player).shop
cap_shop.inventory[1] = None # remove arrow capacity upgrades in retro
if world.bombbag[player]:
if world.shopsanity[player]:
removals = [item for item in world.itempool if item.name == 'Bomb Upgrade (+5)' and item.player == player]
for remove in removals:
world.itempool.remove(remove)
world.itempool.append(ItemFactory('Rupees (50)', player)) # replace the bomb upgrade
else:
cap_shop = world.get_region('Capacity Upgrade', player).shop
cap_shop.inventory[0] = cap_shop.inventory[1] # remove bomb capacity upgrades in bombbag
def customize_shops(world, player):
@@ -561,7 +585,7 @@ def customize_shops(world, player):
shop.shopkeeper_config = shopkeeper
# handle capacity upgrades - randomly choose a bomb bunch or arrow bunch to become capacity upgrades
if world.difficulty[player] == 'normal':
if not found_bomb_upgrade and len(possible_replacements) > 0:
if not found_bomb_upgrade and len(possible_replacements) > 0 and not world.bombbag[player]:
choices = []
for shop, idx, loc, item in possible_replacements:
if item.name in ['Bombs (3)', 'Bombs (10)']:
@@ -587,6 +611,7 @@ def customize_shops(world, player):
upgrade.location = loc
change_shop_items_to_rupees(world, player, shops_to_customize)
balance_prices(world, player)
check_hints(world, player)
def randomize_price(price):
@@ -690,6 +715,15 @@ def balance_prices(world, player):
# loc.parent_region.shop.inventory[slot]['price'] = new_price
def check_hints(world, player):
if world.shuffle[player] in ['simple', 'restricted', 'full', 'crossed', 'insanity']:
for shop, location_list in shop_to_location_table.items():
if shop in ['Capacity Upgrade', 'Light World Death Mountain Shop', 'Potion Shop']:
continue # near the queen, near potions, and near 7 chests are fine
for loc_name in location_list: # other shops are indistinguishable in ER
world.get_location(loc_name, player).hint_text = f'for sale'
repeatable_shop_items = ['Single Arrow', 'Arrows (10)', 'Bombs (3)', 'Bombs (10)', 'Red Potion', 'Small Heart',
'Blue Shield', 'Red Shield', 'Bee', 'Small Key (Universal)', 'Blue Potion', 'Green Potion']
@@ -709,7 +743,7 @@ rupee_chart = {'Rupee (1)': 1, 'Rupees (5)': 5, 'Rupees (20)': 20, 'Rupees (50)'
'Rupees (100)': 100, 'Rupees (300)': 300}
def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, retro, door_shuffle, logic):
def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, retro, bombbag, door_shuffle, logic):
pool = []
placed_items = {}
precollected_items = []
@@ -756,6 +790,11 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer,
diff = difficulties[difficulty]
pool.extend(diff.baseitems)
if bombbag:
pool = [item.replace('Bomb Upgrade (+5)','Rupees (5)') for item in pool]
pool = [item.replace('Bomb Upgrade (+10)','Rupees (5)') for item in pool]
pool.extend(diff.bombbag)
# expert+ difficulties produce the same contents for
# all bottles, since only one bottle is available
if diff.same_bottle:
@@ -850,7 +889,7 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer,
pool.extend(['Small Key (Universal)'])
return (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms)
def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, swords, retro, customitemarray):
def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bombbag, customitemarray):
if isinstance(customitemarray,dict) and 1 in customitemarray:
customitemarray = customitemarray[1]
pool = []
@@ -966,20 +1005,21 @@ def test():
for shuffle in ['full', 'insanity_legacy']:
for logic in ['noglitches', 'minorglitches', 'owglitches', 'nologic']:
for retro in [True, False]:
for door_shuffle in ['basic', 'crossed', 'vanilla']:
out = get_pool_core(progressive, shuffle, difficulty, 30, timer, goal, mode, swords, retro, door_shuffle, logic)
count = len(out[0]) + len(out[1])
for bombbag in [True, False]:
for door_shuffle in ['basic', 'crossed', 'vanilla']:
out = get_pool_core(progressive, shuffle, difficulty, 30, timer, goal, mode, swords, retro, bombbag, door_shuffle, logic)
count = len(out[0]) + len(out[1])
correct_count = total_items_to_place
if goal == 'pedestal' and swords != 'vanilla':
# pedestal goals generate one extra item
correct_count += 1
if retro:
correct_count += 28
try:
assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode, swords, retro))
except AssertionError as e:
print(e)
correct_count = total_items_to_place
if goal == 'pedestal' and swords != 'vanilla':
# pedestal goals generate one extra item
correct_count += 1
if retro:
correct_count += 28
try:
assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bombbag))
except AssertionError as e:
print(e)
if __name__ == '__main__':
test()
+427 -108
View File
@@ -2,9 +2,9 @@ import itertools
import logging
from collections import defaultdict, deque
from BaseClasses import DoorType
from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType
from Regions import dungeon_events
from Dungeons import dungeon_keys, dungeon_bigs
from Dungeons import dungeon_keys, dungeon_bigs, dungeon_table
from DungeonGenerator import ExplorationState, special_big_key_doors
@@ -25,6 +25,9 @@ class KeyLayout(object):
self.all_locations = set()
self.item_locations = set()
self.found_doors = set()
self.prize_relevant = None
self.prize_can_lock = None # if true, then you may need to beat the bo
# bk special?
# bk required? True if big chests or big doors exists
@@ -35,6 +38,7 @@ class KeyLayout(object):
self.max_chests = calc_max_chests(builder, self, world, player)
self.all_locations = set()
self.item_locations = set()
self.prize_relevant = None
class KeyLogic(object):
@@ -54,10 +58,12 @@ class KeyLogic(object):
self.location_rules = {}
self.outside_keys = 0
self.dungeon = dungeon_name
self.sm_doors = {}
self.prize_location = None
def check_placement(self, unplaced_keys, big_key_loc=None):
def check_placement(self, unplaced_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, unplaced_keys):
if not rule.is_satisfiable(self.outside_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):
@@ -65,6 +71,15 @@ class KeyLogic(object):
return False
return True
def reset(self):
self.door_rules.clear()
self.bk_restricted.clear()
self.bk_locked.clear()
self.sm_restricted.clear()
self.bk_doors.clear()
self.bk_chests.clear()
self.placement_rules.clear()
class DoorRules(object):
@@ -79,6 +94,8 @@ class DoorRules(object):
self.small_location = None
self.opposite = None
self.new_rules = {} # keyed by type, or type+lock_item -> number
class LocationRule(object):
def __init__(self):
@@ -101,9 +118,11 @@ class PlacementRule(object):
self.needed_keys_w_bk = None
self.needed_keys_wo_bk = None
self.check_locations_w_bk = None
self.special_bk_avail = False
self.check_locations_wo_bk = None
self.bk_relevant = True
self.key_reduced = False
self.prize_relevance = None
def contradicts(self, rule, unplaced_keys, big_key_loc):
bk_blocked = big_key_loc in self.bk_conditional_set if self.bk_conditional_set else False
@@ -112,6 +131,10 @@ class PlacementRule(object):
rule_locations = rule.check_locations_wo_bk if rule_blocked else rule.check_locations_w_bk
if check_locations is None or rule_locations is None:
return False
if not bk_blocked and big_key_loc not in check_locations: # bk is not available, so rule doesn't apply
return False
if not rule_blocked and big_key_loc not in rule_locations: # bk is not available, so rule doesn't apply
return False
check_locations = check_locations - {big_key_loc}
rule_locations = rule_locations - {big_key_loc}
threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk
@@ -134,13 +157,30 @@ class PlacementRule(object):
left -= rule_needed
return False
def is_satisfiable(self, outside_keys, unplaced_keys):
def is_satisfiable(self, outside_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']:
return True
elif self.prize_relevance == 'GT':
if 'Crystal' not in prize_location.item.name or cr_count < 7:
return True
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
elif len(self.check_locations_w_bk) > self.needed_keys_w_bk:
def loc_has_bk(l):
return (big_key_loc is not None and big_key_loc == l) or (l.item and l.item.bigkey)
# todo: sometimes the bk avail rule doesn't mean the bk must be avail or this rule is invalid
# but sometimes it certainly does
# check threshold vs len(check_loc) maybe to determine bk isn't relevant?
bk_found = self.special_bk_avail or any(loc for loc in self.check_locations_w_bk if loc_has_bk(loc))
if not bk_found:
return True
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
@@ -170,6 +210,8 @@ class KeyCounter(object):
self.important_location = False
self.other_locations = {}
self.important_locations = {}
self.prize_doors_opened = False
self.prize_received = False
def used_smalls_loc(self, reserve=0):
return max(self.used_keys + reserve - len(self.key_only_locations), 0)
@@ -209,16 +251,30 @@ def calc_max_chests(builder, key_layout, world, player):
def analyze_dungeon(key_layout, world, player):
key_layout.key_logic.reset()
key_layout.key_counters = create_key_counters(key_layout, world, player)
key_logic = key_layout.key_logic
for door in key_layout.proposal:
if isinstance(door, tuple):
key_logic.sm_doors[door[0]] = door[1]
key_logic.sm_doors[door[1]] = door[0]
else:
if door.dest and door.type != DoorType.SpiralStairs:
key_logic.sm_doors[door] = door.dest
key_logic.sm_doors[door.dest] = door
else:
key_logic.sm_doors[door] = None
find_bk_locked_sections(key_layout, world, player)
key_logic.bk_chests.update(find_big_chest_locations(key_layout.all_chest_locations))
key_logic.bk_chests.update(find_big_key_locked_locations(key_layout.all_chest_locations))
key_logic.prize_location = dungeon_table[key_layout.sector.name].prize
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, False)
if key_layout.big_key_special and forced_big_key_avail(original_key_counter.other_locations) is not None:
original_key_counter = find_counter({}, True, key_layout, False)
queue = deque([(None, original_key_counter)])
doors_completed = set()
visited_cid = set()
@@ -237,28 +293,39 @@ def analyze_dungeon(key_layout, world, player):
if not big_avail:
if chest_keys == non_big_locs and chest_keys > 0 and available <= possible_smalls and not avail_bigs:
key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations))
# note to self: this is due to the enough_small_locations function in validate_key_layout_sub_loop
# I don't like this exception here or there
elif available < possible_smalls and avail_bigs and non_big_locs > 0:
max_ctr = find_max_counter(key_layout)
bk_lockdown = [x for x in max_ctr.free_locations if x not in key_counter.free_locations]
key_logic.bk_restricted.update(filter_big_chest(bk_lockdown))
# try to relax the rules here? - smallest requirement that doesn't force a softlock
child_queue = deque()
for child in key_counter.child_doors.keys():
if not child.bigKey or not key_layout.big_key_special or big_avail:
if can_open_door_by_counter(child, key_counter, key_layout, world, player):
odd_counter = create_odd_key_counter(child, key_counter, key_layout, world, player)
empty_flag = empty_counter(odd_counter)
child_queue.append((child, odd_counter, empty_flag))
while len(child_queue) > 0:
child, odd_counter, empty_flag = child_queue.popleft()
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)
rule = create_rule(best_counter, key_counter, key_layout, world, player)
prize_flag = key_counter.prize_doors_opened
if child in key_layout.flat_prop and child not in doors_completed:
best_counter = find_best_counter(child, key_layout, odd_counter, False, empty_flag)
rule = create_rule(best_counter, key_counter, world, player)
create_worst_case_rule(rule, best_counter, 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)
key_logic.door_rules[child.name] = rule
elif not child.bigKey and child not in doors_completed:
prize_flag = True
doors_completed.add(child)
next_counter = find_next_counter(child, key_counter, key_layout)
next_counter = find_next_counter(child, key_counter, key_layout, prize_flag)
ctr_id = cid(next_counter, key_layout)
if ctr_id not in visited_cid:
queue.append((child, next_counter))
visited_cid.add(ctr_id)
check_rules(original_key_counter, key_layout, world, player)
# todo: why is this commented out?
# check_rules(original_key_counter, key_layout, world, player)
# Flip bk rules if more restrictive, to prevent placing a big key in a softlocking location
for rule in key_logic.door_rules.values():
@@ -274,6 +341,8 @@ def create_exhaustive_placement_rules(key_layout, world, player):
key_logic = key_layout.key_logic
max_ctr = find_max_counter(key_layout)
for code, key_counter in key_layout.key_counters.items():
if skip_key_counter_due_to_prize(key_layout, key_counter):
continue # we have the prize, we are not concerned about this case
accessible_loc = set()
accessible_loc.update(key_counter.free_locations)
accessible_loc.update(key_counter.key_only_locations)
@@ -294,13 +363,16 @@ def create_exhaustive_placement_rules(key_layout, world, player):
else:
placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player)
rule.check_locations_w_bk = accessible_loc
check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc)
if key_layout.big_key_special:
rule.special_bk_avail = forced_big_key_avail(key_counter.important_locations) is not None
# check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc)
else:
if big_key_progress(key_counter) and only_sm_doors(key_counter):
create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, accessible_loc, min_keys, world, player)
rule.bk_conditional_set = blocked_loc
rule.needed_keys_wo_bk = min_keys
rule.check_locations_wo_bk = set(filter_big_chest(accessible_loc))
rule.prize_relevance = key_layout.prize_relevant if rule_prize_relevant(key_counter) else None
if valid_rule:
key_logic.placement_rules.append(rule)
adjust_locations_rules(key_logic, rule, accessible_loc, key_layout, key_counter, max_ctr)
@@ -308,6 +380,14 @@ def create_exhaustive_placement_rules(key_layout, world, player):
refine_location_rules(key_layout)
def rule_prize_relevant(key_counter):
return not key_counter.prize_doors_opened and not key_counter.prize_received
def skip_key_counter_due_to_prize(key_layout, key_counter):
return key_layout.prize_relevant and key_counter.prize_received and not key_counter.prize_doors_opened
def placement_self_lock_adjustment(rule, max_ctr, blocked_loc, ctr, world, player):
if len(blocked_loc) == 1 and world.accessibility[player] != 'locations':
blocked_others = set(max_ctr.other_locations).difference(set(ctr.other_locations))
@@ -320,6 +400,7 @@ def placement_self_lock_adjustment(rule, max_ctr, blocked_loc, ctr, world, playe
rule.needed_keys_w_bk -= 1
# this rule is suspect - commented out usages for now
def check_sm_restriction_needed(key_layout, max_ctr, rule, blocked):
if rule.needed_keys_w_bk == key_layout.max_chests + len(max_ctr.key_only_locations):
key_layout.key_logic.sm_restricted.update(blocked.difference(max_ctr.key_only_locations))
@@ -402,7 +483,7 @@ def refine_placement_rules(key_layout, max_ctr):
if rule.needed_keys_wo_bk == 0:
rules_to_remove.append(rule)
if len(rule.check_locations_wo_bk) < rule.needed_keys_wo_bk or rule.needed_keys_wo_bk > key_layout.max_chests:
if len(rule.bk_conditional_set) > 0:
if not rule.prize_relevance and len(rule.bk_conditional_set) > 0:
key_logic.bk_restricted.update(rule.bk_conditional_set)
rules_to_remove.append(rule)
changed = True # impossible for bk to be here, I think
@@ -413,7 +494,8 @@ def refine_placement_rules(key_layout, max_ctr):
rule_b = temp
if rule_a.bk_conditional_set and rule_b.check_locations_w_bk:
common_needed = min(rule_a.needed_keys_wo_bk, rule_b.needed_keys_w_bk)
if len(rule_b.check_locations_w_bk & rule_a.check_locations_wo_bk) < common_needed:
common_locs = len(rule_b.check_locations_w_bk & rule_a.check_locations_wo_bk)
if (common_needed - common_locs) * 2 > key_layout.max_chests:
key_logic.bk_restricted.update(rule_a.bk_conditional_set)
rules_to_remove.append(rule_a)
changed = True
@@ -478,7 +560,7 @@ def create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, a
else:
placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player)
rule.check_locations_w_bk = accessible_loc
check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc)
# check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc)
key_logic.placement_rules.append(rule)
adjust_locations_rules(key_logic, rule, accessible_loc, key_layout, key_counter, max_ctr)
@@ -538,6 +620,8 @@ def relative_empty_counter(odd_counter, key_counter):
return False
if len(set(odd_counter.free_locations).difference(key_counter.free_locations)) > 0:
return False
if len(set(odd_counter.other_locations).difference(key_counter.other_locations)) > 0:
return False
# important only
if len(set(odd_counter.important_locations).difference(key_counter.important_locations)) > 0:
return False
@@ -594,33 +678,50 @@ def unique_child_door_2(child, key_counter):
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?
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)
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
# this means the new_door invalidates the door / leads to the same stuff
if not empty_flag and relative_empty_counter(odd_counter, new_counter):
ignored_doors.add(new_door)
elif empty_flag or key_wasted(new_door, door, last_counter, new_counter, key_layout, world, player):
last_counter = new_counter
opened_doors = proposed_doors
bk_opened = bk_open
else:
ignored_doors.add(new_door)
return last_counter
# 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 {}
# 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)
# 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
# # this means the new_door invalidates the door / leads to the same stuff
# if not empty_flag and relative_empty_counter(odd_counter, new_counter):
# ignored_doors.add(new_door)
# elif empty_flag or key_wasted(new_door, door, last_counter, new_counter, key_layout, world, player):
# last_counter = new_counter
# opened_doors = proposed_doors
# bk_opened = bk_open
# else:
# ignored_doors.add(new_door)
# return last_counter
def find_best_counter(door, key_layout, odd_counter, skip_bk, empty_flag):
best, best_ctr, locations = 0, None, 0
for code, counter in key_layout.key_counters.items():
if door not in counter.open_doors:
if best_ctr is None or counter.used_keys > best or (counter.used_keys == best and count_locations(counter) > locations):
if not skip_bk or not counter.big_key_opened:
if empty_flag or not relative_empty_counter(odd_counter, counter):
best = counter.used_keys
best_ctr = counter
locations = count_locations(counter)
return best_ctr
def count_locations(ctr):
return len(ctr.free_locations) + len(ctr.key_only_locations) + len(ctr.other_locations) + len(ctr.important_locations)
def find_worst_counter(door, odd_counter, key_counter, key_layout, skip_bk): # try to waste as many keys as possible?
@@ -638,7 +739,7 @@ def find_worst_counter(door, odd_counter, key_counter, key_layout, skip_bk): #
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)
new_counter = find_counter(proposed_doors, bk_open, key_layout, key_counter.prize_doors_opened)
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)
@@ -690,7 +791,7 @@ def key_wasted(new_door, old_door, old_counter, new_counter, key_layout, world,
for new_child in new_children:
proposed_doors = {**opened_doors, **dict.fromkeys([new_child, new_child.dest])}
bk_open = bk_opened or new_door.bigKey
new_counter = find_counter(proposed_doors, bk_open, key_layout)
new_counter = find_counter(proposed_doors, bk_open, key_layout, current_counter.prize_doors_opened)
if key_wasted(new_child, old_door, current_counter, new_counter, key_layout, world, player):
wasted_keys += 1
if new_avail - wasted_keys < old_avail:
@@ -698,10 +799,11 @@ def key_wasted(new_door, old_door, old_counter, new_counter, key_layout, world,
return False
def find_next_counter(new_door, old_counter, key_layout):
def find_next_counter(new_door, old_counter, key_layout, prize_flag=None):
proposed_doors = {**old_counter.open_doors, **dict.fromkeys([new_door, new_door.dest])}
bk_open = old_counter.big_key_opened or new_door.bigKey
return find_counter(proposed_doors, bk_open, key_layout)
prize_flag = prize_flag if prize_flag else old_counter.prize_doors_opened
return find_counter(proposed_doors, bk_open, key_layout, prize_flag)
def check_special_locations(locations):
@@ -717,7 +819,7 @@ def calc_avail_keys(key_counter, world, player):
return raw_avail - key_counter.used_keys
def create_rule(key_counter, prev_counter, key_layout, world, player):
def create_rule(key_counter, prev_counter, world, player):
# prev_chest_keys = available_chest_small_keys(prev_counter, world)
# prev_avail = prev_chest_keys + len(prev_counter.key_only_locations)
chest_keys = available_chest_small_keys(key_counter, world, player)
@@ -736,6 +838,11 @@ def create_rule(key_counter, prev_counter, key_layout, world, player):
return DoorRules(rule_num, is_valid)
def create_worst_case_rule(rules, key_counter, world, player):
required_keys = key_counter.used_keys + 1 # this makes more sense, if key_counter has wasted all keys
rules.new_rules[KeyRuleType.WorstCase] = required_keys
def check_for_self_lock_key(rule, door, parent_counter, key_layout, world, player):
if world.accessibility[player] != 'locations':
counter = find_inverted_counter(door, parent_counter, key_layout, world, player)
@@ -744,11 +851,12 @@ def check_for_self_lock_key(rule, door, parent_counter, key_layout, world, playe
if len(counter.free_locations) == 1 and len(counter.key_only_locations) == 0 and not counter.important_location:
rule.allow_small = True
rule.small_location = next(iter(counter.free_locations))
rule.new_rules[KeyRuleType.AllowSmall] = rule.new_rules[KeyRuleType.WorstCase] - 1
def find_inverted_counter(door, parent_counter, key_layout, world, player):
# open all doors in counter
counter = open_all_counter(parent_counter, key_layout, door=door)
counter = open_all_counter(parent_counter, key_layout, world, player, door=door)
max_counter = find_max_counter(key_layout)
# find the difference
inverted_counter = KeyCounter(key_layout.max_chests)
@@ -764,7 +872,7 @@ def find_inverted_counter(door, parent_counter, key_layout, world, player):
return inverted_counter
def open_all_counter(parent_counter, key_layout, door=None, skipBk=False):
def open_all_counter(parent_counter, key_layout, world, player, door=None, skipBk=False):
changed = True
counter = parent_counter
proposed_doors = dict.fromkeys(parent_counter.open_doors.keys())
@@ -776,14 +884,12 @@ def open_all_counter(parent_counter, key_layout, door=None, skipBk=False):
if skipBk:
if not child.bigKey:
doors_to_open[child] = None
elif not child.bigKey or not key_layout.big_key_special or counter.big_key_opened:
elif can_open_door_by_counter(child, counter, key_layout, world, player):
doors_to_open[child] = None
if len(doors_to_open.keys()) > 0:
proposed_doors = {**proposed_doors, **doors_to_open}
bk_hint = counter.big_key_opened
for d in doors_to_open.keys():
bk_hint = bk_hint or d.bigKey
counter = find_counter(proposed_doors, bk_hint, key_layout)
bk_hint = counter.big_key_opened or any(d.bigKey for d in doors_to_open.keys())
counter = find_counter(proposed_doors, bk_hint, key_layout, True)
changed = True
return counter
@@ -804,7 +910,7 @@ def open_some_counter(parent_counter, key_layout, ignored_doors):
bk_hint = counter.big_key_opened
for d in doors_to_open.keys():
bk_hint = bk_hint or d.bigKey
counter = find_counter(proposed_doors, bk_hint, key_layout)
counter = find_counter(proposed_doors, bk_hint, key_layout, parent_counter.prize_doors_opened)
changed = True
return counter
@@ -845,16 +951,16 @@ def big_key_drop_available(key_counter):
def bk_restricted_rules(rule, door, odd_counter, empty_flag, key_counter, key_layout, world, player):
if key_counter.big_key_opened:
return
best_counter = find_best_counter(door, odd_counter, key_counter, key_layout, world, player, True, empty_flag)
bk_rule = create_rule(best_counter, key_counter, key_layout, world, player)
best_counter = find_best_counter(door, key_layout, odd_counter, True, empty_flag)
bk_rule = create_rule(best_counter, key_counter, world, player)
if bk_rule.small_key_num >= rule.small_key_num:
return
door_open = find_next_counter(door, best_counter, key_layout)
ignored_doors = dict_intersection(best_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)
for d in ignored_doors.keys():
if d.dest not in ignored_doors:
dest_ignored.append(d.dest)
ignored_doors = {**ignored_doors, **dict.fromkeys(dest_ignored)}
post_counter = open_some_counter(door_open, key_layout, ignored_doors.keys())
unique_loc = dict_difference(post_counter.free_locations, best_counter.free_locations)
@@ -862,8 +968,8 @@ def bk_restricted_rules(rule, door, odd_counter, empty_flag, key_counter, key_la
if len(unique_loc) > 0: # and bk_rule.is_valid
rule.alternate_small_key = bk_rule.small_key_num
rule.alternate_big_key_loc.update(unique_loc)
# elif not bk_rule.is_valid:
# key_layout.key_logic.bk_restricted.update(unique_loc)
if not door.bigKey:
rule.new_rules[(KeyRuleType.Lock, key_layout.key_logic.bk_name)] = best_counter.used_keys + 1
def find_worst_counter_wo_bk(small_key_num, accessible_set, door, odd_ctr, key_counter, key_layout):
@@ -887,12 +993,19 @@ def find_worst_counter_wo_bk(small_key_num, accessible_set, door, odd_ctr, key_c
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, world, player):
if door.bigKey or door.name in special_big_key_doors:
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]))
child_state.big_doors.clear()
elif door in child_state.prize_door_set:
child_state.prize_doors_opened = True
for exp_door in child_state.prize_doors:
new_region = exp_door.door.entrance.parent_region
child_state.visit_region(new_region, key_checks=True)
child_state.add_all_doors_check_keys(new_region, flat_proposal, world, player)
child_state.prize_doors.clear()
else:
child_state.opened_doors.append(door)
doors_to_open = [x for x in child_state.small_doors if x.door == door]
@@ -935,6 +1048,7 @@ def only_sm_doors(key_counter):
return False
return True
# doesn't count dest doors
def count_unique_small_doors(key_counter, proposal):
cnt = 0
@@ -949,7 +1063,7 @@ def count_unique_small_doors(key_counter, proposal):
def exist_relevant_big_doors(key_counter, key_layout):
bk_counter = find_counter(key_counter.open_doors, True, key_layout, False)
bk_counter = find_counter(key_counter.open_doors, True, key_layout, key_counter.prize_doors_opened, False)
if bk_counter is not None:
diff = dict_difference(bk_counter.free_locations, key_counter.free_locations)
if len(diff) > 0:
@@ -982,14 +1096,6 @@ def filter_big_chest(locations):
return [x for x in locations if '- Big Chest' not in x.name]
def count_locations_exclude_logic(locations, key_logic):
cnt = 0
for loc in locations:
if not location_is_bk_locked(loc, key_logic) and not loc.forced_item and not prize_or_event(loc):
cnt += 1
return cnt
def location_is_bk_locked(loc, key_logic):
return loc in key_logic.bk_chests or loc in key_logic.bk_locked
@@ -998,18 +1104,36 @@ def prize_or_event(loc):
return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']
def count_free_locations(state):
def boss_unavail(loc, world, player):
# todo: ambrosia
# return world.bossdrops[player] == 'ambrosia' and "- Boss" in loc.name
return False
def blind_boss_unavail(loc, state, world, player):
if loc.name == "Thieves' Town - Boss":
# todo: check attic
return (loc.parent_region.dungeon.boss.name == 'Blind' and
(not any(x for x in state.found_locations if x.name == 'Suspicious Maiden') or
(world.get_region('Thieves Attic Window', player).dungeon.name == 'Thieves Town' and
not any(x for x in state.found_locations if x.name == 'Attic Cracked Floor'))))
return False
def count_free_locations(state, world, player):
cnt = 0
for loc in state.found_locations:
if not prize_or_event(loc) and not loc.forced_item:
if (not prize_or_event(loc) and not loc.forced_item and not boss_unavail(loc, world, player)
and not blind_boss_unavail(loc, state, world, player)):
cnt += 1
return cnt
def count_locations_exclude_big_chest(state):
def count_locations_exclude_big_chest(state, world, player):
cnt = 0
for loc in state.found_locations:
if '- Big Chest' not in loc.name and not loc.forced_item and not prize_or_event(loc):
if ('- Big Chest' not in loc.name and not loc.forced_item and not boss_unavail(loc, world, player)
and not prize_or_event(loc) and not blind_boss_unavail(loc, state, world, player)):
cnt += 1
return cnt
@@ -1197,7 +1321,7 @@ def check_rules_deep(original_counter, key_layout, world, player):
elif not door.bigKey:
can_open = True
if can_open:
can_progress = smalls_opened or not big_maybe_not_found
can_progress = (big_avail or not big_maybe_not_found) if door.bigKey else smalls_opened
next_counter = find_next_counter(door, counter, key_layout)
c_id = cid(next_counter, key_layout)
if c_id not in completed:
@@ -1265,6 +1389,22 @@ def check_bk_special(regions, world, player):
return False
def forced_big_key_avail(locations):
for loc in locations:
if loc.forced_big_key():
return loc
return None
def prize_relevance(key_layout, dungeon_entrance):
if len(key_layout.start_regions) > 1 and dungeon_entrance and dungeon_table[key_layout.key_logic.dungeon].prize:
if dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower']:
return 'GT'
elif dungeon_entrance.name == 'Pyramid Fairy':
return 'BigBomb'
return None
# Soft lock stuff
def validate_key_layout(key_layout, world, player):
# retro is all good - except for hyrule castle in standard mode
@@ -1275,8 +1415,15 @@ def validate_key_layout(key_layout, world, player):
state.key_locations = key_layout.max_chests
state.big_key_special = check_bk_special(key_layout.sector.regions, world, player)
for region in key_layout.start_regions:
state.visit_region(region, key_checks=True)
state.add_all_doors_check_keys(region, flat_proposal, world, player)
dungeon_entrance, portal_door = find_outside_connection(region)
prize_relevant_flag = prize_relevance(key_layout, dungeon_entrance)
if prize_relevant_flag:
state.append_door_to_list(portal_door, state.prize_doors)
state.prize_door_set[portal_door] = dungeon_entrance
key_layout.prize_relevant = prize_relevant_flag
else:
state.visit_region(region, key_checks=True)
state.add_all_doors_check_keys(region, flat_proposal, world, player)
return validate_key_layout_sub_loop(key_layout, state, {}, flat_proposal, None, 0, world, player)
@@ -1287,7 +1434,10 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa
if not smalls_avail and num_bigs == 0:
return True # I think that's the end
# todo: fix state to separate out these types
ttl_locations = count_free_locations(state) if state.big_key_opened else count_locations_exclude_big_chest(state)
if state.big_key_opened:
ttl_locations = count_free_locations(state, world, player)
else:
ttl_locations = count_locations_exclude_big_chest(state, 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_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player)
@@ -1298,17 +1448,23 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa
found_forced_bk = state.found_forced_bk()
smalls_done = not smalls_avail or not enough_small_locations(state, available_small_locations)
bk_done = state.big_key_opened or num_bigs == 0 or (available_big_locations == 0 and not found_forced_bk)
if smalls_done and bk_done:
# prize door should not be opened if the boss is reachable - but not reached yet
allow_for_prize_lock = (key_layout.prize_can_lock and
not any(x for x in state.found_locations if '- Prize' in x.name))
prize_done = not key_layout.prize_relevant or state.prize_doors_opened or allow_for_prize_lock
if smalls_done and bk_done and prize_done:
return False
else:
# todo: pretty sure you should OR these paths together, maybe when there's one location and it can
# either be small or big key
if smalls_avail and available_small_locations > 0:
for exp_door in state.small_doors:
state_copy = state.copy()
open_a_door(exp_door.door, state_copy, flat_proposal)
open_a_door(exp_door.door, state_copy, flat_proposal, world, player)
state_copy.used_smalls += 1
if state_copy.used_smalls > ttl_small_key_only:
state_copy.used_locations += 1
code = state_id(state_copy, flat_proposal)
code = validate_id(state_copy, flat_proposal)
if code not in checked_states.keys():
valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal,
state, available_small_locations, world, player)
@@ -1319,10 +1475,24 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa
return False
if not state.big_key_opened and (available_big_locations >= num_bigs > 0 or (found_forced_bk and num_bigs > 0)):
state_copy = state.copy()
open_a_door(state.big_doors[0].door, state_copy, flat_proposal)
open_a_door(state.big_doors[0].door, state_copy, flat_proposal, world, player)
if not found_forced_bk:
state_copy.used_locations += 1
code = state_id(state_copy, flat_proposal)
code = validate_id(state_copy, flat_proposal)
if code not in checked_states.keys():
valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal,
state, available_small_locations, world, player)
checked_states[code] = valid
else:
valid = checked_states[code]
if not valid:
return False
# todo: feel like you only open these if the boss is available???
# todo: or if a crystal isn't valid placement on this boss
if not state.prize_doors_opened and key_layout.prize_relevant:
state_copy = state.copy()
open_a_door(next(iter(state_copy.prize_door_set)), state_copy, flat_proposal, world, player)
code = validate_id(state_copy, flat_proposal)
if code not in checked_states.keys():
valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal,
state, available_small_locations, world, player)
@@ -1341,7 +1511,7 @@ def invalid_self_locking_key(key_layout, state, prev_state, prev_avail, world, p
state_copy = state.copy()
while len(new_bk_doors) > 0:
for door in new_bk_doors:
open_a_door(door.door, state_copy, key_layout.flat_prop)
open_a_door(door.door, state_copy, key_layout.flat_prop, world, player)
new_bk_doors = set(state_copy.big_doors).difference(set(prev_state.big_doors))
expand_key_state(state_copy, key_layout.flat_prop, world, player)
new_locations = set(state_copy.found_locations).difference(set(prev_state.found_locations))
@@ -1365,6 +1535,39 @@ def enough_small_locations(state, avail_small_loc):
return avail_small_loc >= len(unique_d_set)
def determine_prize_lock(key_layout, world, player):
if ((world.retro[player] and (world.mode[player] != 'standard' or key_layout.sector.name != 'Hyrule Castle'))
or world.logic[player] == 'nologic'):
return # done, doesn't matter what
flat_proposal = key_layout.flat_prop
state = ExplorationState(dungeon=key_layout.sector.name)
state.key_locations = key_layout.max_chests
state.big_key_special = check_bk_special(key_layout.sector.regions, world, player)
prize_lock_possible = False
for region in key_layout.start_regions:
dungeon_entrance, portal_door = find_outside_connection(region)
prize_relevant_flag = prize_relevance(key_layout, dungeon_entrance)
if prize_relevant_flag:
state.append_door_to_list(portal_door, state.prize_doors)
state.prize_door_set[portal_door] = dungeon_entrance
key_layout.prize_relevant = prize_relevant_flag
prize_lock_possible = True
else:
state.visit_region(region, key_checks=True)
state.add_all_doors_check_keys(region, flat_proposal, world, player)
if not prize_lock_possible:
return # done, no prize entrances to worry about
expand_key_state(state, flat_proposal, world, player)
while len(state.small_doors) > 0 or len(state.big_doors) > 0:
if len(state.big_doors) > 0:
open_a_door(state.big_doors[0].door, state, flat_proposal, world, player)
elif len(state.small_doors) > 0:
open_a_door(state.small_doors[0].door, state, flat_proposal, world, player)
expand_key_state(state, flat_proposal, world, player)
if any(x for x in state.found_locations if '- Prize' in x.name):
key_layout.prize_can_lock = True
def cnt_avail_small_locations(free_locations, key_only, state, world, player):
if not world.keyshuffle[player] and not world.retro[player]:
bk_adj = 1 if state.big_key_opened and not state.big_key_special else 0
@@ -1373,29 +1576,54 @@ def cnt_avail_small_locations(free_locations, key_only, state, world, player):
return state.key_locations - state.used_smalls
def cnt_avail_small_locations_by_ctr(free_locations, counter, layout, world, player):
if not world.keyshuffle[player] and not world.retro[player]:
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)
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
def cnt_avail_big_locations(ttl_locations, state, world, player):
if not world.bigkeyshuffle[player]:
return max(0, ttl_locations - state.used_locations) if not state.big_key_special else 0
return 1 if not state.big_key_special else 0
def cnt_avail_big_locations_by_ctr(ttl_locations, counter, layout, world, player):
if not world.bigkeyshuffle[player]:
bk_adj = 1 if counter.big_key_opened and not layout.big_key_special else 0
used_locations = max(0, counter.used_keys - len(counter.key_only_locations)) + bk_adj
return max(0, ttl_locations - used_locations) if not layout.big_key_special else 0
return 1 if not layout.big_key_special else 0
def create_key_counters(key_layout, world, player):
key_counters = {}
key_layout.found_doors.clear()
flat_proposal = key_layout.flat_prop
state = ExplorationState(dungeon=key_layout.sector.name)
if world.doorShuffle[player] == 'vanilla':
state.key_locations = len(world.get_dungeon(key_layout.sector.name, player).small_keys)
builder = world.dungeon_layouts[player][key_layout.sector.name]
state.key_locations = len(builder.key_door_proposal) - builder.key_drop_cnt
else:
state.key_locations = world.dungeon_layouts[player][key_layout.sector.name].key_doors_num
state.big_key_special, special_region = False, None
builder = world.dungeon_layouts[player][key_layout.sector.name]
state.key_locations = builder.total_keys - builder.key_drop_cnt
state.big_key_special = False
for region in key_layout.sector.regions:
for location in region.locations:
if location.forced_big_key():
state.big_key_special = True
special_region = region
for region in key_layout.start_regions:
state.visit_region(region, key_checks=True)
state.add_all_doors_check_keys(region, flat_proposal, world, player)
dungeon_entrance, portal_door = find_outside_connection(region)
prize_relevant_flag = prize_relevance(key_layout, dungeon_entrance)
if prize_relevant_flag:
state.append_door_to_list(portal_door, state.prize_doors)
state.prize_door_set[portal_door] = dungeon_entrance
key_layout.prize_relevant = prize_relevant_flag
else:
state.visit_region(region, key_checks=True)
state.add_all_doors_check_keys(region, flat_proposal, world, player)
expand_key_state(state, flat_proposal, world, player)
code = state_id(state, key_layout.flat_prop)
key_counters[code] = create_key_counter(state, key_layout, world, player)
@@ -1403,12 +1631,15 @@ def create_key_counters(key_layout, world, player):
while len(queue) > 0:
next_key_counter, parent_state = queue.popleft()
for door in next_key_counter.child_doors:
key_layout.found_doors.add(door)
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:
key_layout.key_logic.bk_doors.add(door)
# open the door, if possible
if not door.bigKey or not child_state.big_key_special or child_state.visited_at_all(special_region):
open_a_door(door, child_state, flat_proposal)
if can_open_door(door, child_state, 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)
if code not in key_counters.keys():
@@ -1418,9 +1649,57 @@ def create_key_counters(key_layout, world, player):
return key_counters
def find_outside_connection(region):
portal = next((x for x in region.entrances if ' Portal' in x.parent_region.name), None)
if portal:
dungeon_entrance = next(x for x in portal.parent_region.entrances
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld])
portal_entrance = next(x for x in portal.parent_region.entrances if x.parent_region == region)
return dungeon_entrance, portal_entrance.door
return None, None
def can_open_door(door, state, world, player):
if state.big_key_opened:
ttl_locations = count_free_locations(state, world, player)
else:
ttl_locations = count_locations_exclude_big_chest(state, 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)
return available_small_locations > 0
elif door.bigKey:
available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player)
found_forced_bk = state.found_forced_bk()
return not state.big_key_opened and (available_big_locations > 0 or found_forced_bk)
else:
return True
def can_open_door_by_counter(door, counter: KeyCounter, layout, world, player):
if counter.big_key_opened:
ttl_locations = len(counter.free_locations)
else:
ttl_locations = len([x for x in counter.free_locations if '- Big Chest' not in x.name])
if door.smallKey:
# ttl_small_key_only = len(counter.key_only_locations)
return cnt_avail_small_locations_by_ctr(ttl_locations, counter, layout, world, player) > 0
elif door.bigKey:
if counter.big_key_opened:
return False
if layout.big_key_special:
return any(x for x in counter.other_locations.keys() if x.forced_item and x.forced_item.bigkey)
else:
available_big_locations = cnt_avail_big_locations_by_ctr(ttl_locations, counter, layout, world, player)
return available_big_locations > 0
else:
return True
def create_key_counter(state, key_layout, world, player):
key_counter = KeyCounter(key_layout.max_chests)
key_counter.child_doors.update(dict.fromkeys(unique_doors(state.small_doors+state.big_doors)))
key_counter.child_doors.update(dict.fromkeys(unique_doors(state.small_doors+state.big_doors+state.prize_doors)))
for loc in state.found_locations:
if important_location(loc, world, player):
key_counter.important_location = True
@@ -1437,6 +1716,10 @@ def create_key_counter(state, key_layout, world, player):
key_counter.open_doors.update(dict.fromkeys(state.opened_doors))
key_counter.used_keys = count_unique_sm_doors(state.opened_doors)
key_counter.big_key_opened = state.big_key_opened
if len(state.prize_door_set) > 0 and state.prize_doors_opened:
key_counter.prize_doors_opened = True
if any(x for x in key_counter.important_locations if '- Prize' in x.name):
key_counter.prize_received = True
return key_counter
@@ -1487,11 +1770,23 @@ def state_id(state, flat_proposal):
s_id = '1' if state.big_key_opened else '0'
for d in flat_proposal:
s_id += '1' if d in state.opened_doors else '0'
if len(state.prize_door_set) > 0:
s_id += '1' if state.prize_doors_opened else '0'
return s_id
def find_counter(opened_doors, bk_hint, key_layout, raise_on_error=True):
counter = find_counter_hint(opened_doors, bk_hint, key_layout)
def validate_id(state, flat_proposal):
s_id = '1' if state.big_key_opened else '0'
for d in flat_proposal:
s_id += '1' if d in state.opened_doors else '0'
if len(state.prize_door_set) > 0:
s_id += '1' if state.prize_doors_opened else '0'
s_id += str(state.used_locations)
return s_id
def find_counter(opened_doors, bk_hint, key_layout, prize_flag, raise_on_error=True):
counter = find_counter_hint(opened_doors, bk_hint, key_layout, prize_flag)
if counter is not None:
return counter
more_doors = []
@@ -1500,43 +1795,47 @@ def find_counter(opened_doors, bk_hint, key_layout, raise_on_error=True):
if door.dest not in opened_doors.keys():
more_doors.append(door.dest)
if len(more_doors) > len(opened_doors.keys()):
counter = find_counter_hint(dict.fromkeys(more_doors), bk_hint, key_layout)
counter = find_counter_hint(dict.fromkeys(more_doors), bk_hint, key_layout, prize_flag)
if counter is not None:
return counter
if raise_on_error:
raise Exception('Unable to find door permutation. Init CID: %s' % counter_id(opened_doors, bk_hint, key_layout.flat_prop))
cid = counter_id(opened_doors, bk_hint, key_layout.flat_prop, key_layout.prize_relevant, prize_flag)
raise Exception(f'Unable to find door permutation. Init CID: {cid}')
return None
def find_counter_hint(opened_doors, bk_hint, key_layout):
cid = counter_id(opened_doors, bk_hint, key_layout.flat_prop)
def find_counter_hint(opened_doors, bk_hint, key_layout, prize_flag):
cid = counter_id(opened_doors, bk_hint, key_layout.flat_prop, key_layout.prize_relevant, prize_flag)
if cid in key_layout.key_counters.keys():
return key_layout.key_counters[cid]
if not bk_hint:
cid = counter_id(opened_doors, True, key_layout.flat_prop)
cid = counter_id(opened_doors, True, key_layout.flat_prop, key_layout.prize_relevant, prize_flag)
if cid in key_layout.key_counters.keys():
return key_layout.key_counters[cid]
return None
def find_max_counter(key_layout):
max_counter = find_counter_hint(dict.fromkeys(key_layout.flat_prop), False, key_layout)
max_counter = find_counter_hint(dict.fromkeys(key_layout.found_doors), False, key_layout, True)
if max_counter is None:
raise Exception("Max Counter is none - something is amiss")
if len(max_counter.child_doors) > 0:
max_counter = find_counter_hint(dict.fromkeys(key_layout.flat_prop), True, key_layout)
max_counter = find_counter_hint(dict.fromkeys(key_layout.found_doors), True, key_layout, True)
return max_counter
def counter_id(opened_doors, bk_unlocked, flat_proposal):
def counter_id(opened_doors, bk_unlocked, flat_proposal, prize_relevant, prize_flag):
s_id = '1' if bk_unlocked else '0'
for d in flat_proposal:
s_id += '1' if d in opened_doors.keys() else '0'
if prize_relevant:
s_id += '1' if prize_flag else '0'
return s_id
def cid(counter, key_layout):
return counter_id(counter.open_doors, counter.big_key_opened, key_layout.flat_prop)
return counter_id(counter.open_doors, counter.big_key_opened, key_layout.flat_prop,
key_layout.prize_relevant, counter.prize_doors_opened)
# class SoftLockException(Exception):
@@ -1746,8 +2045,20 @@ def validate_key_placement(key_layout, world, player):
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
if key_layout.prize_relevant:
found_prize = any(x for x in counter.important_locations if '- Prize' in x.name)
if not found_prize and dungeon_table[key_layout.sector.name].prize:
prize_loc = world.get_location(dungeon_table[key_layout.sector.name].prize, player)
if key_layout.prize_relevant == 'BigBomb':
found_prize = prize_loc.item.name not in ['Crystal 5', 'Crystal 6']
elif key_layout.prize_relevant == 'GT':
found_prize = 'Crystal' not in prize_loc.item.name or world.crystals_needed_for_gt[player] < 7
else:
found_prize = False
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)
found_keys > counter.used_keys and any(not d.bigKey for d in counter.child_doors) or \
self_locked_child_door(key_layout, counter) or \
(key_layout.prize_relevant and not counter.prize_doors_opened and found_prize)
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.name != bigkey_name) or "- Boss" in l.name]
@@ -1761,3 +2072,11 @@ def validate_key_placement(key_layout, world, player):
return True
def self_locked_child_door(key_layout, counter):
if len(counter.child_doors) == 1:
door = next(iter(counter.child_doors.keys()))
return door.smallKey and key_layout.key_logic.door_rules[door.name].allow_small
return False
+40 -15
View File
@@ -10,6 +10,7 @@ import time
import zlib
from BaseClasses import World, CollectionState, Item, Region, Location, Shop, Entrance, Settings
from Bosses import place_bosses
from Items import ItemFactory
from KeyDoorShuffle import validate_key_placement
from OverworldGlitchRules import create_owg_connections
@@ -28,14 +29,24 @@ from Fill import sell_potions, sell_keys, balance_multiworld_progression, balanc
from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops
from Utils import output_path, parse_player_names
__version__ = '0.4.1-dev'
__version__ = '1.0.0-dev'
from source.classes.BabelFish import BabelFish
class EnemizerError(RuntimeError):
pass
def check_python_version():
import sys
version = sys.version_info
if version.major < 3 or version.minor < 7:
logging.warning(BabelFish().translate("cli","cli","old.python.version"), sys.version)
def main(args, seed=None, fish=None):
check_python_version()
if args.outputpath:
os.makedirs(args.outputpath, exist_ok=True)
output_path.cached_path = args.outputpath
@@ -69,6 +80,7 @@ def main(args, seed=None, fish=None):
world.compassshuffle = args.compassshuffle.copy()
world.keyshuffle = args.keyshuffle.copy()
world.bigkeyshuffle = args.bigkeyshuffle.copy()
world.bombbag = args.bombbag.copy()
world.crystals_needed_for_ganon = {player: random.randint(0, 7) if args.crystals_ganon[player] == 'random' else int(args.crystals_ganon[player]) for player in range(1, world.players + 1)}
world.crystals_needed_for_gt = {player: random.randint(0, 7) if args.crystals_gt[player] == 'random' else int(args.crystals_gt[player]) for player in range(1, world.players + 1)}
world.crystals_ganon_orig = args.crystals_ganon.copy()
@@ -135,6 +147,7 @@ def main(args, seed=None, fish=None):
create_rooms(world, player)
create_dungeons(world, player)
adjust_locations(world, player)
place_bosses(world, player)
if any(world.potshuffle.values()):
logger.info(world.fish.translate("cli", "cli", "shuffling.pots"))
@@ -233,7 +246,7 @@ def main(args, seed=None, fish=None):
balance_multiworld_progression(world)
# if we only check for beatable, we can do this sanity check first before creating the rom
if not world.can_beat_game():
if not world.can_beat_game(log_error=True):
raise RuntimeError(world.fish.translate("cli","cli","cannot.beat.game"))
for player in range(1, world.players+1):
@@ -258,11 +271,11 @@ def main(args, seed=None, fish=None):
rom = JsonRom() if args.jsonout or use_enemizer else LocalRom(args.rom)
if use_enemizer and (args.enemizercli or not args.jsonout):
base_patch = LocalRom(args.rom) # update base2current.json (side effect)
local_rom = LocalRom(args.rom) # update base2current.json (side effect)
if args.rom and not(os.path.isfile(args.rom)):
raise RuntimeError("Could not find valid base rom for enemizing at expected path %s." % args.rom)
if os.path.exists(args.enemizercli):
patch_enemizer(world, player, rom, args.rom, args.enemizercli, sprite_random_on_hit)
patch_enemizer(world, player, rom, local_rom, args.enemizercli, sprite_random_on_hit)
enemized = True
if not args.jsonout:
rom = LocalRom.fromJsonRom(rom, args.rom, 0x400000)
@@ -282,7 +295,8 @@ def main(args, seed=None, fish=None):
apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player],
args.fastmenu[player], args.disablemusic[player], args.sprite[player],
args.ow_palettes[player], args.uw_palettes[player], args.reduce_flashing[player])
args.ow_palettes[player], args.uw_palettes[player], args.reduce_flashing[player],
args.shuffle_sfx[player])
if args.jsonout:
jsonout[f'patch_t{team}_p{player}'] = rom.patches
@@ -350,6 +364,7 @@ def copy_world(world):
ret.player_names = copy.deepcopy(world.player_names)
ret.remote_items = world.remote_items.copy()
ret.required_medallions = world.required_medallions.copy()
ret.bottle_refills = world.bottle_refills.copy()
ret.swamp_patch_required = world.swamp_patch_required.copy()
ret.ganon_at_pyramid = world.ganon_at_pyramid.copy()
ret.powder_patch_required = world.powder_patch_required.copy()
@@ -372,6 +387,7 @@ def copy_world(world):
ret.compassshuffle = world.compassshuffle.copy()
ret.keyshuffle = world.keyshuffle.copy()
ret.bigkeyshuffle = world.bigkeyshuffle.copy()
ret.bombbag = world.bombbag.copy()
ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy()
ret.crystals_needed_for_gt = world.crystals_needed_for_gt.copy()
ret.crystals_ganon_orig = world.crystals_ganon_orig.copy()
@@ -389,6 +405,8 @@ def copy_world(world):
ret.mixed_travel = world.mixed_travel.copy()
ret.standardize_palettes = world.standardize_palettes.copy()
ret.exp_cache = world.exp_cache.copy()
for player in range(1, world.players + 1):
if world.mode[player] != 'inverted':
create_regions(ret, player)
@@ -448,6 +466,7 @@ def copy_world(world):
# these need to be modified properly by set_rules
new_location.access_rule = lambda state: True
new_location.item_rule = lambda state: True
new_location.forced_item = location.forced_item
# copy remaining itempool. No item in itempool should have an assigned location
for item in world.itempool:
@@ -525,11 +544,11 @@ def create_playthrough(world):
while sphere_candidates:
state.sweep_for_events(key_only=True)
sphere = []
sphere = set()
# build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
for location in sphere_candidates:
if state.can_reach(location) and state.not_flooding_a_key(world, location):
sphere.append(location)
sphere.add(location)
for location in sphere:
sphere_candidates.remove(location)
@@ -550,21 +569,24 @@ def create_playthrough(world):
# in the second phase, we cull each sphere such that the game is still beatable, reducing each range of influence to the bare minimum required inside it
for num, sphere in reversed(list(enumerate(collection_spheres))):
to_delete = []
to_delete = set()
for location in sphere:
# we remove the item at location and check if game is still beatable
logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player)
old_item = location.item
location.item = None
# todo: this is not very efficient, but I'm not sure how else to do it for this backwards logic
# world.clear_exp_cache()
if world.can_beat_game(state_cache[num]):
to_delete.append(location)
# logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is not required')
to_delete.add(location)
else:
# still required, got to keep it around
# logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is required')
location.item = old_item
# cull entries in spheres for spoiler walkthrough at end
for location in to_delete:
sphere.remove(location)
sphere -= to_delete
# second phase, sphere 0
for item in [i for i in world.precollected_items if i.advancement]:
@@ -580,7 +602,7 @@ def create_playthrough(world):
# used to access it was deemed not required.) So we need to do one final sphere collection pass
# to build up the correct spheres
required_locations = [item for sphere in collection_spheres for item in sphere]
required_locations = {item for sphere in collection_spheres for item in sphere}
state = CollectionState(world)
collection_spheres = []
while required_locations:
@@ -596,7 +618,10 @@ def create_playthrough(world):
logging.getLogger('').debug(world.fish.translate("cli","cli","building.final.spheres"), len(collection_spheres), len(sphere), len(required_locations))
if not sphere:
raise RuntimeError(world.fish.translate("cli","cli","cannot.reach.required"))
if world.has_beaten_game(state):
required_locations.clear()
else:
raise RuntimeError(world.fish.translate("cli","cli","cannot.reach.required"))
# store the required locations for statistical analysis
old_world.required_locations = [(location.name, location.player) for sphere in collection_spheres for location in sphere]
@@ -617,7 +642,7 @@ def create_playthrough(world):
old_world.spoiler.paths = dict()
for player in range(1, world.players + 1):
old_world.spoiler.paths.update({location.gen_name(): get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere if location.player == player})
for _, path in dict(old_world.spoiler.paths).items():
for path in dict(old_world.spoiler.paths).values():
if any(exit == 'Pyramid Fairy' for (_, exit) in path):
if world.mode[player] != 'inverted':
old_world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state, world.get_region('Big Bomb Shop', player))
@@ -625,6 +650,6 @@ def create_playthrough(world):
old_world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = get_path(state, world.get_region('Inverted Big Bomb Shop', player))
# we can finally output our playthrough
old_world.spoiler.playthrough = OrderedDict([("0", [str(item) for item in world.precollected_items if item.advancement])])
old_world.spoiler.playthrough = {"0": [str(item) for item in world.precollected_items if item.advancement]}
for i, sphere in enumerate(collection_spheres):
old_world.spoiler.playthrough[str(i + 1)] = {location.gen_name(): str(location.item) for location in sphere}
+1 -1
View File
@@ -846,7 +846,7 @@ async def track_locations(ctx : Context, roomid, roomdata):
new_locations.append(Regions.lookup_name_to_id[location])
try:
if roomid in location_shop_ids:
if ctx.shop_mode or ctx.retro_mode:
misc_data = await snes_read(ctx, SHOP_ADDR, SHOP_SRAM_LEN)
for cnt, b in enumerate(misc_data):
my_check = Regions.shop_table_by_location_id[0x400000 + cnt]
+45 -25
View File
@@ -31,6 +31,7 @@ def main():
parser.add_argument('--rom')
parser.add_argument('--enemizercli')
parser.add_argument('--outputpath')
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
for player in range(1, multiargs.multi + 1):
parser.add_argument(f'--p{player}', help=argparse.SUPPRESS)
args = parser.parse_args()
@@ -63,6 +64,7 @@ def main():
erargs.race = True
erargs.outputname = seedname
erargs.outputpath = args.outputpath
erargs.loglevel = args.loglevel
if args.rom:
erargs.rom = args.rom
@@ -97,7 +99,8 @@ def get_weights(path):
raise Exception(f'Failed to read weights file: {e}')
def roll_settings(weights):
def get_choice(option, root=weights):
def get_choice(option, root=None):
root = weights if root is None else root
if option not in root:
return None
if type(root[option]) is not dict:
@@ -112,13 +115,24 @@ def roll_settings(weights):
return default
return choice
while True:
subweights = weights.get('subweights', {})
if len(subweights) == 0:
break
chances = ({k: int(v['chance']) for (k, v) in subweights.items()})
subweight_name = random.choices(list(chances.keys()), weights=list(chances.values()))[0]
subweights = weights.get('subweights', {}).get(subweight_name, {}).get('weights', {})
subweights['subweights'] = subweights.get('subweights', {})
weights = {**weights, **subweights}
ret = argparse.Namespace()
glitches_required = get_choice('glitches_required')
if glitches_required not in ['none', 'no_logic']:
print("Only NMG and No Logic supported")
glitches_required = 'none'
ret.logic = {'none': 'noglitches', 'no_logic': 'nologic'}[glitches_required]
if glitches_required is not None:
if glitches_required not in ['none', 'no_logic']:
print("Only NMG and No Logic supported")
glitches_required = 'none'
ret.logic = {'none': 'noglitches', 'no_logic': 'nologic'}[glitches_required]
item_placement = get_choice('item_placement')
# not supported in ER
@@ -150,39 +164,44 @@ def roll_settings(weights):
ret.standardize_palettes = get_choice('standardize_palettes') if 'standardize_palettes' in weights else 'standardize'
goal = get_choice('goals')
ret.goal = {'ganon': 'ganon',
'fast_ganon': 'crystals',
'dungeons': 'dungeons',
'pedestal': 'pedestal',
'triforce-hunt': 'triforcehunt'
}[goal]
if goal is not None:
ret.goal = {'ganon': 'ganon',
'fast_ganon': 'crystals',
'dungeons': 'dungeons',
'pedestal': 'pedestal',
'triforce-hunt': 'triforcehunt'
}[goal]
ret.openpyramid = goal == 'fast_ganon' if ret.shuffle in ['vanilla', 'dungeonsfull', 'dungeonssimple'] else False
ret.crystals_gt = get_choice('tower_open')
ret.crystals_ganon = get_choice('ganon_open')
if ret.goal == 'triforcehunt':
goal_min = get_choice_default('triforce_goal_min', default=20)
goal_max = get_choice_default('triforce_goal_max', default=20)
pool_min = get_choice_default('triforce_pool_min', default=30)
pool_max = get_choice_default('triforce_pool_max', default=30)
ret.triforce_goal = random.randint(int(goal_min), int(goal_max))
min_diff = get_choice_default('triforce_min_difference', default=10)
ret.triforce_pool = random.randint(max(int(pool_min), ret.triforce_goal + int(min_diff)), int(pool_max))
goal_min = get_choice_default('triforce_goal_min', default=20)
goal_max = get_choice_default('triforce_goal_max', default=20)
pool_min = get_choice_default('triforce_pool_min', default=30)
pool_max = get_choice_default('triforce_pool_max', default=30)
ret.triforce_goal = random.randint(int(goal_min), int(goal_max))
min_diff = get_choice_default('triforce_min_difference', default=10)
ret.triforce_pool = random.randint(max(int(pool_min), ret.triforce_goal + int(min_diff)), int(pool_max))
ret.mode = get_choice('world_state')
if ret.mode == 'retro':
ret.mode = 'open'
ret.retro = True
ret.retro = get_choice('retro') == 'on' # this overrides world_state if used
ret.bombbag = get_choice('bombbag') == 'on'
ret.hints = get_choice('hints') == 'on'
ret.swords = {'randomized': 'random',
'assured': 'assured',
'vanilla': 'vanilla',
'swordless': 'swordless'
}[get_choice('weapons')]
swords = get_choice('weapons')
if swords is not None:
ret.swords = {'randomized': 'random',
'assured': 'assured',
'vanilla': 'vanilla',
'swordless': 'swordless'
}[swords]
ret.difficulty = get_choice('item_pool')
@@ -232,6 +251,7 @@ def roll_settings(weights):
ret.heartbeep = get_choice('heartbeep', romweights)
ret.ow_palettes = get_choice('ow_palettes', romweights)
ret.uw_palettes = get_choice('uw_palettes', romweights)
ret.shuffle_sfx = get_choice('shuffle_sfx', romweights) == 'on'
return ret
+3 -3
View File
@@ -51,7 +51,7 @@ vanilla_pots = {
43: [Pot(16, 5, PotItem.Heart, 'PoD Sexy Statue'), Pot(44, 5, PotItem.Switch, 'PoD Sexy Statue'), Pot(16, 6, PotItem.Heart, 'PoD Sexy Statue'), Pot(44, 6, PotItem.Bomb, 'PoD Sexy Statue'), Pot(16, 7, PotItem.Heart, 'PoD Sexy Statue'),
Pot(44, 7, PotItem.Bomb, 'PoD Sexy Statue'), Pot(146, 21, PotItem.Bomb, 'PoD Map Balcony'), Pot(170, 21, PotItem.FiveArrows, 'PoD Map Balcony'), Pot(146, 22, PotItem.Bomb, 'PoD Map Balcony'),
Pot(170, 22, PotItem.FiveArrows, 'PoD Map Balcony')],
44: [Pot(108, 24, PotItem.Heart, 'Hookshot Cave'), Pot(112, 24, PotItem.Heart, 'Hookshot Cave')],
44: [Pot(108, 24, PotItem.Heart, 'Hookshot Cave (Middle)'), Pot(112, 24, PotItem.Heart, 'Hookshot Cave (Middle)')],
47: [Pot(28, 7, PotItem.Heart, 'Kakariko Well (top)'), Pot(32, 7, PotItem.Heart, 'Kakariko Well (top)'), Pot(28, 9, PotItem.FiveRupees, 'Kakariko Well (top)'), Pot(32, 9, PotItem.FiveRupees, 'Kakariko Well (top)'),
Pot(172, 19, PotItem.FiveRupees, 'Kakariko Well (top)'), Pot(180, 19, PotItem.FiveRupees, 'Kakariko Well (top)'), Pot(104, 27, PotItem.Heart, 'Kakariko Well (bottom)'), Pot(104, 28, PotItem.Heart, 'Kakariko Well (bottom)')],
49: [Pot(92, 28, PotItem.Bomb, 'Hera Beetles'), Pot(96, 28, PotItem.Nothing, 'Hera Beetles')],
@@ -66,8 +66,8 @@ vanilla_pots = {
55: [Pot(60, 6, PotItem.Key, 'Swamp Trench 1 Alcove'), Pot(48, 20, PotItem.Nothing, 'Swamp Trench 1 Key Ledge')],
56: [Pot(164, 12, PotItem.Bomb, 'Swamp Pot Row'), Pot(164, 13, PotItem.FiveRupees, 'Swamp Pot Row'), Pot(164, 18, PotItem.Bomb, 'Swamp Pot Row'), Pot(164, 19, PotItem.Key, 'Swamp Pot Row')],
57: [Pot(12, 20, PotItem.Heart, 'Skull Spike Corner'), Pot(48, 28, PotItem.FiveArrows, 'Skull Spike Corner'), Pot(100, 22, PotItem.SmallMagic, 'Skull Final Drop'), Pot(100, 26, PotItem.FiveArrows, 'Skull Final Drop')],
60: [Pot(24, 8, PotItem.SmallMagic, 'Hookshot Cave'), Pot(64, 12, PotItem.FiveRupees, 'Hookshot Cave'), Pot(20, 14, PotItem.OneRupee, 'Hookshot Cave'), Pot(20, 19, PotItem.Nothing, 'Hookshot Cave'),
Pot(68, 18, PotItem.FiveRupees, 'Hookshot Cave'), Pot(96, 19, PotItem.Heart, 'Hookshot Cave'), Pot(64, 20, PotItem.FiveRupees, 'Hookshot Cave'), Pot(64, 26, PotItem.FiveRupees, 'Hookshot Cave')],
60: [Pot(24, 8, PotItem.SmallMagic, 'Hookshot Cave (Front)'), Pot(64, 12, PotItem.FiveRupees, 'Hookshot Cave (Front)'), Pot(20, 14, PotItem.OneRupee, 'Hookshot Cave (Front)'), Pot(20, 19, PotItem.Nothing, 'Hookshot Cave (Front)'),
Pot(68, 18, PotItem.FiveRupees, 'Hookshot Cave (Front)'), Pot(96, 19, PotItem.Heart, 'Hookshot Cave (Front)'), Pot(64, 20, PotItem.FiveRupees, 'Hookshot Cave (Front)'), Pot(64, 26, PotItem.FiveRupees, 'Hookshot Cave (Front)')],
61: [Pot(76, 12, PotItem.Bomb, 'GT Mini Helmasaur Room'), Pot(112, 12, PotItem.Bomb, 'GT Mini Helmasaur Room'), Pot(24, 22, PotItem.Heart, 'GT Crystal Inner Circle'), Pot(40, 22, PotItem.FiveArrows, 'GT Crystal Inner Circle'),
Pot(32, 24, PotItem.Heart, 'GT Crystal Inner Circle'), Pot(20, 26, PotItem.FiveRupees, 'GT Crystal Inner Circle'), Pot(36, 26, PotItem.BigMagic, 'GT Crystal Inner Circle')],
62: [Pot(96, 6, PotItem.Bomb, 'Ice Stalfos Hint'), Pot(100, 6, PotItem.SmallMagic, 'Ice Stalfos Hint'), Pot(88, 10, PotItem.Heart, 'Ice Stalfos Hint'), Pot(92, 10, PotItem.SmallMagic, 'Ice Stalfos Hint')],
+86 -2
View File
@@ -1,4 +1,88 @@
### Bug Fix
# New Features
Fix for boss music in non-DR modes (Thanks codemann8)
## Shuffle SFX
Shuffles a large portion of the sounds effects. Can be used with the adjuster.
CLI: ```--shuffle_sfx```
## Bomb Logic
When enabling this option, you do not start with bomb capacity but rather you must find 1 of 2 bomb bags. (They are represented by the +10 capacity item.) Bomb capacity upgrades are otherwise unavailable.
CLI: ```--bombbag```
# Bug Fixes and Notes.
* 0.5.1.7
* Baserom update
* Fix for Inverted Mode: Dark Lake Hylia shop defaults to selling a blue potion
* Fix for Ijwu's enemizer: Boss door in Thieves' Town no longer closes after the maiden hint if Blind is shuffled to Theives' Town in boss shuffle + crossed mode
* No logic now sets the AllowAccidentalMajorGlitches flag in the rom appropriately
* Houlihan room now exits wherever Link's House is shuffled to
* Rom fixes from Catobat and Codemann8. Thanks!
* 0.5.1.6
* Rules fixes for TT (Boss and Cell) can now have TT Big Key if not otherwise required (boss shuffle + crossed dungeon)
* BUg fix for money balancing
* Add some bomb assumptions for bosses in bombbag mode
* 0.5.1.5
* Fix for hard pool capacity upgrades missing
* Bonk Fairy (Light) is no longer in logic for ER Standard and is forbidden to be a connector, so rain state isn't exitable
* Bug fix for retro + enemizer and arrows appearing under pots
* Added bombbag and shufflelinks to settings code
* Catobat fixes:
* Fairy refills in spoiler
* Subweights support in mystery
* More defaults for mystery weights
* Less camera jank for straight stair transitions
* Bug with Straight stairs with vanilla doors where Link's walking animation stopped early is fixed
* 0.5.1.4
* Revert quadrant glitch fix for baserom
* Fix for inverted
* 0.5.1.3
* Certain lobbies forbidden in standard when rupee bow is enabled
* PoD EG disarmed when mirroring (except in nologic)
* Fixed issue with key logic
* Updated baserom
* 0.5.1.2
* Allowed Blind's Cell to be shuffled anywhere if Blind is not the boss of Thieves Town
* Remove unique annotation from a FastEnum that was causing problems
* Updated prevent mixed_travel setting to prevent more mixed travel
* Prevent key door loops on the same supertile where you could have spent 2 keys on one logical door
* Promoted dynamic soft-lock prevention on "stonewalls" from experimental to be the primary prevention (Stonewalls are now never pre-opened)
* Fix to money balancing algorithm with small item_pool, thanks Catobat
* Many fixes and refinements to key logic and generation
* 0.5.1.1
* Shop hints in ER are now more generic instead of using "near X" because they aren't near that anymore
* Added memory location for mutliworld scripts to read what item was just obtain (longer than one frame)
* Fix for bias in boss shuffle "full"
* Fix for certain lone big chests in keysanity (allowed you to get contents without big key)
* Fix for pinball checking
* Fix for multi-entrance dungeons
* 2 fixes for big key placement logic
* ensure big key is placed early if the validator assumes it)
* Open big key doors appropriately when generating rules and big key is forced somewhere
* Updated cutoff entrances for intensity 3
* 0.5.1.0
* Large logic refactor introducing a new method of key logic
* Some performance optimization
* Some outstanding bug fixes (boss shuffle "full" picks three unique bosses to be duplicated, e.g.)
* 0.5.0.3
* Fixed a bug in retro+vanilla and big key placement
* Fixed a problem with shops not registering in the Multiclient until you visit one
* Fixed a bug in the Mystery code with sfx
* 0.5.0.2
* --shuffle_sfx option added
* 0.5.0.1
* --bombbag option added
* 0.5.0.0
* Handles headered roms for enemizer (Thanks compiling)
* Warning added for earlier version of python (Thanks compiling)
* Minor logic issue for defeating Aga in standard (Thanks compiling)
* Fix for boss music in non-DR modes (Thanks codemann8)
# Known Issues
* Shopsanity Issues
* Forfeit in Multiworld not granting all shop items
+9 -5
View File
@@ -190,8 +190,11 @@ def create_regions(world, player):
create_cave_region(player, 'Superbunny Cave (Top)', 'a connector', ['Superbunny Cave - Top', 'Superbunny Cave - Bottom'], ['Superbunny Cave Exit (Top)']),
create_cave_region(player, 'Superbunny Cave (Bottom)', 'a connector', None, ['Superbunny Cave Climb', 'Superbunny Cave Exit (Bottom)']),
create_cave_region(player, 'Spike Cave', 'Spike Cave', ['Spike Cave']),
create_cave_region(player, 'Hookshot Cave', 'a connector', ['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left'],
['Hookshot Cave Exit (South)', 'Hookshot Cave Exit (North)']),
create_cave_region(player, 'Hookshot Cave (Front)', 'a connector', ['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left'],
['Hookshot Cave Front to Middle', 'Hookshot Cave Front Exit']),
create_cave_region(player, 'Hookshot Cave (Back)', 'a connector', None, ['Hookshot Cave Back to Middle', 'Hookshot Cave Back Exit']),
create_cave_region(player, 'Hookshot Cave (Middle)', 'a connector', None, ['Hookshot Cave Middle to Back', 'Hookshot Cave Middle to Front']),
create_dw_region(player, 'Death Mountain Floating Island (Dark World)', None, ['Floating Island Drop', 'Hookshot Cave Back Entrance', 'Floating Island Mirror Spot']),
create_lw_region(player, 'Death Mountain Floating Island (Light World)', ['Floating Island']),
create_dw_region(player, 'Turtle Rock (Top)', None, ['Turtle Rock Drop']),
@@ -954,6 +957,7 @@ def create_shops(world, player):
if world.mode[player] == 'inverted' and region_name == 'Dark Lake Hylia Shop':
locked = True
inventory = [('Blue Potion', 160), ('Blue Shield', 50), ('Bombs (10)', 50)]
custom = True
region = world.get_region(region_name, player)
shop = Shop(region, room_id, type, shopkeeper, custom, locked, sram)
region.shop = shop
@@ -1371,9 +1375,9 @@ location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'),
'Red Shield Shop - Left': (None, None, False, 'for sale as a curiosity'),
'Red Shield Shop - Middle': (None, None, False, 'for sale as a curiosity'),
'Red Shield Shop - Right': (None, None, False, 'for sale as a curiosity'),
'Potion Shop - Left': (None, None, False, 'for sale near the witch'),
'Potion Shop - Middle': (None, None, False, 'for sale near the witch'),
'Potion Shop - Right': (None, None, False, 'for sale near the witch'),
'Potion Shop - Left': (None, None, False, 'for sale near potions'),
'Potion Shop - Middle': (None, None, False, 'for sale near potions'),
'Potion Shop - Right': (None, None, False, 'for sale near potions'),
}
lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int}
+72 -32
View File
@@ -28,9 +28,11 @@ from Utils import output_path, local_path, int16_as_bytes, int32_as_bytes, snes_
from Items import ItemFactory
from EntranceShuffle import door_addresses, exit_ids
from source.classes.SFX import randomize_sfx
JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = '988f1546b14d8f2e6ee30b9de44882da'
RANDOMIZERBASEHASH = '5112ddd931bda3d9979097bc39a5e768'
class JsonRom(object):
@@ -86,10 +88,12 @@ class LocalRom(object):
self.name = name
self.hash = hash
self.orig_buffer = None
self.file = file
self.has_smc_header = False
if not os.path.isfile(file):
raise RuntimeError("Could not find valid local base rom for patching at expected path %s." % file)
with open(file, 'rb') as stream:
self.buffer = read_rom(stream)
self.buffer, self.has_smc_header = read_rom(stream)
if patch:
self.patch_base_rom()
self.orig_buffer = self.buffer.copy()
@@ -98,8 +102,7 @@ class LocalRom(object):
self.buffer[address] = value
def write_bytes(self, startaddress, values):
for i, value in enumerate(values):
self.write_byte(startaddress + i, value)
self.buffer[startaddress:startaddress + len(values)] = values
def write_to_file(self, file):
with open(file, 'wb') as outfile:
@@ -187,12 +190,21 @@ def write_int32s(rom, startaddress, values):
def read_rom(stream):
"Reads rom into bytearray and strips off any smc header"
buffer = bytearray(stream.read())
has_smc_header = False
if len(buffer)%0x400 == 0x200:
buffer = buffer[0x200:]
return buffer
has_smc_header = True
return buffer, has_smc_header
def patch_enemizer(world, player, rom, baserom_path, enemizercli, random_sprite_on_hit):
baserom_path = os.path.abspath(baserom_path)
def patch_enemizer(world, player, rom, local_rom, enemizercli, random_sprite_on_hit):
baserom_path = os.path.abspath(local_rom.file)
unheadered_path = None
if local_rom.has_smc_header:
headered_path = baserom_path
unheadered_path = baserom_path = os.path.abspath(output_path('unheadered_rom.sfc'))
with open(headered_path, 'rb') as headered:
with open(baserom_path, 'wb') as unheadered:
unheadered.write(headered.read()[0x200:])
basepatch_path = os.path.abspath(local_path(os.path.join("data","base2current.json")))
enemizer_basepatch_path = os.path.join(os.path.dirname(enemizercli), "enemizerBasePatch.json")
randopatch_path = os.path.abspath(output_path('enemizer_randopatch.json'))
@@ -321,6 +333,7 @@ def patch_enemizer(world, player, rom, baserom_path, enemizercli, random_sprite_
rom.write_bytes(0xEA081, [0x5c, 0x00, 0x80, 0xb7, 0xc9, 0x6, 0xf0, 0x24,
0xad, 0x3, 0x4, 0x29, 0x20, 0xf0, 0x1d])
rom.write_byte(0x200101, 0) # Do not close boss room door on entry.
rom.write_byte(0x1B0101, 0) # Do not close boss room door on entry. (for Ijwu's enemizer)
if random_sprite_on_hit:
_populate_sprite_table()
@@ -336,6 +349,12 @@ def patch_enemizer(world, player, rom, baserom_path, enemizercli, random_sprite_
rom.write_bytes(0x307000 + (i * 0x8000), sprite.palette)
rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette)
if local_rom.has_smc_header:
try:
os.remove(unheadered_path)
except OSError:
pass
try:
os.remove(randopatch_path)
except OSError:
@@ -653,11 +672,25 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
dr_flags |= DROptions.Debug
if world.doorShuffle[player] == 'crossed' and world.logic[player] != 'nologic'\
and world.mixed_travel[player] == 'prevent':
dr_flags |= DROptions.Rails
# PoD Falling Bridge or Hammjump
# 1FA607: db $2D, $79, $69 ; 0x0069: Vertical Rail ↕ | { 0B, 1E } | Size: 05
# 1FA60A: db $14, $99, $5D ; 0x005D: Large Horizontal Rail ↔ | { 05, 26 } | Size: 01
rom.write_bytes(0xfa607, [0x2d, 0x79, 0x69, 0x14, 0x99, 0x5d])
# PoD Arena
# 1FA573: db $D4, $B2, $22 ; 0x0022: Horizontal Rail ↔ | { 35, 2C } | Size: 02
# 1FA576: db $D4, $CE, $22 ; 0x0022: Horizontal Rail ↔ | { 35, 33 } | Size: 01
# 1FA579: db $D9, $AE, $69 ; 0x0069: Vertical Rail ↕ | { 36, 2B } | Size: 06
rom.write_bytes(0xfa573, [0xd4, 0xb2, 0x22, 0xd4, 0xce, 0x22, 0xd9, 0xae, 0x69])
# Mire BK Pond
# 1FB1FC: db $C8, $9D, $69 ; 0x0069: Vertical Rail ↕ | { 32, 27 } | Size: 01
# 1FB1FF: db $B4, $AC, $5D ; 0x005D: Large Horizontal Rail ↔ | { 2D, 2B } | Size: 00
rom.write_bytes(0xfb1fc, [0xc8, 0x9d, 0x69, 0xb4, 0xac, 0x5d])
if world.standardize_palettes[player] == 'original':
dr_flags |= DROptions.OriginalPalettes
if world.experimental[player]:
dr_flags |= DROptions.DarkWorld_Spawns
if world.logic[player] != 'nologic':
dr_flags |= DROptions.Fix_EG
# fix hc big key problems (map and compass too)
@@ -724,13 +757,6 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
rom.write_bytes(paired_door.address_a(world, player), paired_door.rom_data_a(world, player))
rom.write_bytes(paired_door.address_b(world, player), paired_door.rom_data_b(world, player))
if world.doorShuffle[player] != 'vanilla':
if not world.experimental[player]:
for builder in world.dungeon_layouts[player].values():
for stonewall in builder.pre_open_stonewalls:
if stonewall.name == 'Desert Wall Slide NW':
dr_flags |= DROptions.Open_Desert_Wall
elif stonewall.name == 'PoD Bow Statue Down Ladder':
dr_flags |= DROptions.Open_PoD_Wall
for name, pair in boss_indicator.items():
dungeon_id, boss_door = pair
opposite_door = world.get_door(boss_door, player).dest
@@ -1032,7 +1058,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
rom.write_bytes(0x184000, [
# original_item, limit, replacement_item, filler
0x12, 0x01, 0x35, 0xFF, # lamp -> 5 rupees
0x51, 0x06, 0x52, 0xFF, # 6 +5 bomb upgrades -> +10 bomb upgrade
0x51, 0x00 if world.bombbag[player] else 0x06, 0x31 if world.bombbag[player] else 0x52, 0xFF, # 6 +5 bomb upgrades -> +10 bomb upgrade. If bombbag -> turns into Bombs (10)
0x53, 0x06, 0x54, 0xFF, # 6 +5 arrow upgrades -> +10 arrow upgrade
0x58, 0x01, 0x36 if world.retro[player] else 0x43, 0xFF, # silver arrows -> single arrow (red 20 in retro mode)
0x3E, difficulty.boss_heart_container_limit, 0x47, 0xff, # boss heart -> green 20
@@ -1041,12 +1067,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
])
# set Fountain bottle exchange items
if world.difficulty[player] in ['hard', 'expert']:
rom.write_byte(0x348FF, [0x16, 0x2B, 0x2C, 0x2D, 0x3C, 0x48][random.randint(0, 5)])
rom.write_byte(0x3493B, [0x16, 0x2B, 0x2C, 0x2D, 0x3C, 0x48][random.randint(0, 5)])
else:
rom.write_byte(0x348FF, [0x16, 0x2B, 0x2C, 0x2D, 0x3C, 0x3D, 0x48][random.randint(0, 6)])
rom.write_byte(0x3493B, [0x16, 0x2B, 0x2C, 0x2D, 0x3C, 0x3D, 0x48][random.randint(0, 6)])
rom.write_byte(0x348FF, ItemFactory(world.bottle_refills[player][0], player).code)
rom.write_byte(0x3493B, ItemFactory(world.bottle_refills[player][1], player).code)
#enable Fat Fairy Chests
rom.write_bytes(0x1FC16, [0xB1, 0xC6, 0xF9, 0xC9, 0xC6, 0xF9])
@@ -1169,7 +1191,10 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
equip[0x36C] = 0x18
equip[0x36D] = 0x18
equip[0x379] = 0x68
starting_max_bombs = 10
if world.bombbag[player]:
starting_max_bombs = 0
else:
starting_max_bombs = 10
starting_max_arrows = 30
startingstate = CollectionState(world)
@@ -1426,6 +1451,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
rom.write_byte(0x180176, 0x0A if world.retro[player] else 0x00) # wood arrow cost
rom.write_byte(0x180178, 0x32 if world.retro[player] else 0x00) # silver arrow cost
rom.write_byte(0x301FC, 0xDA if world.retro[player] else 0xE1) # rupees replace arrows under pots
if enemized:
rom.write_byte(0x1B152e, 0xDA if world.retro[player] else 0xE1)
rom.write_byte(0x30052, 0xDB if world.retro[player] else 0xE2) # replace arrows in fish prize from bottle merchant
rom.write_bytes(0xECB4E, [0xA9, 0x00, 0xEA, 0xEA] if world.retro[player] else [0xAF, 0x77, 0xF3, 0x7E]) # Thief steals rupees instead of arrows
rom.write_bytes(0xF0D96, [0xA9, 0x00, 0xEA, 0xEA] if world.retro[player] else [0xAF, 0x77, 0xF3, 0x7E]) # Pikit steals rupees instead of arrows
@@ -1436,6 +1463,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
rom.write_byte(0x1800A3, 0x01) # enable correct world setting behaviour after agahnim kills
rom.write_byte(0x1800A4, 0x01 if world.logic[player] != 'nologic' else 0x00) # enable POD EG fix
rom.write_byte(0x180042, 0x01 if world.save_and_quit_from_boss else 0x00) # Allow Save and Quit after boss kill
rom.write_byte(0x180358, 0x01 if world.logic[player] == 'nologic' else 0x00)
# remove shield from uncle
rom.write_bytes(0x6D253, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E])
@@ -1461,7 +1489,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
rom.write_bytes(0x180188, [0, 0, 10]) # Zelda respawn refills (magic, bombs, arrows)
rom.write_bytes(0x18018B, [0, 0, 10]) # Mantle respawn refills (magic, bombs, arrows)
bow_max, bow_small = 70, 10
elif uncle_location.item is not None and uncle_location.item.name in ['Bombs (10)']:
elif uncle_location.item is not None and uncle_location.item.name in ['Bomb Upgrade (+10)' if world.bombbag[player] else 'Bombs (10)']:
rom.write_byte(0x18004E, 2) # Escape Fill (bombs)
rom.write_bytes(0x180185, [0, 50, 0]) # Uncle respawn refills (magic, bombs, arrows)
rom.write_bytes(0x180188, [0, 3, 0]) # Zelda respawn refills (magic, bombs, arrows)
@@ -1591,13 +1619,16 @@ def write_custom_shops(rom, world, player):
loc_item = ItemFactory(item['item'], player)
if (not world.shopsanity[player] and shop.region.name == 'Capacity Upgrade'
and world.difficulty[player] != 'normal'):
continue # skip cap upgrades except in normal/shopsanity
item_id = loc_item.code
price = int16_as_bytes(item['price'])
replace = ItemFactory(item['replacement'], player).code if item['replacement'] else 0xFF
replace_price = int16_as_bytes(item['replacement_price'])
# really should be 5A instead of B0 -- surprise!!!
item_id, price, replace, replace_price, item_max = 0xB0, [0, 0], 0xFF, [0, 0], 1
else:
item_id = loc_item.code
price = int16_as_bytes(item['price'])
replace = ItemFactory(item['replacement'], player).code if item['replacement'] else 0xFF
replace_price = int16_as_bytes(item['replacement_price'])
item_max = item['max']
item_player = 0 if item['player'] == player else item['player']
item_data = [shop_id, item_id] + price + [item['max'], replace] + replace_price + [item_player]
item_data = [shop_id, item_id] + price + [item_max, replace] + replace_price + [item_player]
items_data.extend(item_data)
rom.write_bytes(0x184800, shop_data)
@@ -1625,7 +1656,7 @@ def hud_format_text(text):
def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite,
ow_palettes, uw_palettes, reduce_flashing):
ow_palettes, uw_palettes, reduce_flashing, shuffle_sfx):
if not os.path.exists("data/sprites/official/001.link.1.zspr") and rom.orig_buffer:
dump_zspr(rom.orig_buffer[0x80000:0x87000], rom.orig_buffer[0xdd308:0xdd380],
@@ -1728,6 +1759,9 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr
elif uw_palettes == 'blackout':
blackout_uw_palettes(rom)
if shuffle_sfx:
randomize_sfx(rom)
if isinstance(rom, LocalRom):
rom.write_crc()
@@ -2275,6 +2309,12 @@ def set_inverted_mode(world, player, rom):
write_int16(rom, snes_to_pc(0x02E8D5), 0x07C8)
write_int16(rom, snes_to_pc(0x02E8F7), 0x01F8)
rom.write_byte(snes_to_pc(0x08D40C), 0xD0) # morph proof
rom.write_byte(snes_to_pc(0x1BC428), 0x00) # remove diggable light world portals
rom.write_byte(snes_to_pc(0x1BC43A), 0x00)
rom.write_byte(snes_to_pc(0x1BC590), 0x00)
rom.write_byte(snes_to_pc(0x1BC5A1), 0x00)
rom.write_byte(snes_to_pc(0x1BC5B1), 0x00)
rom.write_byte(snes_to_pc(0x1BC5C7), 0x00)
# the following bytes should only be written in vanilla
# or they'll overwrite the randomizer's shuffles
if world.shuffle[player] == 'vanilla':
@@ -2771,7 +2811,7 @@ def write_pots_to_rom(rom, pot_contents):
pots = [pot for pot in pot_contents[i] if pot.item != PotItem.Nothing]
if len(pots) > 0:
write_int16(rom, pot_item_room_table_lookup + 2*i, n)
rom.write_bytes(n, itertools.chain(*((pot.x,pot.y,pot.item) for pot in pots)))
rom.write_bytes(n, list(itertools.chain.from_iterable(((pot.x, pot.y, pot.item) for pot in pots))))
n += 3*len(pots) + 2
rom.write_bytes(n - 2, [0xFF,0xFF])
else:
+67 -22
View File
@@ -3,7 +3,7 @@ import logging
from collections import deque
import OverworldGlitchRules
from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier
from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier, KeyRuleType
from RoomData import DoorKind
from OverworldGlitchRules import overworld_glitches_rules
@@ -274,9 +274,11 @@ def global_rules(world, player):
set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: state.has('Hammer', player))
for entrance in ['Thieves Basement Block Path', 'Thieves Blocked Entry Path', 'Thieves Conveyor Block Path', 'Thieves Conveyor Bridge Block Path']:
set_rule(world.get_entrance(entrance, player), lambda state: state.can_lift_rocks(player))
for location in ['Thieves\' Town - Blind\'s Cell', 'Thieves\' Town - Boss']:
forbid_item(world.get_location(location, player), 'Big Key (Thieves Town)', player)
forbid_item(world.get_location('Thieves\' Town - Blind\'s Cell', player), 'Big Key (Thieves Town)', player)
# I think these rules are unnecessary now - testing needed
# for location in ['Thieves\' Town - Blind\'s Cell', 'Thieves\' Town - Boss']:
# forbid_item(world.get_location(location, player), 'Big Key (Thieves Town)', player)
# forbid_item(world.get_location('Thieves\' Town - Blind\'s Cell', player), 'Big Key (Thieves Town)', player)
for location in ['Suspicious Maiden', 'Thieves\' Town - Blind\'s Cell']:
set_rule(world.get_location(location, player), lambda state: state.has('Big Key (Thieves Town)', player))
set_rule(world.get_location('Revealing Light', player), lambda state: state.has('Shining Light', player) and state.has('Maiden Rescued', player))
@@ -304,7 +306,11 @@ def global_rules(world, player):
set_rule(world.get_entrance('Mire Lobby Gap', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player))
set_rule(world.get_entrance('Mire Post-Gap Gap', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player))
set_rule(world.get_entrance('Mire Falling Bridge WN', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player)) # this is due to the fact the the door opposite is blocked
set_rule(world.get_entrance('Mire 2 NE', player), lambda state: state.has_sword(player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or state.can_shoot_arrows(player)) # need to defeat wizzrobes, bombs don't work ...
set_rule(world.get_entrance('Mire 2 NE', player), lambda state: state.has_sword(player) or
(state.has('Fire Rod', player) and (state.can_use_bombs(player) or state.can_extend_magic(player, 9))) or # 9 fr shots or 8 with some bombs
(state.has('Ice Rod', player) and state.can_use_bombs(player)) or # freeze popo and throw, bomb to finish
state.has('Hammer', player) or state.has('Cane of Somaria', player) or state.can_shoot_arrows(player)) # need to defeat wizzrobes, bombs don't work ...
# byrna could work with sufficient magic
set_rule(world.get_location('Misery Mire - Spike Chest', player), lambda state: (state.world.can_take_damage and state.has_hearts(player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player))
set_rule(world.get_entrance('Mire Left Bridge Hook Path', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_entrance('Mire Tile Room NW', player), lambda state: state.has_fire_source(player))
@@ -561,7 +567,8 @@ def global_rules(world, player):
def bomb_rules(world, player):
bonkable_doors = ['Two Brothers House Exit (West)', 'Two Brothers House Exit (East)'] # Technically this is incorrectly defined, but functionally the same as what is intended.
bombable_doors = ['Ice Rod Cave', 'Light World Bomb Hut', 'Light World Death Mountain Shop', 'Mini Moldorm Cave',
'Hookshot Cave Exit (South)', 'Hookshot Cave Exit (North)', 'Dark Lake Hylia Ledge Fairy', 'Hype Cave', 'Brewery']
'Hookshot Cave Back to Middle', 'Hookshot Cave Front to Middle', 'Hookshot Cave Middle to Front','Hookshot Cave Middle to Back',
'Dark Lake Hylia Ledge Fairy', 'Hype Cave', 'Brewery']
for entrance in bonkable_doors:
add_rule(world.get_entrance(entrance, player), lambda state: state.can_use_bombs(player) or state.has_Boots(player))
for entrance in bombable_doors:
@@ -576,9 +583,10 @@ def bomb_rules(world, player):
for location in bombable_items:
add_rule(world.get_location(location, player), lambda state: state.can_use_bombs(player))
cave_kill_locations = ['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right', 'Mini Moldorm Cave - Generous Guy']
cave_kill_locations = ['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right', 'Mini Moldorm Cave - Generous Guy', 'Spiral Cave']
for location in cave_kill_locations:
add_rule(world.get_location(location, player), lambda state: state.can_kill_most_things(player) or state.can_use_bombs(player))
add_rule(world.get_entrance('Spiral Cave (top to bottom)', player), lambda state: state.can_kill_most_things(player) or state.can_use_bombs(player))
paradox_switch_chests = ['Paradox Cave Lower - Far Left', 'Paradox Cave Lower - Left', 'Paradox Cave Lower - Right', 'Paradox Cave Lower - Far Right', 'Paradox Cave Lower - Middle']
for location in paradox_switch_chests:
@@ -706,7 +714,8 @@ def default_rules(world, player):
set_rule(world.get_entrance('Broken Bridge (East)', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_entrance('East Death Mountain Teleporter', player), lambda state: state.can_lift_heavy_rocks(player))
set_rule(world.get_entrance('Fairy Ascension Rocks', player), lambda state: state.can_lift_heavy_rocks(player))
set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has('Mirror', player)) # can erase block
# can erase block - overridden in noglitches
set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has_Mirror(player))
set_rule(world.get_entrance('Death Mountain (Top)', player), lambda state: state.has('Hammer', player))
set_rule(world.get_entrance('Turtle Rock Teleporter', player), lambda state: state.can_lift_heavy_rocks(player) and state.has('Hammer', player))
set_rule(world.get_entrance('East Death Mountain (Top)', player), lambda state: state.has('Hammer', player))
@@ -843,7 +852,8 @@ def inverted_rules(world, player):
set_rule(world.get_entrance('Broken Bridge (East)', player), lambda state: state.has('Hookshot', player) and state.has_Pearl(player))
set_rule(world.get_entrance('Dark Death Mountain Teleporter (East Bottom)', player), lambda state: state.can_lift_heavy_rocks(player))
set_rule(world.get_entrance('Fairy Ascension Rocks', player), lambda state: state.can_lift_heavy_rocks(player) and state.has_Pearl(player))
set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has('Mirror', player)) # can erase block
# can erase block - overridden in noglitches
set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has_Mirror(player))
set_rule(world.get_entrance('Death Mountain (Top)', player), lambda state: state.has('Hammer', player) and state.has_Pearl(player))
set_rule(world.get_entrance('Dark Death Mountain Teleporter (East)', player), lambda state: state.can_lift_heavy_rocks(player) and state.has('Hammer', player) and state.has_Pearl(player)) # bunny cannot use hammer
set_rule(world.get_entrance('East Death Mountain (Top)', player), lambda state: state.has('Hammer', player) and state.has_Pearl(player)) # bunny can not use hammer
@@ -1149,7 +1159,7 @@ def standard_rules(world, player):
set_rule(entrance, lambda state: state.has('Zelda Delivered', player))
set_rule(world.get_entrance('Sanctuary Exit', player), lambda state: state.has('Zelda Delivered', player))
# zelda should be saved before agahnim is in play
set_rule(world.get_location('Agahnim 1', player), lambda state: state.has('Zelda Delivered', player))
add_rule(world.get_location('Agahnim 1', player), lambda state: state.has('Zelda Delivered', player))
# too restrictive for crossed?
def uncle_item_rule(item):
@@ -1160,7 +1170,7 @@ def standard_rules(world, player):
def bomb_escape_rule():
loc = world.get_location("Link's Uncle", player)
return loc.item and loc.item.name == 'Bombs (10)'
return loc.item and loc.item.name in ['Bomb Upgrade (+10)' if world.bombbag[player] else 'Bombs (10)']
def standard_escape_rule(state):
return state.can_kill_most_things(player) or bomb_escape_rule()
@@ -1206,7 +1216,7 @@ def standard_rules(world, player):
'North Fairy Cave', 'North Fairy Cave Drop', 'Lost Woods Gamble', 'Snitch Lady (East)',
'Snitch Lady (West)', 'Tavern (Front)', 'Bush Covered House', 'Light World Bomb Hut',
'Kakariko Shop', 'Long Fairy Cave', 'Good Bee Cave', '20 Rupee Cave', 'Cave Shop (Lake Hylia)',
'Waterfall of Wishing', 'Hyrule Castle Main Gate', '50 Rupee Cave',
'Waterfall of Wishing', 'Hyrule Castle Main Gate', '50 Rupee Cave', 'Bonk Fairy (Light)',
'Fortune Teller (Light)', 'Lake Hylia Fairy', 'Light Hype Fairy', 'Desert Fairy',
'Lumberjack House', 'Lake Hylia Fortune Teller', 'Kakariko Gamble Game', 'Top of Pyramid']:
add_rule(world.get_entrance(entrance, player), lambda state: state.has('Zelda Delivered', player))
@@ -1671,7 +1681,7 @@ def set_bunny_rules(world, player, inverted):
# regions for the exits of multi-entrace caves/drops that bunny cannot pass
# Note spiral cave may be technically passible, but it would be too absurd to require since OHKO mode is a thing.
bunny_impassable_caves = ['Bumper Cave', 'Two Brothers House', 'Hookshot Cave',
bunny_impassable_caves = ['Bumper Cave', 'Two Brothers House', 'Hookshot Cave (Middle)',
'Pyramid', 'Spiral Cave (Top)', 'Fairy Ascension Cave (Drop)']
bunny_accessible_locations = ['Link\'s Uncle', 'Sahasrahla', 'Sick Kid', 'Lost Woods Hideout', 'Lumberjack Tree',
'Checkerboard Cave', 'Potion Shop', 'Spectacle Rock Cave', 'Pyramid',
@@ -1939,14 +1949,12 @@ bunny_impassible_doors = {
def add_key_logic_rules(world, player):
key_logic = world.key_logic[player]
for d_name, d_logic in key_logic.items():
for door_name, keys in d_logic.door_rules.items():
spot = world.get_entrance(door_name, player)
if not world.retro[player] or world.mode[player] != 'standard' or not retro_in_hc(spot):
rule = create_advanced_key_rule(d_logic, player, keys)
if keys.opposite:
rule = or_rule(rule, create_advanced_key_rule(d_logic, player, keys.opposite))
add_rule(spot, rule)
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))
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))
for location in d_logic.bk_restricted:
if not location.forced_item:
forbid_item(location, d_logic.bk_name, player)
@@ -1955,7 +1963,10 @@ def add_key_logic_rules(world, player):
for door in d_logic.bk_doors:
add_rule(world.get_entrance(door.name, player), create_rule(d_logic.bk_name, player))
for chest in d_logic.bk_chests:
add_rule(world.get_location(chest.name, player), create_rule(d_logic.bk_name, player))
big_chest = world.get_location(chest.name, player)
add_rule(big_chest, create_rule(d_logic.bk_name, player))
if len(d_logic.bk_doors) == 0 and len(d_logic.bk_chests) <= 1:
set_always_allow(big_chest, allow_big_key_in_big_chest(d_logic.bk_name, player))
if world.retro[player]:
for d_name, layout in world.key_layout[player].items():
for door in layout.flat_prop:
@@ -1963,6 +1974,40 @@ def add_key_logic_rules(world, player):
add_rule(door.entrance, create_key_rule('Small Key (Universal)', player, 1))
def eval_small_key_door_main(state, door_name, dungeon, player):
if state.is_door_open(door_name, player):
return True
key_logic = state.world.key_logic[player][dungeon]
door_rule = key_logic.door_rules[door_name]
door_openable = False
for ruleType, number in door_rule.new_rules.items():
if door_openable:
return True
if ruleType == KeyRuleType.WorstCase:
door_openable |= state.has_sm_key(key_logic.small_key_name, player, number)
elif ruleType == KeyRuleType.AllowSmall:
if (door_rule.small_location.item and door_rule.small_location.item.name == key_logic.small_key_name
and door_rule.small_location.item.player == player):
return True # always okay if allow small is on
elif isinstance(ruleType, tuple):
lock, lock_item = ruleType
# this doesn't track logical locks yet, i.e. hammer locks the item and hammer is there, but the item isn't
for loc in door_rule.alternate_big_key_loc:
spot = state.world.get_location(loc, player)
if spot.item and spot.item.name == lock_item:
door_openable |= state.has_sm_key(key_logic.small_key_name, player, number)
break
return door_openable
def eval_small_key_door(door_name, dungeon, player):
return lambda state: eval_small_key_door_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
def retro_in_hc(spot):
return spot.parent_region.dungeon.name == 'Hyrule Castle' if spot.parent_region.dungeon else False
+24
View File
@@ -1,10 +1,12 @@
#!/usr/bin/env python3
import os
import re
import operator as op
import subprocess
import sys
import xml.etree.ElementTree as ET
from collections import defaultdict
from functools import reduce
def int16_as_bytes(value):
@@ -116,6 +118,28 @@ def make_new_base2current(old_rom='Zelda no Densetsu - Kamigami no Triforce (Jap
return "New Rom Hash: " + basemd5.hexdigest()
def kth_combination(k, l, r):
if r == 0:
return []
elif len(l) == r:
return l
else:
i = ncr(len(l)-1, r-1)
if k < i:
return l[0:1] + kth_combination(k, l[1:], r-1)
else:
return kth_combination(k-i, l[1:], r)
def ncr(n, r):
if r == 0:
return 1
r = min(r, n-r)
numerator = reduce(op.mul, range(n, n-r, -1), 1)
denominator = reduce(op.mul, range(1, r+1), 1)
return numerator / denominator
entrance_offsets = {
'Sanctuary': 0x2,
'HC West': 0x3,
+1
View File
@@ -9,6 +9,7 @@
; Normal doors use $FE to store the trap door indicator
; Normal doors use $045e to store Y coordinate when transitioning to in-room stairs
; Normal doors use $045f to determine the order in which supertile quadrants are drawn
; Straight stairs use $046d to store X coordinate on animation start
; Spiral doors use $045e to store stair type
; Gfx uses $b1 to for sub-sub-sub-module thing
+4
View File
@@ -582,6 +582,8 @@ dw $00bc, $00a2, $00a3, $00c2, $001a, $0049, $0014, $008c
dw $009f, $0066, $005d, $00a8, $00a9, $00aa, $00b9, $0052
; HC West Hall, TR Dash Bridge, TR Hub, Pod Arena, GT Petting Zoo, Ice Spike Cross
dw $0050, $00c5, $00c6, $0009, $0003, $002a, $007d, $005e
; Sewer Drop, Mire Cross, GT Crystal Circles
dw $0011, $00b2, $003d
dw $ffff
; dungeon tables
@@ -679,6 +681,8 @@ db $00,$07,$20,$20,$07,$07,$07,$07,$07,$20,$20,$07,$20,$20,$20,$20
db $07,$07,$02,$02,$02,$02,$07,$07,$07,$20,$20,$07,$20,$20,$20,$07
;27f300
DungeonTilesets:
db $04,$04,$05,$12,$04,$08,$07,$0C,$09,$0B,$05,$0A,$0D,$0E,$06,$06
;
;org $27ff00
+11 -3
View File
@@ -76,6 +76,9 @@ nop : jsl OverridePaletteHeader
org $02817e ; Bank02.asm : 414 (LDA $02811E, X)
jsl FixAnimatedTiles
org $0aef43 ; UnderworldMap_RecoverGFX
jsl FixCloseDungeonMap
org $028a06 ; Bank02.asm : 1941 Dungeon_ResetTorchBackgroundAndPlayer
JSL FixWallmasterLamp
@@ -125,6 +128,11 @@ org $07a955 ; <- Bank07.asm : around 6564 (JP is a bit different) (STZ $05FC : S
jsl BlockEraseFix
nop #2
org $02A0A8
Mirror_SaveRoomData:
org $07A95B ; < bank_07.asm ; #_07A95B: JSL Mirror_SaveRoomData
jsl EGFixOnMirror
org $02b82a
jsl FixShopCode
@@ -181,9 +189,9 @@ Main_ShowTextMessage:
; Conditionally disable UW music changes in Door Rando
org $028ADB ; <- Bank02.asm:2088-2095 (LDX.b #$14 : LDA $A0 ...)
JSL.l Underworld_DoorDown_Entry : CPX #$10
db $B0, $21 ; BCS $028B04
BRA + : NOP #6 : +
JSL.l Underworld_DoorDown_Entry : CPX #$FF
BEQ + : db $80, $1C ; BRA $028B04
NOP #6 : +
org $02C3F2 ; <- Bank02.asm:10521 Unused call
Underworld_DoorDown_Call:
+10
View File
@@ -45,6 +45,16 @@ FixAnimatedTiles:
+ LDA $02802E, X ; what we wrote over
RTL
FixCloseDungeonMap:
LDA.l DRMode : CMP #$02 : BNE .vanilla
LDA $040C : BMI .vanilla
LSR : TAX
LDA.l DungeonTilesets,x
RTL
.vanilla
LDA $7EC20E
RTL
FixWallmasterLamp:
ORA $0458
STY $1C : STA $1D : RTL ; what we wrote over
+13 -7
View File
@@ -73,8 +73,9 @@ TrapDoorFixer:
rts
Cleanup:
stz $047a
inc $11
lda.l DRFlags : and #$10 : beq +
stz $047a
+ inc $11
lda $ef
rts
@@ -149,15 +150,14 @@ LoadRoomVert:
.notEdge
lda $01 : and #$03 : cmp #$03 : bne .normal
jsr ScrollToInroomStairs
stz $046d
bra .end
.normal
ldy #$01 : jsr ShiftVariablesMainDir
jsr PrepScrollToNormal
.scroll
lda $01 : and #$40 : pha
lda $01 : and #$40 : sta $046d
jsr ScrollX
pla : beq .end
ldy #$00 : jsr ApplyScroll
.end
plb ; restore db register
rts
@@ -290,6 +290,11 @@ StraightStairsAdj:
stx $0464 : sty $012e ; what we wrote over
lda.l DRMode : beq +
lda $045e : bne .toInroom
lda $046d : beq .noScroll
sta $22
ldy #$00 : jsr ApplyScroll
stz $046d
.noScroll
jsr GetTileAttribute : tax
lda $11 : cmp #$12 : beq .goingNorth
lda $a2 : cmp #$51 : bne ++
@@ -337,9 +342,10 @@ db $d0, $f6, $10, $1a, $f0, $00
StraightStairsFix:
{
pha
lda.l DRMode : bne +
!add $20 : sta $20 ;what we wrote over
+ rtl
pla : !add $20 : sta $20 : rtl ;what we wrote over
+ pla : rtl
}
StraightStairLayerFix:
+7 -5
View File
@@ -35,11 +35,7 @@ rtl
OnFileLoadOverride:
jsl OnFileLoad ; what I wrote over
lda.l DRFlags : and #$80 : beq + ;flag is off
lda $7ef086 : ora #$80 : sta $7ef086
+ lda.l DRFlags : and #$40 : beq + ;flag is off
lda $7ef036 : ora #$80 : sta $7ef036
+ lda.l DRFlags : and #$02 : beq +
+ lda.l DRFlags : and #$02 : beq + ; Mirror Scroll
lda $7ef353 : bne +
lda #$01 : sta $7ef353
+ rtl
@@ -51,6 +47,12 @@ MirrorCheckOverride:
rtl
+ lda.l DRScroll : rtl
EGFixOnMirror:
lda.l DRFlags : and #$10 : beq +
stz $047a
+ jsl Mirror_SaveRoomData
rtl
BlockEraseFix:
lda $7ef353 : and #$02 : beq +
stz $05fc : stz $05fd
+5 -1
View File
@@ -168,7 +168,11 @@ ScrollX: ;change the X offset variables
pla : sta $00
sep #$30
lda $04 : sta $22
lda $04 : ldx $046d : bne .straight
sta $22 : bra +
.straight
sta $046d ; set X position later
+
lda $00 : sta $23 : sta $0609 : sta $060d
lda $01 : sta $a9
lda $0e : asl : ora $ac : sta $ac
Binary file not shown.
+6
View File
@@ -16,6 +16,9 @@
pot_shuffle:
on: 1
off: 3
bombbag:
on: 1
off: 4
entrance_shuffle:
none: 15
dungeonssimple: 3
@@ -132,3 +135,6 @@
half: 0
quarter: 1
off: 0
shuffle_sfx:
on: 1
off: 1
+57
View File
@@ -0,0 +1,57 @@
description: Example for subweights
glitches_required: none
world_state: open
goals: ganon
weapons: randomized
entrance_shuffle: none
intensity: 3
subweights:
vanilla:
chance: 25
weights:
door_shuffle: vanilla
keydropshuffle:
on: 40
off: 60
basic:
chance: 25
weights:
door_shuffle: basic
keydropshuffle:
on: 70
off: 30
crossed:
chance: 25
weights:
door_shuffle: crossed
keydropshuffle:
on: 90
off: 10
chaos:
chance: 25
weights:
door_shuffle: crossed
entrance_shuffle:
none: 30
crossed: 70
keydropshuffle:
on: 90
off: 10
shopsanity:
on: 50
off: 50
bombbag:
on: 25
off: 75
subweights:
normal:
chance: 40
weights: {}
swordless:
chance: 20
weights:
weapons: swordless
keysanity:
chance: 40
weights:
dungeon_items: full
+8
View File
@@ -199,6 +199,10 @@
"action": "store_true",
"type": "bool"
},
"shuffle_sfx": {
"action": "store_true",
"type": "bool"
},
"mapshuffle": {
"action": "store_true",
"type": "bool"
@@ -220,6 +224,10 @@
"type": "bool",
"help": "suppress"
},
"bombbag": {
"action": "store_true",
"type": "bool"
},
"retro": {
"action": "store_true",
"type": "bool"
+5 -2
View File
@@ -2,7 +2,7 @@
"cli": {
"yes": "Yes",
"no": "No",
"app.title": "ALttP Door Randomizer Version %s - Seed: %s, Code: %s",
"app.title": "ALttP Door Randomizer Version %s : --seed %s --code %s",
"version": "Version",
"seed": "Seed",
"player": "Player",
@@ -52,7 +52,8 @@
"enemizer.nothing.applied": "No Enemizer options will be applied until this is resolved.",
"building.collection.spheres": "Building up collection spheres",
"building.calculating.spheres": "Calculated sphere %i, containing %i of %i progress items.",
"building.final.spheres": "Calculated final sphere %i, containing %i of %i progress items."
"building.final.spheres": "Calculated final sphere %i, containing %i of %i progress items.",
"old.python.version": "Door Rando may have issues with python versions earlier than 3.7. Detected version: %s"
},
"help": {
"lang": [ "App Language, if available, defaults to English" ],
@@ -263,6 +264,7 @@
"and a few other little things make this more like Zelda-1. (default: %(default)s)"
],
"pseudoboots": [ " Players starts with pseudo boots that allow dashing but no item checks (default: %(default)s"],
"bombbag": ["Start with 0 bomb capacity. Two capacity upgrades (+10) are added to the pool (default: %(default)s)" ],
"startinventory": [ "Specifies a list of items that will be in your starting inventory (separated by commas). (default: %(default)s)" ],
"usestartinventory": [ "Toggle usage of Starting Inventory." ],
"custom": [ "Not supported." ],
@@ -295,6 +297,7 @@
"sprite that will be extracted."
],
"reduce_flashing": [ "Reduce some in-game flashing (default: %(default)s)" ],
"shuffle_sfx": [ "Shuffle sounds effects (default: %(default)s)" ],
"create_rom": [ "Create an output rom file. (default: %(default)s)" ],
"gui": [ "Launch the GUI. (default: %(default)s)" ],
"jsonout": [
@@ -2,7 +2,8 @@
"checkboxes": {
"nobgm": { "type": "checkbox" },
"quickswap": { "type": "checkbox" },
"reduce_flashing": {"type": "checkbox"}
"reduce_flashing": {"type": "checkbox"},
"shuffle_sfx": {"type": "checkbox"}
},
"leftAdjustFrame": {
"heartcolor": {
+3
View File
@@ -3,6 +3,7 @@
"adjust.nobgm": "Disable Music & MSU-1",
"adjust.quickswap": "L/R Quickswapping",
"adjust.reduce_flashing": "Reduce Flashing",
"adjust.shuffle_sfx": "Shuffle Sound Effects",
"adjust.heartcolor": "Heart Color",
"adjust.heartcolor.red": "Red",
@@ -134,6 +135,7 @@
"randomizer.gameoptions.nobgm": "Disable Music & MSU-1",
"randomizer.gameoptions.quickswap": "L/R Quickswapping",
"randomizer.gameoptions.reduce_flashing": "Reduce Flashing",
"randomizer.gameoptions.shuffle_sfx": "Shuffle Sound Effects",
"randomizer.gameoptions.heartcolor": "Heart Color",
"randomizer.gameoptions.heartcolor.red": "Red",
@@ -190,6 +192,7 @@
"randomizer.item.hints": "Include Helpful Hints",
"randomizer.item.retro": "Retro mode (universal keys)",
"randomizer.item.pseudoboots": "Start with Pseudo Boots",
"randomizer.item.bombbag": "Bombbag",
"randomizer.item.worldstate": "World State",
"randomizer.item.worldstate.standard": "Standard",
@@ -2,7 +2,8 @@
"checkboxes": {
"nobgm": { "type": "checkbox" },
"quickswap": { "type": "checkbox" },
"reduce_flashing": {"type": "checkbox"}
"reduce_flashing": {"type": "checkbox"},
"shuffle_sfx": {"type": "checkbox"}
},
"leftRomOptionsFrame": {
"heartcolor": {
@@ -1,6 +1,7 @@
{
"checkboxes": {
"retro": { "type": "checkbox" },
"bombbag": { "type": "checkbox" },
"shopsanity": { "type": "checkbox" },
"hints": {
"type": "checkbox"
+191
View File
@@ -0,0 +1,191 @@
import random
from Utils import int16_as_bytes
class SFX(object):
def __init__(self, name, sfx_set, orig_id, addr, chain, accomp=False):
self.name = name
self.sfx_set = sfx_set
self.orig_id = orig_id
self.addr = addr
self.chain = chain
self.accomp = accomp
self.target_set = None
self.target_id = None
self.target_chain = None
def init_sfx_data():
sfx_pool = [SFX('Slash1', 0x02, 0x01, 0x2614, []), SFX('Slash2', 0x02, 0x02, 0x2625, []),
SFX('Slash3', 0x02, 0x03, 0x2634, []), SFX('Slash4', 0x02, 0x04, 0x2643, []),
SFX('Wall clink', 0x02, 0x05, 0x25DD, []), SFX('Bombable door clink', 0x02, 0x06, 0x25D7, []),
SFX('Fwoosh shooting', 0x02, 0x07, 0x25B7, []), SFX('Arrow hitting wall', 0x02, 0x08, 0x25E3, []),
SFX('Boomerang whooshing', 0x02, 0x09, 0x25AD, []), SFX('Hookshot', 0x02, 0x0A, 0x25C7, []),
SFX('Placing bomb', 0x02, 0x0B, 0x2478, []),
SFX('Bomb exploding/Quake/Bombos/Exploding wall', 0x02, 0x0C, 0x269C, []),
SFX('Powder', 0x02, 0x0D, 0x2414, [0x3f]), SFX('Fire rod shot', 0x02, 0x0E, 0x2404, []),
SFX('Ice rod shot', 0x02, 0x0F, 0x24C3, []), SFX('Hammer use', 0x02, 0x10, 0x23FA, []),
SFX('Hammering peg', 0x02, 0x11, 0x23F0, []), SFX('Digging', 0x02, 0x12, 0x23CD, []),
SFX('Flute use', 0x02, 0x13, 0x23A0, [0x3e]), SFX('Cape on', 0x02, 0x14, 0x2380, []),
SFX('Cape off/Wallmaster grab', 0x02, 0x15, 0x2390, []), SFX('Staircase', 0x02, 0x16, 0x232C, []),
SFX('Staircase', 0x02, 0x17, 0x2344, []), SFX('Staircase', 0x02, 0x18, 0x2356, []),
SFX('Staircase', 0x02, 0x19, 0x236E, []), SFX('Tall grass/Hammer hitting bush', 0x02, 0x1A, 0x2316, []),
SFX('Mire shallow water', 0x02, 0x1B, 0x2307, []), SFX('Shallow water', 0x02, 0x1C, 0x2301, []),
SFX('Lifting object', 0x02, 0x1D, 0x22BB, []), SFX('Cutting grass', 0x02, 0x1E, 0x2577, []),
SFX('Item breaking', 0x02, 0x1F, 0x22E9, []), SFX('Item falling in pit', 0x02, 0x20, 0x22DA, []),
SFX('Bomb hitting ground/General bang', 0x02, 0x21, 0x22CF, []),
SFX('Pushing object/Armos bounce', 0x02, 0x22, 0x2107, []), SFX('Boots dust', 0x02, 0x23, 0x22B1, []),
SFX('Splashing', 0x02, 0x24, 0x22A5, [0x3d]), SFX('Mire shallow water again?', 0x02, 0x25, 0x2296, []),
SFX('Link taking damage', 0x02, 0x26, 0x2844, []), SFX('Fainting', 0x02, 0x27, 0x2252, []),
SFX('Item splash', 0x02, 0x28, 0x2287, []), SFX('Rupee refill', 0x02, 0x29, 0x243F, [0x3b]),
SFX('Fire rod shot hitting wall/Bombos spell', 0x02, 0x2A, 0x2033, []),
SFX('Heart beep/Text box', 0x02, 0x2B, 0x1FF2, []), SFX('Sword up', 0x02, 0x2C, 0x1FD9, [0x3a]),
SFX('Magic drain', 0x02, 0x2D, 0x20A6, []), SFX('GT opening', 0x02, 0x2E, 0x1FCA, [0x39]),
SFX('GT opening/Water drain', 0x02, 0x2F, 0x1F47, [0x38]), SFX('Cucco', 0x02, 0x30, 0x1EF1, []),
SFX('Fairy', 0x02, 0x31, 0x20CE, []), SFX('Bug net', 0x02, 0x32, 0x1D47, []),
SFX('Teleport2', 0x02, 0x33, 0x1CDC, [], True), SFX('Teleport1', 0x02, 0x34, 0x1F6F, [0x33]),
SFX('Quake/Vitreous/Zora king/Armos/Pyramid/Lanmo', 0x02, 0x35, 0x1C67, [0x36]),
SFX('Mire entrance (extends above)', 0x02, 0x36, 0x1C64, [], True),
SFX('Spin charged', 0x02, 0x37, 0x1A43, []), SFX('Water sound', 0x02, 0x38, 0x1F6F, [], True),
SFX('GT opening thunder', 0x02, 0x39, 0x1F9C, [], True), SFX('Sword up', 0x02, 0x3A, 0x1FE7, [], True),
SFX('Quiet rupees', 0x02, 0x3B, 0x2462, [], True), SFX('Error beep', 0x02, 0x3C, 0x1A37, []),
SFX('Big splash', 0x02, 0x3D, 0x22AB, [], True), SFX('Flute again', 0x02, 0x3E, 0x23B5, [], True),
SFX('Powder paired', 0x02, 0x3F, 0x2435, [], True),
SFX('Sword beam', 0x03, 0x01, 0x1A18, []),
SFX('TR opening', 0x03, 0x02, 0x254E, []), SFX('Pyramid hole', 0x03, 0x03, 0x224A, []),
SFX('Angry soldier', 0x03, 0x04, 0x220E, []), SFX('Lynel shot/Javelin toss', 0x03, 0x05, 0x25B7, []),
SFX('BNC swing/Phantom ganon/Helma tail/Arrghus swoosh', 0x03, 0x06, 0x21F5, []),
SFX('Cannon fire', 0x03, 0x07, 0x223D, []), SFX('Damage to enemy; $0BEX.4=1', 0x03, 0x08, 0x21E6, []),
SFX('Enemy death', 0x03, 0x09, 0x21C1, []), SFX('Collecting rupee', 0x03, 0x0A, 0x21A9, []),
SFX('Collecting heart', 0x03, 0x0B, 0x2198, []),
SFX('Non-blank text character', 0x03, 0x0C, 0x218E, []),
SFX('HUD heart (used explicitly by sanc heart?)', 0x03, 0x0D, 0x21B5, []),
SFX('Opening chest', 0x03, 0x0E, 0x2182, []),
SFX('♪Do do do doooooo♫', 0x03, 0x0F, 0x24B9, [0x3C, 0x3D, 0x3E, 0x3F]),
SFX('Opening/Closing map (paired)', 0x03, 0x10, 0x216D, [0x3b]),
SFX('Opening item menu/Bomb shop guy breathing', 0x03, 0x11, 0x214F, []),
SFX('Closing item menu/Bomb shop guy breathing', 0x03, 0x12, 0x215E, []),
SFX('Throwing object (sprites use it as well)/Stalfos jump', 0x03, 0x13, 0x213B, []),
SFX('Key door/Trinecks/Dash key landing/Stalfos Knight collapse', 0x03, 0x14, 0x246C, []),
SFX('Door closing/OW door opening/Chest opening (w/ $29 in $012E)', 0x03, 0x15, 0x212F, []),
SFX('Armos Knight thud', 0x03, 0x16, 0x2123, []), SFX('Rat squeak', 0x03, 0x17, 0x25A6, []),
SFX('Dragging/Mantle moving', 0x03, 0x18, 0x20DD, []),
SFX('Fireball/Laser shot; Somehow used by Trinexx???', 0x03, 0x19, 0x250A, []),
SFX('Chest reveal jingle ', 0x03, 0x1A, 0x1E8A, [0x38]),
SFX('Puzzle jingle', 0x03, 0x1B, 0x20B6, [0x3a]), SFX('Damage to enemy', 0x03, 0x1C, 0x1A62, []),
SFX('Potion refill/Magic drain', 0x03, 0x1D, 0x20A6, []),
SFX('Flapping (Duck/Cucco swarm/Ganon bats/Keese/Raven/Vulture)', 0x03, 0x1E, 0x2091, []),
SFX('Link falling', 0x03, 0x1F, 0x204B, []), SFX('Menu/Text cursor moved', 0x03, 0x20, 0x276C, []),
SFX('Damage to boss', 0x03, 0x21, 0x27E2, []), SFX('Boss dying/Deleting file', 0x03, 0x22, 0x26CF, []),
SFX('Spin attack/Medallion swoosh', 0x03, 0x23, 0x2001, [0x39]),
SFX('OW map perspective change', 0x03, 0x24, 0x2043, []),
SFX('Pressure switch', 0x03, 0x25, 0x1E9D, []),
SFX('Lightning/Game over/Laser/Ganon bat/Trinexx lunge', 0x03, 0x26, 0x1E7B, []),
SFX('Agahnim charge', 0x03, 0x27, 0x1E40, []), SFX('Agahnim/Ganon teleport', 0x03, 0x28, 0x26F7, []),
SFX('Agahnim shot', 0x03, 0x29, 0x1E21, []),
SFX('Somaria/Byrna/Ether spell/Helma fire ball', 0x03, 0x2A, 0x1E12, []),
SFX('Electrocution', 0x03, 0x2B, 0x1DF3, []), SFX('Bees', 0x03, 0x2C, 0x1DC0, []),
SFX('Milestone, also via text', 0x03, 0x2D, 0x1DA9, [0x37]),
SFX('Collecting heart container', 0x03, 0x2E, 0x1D5D, [0x35, 0x34]),
SFX('Collecting absorbable key', 0x03, 0x2F, 0x1D80, [0x33]),
SFX('Byrna spark/Item plop/Magic bat zap/Blob emerge', 0x03, 0x30, 0x1B53, []),
SFX('Sprite falling/Moldorm shuffle', 0x03, 0x31, 0x1ACA, []),
SFX('Bumper boing/Somaria punt/Blob transmutation/Sprite boings', 0x03, 0x32, 0x1A78, []),
SFX('Jingle (paired $2F→$33)', 0x03, 0x33, 0x1D93, [], True),
SFX('Depressing jingle (paired $2E→$35→$34)', 0x03, 0x34, 0x1D66, [], True),
SFX('Ugly jingle (paired $2E→$35→$34)', 0x03, 0x35, 0x1D73, [], True),
SFX('Wizzrobe shot/Helma fireball split/Mothula beam/Blue balls', 0x03, 0x36, 0x1AA7, []),
SFX('Dinky jingle (paired $2D→$37)', 0x03, 0x37, 0x1DB4, [], True),
SFX('Apathetic jingle (paired $1A→$38)', 0x03, 0x38, 0x1E93, [], True),
SFX('Quiet swish (paired $23→$39)', 0x03, 0x39, 0x2017, [], True),
SFX('Defective jingle (paired $1B→$3A)', 0x03, 0x3A, 0x20C0, [], True),
SFX('Petulant jingle (paired $10→$3B)', 0x03, 0x3B, 0x2176, [], True),
SFX('Triumphant jingle (paired $0F→$3C→$3D→$3E→$3F)', 0x03, 0x3C, 0x248A, [], True),
SFX('Less triumphant jingle ($0F→$3C→$3D→$3E→$3F)', 0x03, 0x3D, 0x2494, [], True),
SFX('"You tried, I guess" jingle (paired $0F→$3C→$3D→$3E→$3F)', 0x03, 0x3E, 0x249E, [], True),
SFX('"You didn\'t really try" jingle (paired $0F→$3C→$3D→$3E→$3F)', 0x03, 0x3F, 0x2480, [], True)]
return sfx_pool
def shuffle_sfx_data():
sfx_pool = init_sfx_data()
sfx_map = {2: {}, 3: {}}
accompaniment_map = {2: set(), 3: set()}
candidates = []
for sfx in sfx_pool:
sfx_map[sfx.sfx_set][sfx.orig_id] = sfx
if not sfx.accomp:
candidates.append((sfx.sfx_set, sfx.orig_id))
else:
accompaniment_map[sfx.sfx_set].add(sfx.orig_id)
chained_sfx = [x for x in sfx_pool if len(x.chain) > 0]
random.shuffle(candidates)
# place chained sfx first
random.shuffle(chained_sfx) # todo: sort largest to smallest
chained_sfx = sorted(chained_sfx, key=lambda x: len(x.chain), reverse=True)
for chained in chained_sfx:
chosen_slot = next(x for x in candidates if len(accompaniment_map[x[0]]) - len(chained.chain) >= 0)
if chosen_slot is None:
raise Exception('Something went wrong with sfx chains')
chosen_set, chosen_id = chosen_slot
chained.target_set, chained.target_id = chosen_slot
chained.target_chain = []
for downstream in chained.chain:
next_slot = accompaniment_map[chosen_set].pop()
ds_acc = sfx_map[chained.sfx_set][downstream]
ds_acc.target_set, ds_acc.target_id = chosen_set, next_slot
chained.target_chain.append(next_slot)
candidates.remove(chosen_slot)
sfx_pool.remove(chained)
unchained_sfx = [x for x in sfx_pool if not x.accomp]
# do the rest
for sfx in unchained_sfx:
chosen_slot = candidates.pop()
sfx.target_set, sfx.target_id = chosen_slot
return sfx_map
sfx_table = {
2: 0x1a8c29,
3: 0x1A8D25
}
# 0x1a8c29
# d8059
sfx_accompaniment_table = {
2: 0x1A8CA7,
3: 0x1A8DA3
}
def randomize_sfx(rom):
sfx_map = shuffle_sfx_data()
for shuffled_sfx in sfx_map.values():
for sfx in shuffled_sfx.values():
base_address = sfx_table[sfx.target_set]
rom.write_bytes(base_address + (sfx.target_id * 2) - 2, int16_as_bytes(sfx.addr))
ac_base = sfx_accompaniment_table[sfx.target_set]
last = sfx.target_id
if sfx.target_chain:
for chained in sfx.target_chain:
rom.write_byte(ac_base + last - 1, chained)
last = chained
rom.write_byte(ac_base + last - 1, 0)
+3 -1
View File
@@ -57,6 +57,7 @@ SETTINGSTOPROCESS = {
"item": {
"hints": "hints",
"retro": "retro",
"bombbag": "bombbag",
"shopsanity": "shopsanity",
"pseudoboots": "pseudoboots",
"worldstate": "mode",
@@ -107,7 +108,8 @@ SETTINGSTOPROCESS = {
"menuspeed": "fastmenu",
"owpalettes": "ow_palettes",
"uwpalettes": "uw_palettes",
"reduce_flashing": "reduce_flashing"
"reduce_flashing": "reduce_flashing",
"shuffle_sfx": "shuffle_sfx",
},
"generation": {
"createspoiler": "create_spoiler",
+1
View File
@@ -103,6 +103,7 @@ def adjust_page(top, parent, settings):
"quickswap": "quickswap",
"nobgm": "disablemusic",
"reduce_flashing": "reduce_flashing",
"shuffle_sfx": "shuffle_sfx",
}
guiargs = Namespace()
for option in options: