Combinatoric approach revised (KLA1)
Backported some fixes
This commit is contained in:
309
BaseClasses.py
309
BaseClasses.py
@@ -458,9 +458,10 @@ 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
|
||||||
|
if not skip_init:
|
||||||
|
self.prog_items = Counter()
|
||||||
self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)}
|
self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)}
|
||||||
self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)}
|
self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)}
|
||||||
self.events = []
|
self.events = []
|
||||||
@@ -469,6 +470,14 @@ class CollectionState(object):
|
|||||||
self.stale = {player: True for player in range(1, parent.players + 1)}
|
self.stale = {player: True for player in range(1, parent.players + 1)}
|
||||||
for item in parent.precollected_items:
|
for item in parent.precollected_items:
|
||||||
self.collect(item, True)
|
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.ghost_keys = Counter()
|
||||||
|
self.dungeon_limits = None
|
||||||
|
|
||||||
def update_reachable_regions(self, player):
|
def update_reachable_regions(self, player):
|
||||||
self.stale[player] = False
|
self.stale[player] = False
|
||||||
@@ -479,66 +488,261 @@ 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)]
|
||||||
|
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 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 self.should_visit(new_region, rrp, crystal_state, player):
|
||||||
bc.pop(connection, None)
|
bc.pop(connection, None)
|
||||||
elif connection.can_reach(self):
|
elif connection.can_reach(self):
|
||||||
|
bc.pop(connection, None)
|
||||||
if new_region.type == RegionType.Dungeon:
|
if new_region.type == RegionType.Dungeon:
|
||||||
new_crystal_state = crystal_state
|
new_crystal_state = crystal_state
|
||||||
for exit in new_region.exits:
|
|
||||||
door = exit.door
|
|
||||||
if door is not None and door.crystal == CrystalBarrier.Either and door.entrance.can_reach(self):
|
|
||||||
new_crystal_state = CrystalBarrier.Either
|
|
||||||
break
|
|
||||||
if new_region in rrp:
|
if new_region in rrp:
|
||||||
new_crystal_state |= rrp[new_region]
|
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)?
|
||||||
|
queue.append((conn, new_crystal_state))
|
||||||
else:
|
else:
|
||||||
new_crystal_state = CrystalBarrier.Orange
|
new_crystal_state = CrystalBarrier.Orange
|
||||||
rrp[new_region] = new_crystal_state
|
rrp[new_region] = new_crystal_state
|
||||||
bc.pop(connection, None)
|
for conn in new_region.exits:
|
||||||
for exit in new_region.exits:
|
bc[conn] = new_crystal_state
|
||||||
bc[exit] = new_crystal_state
|
queue.append((conn, new_crystal_state))
|
||||||
queue.append((exit, 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) and not self.world.retro[player]: # todo: retro
|
||||||
|
door = connection.door
|
||||||
|
dungeon_name = connection.parent_region.dungeon.name # todo: universal
|
||||||
|
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 = self.dungeons_to_check[player][dungeon_name]
|
||||||
|
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():
|
||||||
|
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, done, common_regions, common_bc, common_doors = [], False, {}, {}, set()
|
||||||
|
while not done:
|
||||||
|
terminal_states.clear()
|
||||||
|
while len(child_states) > 0:
|
||||||
|
next_child = child_states.popleft()
|
||||||
|
door_candidates = CollectionState.should_explore_child_state(next_child, dungeon_name, player)
|
||||||
|
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 checklist:
|
||||||
|
child_queue.append(checklist[chosen_door[0]])
|
||||||
|
if chosen_door[1] in checklist:
|
||||||
|
child_queue.append(checklist[chosen_door[1]])
|
||||||
|
else:
|
||||||
|
child_state.opened_doors[player].add(chosen_door)
|
||||||
|
if chosen_door in checklist:
|
||||||
|
child_queue.append(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]
|
||||||
|
self.dungeon_limits = [dungeon_name]
|
||||||
|
child_state.traverse_world(child_queue, rrp_, bc_, player)
|
||||||
|
new_events = child_state.sweep_for_events_once()
|
||||||
|
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
|
||||||
|
visited_opened_doors.add(frozenset(child_state.opened_doors[player]))
|
||||||
|
child_states.append(child_state)
|
||||||
|
else:
|
||||||
|
terminal_states.append(next_child)
|
||||||
|
common_regions, common_doors, first = {}, set(), True
|
||||||
|
for term_state in terminal_states:
|
||||||
|
t_rrp = term_state.reachable_regions[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_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_doors &= {x for x in term_state.opened_doors[player] - self.opened_doors[player]
|
||||||
|
if valid_d_door(x)}
|
||||||
|
done = len(child_states) == 0
|
||||||
|
|
||||||
|
terminal_queue = deque()
|
||||||
|
for door in common_doors:
|
||||||
|
self.opened_doors[player].add(door)
|
||||||
|
if door in checklist:
|
||||||
|
terminal_queue.append(checklist[door])
|
||||||
|
if self.find_door_pair(player, dungeon_name, door) not in self.opened_doors[player]:
|
||||||
|
self.door_counter[player][1][dungeon_name] += 1
|
||||||
|
|
||||||
|
self.dungeon_limits = [dungeon_name]
|
||||||
|
rrp_ = self.reachable_regions[player]
|
||||||
|
bc_ = self.blocked_connections[player]
|
||||||
|
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}
|
||||||
|
for k in missing_regions:
|
||||||
|
rrp[k] = missing_regions[k]
|
||||||
|
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
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def should_explore_child_state(state, dungeon_name, player):
|
||||||
|
small_key_name = dungeon_keys[dungeon_name] # todo: universal
|
||||||
|
key_total = state.prog_items[(small_key_name, player)] + state.ghost_keys[(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] # todo: universal
|
||||||
|
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)}
|
||||||
|
# todo: verify if this isn't copied deep enough
|
||||||
|
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)}
|
||||||
|
ret.ghost_keys = self.ghost_keys.copy()
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def can_reach(self, spot, resolution_hint=None, player=None):
|
def can_reach(self, spot, resolution_hint=None, player=None):
|
||||||
@@ -556,6 +760,19 @@ class CollectionState(object):
|
|||||||
|
|
||||||
return spot.can_reach(self)
|
return spot.can_reach(self)
|
||||||
|
|
||||||
|
def sweep_for_events_once(self, key_only=False, locations=None):
|
||||||
|
if locations is None:
|
||||||
|
locations = self.world.get_filled_locations()
|
||||||
|
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
|
||||||
|
(not key_only or (not self.world.keyshuffle[location.item.player] and location.item.smallkey) or (not self.world.bigkeyshuffle[location.item.player] and location.item.bigkey))
|
||||||
|
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
|
||||||
@@ -603,6 +820,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']
|
||||||
@@ -1806,6 +2030,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__())
|
||||||
|
|
||||||
@@ -2196,6 +2429,21 @@ 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)'
|
||||||
|
}
|
||||||
|
|
||||||
class PotItem(FastEnum):
|
class PotItem(FastEnum):
|
||||||
Nothing = 0x0
|
Nothing = 0x0
|
||||||
@@ -2346,3 +2594,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(Enum):
|
||||||
|
WorstCase = 0
|
||||||
|
AllowSmall = 1
|
||||||
|
Lock = 2
|
||||||
|
|||||||
@@ -1,22 +1,21 @@
|
|||||||
import random
|
import 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):
|
||||||
@@ -212,8 +211,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
|
||||||
@@ -1576,28 +1575,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)
|
||||||
|
|||||||
15
Dungeons.py
15
Dungeons.py
@@ -375,21 +375,6 @@ flexible_starts = {
|
|||||||
'Skull Woods': ['Skull Left Drop', 'Skull Pinball']
|
'Skull Woods': ['Skull Left Drop', 'Skull Pinball']
|
||||||
}
|
}
|
||||||
|
|
||||||
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)'
|
|
||||||
}
|
|
||||||
|
|
||||||
dungeon_bigs = {
|
dungeon_bigs = {
|
||||||
'Hyrule Castle': 'Big Key (Escape)',
|
'Hyrule Castle': 'Big Key (Escape)',
|
||||||
|
|||||||
14
Fill.py
14
Fill.py
@@ -199,6 +199,7 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool =
|
|||||||
|
|
||||||
spot_to_fill = None
|
spot_to_fill = None
|
||||||
|
|
||||||
|
valid_locations = []
|
||||||
for location in locations:
|
for location in locations:
|
||||||
if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there
|
if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there
|
||||||
location.item = item_to_place
|
location.item = item_to_place
|
||||||
@@ -209,11 +210,16 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool =
|
|||||||
if (not single_player_placement or location.player == item_to_place.player)\
|
if (not single_player_placement or location.player == item_to_place.player)\
|
||||||
and location.can_fill(test_state, item_to_place, perform_access_check)\
|
and location.can_fill(test_state, item_to_place, perform_access_check)\
|
||||||
and valid_key_placement(item_to_place, location, itempool 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):
|
||||||
|
# todo: optimization: break instead of cataloging all valid locations
|
||||||
|
if not spot_to_fill:
|
||||||
spot_to_fill = location
|
spot_to_fill = location
|
||||||
break
|
valid_locations.append(location)
|
||||||
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
|
||||||
|
|
||||||
|
logging.getLogger('').debug(f'{item_to_place} valid placement at {len(valid_locations)} locations')
|
||||||
|
|
||||||
if spot_to_fill is None:
|
if spot_to_fill is None:
|
||||||
# we filled all reachable spots. Maybe the game can be beaten anyway?
|
# we filled all reachable spots. Maybe the game can be beaten anyway?
|
||||||
unplaced_items.insert(0, item_to_place)
|
unplaced_items.insert(0, item_to_place)
|
||||||
@@ -250,9 +256,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:
|
||||||
|
|||||||
@@ -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
|
||||||
from Regions import dungeon_events
|
from Regions import dungeon_events
|
||||||
from Dungeons import dungeon_keys, dungeon_bigs
|
from Dungeons import dungeon_bigs
|
||||||
from DungeonGenerator import ExplorationState, special_big_key_doors
|
from DungeonGenerator import ExplorationState, special_big_key_doors
|
||||||
|
|
||||||
|
|
||||||
@@ -25,6 +25,7 @@ class KeyLayout(object):
|
|||||||
self.all_locations = set()
|
self.all_locations = set()
|
||||||
self.item_locations = set()
|
self.item_locations = set()
|
||||||
|
|
||||||
|
self.found_doors = set()
|
||||||
# bk special?
|
# bk special?
|
||||||
# bk required? True if big chests or big doors exists
|
# bk required? True if big chests or big doors exists
|
||||||
|
|
||||||
@@ -54,6 +55,7 @@ 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:
|
||||||
@@ -65,6 +67,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 +90,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):
|
||||||
@@ -209,8 +222,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))
|
||||||
@@ -247,8 +271,9 @@ def analyze_dungeon(key_layout, world, player):
|
|||||||
while len(child_queue) > 0:
|
while len(child_queue) > 0:
|
||||||
child, odd_counter, empty_flag = child_queue.popleft()
|
child, odd_counter, empty_flag = child_queue.popleft()
|
||||||
if not child.bigKey and child not in doors_completed:
|
if not child.bigKey and child not in doors_completed:
|
||||||
best_counter = find_best_counter(child, odd_counter, key_counter, key_layout, world, player, False, empty_flag)
|
best_counter = find_best_counter(child, key_layout, odd_counter, False, empty_flag)
|
||||||
rule = create_rule(best_counter, key_counter, key_layout, world, player)
|
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
|
||||||
@@ -258,7 +283,8 @@ def analyze_dungeon(key_layout, world, player):
|
|||||||
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():
|
||||||
@@ -294,7 +320,7 @@ 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)
|
# 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)
|
||||||
@@ -320,6 +346,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))
|
||||||
@@ -478,7 +505,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 +565,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 +623,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?
|
||||||
@@ -717,7 +763,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 +782,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)
|
||||||
@@ -845,16 +896,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 +913,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):
|
||||||
@@ -935,6 +986,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
|
||||||
@@ -1197,7 +1249,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:
|
||||||
@@ -1381,6 +1433,7 @@ def cnt_avail_big_locations(ttl_locations, state, world, player):
|
|||||||
|
|
||||||
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':
|
||||||
@@ -1403,6 +1456,9 @@ 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)
|
||||||
@@ -1520,11 +1576,11 @@ def find_counter_hint(opened_doors, bk_hint, key_layout):
|
|||||||
|
|
||||||
|
|
||||||
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)
|
||||||
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)
|
||||||
return max_counter
|
return max_counter
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
1
Main.py
1
Main.py
@@ -448,6 +448,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:
|
||||||
|
|||||||
48
Rules.py
48
Rules.py
@@ -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
|
||||||
|
|
||||||
@@ -1939,14 +1939,10 @@ 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):
|
if rule.allow_small:
|
||||||
rule = create_advanced_key_rule(d_logic, player, keys)
|
set_always_allow(rule.small_location, allow_self_locking_small(d_logic, player))
|
||||||
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)
|
||||||
@@ -1954,6 +1950,7 @@ 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))
|
||||||
|
if len(d_logic.bk_doors) > 0 or len(d_logic.bk_chests) > 1:
|
||||||
for chest in d_logic.bk_chests:
|
for chest in d_logic.bk_chests:
|
||||||
add_rule(world.get_location(chest.name, player), create_rule(d_logic.bk_name, player))
|
add_rule(world.get_location(chest.name, player), create_rule(d_logic.bk_name, player))
|
||||||
if world.retro[player]:
|
if world.retro[player]:
|
||||||
@@ -1963,6 +1960,39 @@ 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 allow_self_locking_small(logic, player):
|
||||||
|
return lambda state, item: item.player == player and logic.small_key_name == item.name
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
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
|
||||||
|
|
||||||
|
|||||||
24
Utils.py
24
Utils.py
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user