Merge in combinatoric key logic from keyLogicAttempt1 branch

This commit is contained in:
aerinon
2021-08-20 10:03:40 -06:00
15 changed files with 1155 additions and 295 deletions

View File

@@ -140,6 +140,8 @@ class World(object):
set_player_attr('standardize_palettes', 'standardize') set_player_attr('standardize_palettes', 'standardize')
set_player_attr('force_fix', {'gt': False, 'sw': False, 'pod': False, 'tr': False}) set_player_attr('force_fix', {'gt': False, 'sw': False, 'pod': False, 'tr': False})
set_player_attr('exp_cache', defaultdict(dict))
def get_name_string_for_object(self, obj): 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)})' return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})'
@@ -394,6 +396,10 @@ class World(object):
def clear_location_cache(self): def clear_location_cache(self):
self._cached_locations = None 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): 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] return [location for location in self.get_locations() if (player is None or location.player == player) and location.item is None]
@@ -426,10 +432,14 @@ class World(object):
else: else:
return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1))) 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 starting_state:
if self.has_beaten_game(starting_state):
return True
state = starting_state.copy() state = starting_state.copy()
else: else:
if self.has_beaten_game(self.state):
return True
state = CollectionState(self) state = CollectionState(self)
if self.has_beaten_game(state): if self.has_beaten_game(state):
@@ -446,6 +456,9 @@ class World(object):
if not sphere: if not sphere:
# ran out of places and did not finish yet, quit # 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 return False
for location in sphere: for location in sphere:
@@ -460,17 +473,25 @@ class World(object):
class CollectionState(object): class CollectionState(object):
def __init__(self, parent): def __init__(self, parent, skip_init=False):
self.prog_items = Counter()
self.world = parent self.world = parent
self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)} if not skip_init:
self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)} self.prog_items = Counter()
self.events = [] self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)}
self.path = {} self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)}
self.locations_checked = set() self.events = []
self.stale = {player: True for player in range(1, parent.players + 1)} self.path = {}
for item in parent.precollected_items: self.locations_checked = set()
self.collect(item, True) 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): def update_reachable_regions(self, player):
self.stale[player] = False self.stale[player] = False
@@ -481,68 +502,386 @@ class CollectionState(object):
start = self.world.get_region('Menu', player) start = self.world.get_region('Menu', player)
if not start in rrp: if not start in rrp:
rrp[start] = CrystalBarrier.Orange rrp[start] = CrystalBarrier.Orange
for exit in start.exits: for conn in start.exits:
bc[exit] = CrystalBarrier.Orange bc[conn] = CrystalBarrier.Orange
queue = deque(self.blocked_connections[player].items()) 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 # run BFS on all connections, and keep track of those blocked by missing items
while True: while len(queue) > 0:
try: connection, crystal_state = queue.popleft()
connection, crystal_state = queue.popleft() new_region = connection.connected_region
new_region = connection.connected_region if not self.should_visit(new_region, rrp, crystal_state, player):
if new_region is None or new_region in rrp and (new_region.type != RegionType.Dungeon or (rrp[new_region] & crystal_state) == crystal_state): if not new_region or not self.dungeon_limits or self.possibly_connected_to_dungeon(new_region, player):
bc.pop(connection, None) bc.pop(connection, None)
elif connection.can_reach(self): elif connection.can_reach(self):
if new_region.type == RegionType.Dungeon: bc.pop(connection, None)
new_crystal_state = crystal_state if new_region.type == RegionType.Dungeon:
for exit in new_region.exits: new_crystal_state = crystal_state
door = exit.door if new_region in rrp:
if door is not None and door.crystal == CrystalBarrier.Either and door.entrance.can_reach(self): new_crystal_state |= rrp[new_region]
new_crystal_state = CrystalBarrier.Either
break
if new_region in rrp:
new_crystal_state |= rrp[new_region]
rrp[new_region] = new_crystal_state rrp[new_region] = new_crystal_state
for conn in new_region.exits:
for exit in new_region.exits: door = conn.door
door = exit.door if door is not None and not door.blocked:
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 door_crystal_state = door.crystal if door.crystal else new_crystal_state
bc[exit] = door_crystal_state bc[conn] = door_crystal_state
queue.append((exit, door_crystal_state)) queue.append((conn, door_crystal_state))
elif door is None: elif door is None:
queue.append((exit, new_crystal_state)) # note: no door in dungeon indicates what exactly? (always traversable)?
else: queue.append((conn, new_crystal_state))
new_crystal_state = CrystalBarrier.Orange else:
rrp[new_region] = new_crystal_state new_crystal_state = CrystalBarrier.Orange
bc.pop(connection, None) rrp[new_region] = new_crystal_state
for exit in new_region.exits: for conn in new_region.exits:
bc[exit] = new_crystal_state bc[conn] = new_crystal_state
queue.append((exit, 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 # Retry connections if the new region can unblock them
if new_region.name in indirect_connections: if new_region.name in indirect_connections:
new_entrance = self.world.get_entrance(indirect_connections[new_region.name], player) 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: if new_entrance in bc and new_entrance.parent_region in rrp:
queue.append((new_entrance, rrp[new_entrance.parent_region])) new_crystal_state = rrp[new_entrance.parent_region]
except IndexError: if (new_entrance, new_crystal_state) not in queue:
break queue.append((new_entrance, new_crystal_state))
# else those connections that are not accessible yet
if self.is_small_door(connection):
door = connection.door
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 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): def copy(self):
ret = CollectionState(self.world) ret = CollectionState(self.world, skip_init=True)
ret.prog_items = self.prog_items.copy() 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.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.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.events = copy.copy(self.events)
ret.path = copy.copy(self.path) ret.path = copy.copy(self.path)
ret.locations_checked = copy.copy(self.locations_checked) 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 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): def can_reach(self, spot, resolution_hint=None, player=None):
try: try:
spot_type = spot.spot_type spot_type = spot.spot_type
@@ -558,6 +897,16 @@ class CollectionState(object):
return spot.can_reach(self) 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): def sweep_for_events(self, key_only=False, locations=None):
# this may need improvement # this may need improvement
@@ -605,6 +954,13 @@ class CollectionState(object):
or not self.location_can_be_flooded(flood_location)) or not self.location_can_be_flooded(flood_location))
return True return True
@staticmethod
def is_small_door(connection):
return connection and connection.door and connection.door.smallKey
def is_door_open(self, door_name, player):
return door_name in self.opened_doors[player]
@staticmethod @staticmethod
def location_can_be_flooded(location): def location_can_be_flooded(location):
return location.parent_region.name in ['Swamp Trench 1 Alcove', 'Swamp Trench 2 Alcove'] return location.parent_region.name in ['Swamp Trench 1 Alcove', 'Swamp Trench 2 Alcove']
@@ -1484,6 +1840,7 @@ class Sector(object):
self.entrance_sector = None self.entrance_sector = None
self.destination_entrance = False self.destination_entrance = False
self.equations = None self.equations = None
self.item_logic = set()
def region_set(self): def region_set(self):
if self.r_name_set is None: if self.r_name_set is None:
@@ -1736,9 +2093,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))) 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): def can_reach(self, state):
if self.parent_region.can_reach(state) and self.access_rule(state): return self.parent_region.can_reach(state) and self.access_rule(state)
return True
return False
def forced_big_key(self): def forced_big_key(self):
if self.forced_item and self.forced_item.bigkey and self.player == self.forced_item.player: if self.forced_item and self.forced_item.bigkey and self.player == self.forced_item.player:
@@ -1765,6 +2120,12 @@ class Location(object):
world = self.parent_region.world if self.parent_region and self.parent_region.world else None 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})' 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): class Item(object):
@@ -1808,6 +2169,15 @@ class Item(object):
def compass(self): def compass(self):
return self.type == 'Compass' 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): def __str__(self):
return str(self.__unicode__()) return str(self.__unicode__())
@@ -2157,7 +2527,7 @@ class Spoiler(object):
for player in range(1, self.world.players + 1): for player in range(1, self.world.players + 1):
if self.world.boss_shuffle[player] != 'none': 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(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']])) outfile.write('\n'.join([f'{x}: {y}' for x, y in bossmap.items() if y not in ['Agahnim', 'Agahnim 2', 'Ganon']]))
@@ -2200,6 +2570,22 @@ dungeon_names = [
'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace', 'Misery Mire', 'Turtle Rock', 'Ganons Tower' '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): class PotItem(FastEnum):
Nothing = 0x0 Nothing = 0x0
@@ -2350,3 +2736,10 @@ class Settings(object):
args.enemy_health[p] = r(e_health)[(settings[7] & 0xE0) >> 5] args.enemy_health[p] = r(e_health)[(settings[7] & 0xE0) >> 5]
args.enemy_damage[p] = r(e_dmg)[(settings[7] & 0x18) >> 3] args.enemy_damage[p] = r(e_dmg)[(settings[7] & 0x18) >> 3]
args.shufflepots[p] = True if settings[7] & 0x4 else False args.shufflepots[p] = True if settings[7] & 0x4 else False
@unique
class KeyRuleType(FastEnum):
WorstCase = 0
AllowSmall = 1
Lock = 2

View File

@@ -176,8 +176,8 @@ def place_bosses(world, player):
if world.boss_shuffle[player] == "simple": # vanilla bosses shuffled if world.boss_shuffle[player] == "simple": # vanilla bosses shuffled
bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm'] bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm']
else: # all bosses present, the three duplicates chosen at random else: # all bosses present, the three duplicates chosen at random
bosses = all_bosses + [random.choice(placeable_bosses) for _ in range(3)] bosses = all_bosses + random.sample(placeable_bosses, 3)
logging.getLogger('').debug('Bosses chosen %s', bosses) logging.getLogger('').debug('Bosses chosen %s', bosses)

View File

@@ -1,22 +1,21 @@
import RaceRandom as random import RaceRandom as random
from collections import defaultdict, deque from collections import defaultdict, deque
import logging import logging
import operator as op
import time import time
from enum import unique, Flag from enum import unique, Flag
from typing import DefaultDict, Dict, List from typing import DefaultDict, Dict, List
from functools import reduce from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo, dungeon_keys
from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo
from Doors import reset_portals from Doors import reset_portals
from Dungeons import dungeon_regions, region_starts, standard_starts, split_region_starts 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 Items import ItemFactory
from RoomData import DoorKind, PairedDoor, reset_rooms 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 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 create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances
from DungeonGenerator import dungeon_portals, dungeon_drops, GenerationException 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
from Utils import ncr, kth_combination
def link_doors(world, player): def link_doors(world, player):
@@ -102,6 +101,8 @@ def link_doors_main(world, player):
connect_portal(portal, world, player) connect_portal(portal, world, player)
if not world.doorShuffle[player] == 'vanilla': if not world.doorShuffle[player] == 'vanilla':
fix_big_key_doors_with_ugly_smalls(world, player) fix_big_key_doors_with_ugly_smalls(world, player)
else:
unmark_ugly_smalls(world, player)
if world.doorShuffle[player] == 'vanilla': if world.doorShuffle[player] == 'vanilla':
for entrance, ext in open_edges: for entrance, ext in open_edges:
connect_two_way(world, entrance, ext, player) connect_two_way(world, entrance, ext, player)
@@ -214,8 +215,8 @@ def vanilla_key_logic(world, player):
analyze_dungeon(key_layout, world, player) analyze_dungeon(key_layout, world, player)
world.key_logic[player][builder.name] = key_layout.key_logic world.key_logic[player][builder.name] = key_layout.key_logic
log_key_logic(builder.name, key_layout.key_logic) 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]: # 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) # validate_vanilla_key_logic(world, player)
# some useful functions # some useful functions
@@ -317,6 +318,13 @@ def connect_one_way(world, entrancename, exitname, player):
y.dest = x 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): def fix_big_key_doors_with_ugly_smalls(world, player):
remove_ugly_small_key_doors(world, player) remove_ugly_small_key_doors(world, player)
unpair_big_key_doors(world, player) unpair_big_key_doors(world, player)
@@ -503,6 +511,9 @@ def analyze_portals(world, player):
raise Exception('please inspect this case') raise Exception('please inspect this case')
if len(reachable_portals) == 1: if len(reachable_portals) == 1:
info.sole_entrance = reachable_portals[0] 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 info_map[dungeon] = info
for dungeon, info in info_map.items(): for dungeon, info in info_map.items():
@@ -1104,9 +1115,9 @@ def assign_cross_keys(dungeon_builders, world, player):
logger.debug('Cross Dungeon: Keys unable to assign in pool %s', remaining) logger.debug('Cross Dungeon: Keys unable to assign in pool %s', remaining)
# Last Step: Adjust Small Key Dungeon Pool # Last Step: Adjust Small Key Dungeon Pool
if not world.retro[player]: for name, builder in dungeon_builders.items():
for name, builder in dungeon_builders.items(): reassign_key_doors(builder, world, player)
reassign_key_doors(builder, world, player) if not world.retro[player]:
log_key_logic(builder.name, world.key_logic[player][builder.name]) log_key_logic(builder.name, world.key_logic[player][builder.name])
actual_chest_keys = max(builder.key_doors_num - builder.key_drop_cnt, 0) actual_chest_keys = max(builder.key_doors_num - builder.key_drop_cnt, 0)
dungeon = world.get_dungeon(name, player) dungeon = world.get_dungeon(name, player)
@@ -1580,28 +1591,6 @@ def find_key_door_candidates(region, checked, world, player):
return candidates, checked_doors 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 reassign_key_doors(builder, world, player): def reassign_key_doors(builder, world, player):
logger = logging.getLogger('') logger = logging.getLogger('')
logger.debug('Key doors for %s', builder.name) logger.debug('Key doors for %s', builder.name)
@@ -1830,6 +1819,10 @@ def find_inaccessible_regions(world, player):
if connect.type is not RegionType.Dungeon or connect.name.endswith(' Portal'): if connect.type is not RegionType.Dungeon or connect.name.endswith(' Portal'):
queue.append(connect) queue.append(connect)
world.inaccessible_regions[player].extend([r.name for r in all_regions.difference(visited_regions) if valid_inaccessible_region(r)]) 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', 1)
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 = logging.getLogger('')
logger.debug('Inaccessible Regions:') logger.debug('Inaccessible Regions:')
for r in world.inaccessible_regions[player]: for r in world.inaccessible_regions[player]:

View File

@@ -39,14 +39,15 @@ def pre_validate(builder, entrance_region_names, split_dungeon, world, player):
proposed_map = {} proposed_map = {}
doors_to_connect = {} doors_to_connect = {}
all_regions = set() all_regions = set()
bk_needed = False
bk_special = False bk_special = False
for sector in builder.sectors: for sector in builder.sectors:
for door in sector.outstanding_doors: for door in sector.outstanding_doors:
doors_to_connect[door.name] = door doors_to_connect[door.name] = door
all_regions.update(sector.regions) all_regions.update(sector.regions)
bk_needed = bk_needed or determine_if_bk_needed(sector, split_dungeon, world, player) bk_special |= check_for_special(sector.regions)
bk_special = bk_special or check_for_special(sector) 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) 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, 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) proposed_map, doors_to_connect, bk_needed, bk_special, world, player)
@@ -101,19 +102,27 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon
excluded = {} excluded = {}
for region in entrance_regions: for region in entrance_regions:
portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None) portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None)
if portal and portal.destination: if portal:
excluded[region] = None 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]:
excluded[region] = None
entrance_regions = [x for x in entrance_regions if x not in excluded.keys()] entrance_regions = [x for x in entrance_regions if x not in excluded.keys()]
doors_to_connect = {} doors_to_connect = {}
all_regions = set() all_regions = set()
bk_needed = False
bk_special = False bk_special = False
for sector in builder.sectors: for sector in builder.sectors:
for door in sector.outstanding_doors: for door in sector.outstanding_doors:
doors_to_connect[door.name] = door doors_to_connect[door.name] = door
all_regions.update(sector.regions) all_regions.update(sector.regions)
bk_needed = bk_needed or determine_if_bk_needed(sector, split_dungeon, world, player) bk_special |= check_for_special(sector.regions)
bk_special = bk_special or check_for_special(sector) bk_needed = False
for sector in builder.sectors:
bk_needed |= determine_if_bk_needed(sector, split_dungeon, bk_special, world, player)
proposed_map = {} proposed_map = {}
choices_master = [[]] choices_master = [[]]
depth = 0 depth = 0
@@ -187,8 +196,8 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon
return proposed_map return proposed_map
def determine_if_bk_needed(sector, split_dungeon, world, player): def determine_if_bk_needed(sector, split_dungeon, bk_special, world, player):
if not split_dungeon: if not split_dungeon or bk_special:
for region in sector.regions: for region in sector.regions:
for ext in region.exits: for ext in region.exits:
door = world.check_for_door(ext.name, player) door = world.check_for_door(ext.name, player)
@@ -197,8 +206,8 @@ def determine_if_bk_needed(sector, split_dungeon, world, player):
return False return False
def check_for_special(sector): def check_for_special(regions):
for region in sector.regions: for region in regions:
for loc in region.locations: for loc in region.locations:
if loc.forced_big_key(): if loc.forced_big_key():
return True return True
@@ -417,6 +426,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 # origin has no more hooks, but not all doors have been proposed
if not world.bigkeyshuffle[player]: if not world.bigkeyshuffle[player]:
possible_bks = len(dungeon['Origin'].possible_bk_locations) 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] 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): if len(true_origin_hooks) == 0 and len(proposed_map.keys()) < len(doors_to_connect):
return False return False
@@ -450,7 +461,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) bk_possible = not bk_needed or (world.bigkeyshuffle[player] and not bk_special)
for piece in dungeon.values(): for piece in dungeon.values():
all_visited.update(piece.visited_regions) 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 bk_possible = True
if len(all_regions.difference(all_visited)) > 0: if len(all_regions.difference(all_visited)) > 0:
return False return False
@@ -807,6 +819,10 @@ class ExplorationState(object):
self.dungeon = dungeon self.dungeon = dungeon
self.pinball_used = False self.pinball_used = False
self.prize_door_set = {}
self.prize_doors = []
self.prize_doors_opened = False
def copy(self): def copy(self):
ret = ExplorationState(dungeon=self.dungeon) ret = ExplorationState(dungeon=self.dungeon)
ret.unattached_doors = list(self.unattached_doors) ret.unattached_doors = list(self.unattached_doors)
@@ -833,6 +849,10 @@ class ExplorationState(object):
ret.non_door_entrances = list(self.non_door_entrances) ret.non_door_entrances = list(self.non_door_entrances)
ret.dungeon = self.dungeon ret.dungeon = self.dungeon
ret.pinball_used = self.pinball_used 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 return ret
def next_avail_door(self): def next_avail_door(self):
@@ -842,6 +862,8 @@ class ExplorationState(object):
return exp_door return exp_door
def visit_region(self, region, key_region=None, key_checks=False, bk_Flag=False): 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 self.crystal == CrystalBarrier.Either:
if region not in self.visited_blue: if region not in self.visited_blue:
self.visited_blue.append(region) self.visited_blue.append(region)
@@ -868,6 +890,8 @@ class ExplorationState(object):
if location.name in flooded_keys_reverse.keys() and self.location_found( if location.name in flooded_keys_reverse.keys() and self.location_found(
flooded_keys_reverse[location.name]): flooded_keys_reverse[location.name]):
self.perform_event(flooded_keys_reverse[location.name], key_region) 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): def flooded_key_check(self, location):
if location.name not in flooded_keys.keys(): if location.name not in flooded_keys.keys():
@@ -1103,9 +1127,9 @@ def valid_region_to_explore_in_regions(region, all_regions, world, player):
def valid_region_to_explore(region, name, world, player): def valid_region_to_explore(region, name, world, player):
if region is None: if region is None:
return False return False
return (region.type == RegionType.Dungeon and region.dungeon.name in name)\ 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 in world.inaccessible_regions[player]
or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard') or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard'))
def get_doors(world, region, player): def get_doors(world, region, player):
@@ -1291,13 +1315,16 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
sanc_builder = random.choice(lw_builders) sanc_builder = random.choice(lw_builders)
assign_sector(sanc, sanc_builder, candidate_sectors, global_pole) 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 = {} free_location_sectors = {}
crystal_switches = {} crystal_switches = {}
crystal_barriers = {} crystal_barriers = {}
polarized_sectors = {} polarized_sectors = {}
neutral_sectors = {} neutral_sectors = {}
for sector in candidate_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 free_location_sectors[sector] = None
elif sector.c_switch: elif sector.c_switch:
crystal_switches[sector] = None crystal_switches[sector] = None
@@ -1307,6 +1334,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
neutral_sectors[sector] = None neutral_sectors[sector] = None
else: else:
polarized_sectors[sector] = None 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) assign_location_sectors(dungeon_map, free_location_sectors, global_pole)
leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, 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) ensure_crystal_switches_reachable(dungeon_map, leftover, polarized_sectors, crystal_barriers, global_pole)
@@ -1472,6 +1501,9 @@ def define_sector_features(sectors):
sector.blue_barrier = True sector.blue_barrier = True
if door.bigKey: if door.bigKey:
sector.bk_required = True 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): def assign_sector(sector, dungeon, candidate_sectors, global_pole):
@@ -1522,6 +1554,19 @@ def find_sector(r_name, sectors):
return None 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): def assign_location_sectors(dungeon_map, free_location_sectors, global_pole):
valid = False valid = False
choices = None choices = None
@@ -3275,6 +3320,7 @@ def check_for_valid_layout(builder, sector_list, builder_info):
possible_regions.add(portal.door.entrance.parent_region.name) possible_regions.add(portal.door.entrance.parent_region.name)
if builder.name in dungeon_drops.keys(): if builder.name in dungeon_drops.keys():
possible_regions.update(dungeon_drops[builder.name]) 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(): for name, split_build in builder.split_dungeon_map.items():
name_bits = name.split(" ") name_bits = name.split(" ")
orig_name = " ".join(name_bits[:-1]) orig_name = " ".join(name_bits[:-1])
@@ -3287,7 +3333,8 @@ def check_for_valid_layout(builder, sector_list, builder_info):
if r_name not in entrance_regions: if r_name not in entrance_regions:
entrance_regions.append(r_name) entrance_regions.append(r_name)
# entrance_regions = [x for x in entrance_regions if x not in split_check_entrance_invalid] # 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 # record split proposals
builder.valid_proposal[name] = proposal builder.valid_proposal[name] = proposal
builder.exception_list = list(sector_list) builder.exception_list = list(sector_list)
@@ -3302,6 +3349,32 @@ def check_for_valid_layout(builder, sector_list, builder_info):
return len(unreached_doors) == 0, unreached_doors 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): def resolve_equations(builder, sector_list):
unreached_doors = defaultdict(list) unreached_doors = defaultdict(list)
equations = {x: y for x, y in copy_door_equations(builder, sector_list).items() if len(y) > 0} equations = {x: y for x, y in copy_door_equations(builder, sector_list).items() if len(y) > 0}

View File

@@ -375,13 +375,6 @@ flexible_starts = {
'Skull Woods': ['Skull Left Drop', 'Skull Pinball'] 'Skull Woods': ['Skull Left Drop', 'Skull Pinball']
} }
default_key_counts = {
'Hyrule Castle': 1, 'Eastern Palace': 0, 'Desert Palace': 1,
'Tower of Hera': 1, 'Agahnims Tower': 2, 'Palace of Darkness': 6,
'Swamp Palace': 1, 'Skull Woods': 3, 'Thieves Town': 1,
'Ice Palace': 2, 'Misery Mire': 3, 'Turtle Rock': 4, 'Ganons Tower': 4
}
dungeon_keys = { dungeon_keys = {
'Hyrule Castle': 'Small Key (Escape)', 'Hyrule Castle': 'Small Key (Escape)',
'Eastern Palace': 'Small Key (Eastern Palace)', 'Eastern Palace': 'Small Key (Eastern Palace)',
@@ -414,6 +407,19 @@ dungeon_bigs = {
'Ganons Tower': 'Big Key (Ganons Tower)' 'Ganons Tower': 'Big Key (Ganons Tower)'
} }
dungeon_prize = {
'Eastern Palace': 'Eastern Palace - Prize',
'Desert Palace': 'Desert Palace - Prize',
'Tower of Hera': 'Tower of Hera - Prize',
'Palace of Darkness': 'Palace of Darkness - Prize',
'Swamp Palace': 'Swamp Palace - Prize',
'Skull Woods': 'Skull Woods - Prize',
'Thieves Town': 'Thieves Town - Prize',
'Ice Palace': 'Ice Palace - Prize',
'Misery Mire': 'Misery Mire - Prize',
'Turtle Rock': 'Turtle Rock - Prize',
}
dungeon_hints = { dungeon_hints = {
'Hyrule Castle': 'in Hyrule Castle', 'Hyrule Castle': 'in Hyrule Castle',
'Eastern Palace': 'in Eastern Palace', 'Eastern Palace': 'in Eastern Palace',

View File

@@ -2120,10 +2120,11 @@ def connect_doors(world, doors, targets, player):
"""This works inplace""" """This works inplace"""
random.shuffle(doors) random.shuffle(doors)
random.shuffle(targets) random.shuffle(targets)
while doors: placing = min(len(doors), len(targets))
door = doors.pop() for door, target in zip(doors, targets):
target = targets.pop()
connect_entrance(world, door, target, player) connect_entrance(world, door, target, player)
doors[:] = doors[placing:]
targets[:] = targets[placing:]
def skull_woods_shuffle(world, player): def skull_woods_shuffle(world, player):

173
Fill.py
View File

@@ -1,4 +1,6 @@
import RaceRandom as random import RaceRandom as random
import collections
import itertools
import logging import logging
from BaseClasses import CollectionState 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): 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 spot_to_fill = location
break break
elif item_to_place.smallkey or item_to_place.bigkey: if item_to_place.smallkey or item_to_place.bigkey:
location.item = None location.item = None
if spot_to_fill is 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': 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) logging.getLogger('').warning('Not all items placed. Game beatable anyway. (Could not place %s)' % item_to_place)
continue 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) world.push_item(spot_to_fill, item_to_place, False)
track_outside_keys(item_to_place, spot_to_fill, world) track_outside_keys(item_to_place, spot_to_fill, world)
@@ -250,9 +255,7 @@ def valid_key_placement(item, location, itempool, world):
def track_outside_keys(item, location, world): def track_outside_keys(item, location, world):
if not item.smallkey: if not item.smallkey:
return return
item_dungeon = item.name.split('(')[1][:-1] item_dungeon = item.dungeon
if item_dungeon == 'Escape':
item_dungeon = 'Hyrule Castle'
if location.player == item.player: if location.player == item.player:
loc_dungeon = location.parent_region.dungeon loc_dungeon = location.parent_region.dungeon
if loc_dungeon and loc_dungeon.name == item_dungeon: if loc_dungeon and loc_dungeon.name == item_dungeon:
@@ -260,6 +263,80 @@ def track_outside_keys(item, location, world):
world.key_logic[item.player][item_dungeon].outside_keys += 1 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): def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None):
# If not passed in, then get a shuffled list of locations to fill in # If not passed in, then get a shuffled list of locations to fill in
if not fill_locations: if not fill_locations:
@@ -420,9 +497,8 @@ def sell_keys(world, player):
def balance_multiworld_progression(world): def balance_multiworld_progression(world):
state = CollectionState(world) state = CollectionState(world)
checked_locations = [] checked_locations = set()
unchecked_locations = world.get_locations().copy() unchecked_locations = set(world.get_locations())
random.shuffle(unchecked_locations)
reachable_locations_count = {} reachable_locations_count = {}
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
@@ -430,7 +506,7 @@ def balance_multiworld_progression(world):
def get_sphere_locations(sphere_state, locations): def get_sphere_locations(sphere_state, locations):
sphere_state.sweep_for_events(key_only=True, locations=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: while True:
sphere_locations = get_sphere_locations(state, unchecked_locations) sphere_locations = get_sphere_locations(state, unchecked_locations)
@@ -441,38 +517,42 @@ def balance_multiworld_progression(world):
if checked_locations: if checked_locations:
threshold = max(reachable_locations_count.values()) - 20 threshold = max(reachable_locations_count.values()) - 20
balancing_players = [player for player, reachables in reachable_locations_count.items() if reachables < threshold] 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: if balancing_players:
balancing_state = state.copy() balancing_state = state.copy()
balancing_unchecked_locations = unchecked_locations.copy() balancing_unchecked_locations = unchecked_locations.copy()
balancing_reachables = reachable_locations_count.copy() balancing_reachables = reachable_locations_count.copy()
balancing_sphere = sphere_locations.copy() balancing_sphere = sphere_locations.copy()
candidate_items = [] candidate_items = collections.defaultdict(set)
while True: while True:
for location in balancing_sphere: 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): 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) balancing_state.collect(location.item, True, location)
if location.item.player in balancing_players and not location.locked: player = location.item.player
candidate_items.append(location) 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) balancing_sphere = get_sphere_locations(balancing_state, balancing_unchecked_locations)
for location in balancing_sphere: for location in balancing_sphere:
balancing_unchecked_locations.remove(location) balancing_unchecked_locations.remove(location)
balancing_reachables[location.player] += 1 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 break
elif not balancing_sphere: elif not balancing_sphere:
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.') 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 = [] items_to_replace = []
for player in balancing_players: for player in balancing_players:
locations_to_test = [l for l in unlocked_locations if l.player == player] locations_to_test = unlocked_locations[player]
# only replace items that end up in another player's world items_to_test = candidate_items[player]
items_to_test = [l for l in candidate_items if l.item.player == player and l.player != player]
while items_to_test: while items_to_test:
testing = items_to_test.pop() testing = items_to_test.pop()
reducing_state = state.copy() 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.collect(location.item, True, location)
reducing_state.sweep_for_events(locations=locations_to_test) reducing_state.sweep_for_events(locations=locations_to_test)
@@ -486,33 +566,44 @@ def balance_multiworld_progression(world):
items_to_replace.append(testing) items_to_replace.append(testing)
replaced_items = False 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: while replacement_locations and items_to_replace:
new_location = replacement_locations.pop()
old_location = items_to_replace.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: 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) unchecked_locations.remove(location)
reachable_locations_count[location.player] += 1 reachable_locations_count[location.player] += 1
sphere_locations.append(location) sphere_locations.add(location)
for location in sphere_locations: 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): 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) state.collect(location.item, True, location)
checked_locations.extend(sphere_locations) checked_locations |= sphere_locations
if world.has_beaten_game(state): if world.has_beaten_game(state):
break break
@@ -651,7 +742,7 @@ def balance_money_progression(world):
if room not in rooms_visited[player] and world.get_region(room, player) in state.reachable_regions[player]: if room not in rooms_visited[player] and world.get_region(room, player) in state.reachable_regions[player]:
wallet[player] += income wallet[player] += income
rooms_visited[player].add(room) rooms_visited[player].add(room)
if checked_locations: if checked_locations or len(unchecked_locations) == 0:
if world.has_beaten_game(state): if world.has_beaten_game(state):
done = True done = True
continue continue
@@ -661,11 +752,11 @@ def balance_money_progression(world):
solvent = set() solvent = set()
insolvent = set() insolvent = set()
for player in range(1, world.players+1): 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) solvent.add(player)
if sphere_costs[player] > 0 and sphere_costs[player] > wallet[player]: if sphere_costs[player] > 0 and sphere_costs[player] > wallet[player]:
insolvent.add(player) insolvent.add(player)
if len(solvent) == 0: if len([p for p in solvent if len(locked_by_money[p]) > 0]) == 0:
target_player = min(insolvent, key=lambda p: sphere_costs[p]-wallet[p]) target_player = min(insolvent, key=lambda p: sphere_costs[p]-wallet[p])
difference = sphere_costs[target_player]-wallet[target_player] difference = sphere_costs[target_player]-wallet[target_player]
logger.debug(f'Money balancing needed: Player {target_player} short {difference}') logger.debug(f'Money balancing needed: Player {target_player} short {difference}')
@@ -705,7 +796,7 @@ def balance_money_progression(world):
for player in solvent: for player in solvent:
wallet[player] -= sphere_costs[player] wallet[player] -= sphere_costs[player]
for location in locked_by_money[player]: for location in locked_by_money[player]:
if location == 'Kiki': if isinstance(location, str) and location == 'Kiki':
kiki_paid[player] = True kiki_paid[player] = True
else: else:
state.collect(location.item, True, location) state.collect(location.item, True, location)

View File

@@ -347,11 +347,11 @@ def generate_itempool(world, player):
# rather than making all hearts/heart pieces progression items (which slows down generation considerably) # 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) # 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): 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): 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] adv_heart_pieces = (item for item in items if item.name == 'Piece of Heart')
for hp in adv_heart_pieces: for i in range(4):
hp.advancement = True next(adv_heart_pieces).advancement = True
beeweights = {'0': {None: 100}, beeweights = {'0': {None: 100},
'1': {None: 75, 'trap': 25}, '1': {None: 75, 'trap': 25},

View File

@@ -2,9 +2,9 @@ import itertools
import logging import logging
from collections import defaultdict, deque from collections import defaultdict, deque
from BaseClasses import DoorType from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType
from Regions import dungeon_events from Regions import dungeon_events
from Dungeons import dungeon_keys, dungeon_bigs, default_key_counts from Dungeons import dungeon_keys, dungeon_bigs, dungeon_prize
from DungeonGenerator import ExplorationState, special_big_key_doors from DungeonGenerator import ExplorationState, special_big_key_doors
@@ -25,6 +25,8 @@ class KeyLayout(object):
self.all_locations = set() self.all_locations = set()
self.item_locations = set() self.item_locations = set()
self.found_doors = set()
self.prize_relevant = False
# bk special? # bk special?
# bk required? True if big chests or big doors exists # bk required? True if big chests or big doors exists
@@ -35,6 +37,7 @@ class KeyLayout(object):
self.max_chests = calc_max_chests(builder, self, world, player) self.max_chests = calc_max_chests(builder, self, world, player)
self.all_locations = set() self.all_locations = set()
self.item_locations = set() self.item_locations = set()
self.prize_relevant = False
class KeyLogic(object): class KeyLogic(object):
@@ -54,10 +57,11 @@ class KeyLogic(object):
self.location_rules = {} self.location_rules = {}
self.outside_keys = 0 self.outside_keys = 0
self.dungeon = dungeon_name self.dungeon = dungeon_name
self.sm_doors = {}
def check_placement(self, unplaced_keys, big_key_loc=None): def check_placement(self, unplaced_keys, big_key_loc=None):
for rule in self.placement_rules: for rule in self.placement_rules:
if not rule.is_satisfiable(self.outside_keys, unplaced_keys): if not rule.is_satisfiable(self.outside_keys, unplaced_keys, big_key_loc):
return False return False
if big_key_loc: if big_key_loc:
for rule_a, rule_b in itertools.combinations(self.placement_rules, 2): for rule_a, rule_b in itertools.combinations(self.placement_rules, 2):
@@ -65,6 +69,15 @@ class KeyLogic(object):
return False return False
return True 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): class DoorRules(object):
@@ -79,6 +92,8 @@ class DoorRules(object):
self.small_location = None self.small_location = None
self.opposite = None self.opposite = None
self.new_rules = {} # keyed by type, or type+lock_item -> number
class LocationRule(object): class LocationRule(object):
def __init__(self): def __init__(self):
@@ -101,6 +116,7 @@ class PlacementRule(object):
self.needed_keys_w_bk = None self.needed_keys_w_bk = None
self.needed_keys_wo_bk = None self.needed_keys_wo_bk = None
self.check_locations_w_bk = None self.check_locations_w_bk = None
self.special_bk_avail = False
self.check_locations_wo_bk = None self.check_locations_wo_bk = None
self.bk_relevant = True self.bk_relevant = True
self.key_reduced = False self.key_reduced = False
@@ -112,6 +128,10 @@ class PlacementRule(object):
rule_locations = rule.check_locations_wo_bk if rule_blocked else rule.check_locations_w_bk rule_locations = rule.check_locations_wo_bk if rule_blocked else rule.check_locations_w_bk
if check_locations is None or rule_locations is None: if check_locations is None or rule_locations is None:
return False 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} check_locations = check_locations - {big_key_loc}
rule_locations = rule_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 threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk
@@ -134,13 +154,23 @@ class PlacementRule(object):
left -= rule_needed left -= rule_needed
return False return False
def is_satisfiable(self, outside_keys, unplaced_keys): def is_satisfiable(self, outside_keys, unplaced_keys, big_key_loc):
bk_blocked = False bk_blocked = False
if self.bk_conditional_set: if self.bk_conditional_set:
for loc in self.bk_conditional_set: for loc in self.bk_conditional_set:
if loc.item and loc.item.bigkey: if loc.item and loc.item.bigkey:
bk_blocked = True bk_blocked = True
break break
else:
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 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: if not bk_blocked and check_locations is None:
return True return True
@@ -170,6 +200,8 @@ class KeyCounter(object):
self.important_location = False self.important_location = False
self.other_locations = {} self.other_locations = {}
self.important_locations = {} self.important_locations = {}
self.prize_doors_opened = False
self.prize_received = False
def used_smalls_loc(self, reserve=0): def used_smalls_loc(self, reserve=0):
return max(self.used_keys + reserve - len(self.key_only_locations), 0) return max(self.used_keys + reserve - len(self.key_only_locations), 0)
@@ -209,8 +241,19 @@ def calc_max_chests(builder, key_layout, world, player):
def analyze_dungeon(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_layout.key_counters = create_key_counters(key_layout, world, player)
key_logic = key_layout.key_logic 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) 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_chest_locations(key_layout.all_chest_locations))
@@ -218,7 +261,9 @@ def analyze_dungeon(key_layout, world, player):
if world.retro[player] and world.mode[player] != 'standard': if world.retro[player] and world.mode[player] != 'standard':
return 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)]) queue = deque([(None, original_key_counter)])
doors_completed = set() doors_completed = set()
visited_cid = set() visited_cid = set()
@@ -240,25 +285,30 @@ def analyze_dungeon(key_layout, world, player):
# try to relax the rules here? - smallest requirement that doesn't force a softlock # try to relax the rules here? - smallest requirement that doesn't force a softlock
child_queue = deque() child_queue = deque()
for child in key_counter.child_doors.keys(): for child in key_counter.child_doors.keys():
if not child.bigKey or not key_layout.big_key_special or 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) odd_counter = create_odd_key_counter(child, key_counter, key_layout, world, player)
empty_flag = empty_counter(odd_counter) empty_flag = empty_counter(odd_counter)
child_queue.append((child, odd_counter, empty_flag)) child_queue.append((child, odd_counter, empty_flag))
while len(child_queue) > 0: while len(child_queue) > 0:
child, odd_counter, empty_flag = child_queue.popleft() child, odd_counter, empty_flag = child_queue.popleft()
if not child.bigKey and child not in doors_completed: prize_flag = key_counter.prize_doors_opened
best_counter = find_best_counter(child, odd_counter, key_counter, key_layout, world, player, False, empty_flag) if child in key_layout.flat_prop and child not in doors_completed:
rule = create_rule(best_counter, key_counter, key_layout, world, player) 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) check_for_self_lock_key(rule, child, best_counter, key_layout, world, player)
bk_restricted_rules(rule, child, odd_counter, empty_flag, key_counter, key_layout, world, player) bk_restricted_rules(rule, child, odd_counter, empty_flag, key_counter, key_layout, world, player)
key_logic.door_rules[child.name] = rule key_logic.door_rules[child.name] = rule
elif not child.bigKey and child not in doors_completed:
prize_flag = True
doors_completed.add(child) doors_completed.add(child)
next_counter = find_next_counter(child, key_counter, key_layout) next_counter = find_next_counter(child, key_counter, key_layout, prize_flag)
ctr_id = cid(next_counter, key_layout) ctr_id = cid(next_counter, key_layout)
if ctr_id not in visited_cid: if ctr_id not in visited_cid:
queue.append((child, next_counter)) queue.append((child, next_counter))
visited_cid.add(ctr_id) visited_cid.add(ctr_id)
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 # Flip bk rules if more restrictive, to prevent placing a big key in a softlocking location
for rule in key_logic.door_rules.values(): for rule in key_logic.door_rules.values():
@@ -274,6 +324,8 @@ def create_exhaustive_placement_rules(key_layout, world, player):
key_logic = key_layout.key_logic key_logic = key_layout.key_logic
max_ctr = find_max_counter(key_layout) max_ctr = find_max_counter(key_layout)
for code, key_counter in key_layout.key_counters.items(): 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 = set()
accessible_loc.update(key_counter.free_locations) accessible_loc.update(key_counter.free_locations)
accessible_loc.update(key_counter.key_only_locations) accessible_loc.update(key_counter.key_only_locations)
@@ -294,7 +346,9 @@ def create_exhaustive_placement_rules(key_layout, world, player):
else: else:
placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player) placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player)
rule.check_locations_w_bk = accessible_loc 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: else:
if big_key_progress(key_counter) and only_sm_doors(key_counter): 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) create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, accessible_loc, min_keys, world, player)
@@ -308,6 +362,10 @@ def create_exhaustive_placement_rules(key_layout, world, player):
refine_location_rules(key_layout) refine_location_rules(key_layout)
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): def placement_self_lock_adjustment(rule, max_ctr, blocked_loc, ctr, world, player):
if len(blocked_loc) == 1 and world.accessibility[player] != 'locations': if len(blocked_loc) == 1 and world.accessibility[player] != 'locations':
blocked_others = set(max_ctr.other_locations).difference(set(ctr.other_locations)) blocked_others = set(max_ctr.other_locations).difference(set(ctr.other_locations))
@@ -320,6 +378,7 @@ def placement_self_lock_adjustment(rule, max_ctr, blocked_loc, ctr, world, playe
rule.needed_keys_w_bk -= 1 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): 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): 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)) key_layout.key_logic.sm_restricted.update(blocked.difference(max_ctr.key_only_locations))
@@ -413,7 +472,8 @@ def refine_placement_rules(key_layout, max_ctr):
rule_b = temp rule_b = temp
if rule_a.bk_conditional_set and rule_b.check_locations_w_bk: 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) 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) key_logic.bk_restricted.update(rule_a.bk_conditional_set)
rules_to_remove.append(rule_a) rules_to_remove.append(rule_a)
changed = True changed = True
@@ -478,7 +538,7 @@ def create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, a
else: else:
placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player) placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player)
rule.check_locations_w_bk = accessible_loc 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) key_logic.placement_rules.append(rule)
adjust_locations_rules(key_logic, rule, accessible_loc, key_layout, key_counter, max_ctr) adjust_locations_rules(key_logic, rule, accessible_loc, key_layout, key_counter, max_ctr)
@@ -538,6 +598,8 @@ def relative_empty_counter(odd_counter, key_counter):
return False return False
if len(set(odd_counter.free_locations).difference(key_counter.free_locations)) > 0: if len(set(odd_counter.free_locations).difference(key_counter.free_locations)) > 0:
return False return False
if len(set(odd_counter.other_locations).difference(key_counter.other_locations)) > 0:
return False
# important only # important only
if len(set(odd_counter.important_locations).difference(key_counter.important_locations)) > 0: if len(set(odd_counter.important_locations).difference(key_counter.important_locations)) > 0:
return False return False
@@ -594,33 +656,50 @@ def unique_child_door_2(child, key_counter):
return True return True
def find_best_counter(door, odd_counter, key_counter, key_layout, world, player, skip_bk, empty_flag): # try to waste as many keys as possible? # def find_best_counter(door, odd_counter, key_counter, key_layout, world, player, skip_bk, empty_flag): # try to waste as many keys as possible?
ignored_doors = {door, door.dest} if door is not None else {} # ignored_doors = {door, door.dest} if door is not None else {}
finished = False # finished = False
opened_doors = dict(key_counter.open_doors) # opened_doors = dict(key_counter.open_doors)
bk_opened = key_counter.big_key_opened # bk_opened = key_counter.big_key_opened
# new_counter = key_counter # # new_counter = key_counter
last_counter = key_counter # last_counter = key_counter
while not finished: # while not finished:
door_set = find_potential_open_doors(last_counter, ignored_doors, key_layout, skip_bk) # door_set = find_potential_open_doors(last_counter, ignored_doors, key_layout, skip_bk)
if door_set is None or len(door_set) == 0: # if door_set is None or len(door_set) == 0:
finished = True # finished = True
continue # continue
for new_door in door_set: # for new_door in door_set:
proposed_doors = {**opened_doors, **dict.fromkeys([new_door, new_door.dest])} # proposed_doors = {**opened_doors, **dict.fromkeys([new_door, new_door.dest])}
bk_open = bk_opened or new_door.bigKey # 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)
bk_open = new_counter.big_key_opened # bk_open = new_counter.big_key_opened
# this means the new_door invalidates the door / leads to the same stuff # # 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): # if not empty_flag and relative_empty_counter(odd_counter, new_counter):
ignored_doors.add(new_door) # ignored_doors.add(new_door)
elif empty_flag or key_wasted(new_door, door, last_counter, new_counter, key_layout, world, player): # elif empty_flag or key_wasted(new_door, door, last_counter, new_counter, key_layout, world, player):
last_counter = new_counter # last_counter = new_counter
opened_doors = proposed_doors # opened_doors = proposed_doors
bk_opened = bk_open # bk_opened = bk_open
else: # else:
ignored_doors.add(new_door) # ignored_doors.add(new_door)
return last_counter # 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? def find_worst_counter(door, odd_counter, key_counter, key_layout, skip_bk): # try to waste as many keys as possible?
@@ -638,7 +717,7 @@ def find_worst_counter(door, odd_counter, key_counter, key_layout, skip_bk): #
for new_door in door_set: for new_door in door_set:
proposed_doors = {**opened_doors, **dict.fromkeys([new_door, new_door.dest])} proposed_doors = {**opened_doors, **dict.fromkeys([new_door, new_door.dest])}
bk_open = bk_opened or new_door.bigKey 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 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): 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) ignored_doors.add(new_door)
@@ -690,7 +769,7 @@ def key_wasted(new_door, old_door, old_counter, new_counter, key_layout, world,
for new_child in new_children: for new_child in new_children:
proposed_doors = {**opened_doors, **dict.fromkeys([new_child, new_child.dest])} proposed_doors = {**opened_doors, **dict.fromkeys([new_child, new_child.dest])}
bk_open = bk_opened or new_door.bigKey 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): if key_wasted(new_child, old_door, current_counter, new_counter, key_layout, world, player):
wasted_keys += 1 wasted_keys += 1
if new_avail - wasted_keys < old_avail: if new_avail - wasted_keys < old_avail:
@@ -698,10 +777,11 @@ def key_wasted(new_door, old_door, old_counter, new_counter, key_layout, world,
return False 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])} proposed_doors = {**old_counter.open_doors, **dict.fromkeys([new_door, new_door.dest])}
bk_open = old_counter.big_key_opened or new_door.bigKey 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): def check_special_locations(locations):
@@ -717,7 +797,7 @@ def calc_avail_keys(key_counter, world, player):
return raw_avail - key_counter.used_keys 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_chest_keys = available_chest_small_keys(prev_counter, world)
# prev_avail = prev_chest_keys + len(prev_counter.key_only_locations) # prev_avail = prev_chest_keys + len(prev_counter.key_only_locations)
chest_keys = available_chest_small_keys(key_counter, world, player) chest_keys = available_chest_small_keys(key_counter, world, player)
@@ -736,6 +816,11 @@ def create_rule(key_counter, prev_counter, key_layout, world, player):
return DoorRules(rule_num, is_valid) 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): def check_for_self_lock_key(rule, door, parent_counter, key_layout, world, player):
if world.accessibility[player] != 'locations': if world.accessibility[player] != 'locations':
counter = find_inverted_counter(door, parent_counter, key_layout, world, player) counter = find_inverted_counter(door, parent_counter, key_layout, world, player)
@@ -744,11 +829,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: if len(counter.free_locations) == 1 and len(counter.key_only_locations) == 0 and not counter.important_location:
rule.allow_small = True rule.allow_small = True
rule.small_location = next(iter(counter.free_locations)) 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): def find_inverted_counter(door, parent_counter, key_layout, world, player):
# open all doors in counter # 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) max_counter = find_max_counter(key_layout)
# find the difference # find the difference
inverted_counter = KeyCounter(key_layout.max_chests) inverted_counter = KeyCounter(key_layout.max_chests)
@@ -764,7 +850,7 @@ def find_inverted_counter(door, parent_counter, key_layout, world, player):
return inverted_counter 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 changed = True
counter = parent_counter counter = parent_counter
proposed_doors = dict.fromkeys(parent_counter.open_doors.keys()) proposed_doors = dict.fromkeys(parent_counter.open_doors.keys())
@@ -776,14 +862,12 @@ def open_all_counter(parent_counter, key_layout, door=None, skipBk=False):
if skipBk: if skipBk:
if not child.bigKey: if not child.bigKey:
doors_to_open[child] = None 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 doors_to_open[child] = None
if len(doors_to_open.keys()) > 0: if len(doors_to_open.keys()) > 0:
proposed_doors = {**proposed_doors, **doors_to_open} proposed_doors = {**proposed_doors, **doors_to_open}
bk_hint = counter.big_key_opened bk_hint = counter.big_key_opened or any(d.bigKey for d in doors_to_open.keys())
for d in doors_to_open.keys(): counter = find_counter(proposed_doors, bk_hint, key_layout, True)
bk_hint = bk_hint or d.bigKey
counter = find_counter(proposed_doors, bk_hint, key_layout)
changed = True changed = True
return counter return counter
@@ -804,7 +888,7 @@ def open_some_counter(parent_counter, key_layout, ignored_doors):
bk_hint = counter.big_key_opened bk_hint = counter.big_key_opened
for d in doors_to_open.keys(): for d in doors_to_open.keys():
bk_hint = bk_hint or d.bigKey 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 changed = True
return counter return counter
@@ -845,16 +929,16 @@ def big_key_drop_available(key_counter):
def bk_restricted_rules(rule, door, odd_counter, empty_flag, key_counter, key_layout, world, player): def bk_restricted_rules(rule, door, odd_counter, empty_flag, key_counter, key_layout, world, player):
if key_counter.big_key_opened: if key_counter.big_key_opened:
return return
best_counter = find_best_counter(door, odd_counter, key_counter, key_layout, world, player, True, empty_flag) best_counter = find_best_counter(door, key_layout, odd_counter, True, empty_flag)
bk_rule = create_rule(best_counter, key_counter, key_layout, world, player) bk_rule = create_rule(best_counter, key_counter, world, player)
if bk_rule.small_key_num >= rule.small_key_num: if bk_rule.small_key_num >= rule.small_key_num:
return return
door_open = find_next_counter(door, best_counter, key_layout) door_open = find_next_counter(door, best_counter, key_layout)
ignored_doors = dict_intersection(best_counter.child_doors, door_open.child_doors) ignored_doors = dict_intersection(best_counter.child_doors, door_open.child_doors)
dest_ignored = [] dest_ignored = []
for door in ignored_doors.keys(): for d in ignored_doors.keys():
if door.dest not in ignored_doors: if d.dest not in ignored_doors:
dest_ignored.append(door.dest) dest_ignored.append(d.dest)
ignored_doors = {**ignored_doors, **dict.fromkeys(dest_ignored)} ignored_doors = {**ignored_doors, **dict.fromkeys(dest_ignored)}
post_counter = open_some_counter(door_open, key_layout, ignored_doors.keys()) post_counter = open_some_counter(door_open, key_layout, ignored_doors.keys())
unique_loc = dict_difference(post_counter.free_locations, best_counter.free_locations) unique_loc = dict_difference(post_counter.free_locations, best_counter.free_locations)
@@ -862,8 +946,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 if len(unique_loc) > 0: # and bk_rule.is_valid
rule.alternate_small_key = bk_rule.small_key_num rule.alternate_small_key = bk_rule.small_key_num
rule.alternate_big_key_loc.update(unique_loc) rule.alternate_big_key_loc.update(unique_loc)
# elif not bk_rule.is_valid: if not door.bigKey:
# key_layout.key_logic.bk_restricted.update(unique_loc) 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): def find_worst_counter_wo_bk(small_key_num, accessible_set, door, odd_ctr, key_counter, key_layout):
@@ -887,12 +971,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 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: if door.bigKey or door.name in special_big_key_doors:
child_state.big_key_opened = True child_state.big_key_opened = True
child_state.avail_doors.extend(child_state.big_doors) 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.opened_doors.extend(set([d.door for d in child_state.big_doors]))
child_state.big_doors.clear() 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: else:
child_state.opened_doors.append(door) child_state.opened_doors.append(door)
doors_to_open = [x for x in child_state.small_doors if x.door == door] doors_to_open = [x for x in child_state.small_doors if x.door == door]
@@ -935,6 +1026,7 @@ def only_sm_doors(key_counter):
return False return False
return True return True
# doesn't count dest doors # doesn't count dest doors
def count_unique_small_doors(key_counter, proposal): def count_unique_small_doors(key_counter, proposal):
cnt = 0 cnt = 0
@@ -949,7 +1041,7 @@ def count_unique_small_doors(key_counter, proposal):
def exist_relevant_big_doors(key_counter, key_layout): 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: if bk_counter is not None:
diff = dict_difference(bk_counter.free_locations, key_counter.free_locations) diff = dict_difference(bk_counter.free_locations, key_counter.free_locations)
if len(diff) > 0: if len(diff) > 0:
@@ -982,14 +1074,6 @@ def filter_big_chest(locations):
return [x for x in locations if '- Big Chest' not in x.name] 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): def location_is_bk_locked(loc, key_logic):
return loc in key_logic.bk_chests or loc in key_logic.bk_locked return loc in key_logic.bk_chests or loc in key_logic.bk_locked
@@ -998,18 +1082,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'] 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 cnt = 0
for loc in state.found_locations: 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 cnt += 1
return cnt return cnt
def count_locations_exclude_big_chest(state): def count_locations_exclude_big_chest(state, world, player):
cnt = 0 cnt = 0
for loc in state.found_locations: 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 cnt += 1
return cnt return cnt
@@ -1197,7 +1299,7 @@ def check_rules_deep(original_counter, key_layout, world, player):
elif not door.bigKey: elif not door.bigKey:
can_open = True can_open = True
if can_open: 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) next_counter = find_next_counter(door, counter, key_layout)
c_id = cid(next_counter, key_layout) c_id = cid(next_counter, key_layout)
if c_id not in completed: if c_id not in completed:
@@ -1265,6 +1367,13 @@ def check_bk_special(regions, world, player):
return False return False
def forced_big_key_avail(locations):
for loc in locations:
if loc.forced_big_key():
return loc
return None
# Soft lock stuff # Soft lock stuff
def validate_key_layout(key_layout, world, player): def validate_key_layout(key_layout, world, player):
# retro is all good - except for hyrule castle in standard mode # retro is all good - except for hyrule castle in standard mode
@@ -1275,8 +1384,16 @@ def validate_key_layout(key_layout, world, player):
state.key_locations = key_layout.max_chests state.key_locations = key_layout.max_chests
state.big_key_special = check_bk_special(key_layout.sector.regions, world, player) state.big_key_special = check_bk_special(key_layout.sector.regions, world, player)
for region in key_layout.start_regions: for region in key_layout.start_regions:
state.visit_region(region, key_checks=True) dungeon_entrance, portal_door = find_outside_connection(region)
state.add_all_doors_check_keys(region, flat_proposal, world, player) if (len(key_layout.start_regions) > 1 and dungeon_entrance and
dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy']
and key_layout.key_logic.dungeon in dungeon_prize):
state.append_door_to_list(portal_door, state.prize_doors)
state.prize_door_set[portal_door] = dungeon_entrance
key_layout.prize_relevant = True
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) return validate_key_layout_sub_loop(key_layout, state, {}, flat_proposal, None, 0, world, player)
@@ -1287,7 +1404,10 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa
if not smalls_avail and num_bigs == 0: if not smalls_avail and num_bigs == 0:
return True # I think that's the end return True # I think that's the end
# todo: fix state to separate out these types # 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) ttl_small_key_only = count_small_key_only_locations(state)
available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player) available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player)
available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player) available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player)
@@ -1301,14 +1421,16 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa
if smalls_done and bk_done: if smalls_done and bk_done:
return False return False
else: 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: if smalls_avail and available_small_locations > 0:
for exp_door in state.small_doors: for exp_door in state.small_doors:
state_copy = state.copy() 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 state_copy.used_smalls += 1
if state_copy.used_smalls > ttl_small_key_only: if state_copy.used_smalls > ttl_small_key_only:
state_copy.used_locations += 1 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(): if code not in checked_states.keys():
valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal, valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal,
state, available_small_locations, world, player) state, available_small_locations, world, player)
@@ -1319,10 +1441,23 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa
return False return False
if not state.big_key_opened and (available_big_locations >= num_bigs > 0 or (found_forced_bk and num_bigs > 0)): 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() 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: if not found_forced_bk:
state_copy.used_locations += 1 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???
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(): if code not in checked_states.keys():
valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal, valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal,
state, available_small_locations, world, player) state, available_small_locations, world, player)
@@ -1341,7 +1476,7 @@ def invalid_self_locking_key(key_layout, state, prev_state, prev_avail, world, p
state_copy = state.copy() state_copy = state.copy()
while len(new_bk_doors) > 0: while len(new_bk_doors) > 0:
for door in new_bk_doors: 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)) 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) expand_key_state(state_copy, key_layout.flat_prop, world, player)
new_locations = set(state_copy.found_locations).difference(set(prev_state.found_locations)) new_locations = set(state_copy.found_locations).difference(set(prev_state.found_locations))
@@ -1373,30 +1508,55 @@ def cnt_avail_small_locations(free_locations, key_only, state, world, player):
return state.key_locations - state.used_smalls 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): def cnt_avail_big_locations(ttl_locations, state, world, player):
if not world.bigkeyshuffle[player]: if not world.bigkeyshuffle[player]:
return max(0, ttl_locations - state.used_locations) if not state.big_key_special else 0 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 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): def create_key_counters(key_layout, world, player):
key_counters = {} key_counters = {}
key_layout.found_doors.clear()
flat_proposal = key_layout.flat_prop flat_proposal = key_layout.flat_prop
state = ExplorationState(dungeon=key_layout.sector.name) state = ExplorationState(dungeon=key_layout.sector.name)
if world.doorShuffle[player] == 'vanilla': if world.doorShuffle[player] == 'vanilla':
state.key_locations = default_key_counts[key_layout.sector.name] builder = world.dungeon_layouts[player][key_layout.sector.name]
state.key_locations = len(builder.key_door_proposal) - builder.key_drop_cnt
else: else:
builder = world.dungeon_layouts[player][key_layout.sector.name] builder = world.dungeon_layouts[player][key_layout.sector.name]
state.key_locations = builder.total_keys - builder.key_drop_cnt state.key_locations = builder.total_keys - builder.key_drop_cnt
state.big_key_special, special_region = False, None state.big_key_special = False
for region in key_layout.sector.regions: for region in key_layout.sector.regions:
for location in region.locations: for location in region.locations:
if location.forced_big_key(): if location.forced_big_key():
state.big_key_special = True state.big_key_special = True
special_region = region
for region in key_layout.start_regions: for region in key_layout.start_regions:
state.visit_region(region, key_checks=True) dungeon_entrance, portal_door = find_outside_connection(region)
state.add_all_doors_check_keys(region, flat_proposal, world, player) if (len(key_layout.start_regions) > 1 and dungeon_entrance and
dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy']
and key_layout.key_logic.dungeon in dungeon_prize):
state.append_door_to_list(portal_door, state.prize_doors)
state.prize_door_set[portal_door] = dungeon_entrance
key_layout.prize_relevant = True
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) expand_key_state(state, flat_proposal, world, player)
code = state_id(state, key_layout.flat_prop) code = state_id(state, key_layout.flat_prop)
key_counters[code] = create_key_counter(state, key_layout, world, player) key_counters[code] = create_key_counter(state, key_layout, world, player)
@@ -1404,12 +1564,15 @@ def create_key_counters(key_layout, world, player):
while len(queue) > 0: while len(queue) > 0:
next_key_counter, parent_state = queue.popleft() next_key_counter, parent_state = queue.popleft()
for door in next_key_counter.child_doors: 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() child_state = parent_state.copy()
if door.bigKey or door.name in special_big_key_doors: if door.bigKey or door.name in special_big_key_doors:
key_layout.key_logic.bk_doors.add(door) key_layout.key_logic.bk_doors.add(door)
# open the door, if possible # open the door, if possible
if not door.bigKey or not child_state.big_key_special or child_state.visited_at_all(special_region): if can_open_door(door, child_state, world, player):
open_a_door(door, child_state, flat_proposal) open_a_door(door, child_state, flat_proposal, world, player)
expand_key_state(child_state, flat_proposal, world, player) expand_key_state(child_state, flat_proposal, world, player)
code = state_id(child_state, key_layout.flat_prop) code = state_id(child_state, key_layout.flat_prop)
if code not in key_counters.keys(): if code not in key_counters.keys():
@@ -1419,9 +1582,52 @@ def create_key_counters(key_layout, world, player):
return key_counters 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:
available_big_locations = cnt_avail_big_locations_by_ctr(ttl_locations, counter, layout, world, player)
return not counter.big_key_opened and available_big_locations > 0 and not layout.big_key_special
else:
return True
def create_key_counter(state, key_layout, world, player): def create_key_counter(state, key_layout, world, player):
key_counter = KeyCounter(key_layout.max_chests) 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: for loc in state.found_locations:
if important_location(loc, world, player): if important_location(loc, world, player):
key_counter.important_location = True key_counter.important_location = True
@@ -1438,6 +1644,10 @@ def create_key_counter(state, key_layout, world, player):
key_counter.open_doors.update(dict.fromkeys(state.opened_doors)) key_counter.open_doors.update(dict.fromkeys(state.opened_doors))
key_counter.used_keys = count_unique_sm_doors(state.opened_doors) key_counter.used_keys = count_unique_sm_doors(state.opened_doors)
key_counter.big_key_opened = state.big_key_opened 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 return key_counter
@@ -1488,11 +1698,23 @@ def state_id(state, flat_proposal):
s_id = '1' if state.big_key_opened else '0' s_id = '1' if state.big_key_opened else '0'
for d in flat_proposal: for d in flat_proposal:
s_id += '1' if d in state.opened_doors else '0' 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 return s_id
def find_counter(opened_doors, bk_hint, key_layout, raise_on_error=True): def validate_id(state, flat_proposal):
counter = find_counter_hint(opened_doors, bk_hint, key_layout) 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: if counter is not None:
return counter return counter
more_doors = [] more_doors = []
@@ -1501,43 +1723,47 @@ def find_counter(opened_doors, bk_hint, key_layout, raise_on_error=True):
if door.dest not in opened_doors.keys(): if door.dest not in opened_doors.keys():
more_doors.append(door.dest) more_doors.append(door.dest)
if len(more_doors) > len(opened_doors.keys()): 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: if counter is not None:
return counter return counter
if raise_on_error: 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 return None
def find_counter_hint(opened_doors, bk_hint, key_layout): def find_counter_hint(opened_doors, bk_hint, key_layout, prize_flag):
cid = 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)
if cid in key_layout.key_counters.keys(): if cid in key_layout.key_counters.keys():
return key_layout.key_counters[cid] return key_layout.key_counters[cid]
if not bk_hint: 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(): if cid in key_layout.key_counters.keys():
return key_layout.key_counters[cid] return key_layout.key_counters[cid]
return None return None
def find_max_counter(key_layout): 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: if max_counter is None:
raise Exception("Max Counter is none - something is amiss") raise Exception("Max Counter is none - something is amiss")
if len(max_counter.child_doors) > 0: 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 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' s_id = '1' if bk_unlocked else '0'
for d in flat_proposal: for d in flat_proposal:
s_id += '1' if d in opened_doors.keys() else '0' 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 return s_id
def cid(counter, key_layout): 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): # class SoftLockException(Exception):
@@ -1747,8 +1973,18 @@ 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_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) + \ 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 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 key_layout.sector.name in dungeon_prize:
prize_loc = world.get_location(dungeon_prize[key_layout.sector.name], player)
# todo: pyramid fairy only care about crystals 5 & 6
found_prize = 'Crystal' not in prize_loc.item.name
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 \ 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: if not can_progress:
missing_locations = set(max_counter.free_locations.keys()).difference(found_locations) 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] 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]
@@ -1762,3 +1998,11 @@ def validate_key_placement(key_layout, world, player):
return True 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

33
Main.py
View File

@@ -28,7 +28,7 @@ from Fill import sell_potions, sell_keys, balance_multiworld_progression, balanc
from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops
from Utils import output_path, parse_player_names from Utils import output_path, parse_player_names
__version__ = '0.5.0.3-u' __version__ = '0.5.1.0-u'
from source.classes.BabelFish import BabelFish from source.classes.BabelFish import BabelFish
@@ -244,7 +244,7 @@ def main(args, seed=None, fish=None):
balance_multiworld_progression(world) balance_multiworld_progression(world)
# if we only check for beatable, we can do this sanity check first before creating the rom # 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")) raise RuntimeError(world.fish.translate("cli","cli","cannot.beat.game"))
for player in range(1, world.players+1): for player in range(1, world.players+1):
@@ -402,6 +402,8 @@ def copy_world(world):
ret.mixed_travel = world.mixed_travel.copy() ret.mixed_travel = world.mixed_travel.copy()
ret.standardize_palettes = world.standardize_palettes.copy() ret.standardize_palettes = world.standardize_palettes.copy()
ret.exp_cache = world.exp_cache.copy()
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
if world.mode[player] != 'inverted': if world.mode[player] != 'inverted':
create_regions(ret, player) create_regions(ret, player)
@@ -461,6 +463,7 @@ def copy_world(world):
# these need to be modified properly by set_rules # these need to be modified properly by set_rules
new_location.access_rule = lambda state: True new_location.access_rule = lambda state: True
new_location.item_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 # copy remaining itempool. No item in itempool should have an assigned location
for item in world.itempool: for item in world.itempool:
@@ -538,11 +541,11 @@ def create_playthrough(world):
while sphere_candidates: while sphere_candidates:
state.sweep_for_events(key_only=True) 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 # 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: for location in sphere_candidates:
if state.can_reach(location) and state.not_flooding_a_key(world, location): if state.can_reach(location) and state.not_flooding_a_key(world, location):
sphere.append(location) sphere.add(location)
for location in sphere: for location in sphere:
sphere_candidates.remove(location) sphere_candidates.remove(location)
@@ -563,21 +566,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 # 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))): for num, sphere in reversed(list(enumerate(collection_spheres))):
to_delete = [] to_delete = set()
for location in sphere: for location in sphere:
# we remove the item at location and check if game is still beatable # 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) logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player)
old_item = location.item old_item = location.item
location.item = None 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]): 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: else:
# still required, got to keep it around # still required, got to keep it around
# logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is required')
location.item = old_item location.item = old_item
# cull entries in spheres for spoiler walkthrough at end # cull entries in spheres for spoiler walkthrough at end
for location in to_delete: sphere -= to_delete
sphere.remove(location)
# second phase, sphere 0 # second phase, sphere 0
for item in [i for i in world.precollected_items if i.advancement]: for item in [i for i in world.precollected_items if i.advancement]:
@@ -593,7 +599,7 @@ def create_playthrough(world):
# used to access it was deemed not required.) So we need to do one final sphere collection pass # used to access it was deemed not required.) So we need to do one final sphere collection pass
# to build up the correct spheres # 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) state = CollectionState(world)
collection_spheres = [] collection_spheres = []
while required_locations: while required_locations:
@@ -609,7 +615,10 @@ def create_playthrough(world):
logging.getLogger('').debug(world.fish.translate("cli","cli","building.final.spheres"), len(collection_spheres), len(sphere), len(required_locations)) logging.getLogger('').debug(world.fish.translate("cli","cli","building.final.spheres"), len(collection_spheres), len(sphere), len(required_locations))
if not sphere: 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 # store the required locations for statistical analysis
old_world.required_locations = [(location.name, location.player) for sphere in collection_spheres for location in sphere] old_world.required_locations = [(location.name, location.player) for sphere in collection_spheres for location in sphere]
@@ -630,7 +639,7 @@ def create_playthrough(world):
old_world.spoiler.paths = dict() old_world.spoiler.paths = dict()
for player in range(1, world.players + 1): 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}) 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 any(exit == 'Pyramid Fairy' for (_, exit) in path):
if world.mode[player] != 'inverted': 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)) old_world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state, world.get_region('Big Bomb Shop', player))
@@ -638,6 +647,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)) 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 # 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): for i, sphere in enumerate(collection_spheres):
old_world.spoiler.playthrough[str(i + 1)] = {location.gen_name(): str(location.item) for location in sphere} old_world.spoiler.playthrough[str(i + 1)] = {location.gen_name(): str(location.item) for location in sphere}

View File

@@ -31,6 +31,7 @@ def main():
parser.add_argument('--rom') parser.add_argument('--rom')
parser.add_argument('--enemizercli') parser.add_argument('--enemizercli')
parser.add_argument('--outputpath') parser.add_argument('--outputpath')
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
for player in range(1, multiargs.multi + 1): for player in range(1, multiargs.multi + 1):
parser.add_argument(f'--p{player}', help=argparse.SUPPRESS) parser.add_argument(f'--p{player}', help=argparse.SUPPRESS)
args = parser.parse_args() args = parser.parse_args()
@@ -63,6 +64,7 @@ def main():
erargs.race = True erargs.race = True
erargs.outputname = seedname erargs.outputname = seedname
erargs.outputpath = args.outputpath erargs.outputpath = args.outputpath
erargs.loglevel = args.loglevel
if args.rom: if args.rom:
erargs.rom = args.rom erargs.rom = args.rom

5
Rom.py
View File

@@ -102,8 +102,7 @@ class LocalRom(object):
self.buffer[address] = value self.buffer[address] = value
def write_bytes(self, startaddress, values): def write_bytes(self, startaddress, values):
for i, value in enumerate(values): self.buffer[startaddress:startaddress + len(values)] = values
self.write_byte(startaddress + i, value)
def write_to_file(self, file): def write_to_file(self, file):
with open(file, 'wb') as outfile: with open(file, 'wb') as outfile:
@@ -2796,7 +2795,7 @@ def write_pots_to_rom(rom, pot_contents):
pots = [pot for pot in pot_contents[i] if pot.item != PotItem.Nothing] pots = [pot for pot in pot_contents[i] if pot.item != PotItem.Nothing]
if len(pots) > 0: if len(pots) > 0:
write_int16(rom, pot_item_room_table_lookup + 2*i, n) 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 n += 3*len(pots) + 2
rom.write_bytes(n - 2, [0xFF,0xFF]) rom.write_bytes(n - 2, [0xFF,0xFF])
else: else:

View File

@@ -3,7 +3,7 @@ import logging
from collections import deque from collections import deque
import OverworldGlitchRules import OverworldGlitchRules
from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier, KeyRuleType
from RoomData import DoorKind from RoomData import DoorKind
from OverworldGlitchRules import overworld_glitches_rules from OverworldGlitchRules import overworld_glitches_rules
@@ -1941,14 +1941,8 @@ bunny_impassible_doors = {
def add_key_logic_rules(world, player): def add_key_logic_rules(world, player):
key_logic = world.key_logic[player] key_logic = world.key_logic[player]
for d_name, d_logic in key_logic.items(): for d_name, d_logic in key_logic.items():
for door_name, keys in d_logic.door_rules.items(): for door_name, rule in d_logic.door_rules.items():
spot = world.get_entrance(door_name, player) add_rule(world.get_entrance(door_name, player), eval_small_key_door(door_name, d_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 location in d_logic.bk_restricted: for location in d_logic.bk_restricted:
if not location.forced_item: if not location.forced_item:
forbid_item(location, d_logic.bk_name, player) forbid_item(location, d_logic.bk_name, player)
@@ -1956,8 +1950,9 @@ def add_key_logic_rules(world, player):
forbid_item(location, d_logic.small_key_name, player) forbid_item(location, d_logic.small_key_name, player)
for door in d_logic.bk_doors: for door in d_logic.bk_doors:
add_rule(world.get_entrance(door.name, player), create_rule(d_logic.bk_name, player)) add_rule(world.get_entrance(door.name, player), create_rule(d_logic.bk_name, player))
for chest in d_logic.bk_chests: if len(d_logic.bk_doors) > 0 or len(d_logic.bk_chests) > 1:
add_rule(world.get_location(chest.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))
if world.retro[player]: if world.retro[player]:
for d_name, layout in world.key_layout[player].items(): for d_name, layout in world.key_layout[player].items():
for door in layout.flat_prop: for door in layout.flat_prop:
@@ -1965,6 +1960,36 @@ def add_key_logic_rules(world, player):
add_rule(door.entrance, create_key_rule('Small Key (Universal)', player, 1)) 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 retro_in_hc(spot): def retro_in_hc(spot):
return spot.parent_region.dungeon.name == 'Hyrule Castle' if spot.parent_region.dungeon else False return spot.parent_region.dungeon.name == 'Hyrule Castle' if spot.parent_region.dungeon else False

View File

@@ -1,10 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os import os
import re import re
import operator as op
import subprocess import subprocess
import sys import sys
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from collections import defaultdict from collections import defaultdict
from functools import reduce
def int16_as_bytes(value): 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() 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 = { entrance_offsets = {
'Sanctuary': 0x2, 'Sanctuary': 0x2,
'HC West': 0x3, 'HC West': 0x3,

View File

@@ -2,7 +2,7 @@
"cli": { "cli": {
"yes": "Yes", "yes": "Yes",
"no": "No", "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", "version": "Version",
"seed": "Seed", "seed": "Seed",
"player": "Player", "player": "Player",