Merge in combinatoric key logic from keyLogicAttempt1 branch
This commit is contained in:
507
BaseClasses.py
507
BaseClasses.py
@@ -140,6 +140,8 @@ class World(object):
|
||||
set_player_attr('standardize_palettes', 'standardize')
|
||||
set_player_attr('force_fix', {'gt': False, 'sw': False, 'pod': False, 'tr': False})
|
||||
|
||||
set_player_attr('exp_cache', defaultdict(dict))
|
||||
|
||||
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)})'
|
||||
|
||||
@@ -394,6 +396,10 @@ class World(object):
|
||||
def clear_location_cache(self):
|
||||
self._cached_locations = None
|
||||
|
||||
def clear_exp_cache(self):
|
||||
for p in range(1, self.players + 1):
|
||||
self.exp_cache[p].clear()
|
||||
|
||||
def get_unfilled_locations(self, player=None):
|
||||
return [location for location in self.get_locations() if (player is None or location.player == player) and location.item is None]
|
||||
|
||||
@@ -426,10 +432,14 @@ class World(object):
|
||||
else:
|
||||
return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1)))
|
||||
|
||||
def can_beat_game(self, starting_state=None):
|
||||
def can_beat_game(self, starting_state=None, log_error=False):
|
||||
if starting_state:
|
||||
if self.has_beaten_game(starting_state):
|
||||
return True
|
||||
state = starting_state.copy()
|
||||
else:
|
||||
if self.has_beaten_game(self.state):
|
||||
return True
|
||||
state = CollectionState(self)
|
||||
|
||||
if self.has_beaten_game(state):
|
||||
@@ -446,6 +456,9 @@ class World(object):
|
||||
|
||||
if not sphere:
|
||||
# ran out of places and did not finish yet, quit
|
||||
if log_error:
|
||||
missing_locations = ", ".join([x.name for x in prog_locations])
|
||||
logging.getLogger('').error(f'Cannot reach the following locations: {missing_locations}')
|
||||
return False
|
||||
|
||||
for location in sphere:
|
||||
@@ -460,17 +473,25 @@ class World(object):
|
||||
|
||||
class CollectionState(object):
|
||||
|
||||
def __init__(self, parent):
|
||||
self.prog_items = Counter()
|
||||
def __init__(self, parent, skip_init=False):
|
||||
self.world = parent
|
||||
self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)}
|
||||
self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)}
|
||||
self.events = []
|
||||
self.path = {}
|
||||
self.locations_checked = set()
|
||||
self.stale = {player: True for player in range(1, parent.players + 1)}
|
||||
for item in parent.precollected_items:
|
||||
self.collect(item, True)
|
||||
if not skip_init:
|
||||
self.prog_items = Counter()
|
||||
self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)}
|
||||
self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)}
|
||||
self.events = []
|
||||
self.path = {}
|
||||
self.locations_checked = set()
|
||||
self.stale = {player: True for player in range(1, parent.players + 1)}
|
||||
for item in parent.precollected_items:
|
||||
self.collect(item, True)
|
||||
# reached vs. opened in the counter
|
||||
self.door_counter = {player: (Counter(), Counter()) for player in range(1, parent.players + 1)}
|
||||
self.reached_doors = {player: set() for player in range(1, parent.players + 1)}
|
||||
self.opened_doors = {player: set() for player in range(1, parent.players + 1)}
|
||||
self.dungeons_to_check = {player: defaultdict(dict) for player in range(1, parent.players + 1)}
|
||||
self.dungeon_limits = None
|
||||
# self.trace = None
|
||||
|
||||
def update_reachable_regions(self, player):
|
||||
self.stale[player] = False
|
||||
@@ -481,68 +502,386 @@ class CollectionState(object):
|
||||
start = self.world.get_region('Menu', player)
|
||||
if not start in rrp:
|
||||
rrp[start] = CrystalBarrier.Orange
|
||||
for exit in start.exits:
|
||||
bc[exit] = CrystalBarrier.Orange
|
||||
for conn in start.exits:
|
||||
bc[conn] = CrystalBarrier.Orange
|
||||
|
||||
queue = deque(self.blocked_connections[player].items())
|
||||
|
||||
self.traverse_world(queue, rrp, bc, player)
|
||||
unresolved_events = [x for y in self.reachable_regions[player] for x in y.locations
|
||||
if x.event and x.item and (x.item.smallkey or x.item.bigkey or x.item.advancement)
|
||||
and x not in self.locations_checked and x.can_reach(self)]
|
||||
unresolved_events = self._do_not_flood_the_keys(unresolved_events)
|
||||
if len(unresolved_events) == 0:
|
||||
self.check_key_doors_in_dungeons(rrp, player)
|
||||
|
||||
def traverse_world(self, queue, rrp, bc, player):
|
||||
# run BFS on all connections, and keep track of those blocked by missing items
|
||||
while True:
|
||||
try:
|
||||
connection, crystal_state = queue.popleft()
|
||||
new_region = connection.connected_region
|
||||
if new_region is None or new_region in rrp and (new_region.type != RegionType.Dungeon or (rrp[new_region] & crystal_state) == crystal_state):
|
||||
while len(queue) > 0:
|
||||
connection, crystal_state = queue.popleft()
|
||||
new_region = connection.connected_region
|
||||
if not self.should_visit(new_region, rrp, crystal_state, player):
|
||||
if not new_region or not self.dungeon_limits or self.possibly_connected_to_dungeon(new_region, player):
|
||||
bc.pop(connection, None)
|
||||
elif connection.can_reach(self):
|
||||
if new_region.type == RegionType.Dungeon:
|
||||
new_crystal_state = crystal_state
|
||||
for exit in new_region.exits:
|
||||
door = exit.door
|
||||
if door is not None and door.crystal == CrystalBarrier.Either and door.entrance.can_reach(self):
|
||||
new_crystal_state = CrystalBarrier.Either
|
||||
break
|
||||
if new_region in rrp:
|
||||
new_crystal_state |= rrp[new_region]
|
||||
elif connection.can_reach(self):
|
||||
bc.pop(connection, None)
|
||||
if new_region.type == RegionType.Dungeon:
|
||||
new_crystal_state = crystal_state
|
||||
if new_region in rrp:
|
||||
new_crystal_state |= rrp[new_region]
|
||||
|
||||
rrp[new_region] = new_crystal_state
|
||||
|
||||
for exit in new_region.exits:
|
||||
door = exit.door
|
||||
if door is not None and not door.blocked:
|
||||
rrp[new_region] = new_crystal_state
|
||||
for conn in new_region.exits:
|
||||
door = conn.door
|
||||
if door is not None and not door.blocked:
|
||||
if self.valid_crystal(door, new_crystal_state):
|
||||
door_crystal_state = door.crystal if door.crystal else new_crystal_state
|
||||
bc[exit] = door_crystal_state
|
||||
queue.append((exit, door_crystal_state))
|
||||
elif door is None:
|
||||
queue.append((exit, new_crystal_state))
|
||||
else:
|
||||
new_crystal_state = CrystalBarrier.Orange
|
||||
rrp[new_region] = new_crystal_state
|
||||
bc.pop(connection, None)
|
||||
for exit in new_region.exits:
|
||||
bc[exit] = new_crystal_state
|
||||
queue.append((exit, new_crystal_state))
|
||||
bc[conn] = door_crystal_state
|
||||
queue.append((conn, door_crystal_state))
|
||||
elif door is None:
|
||||
# note: no door in dungeon indicates what exactly? (always traversable)?
|
||||
queue.append((conn, new_crystal_state))
|
||||
else:
|
||||
new_crystal_state = CrystalBarrier.Orange
|
||||
rrp[new_region] = new_crystal_state
|
||||
for conn in new_region.exits:
|
||||
bc[conn] = new_crystal_state
|
||||
queue.append((conn, new_crystal_state))
|
||||
|
||||
self.path[new_region] = (new_region.name, self.path.get(connection, None))
|
||||
self.path[new_region] = (new_region.name, self.path.get(connection, None))
|
||||
|
||||
# Retry connections if the new region can unblock them
|
||||
if new_region.name in indirect_connections:
|
||||
new_entrance = self.world.get_entrance(indirect_connections[new_region.name], player)
|
||||
if new_entrance in bc and new_entrance not in queue and new_entrance.parent_region in rrp:
|
||||
queue.append((new_entrance, rrp[new_entrance.parent_region]))
|
||||
except IndexError:
|
||||
break
|
||||
# Retry connections if the new region can unblock them
|
||||
if new_region.name in indirect_connections:
|
||||
new_entrance = self.world.get_entrance(indirect_connections[new_region.name], player)
|
||||
if new_entrance in bc and new_entrance.parent_region in rrp:
|
||||
new_crystal_state = rrp[new_entrance.parent_region]
|
||||
if (new_entrance, new_crystal_state) not in queue:
|
||||
queue.append((new_entrance, new_crystal_state))
|
||||
# else those connections that are not accessible yet
|
||||
if self.is_small_door(connection):
|
||||
door = connection.door
|
||||
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):
|
||||
ret = CollectionState(self.world)
|
||||
ret = CollectionState(self.world, skip_init=True)
|
||||
ret.prog_items = self.prog_items.copy()
|
||||
ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in range(1, self.world.players + 1)}
|
||||
ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in range(1, self.world.players + 1)}
|
||||
ret.events = copy.copy(self.events)
|
||||
ret.path = copy.copy(self.path)
|
||||
ret.locations_checked = copy.copy(self.locations_checked)
|
||||
ret.stale = {player: self.stale[player] for player in range(1, self.world.players + 1)}
|
||||
ret.door_counter = {player: (copy.copy(self.door_counter[player][0]), copy.copy(self.door_counter[player][1]))
|
||||
for player in range(1, self.world.players + 1)}
|
||||
ret.reached_doors = {player: copy.copy(self.reached_doors[player]) for player in range(1, self.world.players + 1)}
|
||||
ret.opened_doors = {player: copy.copy(self.opened_doors[player]) for player in range(1, self.world.players + 1)}
|
||||
ret.dungeons_to_check = {
|
||||
player: defaultdict(dict, {name: copy.copy(checklist)
|
||||
for name, checklist in self.dungeons_to_check[player].items()})
|
||||
for player in range(1, self.world.players + 1)}
|
||||
return ret
|
||||
|
||||
def apply_dungeon_exploration(self, rrp, player, dungeon_name, checklist):
|
||||
bc = self.blocked_connections[player]
|
||||
ec = self.world.exp_cache[player]
|
||||
prog_set = self.reduce_prog_items(player, dungeon_name)
|
||||
exp_key = (prog_set, frozenset(checklist))
|
||||
if dungeon_name in ec and exp_key in ec[dungeon_name]:
|
||||
# apply
|
||||
common_doors, missing_regions, missing_bc, paths = ec[dungeon_name][exp_key]
|
||||
terminal_queue = deque()
|
||||
for door in common_doors:
|
||||
pair = self.find_door_pair(player, dungeon_name, door)
|
||||
if door not in self.reached_doors[player]:
|
||||
self.door_counter[player][0][dungeon_name] += 1
|
||||
self.reached_doors[player].add(door)
|
||||
if pair not in self.reached_doors[player]:
|
||||
self.reached_doors[player].add(pair)
|
||||
self.opened_doors[player].add(door)
|
||||
if door in checklist:
|
||||
terminal_queue.append(checklist[door])
|
||||
if pair not in self.opened_doors[player]:
|
||||
self.door_counter[player][1][dungeon_name] += 1
|
||||
|
||||
self.set_dungeon_limits(player, dungeon_name)
|
||||
rrp_ = self.reachable_regions[player]
|
||||
bc_ = self.blocked_connections[player]
|
||||
for block, crystal in bc_.items():
|
||||
if (block, crystal) not in terminal_queue and self.possibly_connected_to_dungeon(block.connected_region, player):
|
||||
terminal_queue.append((block, crystal))
|
||||
self.traverse_world(terminal_queue, rrp_, bc_, player)
|
||||
self.dungeon_limits = None
|
||||
|
||||
for k in missing_regions:
|
||||
rrp[k] = missing_regions[k]
|
||||
for r, path in paths.items():
|
||||
self.path[r] = path
|
||||
for k in missing_bc:
|
||||
bc[k] = missing_bc[k]
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
def record_dungeon_exploration(self, player, dungeon_name, checklist,
|
||||
common_doors, missing_regions, missing_bc, paths):
|
||||
ec = self.world.exp_cache[player]
|
||||
prog_set = self.reduce_prog_items(player, dungeon_name)
|
||||
exp_key = (prog_set, frozenset(checklist))
|
||||
ec[dungeon_name][exp_key] = (common_doors, missing_regions, missing_bc, paths)
|
||||
|
||||
def reduce_prog_items(self, player, dungeon_name):
|
||||
# todo: possibly could include an analysis of dungeon items req. like Hammer, Hookshot, etc
|
||||
# cross dungeon requirements may be necessary for keysanity - which invalidates the above
|
||||
# todo: universal smalls where needed
|
||||
life_count, bottle_count = 0, 0
|
||||
reduced = Counter()
|
||||
for item, cnt in self.prog_items.items():
|
||||
item_name, item_player = item
|
||||
if item_player == player and self.check_if_progressive(item_name):
|
||||
if item_name.startswith('Bottle'): # I think magic requirements can require multiple bottles
|
||||
bottle_count += cnt
|
||||
elif item_name in ['Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart']:
|
||||
if 'Container' in item_name:
|
||||
life_count += 1
|
||||
elif 'Piece of Heart' == item_name:
|
||||
life_count += .25
|
||||
else:
|
||||
reduced[item] = cnt
|
||||
if bottle_count > 0:
|
||||
reduced[('Bottle', player)] = 1
|
||||
if life_count >= 1:
|
||||
reduced[('Heart Container', player)] = 1
|
||||
return frozenset(reduced.items())
|
||||
|
||||
@staticmethod
|
||||
def check_if_progressive(item_name):
|
||||
return (item_name in
|
||||
['Bow', 'Progressive Bow', 'Progressive Bow (Alt)', 'Book of Mudora', 'Hammer', 'Hookshot',
|
||||
'Magic Mirror', 'Ocarina', 'Pegasus Boots', 'Power Glove', 'Cape', 'Mushroom', 'Shovel',
|
||||
'Lamp', 'Magic Powder', 'Moon Pearl', 'Cane of Somaria', 'Fire Rod', 'Flippers', 'Ice Rod',
|
||||
'Titans Mitts', 'Bombos', 'Ether', 'Quake', 'Master Sword', 'Tempered Sword', 'Fighter Sword',
|
||||
'Golden Sword', 'Progressive Sword', 'Progressive Glove', 'Silver Arrows', 'Green Pendant',
|
||||
'Blue Pendant', 'Red Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5',
|
||||
'Crystal 6', 'Crystal 7', 'Blue Boomerang', 'Red Boomerang', 'Blue Shield', 'Red Shield',
|
||||
'Mirror Shield', 'Progressive Shield', 'Bug Catching Net', 'Cane of Byrna',
|
||||
'Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart', 'Magic Upgrade (1/2)',
|
||||
'Magic Upgrade (1/4)']
|
||||
or item_name.startswith(('Bottle', 'Small Key', 'Big Key')))
|
||||
|
||||
def can_reach(self, spot, resolution_hint=None, player=None):
|
||||
try:
|
||||
spot_type = spot.spot_type
|
||||
@@ -558,6 +897,16 @@ class CollectionState(object):
|
||||
|
||||
return spot.can_reach(self)
|
||||
|
||||
def sweep_for_events_once(self, player):
|
||||
locations = self.world.get_filled_locations(player)
|
||||
checked_locations = set([l for l in locations if l in self.locations_checked])
|
||||
reachable_events = [location for location in locations if location.event and location.can_reach(self)]
|
||||
reachable_events = self._do_not_flood_the_keys(reachable_events)
|
||||
for event in reachable_events:
|
||||
if event not in checked_locations:
|
||||
self.events.append((event.name, event.player))
|
||||
self.collect(event.item, True, event)
|
||||
return len(reachable_events) > len(checked_locations)
|
||||
|
||||
def sweep_for_events(self, key_only=False, locations=None):
|
||||
# this may need improvement
|
||||
@@ -605,6 +954,13 @@ class CollectionState(object):
|
||||
or not self.location_can_be_flooded(flood_location))
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def is_small_door(connection):
|
||||
return connection and connection.door and connection.door.smallKey
|
||||
|
||||
def is_door_open(self, door_name, player):
|
||||
return door_name in self.opened_doors[player]
|
||||
|
||||
@staticmethod
|
||||
def location_can_be_flooded(location):
|
||||
return location.parent_region.name in ['Swamp Trench 1 Alcove', 'Swamp Trench 2 Alcove']
|
||||
@@ -1484,6 +1840,7 @@ class Sector(object):
|
||||
self.entrance_sector = None
|
||||
self.destination_entrance = False
|
||||
self.equations = None
|
||||
self.item_logic = set()
|
||||
|
||||
def region_set(self):
|
||||
if self.r_name_set is None:
|
||||
@@ -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)))
|
||||
|
||||
def can_reach(self, state):
|
||||
if self.parent_region.can_reach(state) and self.access_rule(state):
|
||||
return True
|
||||
return False
|
||||
return self.parent_region.can_reach(state) and self.access_rule(state)
|
||||
|
||||
def forced_big_key(self):
|
||||
if self.forced_item and self.forced_item.bigkey and self.player == self.forced_item.player:
|
||||
@@ -1765,6 +2120,12 @@ class Location(object):
|
||||
world = self.parent_region.world if self.parent_region and self.parent_region.world else None
|
||||
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.name == other.name and self.player == other.player
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.name, self.player))
|
||||
|
||||
|
||||
class Item(object):
|
||||
|
||||
@@ -1808,6 +2169,15 @@ class Item(object):
|
||||
def compass(self):
|
||||
return self.type == 'Compass'
|
||||
|
||||
@property
|
||||
def dungeon(self):
|
||||
if not self.smallkey and not self.bigkey and not self.map and not self.compass:
|
||||
return None
|
||||
item_dungeon = self.name.split('(')[1][:-1]
|
||||
if item_dungeon == 'Escape':
|
||||
item_dungeon = 'Hyrule Castle'
|
||||
return item_dungeon
|
||||
|
||||
def __str__(self):
|
||||
return str(self.__unicode__())
|
||||
|
||||
@@ -2157,7 +2527,7 @@ class Spoiler(object):
|
||||
|
||||
for player in range(1, self.world.players + 1):
|
||||
if self.world.boss_shuffle[player] != 'none':
|
||||
bossmap = self.bosses[player] if self.world.players > 1 else self.bosses
|
||||
bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses
|
||||
outfile.write(f'\n\nBosses ({self.world.get_player_names(player)}):\n\n')
|
||||
outfile.write('\n'.join([f'{x}: {y}' for x, y in bossmap.items() if y not in ['Agahnim', 'Agahnim 2', 'Ganon']]))
|
||||
|
||||
@@ -2200,6 +2570,22 @@ dungeon_names = [
|
||||
'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace', 'Misery Mire', 'Turtle Rock', 'Ganons Tower'
|
||||
]
|
||||
|
||||
dungeon_keys = {
|
||||
'Hyrule Castle': 'Small Key (Escape)',
|
||||
'Eastern Palace': 'Small Key (Eastern Palace)',
|
||||
'Desert Palace': 'Small Key (Desert Palace)',
|
||||
'Tower of Hera': 'Small Key (Tower of Hera)',
|
||||
'Agahnims Tower': 'Small Key (Agahnims Tower)',
|
||||
'Palace of Darkness': 'Small Key (Palace of Darkness)',
|
||||
'Swamp Palace': 'Small Key (Swamp Palace)',
|
||||
'Skull Woods': 'Small Key (Skull Woods)',
|
||||
'Thieves Town': 'Small Key (Thieves Town)',
|
||||
'Ice Palace': 'Small Key (Ice Palace)',
|
||||
'Misery Mire': 'Small Key (Misery Mire)',
|
||||
'Turtle Rock': 'Small Key (Turtle Rock)',
|
||||
'Ganons Tower': 'Small Key (Ganons Tower)',
|
||||
'Universal': 'Small Key (Universal)'
|
||||
}
|
||||
|
||||
class PotItem(FastEnum):
|
||||
Nothing = 0x0
|
||||
@@ -2350,3 +2736,10 @@ class Settings(object):
|
||||
args.enemy_health[p] = r(e_health)[(settings[7] & 0xE0) >> 5]
|
||||
args.enemy_damage[p] = r(e_dmg)[(settings[7] & 0x18) >> 3]
|
||||
args.shufflepots[p] = True if settings[7] & 0x4 else False
|
||||
|
||||
|
||||
@unique
|
||||
class KeyRuleType(FastEnum):
|
||||
WorstCase = 0
|
||||
AllowSmall = 1
|
||||
Lock = 2
|
||||
|
||||
@@ -176,8 +176,8 @@ def place_bosses(world, player):
|
||||
|
||||
if world.boss_shuffle[player] == "simple": # vanilla bosses shuffled
|
||||
bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm']
|
||||
else: # all bosses present, the three duplicates chosen at random
|
||||
bosses = all_bosses + [random.choice(placeable_bosses) for _ in range(3)]
|
||||
else: # all bosses present, the three duplicates chosen at random
|
||||
bosses = all_bosses + random.sample(placeable_bosses, 3)
|
||||
|
||||
logging.getLogger('').debug('Bosses chosen %s', bosses)
|
||||
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import RaceRandom as random
|
||||
from collections import defaultdict, deque
|
||||
import logging
|
||||
import operator as op
|
||||
import time
|
||||
from enum import unique, Flag
|
||||
from typing import DefaultDict, Dict, List
|
||||
|
||||
from functools import reduce
|
||||
from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo
|
||||
from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo, dungeon_keys
|
||||
from Doors import reset_portals
|
||||
from Dungeons import dungeon_regions, region_starts, standard_starts, split_region_starts
|
||||
from Dungeons import dungeon_bigs, dungeon_keys, dungeon_hints
|
||||
from Dungeons import dungeon_bigs, dungeon_hints
|
||||
from Items import ItemFactory
|
||||
from RoomData import DoorKind, PairedDoor, reset_rooms
|
||||
from DungeonGenerator import ExplorationState, convert_regions, generate_dungeon, pre_validate, determine_required_paths, drop_entrances
|
||||
from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances
|
||||
from DungeonGenerator import dungeon_portals, dungeon_drops, GenerationException
|
||||
from KeyDoorShuffle import analyze_dungeon, validate_vanilla_key_logic, build_key_layout, validate_key_layout
|
||||
from KeyDoorShuffle import analyze_dungeon, build_key_layout, validate_key_layout
|
||||
from Utils import ncr, kth_combination
|
||||
|
||||
|
||||
def link_doors(world, player):
|
||||
@@ -102,6 +101,8 @@ def link_doors_main(world, player):
|
||||
connect_portal(portal, world, player)
|
||||
if not world.doorShuffle[player] == 'vanilla':
|
||||
fix_big_key_doors_with_ugly_smalls(world, player)
|
||||
else:
|
||||
unmark_ugly_smalls(world, player)
|
||||
if world.doorShuffle[player] == 'vanilla':
|
||||
for entrance, ext in open_edges:
|
||||
connect_two_way(world, entrance, ext, player)
|
||||
@@ -214,8 +215,8 @@ def vanilla_key_logic(world, player):
|
||||
analyze_dungeon(key_layout, world, player)
|
||||
world.key_logic[player][builder.name] = key_layout.key_logic
|
||||
log_key_logic(builder.name, key_layout.key_logic)
|
||||
if world.shuffle[player] == 'vanilla' and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]:
|
||||
validate_vanilla_key_logic(world, player)
|
||||
# if world.shuffle[player] == 'vanilla' and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]:
|
||||
# validate_vanilla_key_logic(world, player)
|
||||
|
||||
|
||||
# some useful functions
|
||||
@@ -317,6 +318,13 @@ def connect_one_way(world, entrancename, exitname, player):
|
||||
y.dest = x
|
||||
|
||||
|
||||
def unmark_ugly_smalls(world, player):
|
||||
for d in ['Eastern Hint Tile Blocked Path SE', 'Eastern Darkness S', 'Thieves Hallway SE', 'Mire Left Bridge S',
|
||||
'TR Lava Escape SE', 'GT Hidden Spikes SE']:
|
||||
door = world.get_door(d, player)
|
||||
door.smallKey = False
|
||||
|
||||
|
||||
def fix_big_key_doors_with_ugly_smalls(world, player):
|
||||
remove_ugly_small_key_doors(world, player)
|
||||
unpair_big_key_doors(world, player)
|
||||
@@ -503,6 +511,9 @@ def analyze_portals(world, player):
|
||||
raise Exception('please inspect this case')
|
||||
if len(reachable_portals) == 1:
|
||||
info.sole_entrance = reachable_portals[0]
|
||||
if world.intensity[player] < 2 and world.doorShuffle[player] == 'basic' and dungeon == 'Desert Palace':
|
||||
if len(inaccessible_portals) == 1 and inaccessible_portals[0] == 'Desert Back':
|
||||
info.required_passage.clear() # can't make a passage at this intensity level, something else must exit
|
||||
info_map[dungeon] = info
|
||||
|
||||
for dungeon, info in info_map.items():
|
||||
@@ -1104,9 +1115,9 @@ def assign_cross_keys(dungeon_builders, world, player):
|
||||
logger.debug('Cross Dungeon: Keys unable to assign in pool %s', remaining)
|
||||
|
||||
# Last Step: Adjust Small Key Dungeon Pool
|
||||
if not world.retro[player]:
|
||||
for name, builder in dungeon_builders.items():
|
||||
reassign_key_doors(builder, world, player)
|
||||
for name, builder in dungeon_builders.items():
|
||||
reassign_key_doors(builder, world, player)
|
||||
if not world.retro[player]:
|
||||
log_key_logic(builder.name, world.key_logic[player][builder.name])
|
||||
actual_chest_keys = max(builder.key_doors_num - builder.key_drop_cnt, 0)
|
||||
dungeon = world.get_dungeon(name, player)
|
||||
@@ -1580,28 +1591,6 @@ def find_key_door_candidates(region, checked, world, player):
|
||||
return candidates, checked_doors
|
||||
|
||||
|
||||
def kth_combination(k, l, r):
|
||||
if r == 0:
|
||||
return []
|
||||
elif len(l) == r:
|
||||
return l
|
||||
else:
|
||||
i = ncr(len(l)-1, r-1)
|
||||
if k < i:
|
||||
return l[0:1] + kth_combination(k, l[1:], r-1)
|
||||
else:
|
||||
return kth_combination(k-i, l[1:], r)
|
||||
|
||||
|
||||
def ncr(n, r):
|
||||
if r == 0:
|
||||
return 1
|
||||
r = min(r, n-r)
|
||||
numerator = reduce(op.mul, range(n, n-r, -1), 1)
|
||||
denominator = reduce(op.mul, range(1, r+1), 1)
|
||||
return numerator / denominator
|
||||
|
||||
|
||||
def reassign_key_doors(builder, world, player):
|
||||
logger = logging.getLogger('')
|
||||
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'):
|
||||
queue.append(connect)
|
||||
world.inaccessible_regions[player].extend([r.name for r in all_regions.difference(visited_regions) if valid_inaccessible_region(r)])
|
||||
if world.mode[player] == 'inverted':
|
||||
ledge = world.get_region('Hyrule Castle Ledge', 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.debug('Inaccessible Regions:')
|
||||
for r in world.inaccessible_regions[player]:
|
||||
|
||||
@@ -39,14 +39,15 @@ def pre_validate(builder, entrance_region_names, split_dungeon, world, player):
|
||||
proposed_map = {}
|
||||
doors_to_connect = {}
|
||||
all_regions = set()
|
||||
bk_needed = False
|
||||
bk_special = False
|
||||
for sector in builder.sectors:
|
||||
for door in sector.outstanding_doors:
|
||||
doors_to_connect[door.name] = door
|
||||
all_regions.update(sector.regions)
|
||||
bk_needed = bk_needed or determine_if_bk_needed(sector, split_dungeon, world, player)
|
||||
bk_special = bk_special or check_for_special(sector)
|
||||
bk_special |= check_for_special(sector.regions)
|
||||
bk_needed = False
|
||||
for sector in builder.sectors:
|
||||
bk_needed |= determine_if_bk_needed(sector, split_dungeon, bk_special, world, player)
|
||||
paths = determine_paths_for_dungeon(world, player, all_regions, builder.name)
|
||||
dungeon, hangers, hooks = gen_dungeon_info(builder.name, builder.sectors, entrance_regions, all_regions,
|
||||
proposed_map, doors_to_connect, bk_needed, bk_special, world, player)
|
||||
@@ -101,19 +102,27 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon
|
||||
excluded = {}
|
||||
for region in entrance_regions:
|
||||
portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None)
|
||||
if portal and portal.destination:
|
||||
excluded[region] = None
|
||||
if portal:
|
||||
if portal.destination:
|
||||
excluded[region] = None
|
||||
elif len(entrance_regions) > 1:
|
||||
p_region = portal.door.entrance.connected_region
|
||||
access_region = next(x.parent_region for x in p_region.entrances
|
||||
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld])
|
||||
if access_region.name in world.inaccessible_regions[player]:
|
||||
excluded[region] = None
|
||||
entrance_regions = [x for x in entrance_regions if x not in excluded.keys()]
|
||||
doors_to_connect = {}
|
||||
all_regions = set()
|
||||
bk_needed = False
|
||||
bk_special = False
|
||||
for sector in builder.sectors:
|
||||
for door in sector.outstanding_doors:
|
||||
doors_to_connect[door.name] = door
|
||||
all_regions.update(sector.regions)
|
||||
bk_needed = bk_needed or determine_if_bk_needed(sector, split_dungeon, world, player)
|
||||
bk_special = bk_special or check_for_special(sector)
|
||||
bk_special |= check_for_special(sector.regions)
|
||||
bk_needed = False
|
||||
for sector in builder.sectors:
|
||||
bk_needed |= determine_if_bk_needed(sector, split_dungeon, bk_special, world, player)
|
||||
proposed_map = {}
|
||||
choices_master = [[]]
|
||||
depth = 0
|
||||
@@ -187,8 +196,8 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon
|
||||
return proposed_map
|
||||
|
||||
|
||||
def determine_if_bk_needed(sector, split_dungeon, world, player):
|
||||
if not split_dungeon:
|
||||
def determine_if_bk_needed(sector, split_dungeon, bk_special, world, player):
|
||||
if not split_dungeon or bk_special:
|
||||
for region in sector.regions:
|
||||
for ext in region.exits:
|
||||
door = world.check_for_door(ext.name, player)
|
||||
@@ -197,8 +206,8 @@ def determine_if_bk_needed(sector, split_dungeon, world, player):
|
||||
return False
|
||||
|
||||
|
||||
def check_for_special(sector):
|
||||
for region in sector.regions:
|
||||
def check_for_special(regions):
|
||||
for region in regions:
|
||||
for loc in region.locations:
|
||||
if loc.forced_big_key():
|
||||
return True
|
||||
@@ -417,6 +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
|
||||
if not world.bigkeyshuffle[player]:
|
||||
possible_bks = len(dungeon['Origin'].possible_bk_locations)
|
||||
if bk_special and check_for_special(dungeon['Origin'].visited_regions):
|
||||
possible_bks = 1
|
||||
true_origin_hooks = [x for x in dungeon['Origin'].hooks.keys() if not x.bigKey or possible_bks > 0 or not bk_needed]
|
||||
if len(true_origin_hooks) == 0 and len(proposed_map.keys()) < len(doors_to_connect):
|
||||
return False
|
||||
@@ -450,7 +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)
|
||||
for piece in dungeon.values():
|
||||
all_visited.update(piece.visited_regions)
|
||||
if not bk_possible and len(piece.possible_bk_locations) > 0:
|
||||
if ((not bk_possible and len(piece.possible_bk_locations) > 0) or
|
||||
(bk_special and check_for_special(piece.visited_regions))):
|
||||
bk_possible = True
|
||||
if len(all_regions.difference(all_visited)) > 0:
|
||||
return False
|
||||
@@ -807,6 +819,10 @@ class ExplorationState(object):
|
||||
self.dungeon = dungeon
|
||||
self.pinball_used = False
|
||||
|
||||
self.prize_door_set = {}
|
||||
self.prize_doors = []
|
||||
self.prize_doors_opened = False
|
||||
|
||||
def copy(self):
|
||||
ret = ExplorationState(dungeon=self.dungeon)
|
||||
ret.unattached_doors = list(self.unattached_doors)
|
||||
@@ -833,6 +849,10 @@ class ExplorationState(object):
|
||||
ret.non_door_entrances = list(self.non_door_entrances)
|
||||
ret.dungeon = self.dungeon
|
||||
ret.pinball_used = self.pinball_used
|
||||
|
||||
ret.prize_door_set = dict(self.prize_door_set)
|
||||
ret.prize_doors = list(self.prize_doors)
|
||||
ret.prize_doors_opened = self.prize_doors_opened
|
||||
return ret
|
||||
|
||||
def next_avail_door(self):
|
||||
@@ -842,6 +862,8 @@ class ExplorationState(object):
|
||||
return exp_door
|
||||
|
||||
def visit_region(self, region, key_region=None, key_checks=False, bk_Flag=False):
|
||||
if region.type != RegionType.Dungeon:
|
||||
self.crystal = CrystalBarrier.Orange
|
||||
if self.crystal == CrystalBarrier.Either:
|
||||
if region not in self.visited_blue:
|
||||
self.visited_blue.append(region)
|
||||
@@ -868,6 +890,8 @@ class ExplorationState(object):
|
||||
if location.name in flooded_keys_reverse.keys() and self.location_found(
|
||||
flooded_keys_reverse[location.name]):
|
||||
self.perform_event(flooded_keys_reverse[location.name], key_region)
|
||||
if '- Prize' in location.name:
|
||||
self.prize_received = True
|
||||
|
||||
def flooded_key_check(self, location):
|
||||
if location.name not in flooded_keys.keys():
|
||||
@@ -1103,9 +1127,9 @@ def valid_region_to_explore_in_regions(region, all_regions, world, player):
|
||||
def valid_region_to_explore(region, name, world, player):
|
||||
if region is None:
|
||||
return False
|
||||
return (region.type == RegionType.Dungeon and region.dungeon.name in name)\
|
||||
or region.name in world.inaccessible_regions[player]\
|
||||
or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard')
|
||||
return ((region.type == RegionType.Dungeon and region.dungeon and region.dungeon.name in name)
|
||||
or region.name in world.inaccessible_regions[player]
|
||||
or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard'))
|
||||
|
||||
|
||||
def get_doors(world, region, player):
|
||||
@@ -1291,13 +1315,16 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
|
||||
sanc_builder = random.choice(lw_builders)
|
||||
assign_sector(sanc, sanc_builder, candidate_sectors, global_pole)
|
||||
|
||||
bow_sectors, retro_std_flag = {}, world.retro[player] and world.mode[player] == 'standard'
|
||||
free_location_sectors = {}
|
||||
crystal_switches = {}
|
||||
crystal_barriers = {}
|
||||
polarized_sectors = {}
|
||||
neutral_sectors = {}
|
||||
for sector in candidate_sectors:
|
||||
if sector.chest_locations > 0:
|
||||
if retro_std_flag and 'Bow' in sector.item_logic: # these need to be distributed outside of HC
|
||||
bow_sectors[sector] = None
|
||||
elif sector.chest_locations > 0:
|
||||
free_location_sectors[sector] = None
|
||||
elif sector.c_switch:
|
||||
crystal_switches[sector] = None
|
||||
@@ -1307,6 +1334,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
|
||||
neutral_sectors[sector] = None
|
||||
else:
|
||||
polarized_sectors[sector] = None
|
||||
if bow_sectors:
|
||||
assign_bow_sectors(dungeon_map, bow_sectors, global_pole)
|
||||
assign_location_sectors(dungeon_map, free_location_sectors, global_pole)
|
||||
leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole)
|
||||
ensure_crystal_switches_reachable(dungeon_map, leftover, polarized_sectors, crystal_barriers, global_pole)
|
||||
@@ -1472,6 +1501,9 @@ def define_sector_features(sectors):
|
||||
sector.blue_barrier = True
|
||||
if door.bigKey:
|
||||
sector.bk_required = True
|
||||
if region.name in ['PoD Mimics 2', 'PoD Bow Statue Right', 'PoD Mimics 1', 'GT Mimics 1', 'GT Mimics 2',
|
||||
'Eastern Single Eyegore', 'Eastern Duo Eyegores']:
|
||||
sector.item_logic.add('Bow')
|
||||
|
||||
|
||||
def assign_sector(sector, dungeon, candidate_sectors, global_pole):
|
||||
@@ -1522,6 +1554,19 @@ def find_sector(r_name, sectors):
|
||||
return None
|
||||
|
||||
|
||||
def assign_bow_sectors(dungeon_map, bow_sectors, global_pole):
|
||||
sector_list = list(bow_sectors)
|
||||
random.shuffle(sector_list)
|
||||
population = []
|
||||
for name in dungeon_map:
|
||||
if name != 'Hyrule Castle':
|
||||
population.append(name)
|
||||
choices = random.choices(population, k=len(sector_list))
|
||||
for i, choice in enumerate(choices):
|
||||
builder = dungeon_map[choice]
|
||||
assign_sector(sector_list[i], builder, bow_sectors, global_pole)
|
||||
|
||||
|
||||
def assign_location_sectors(dungeon_map, free_location_sectors, global_pole):
|
||||
valid = False
|
||||
choices = None
|
||||
@@ -3275,6 +3320,7 @@ def check_for_valid_layout(builder, sector_list, builder_info):
|
||||
possible_regions.add(portal.door.entrance.parent_region.name)
|
||||
if builder.name in dungeon_drops.keys():
|
||||
possible_regions.update(dungeon_drops[builder.name])
|
||||
independents = find_independent_entrances(possible_regions, world, player)
|
||||
for name, split_build in builder.split_dungeon_map.items():
|
||||
name_bits = name.split(" ")
|
||||
orig_name = " ".join(name_bits[:-1])
|
||||
@@ -3287,7 +3333,8 @@ def check_for_valid_layout(builder, sector_list, builder_info):
|
||||
if r_name not in entrance_regions:
|
||||
entrance_regions.append(r_name)
|
||||
# entrance_regions = [x for x in entrance_regions if x not in split_check_entrance_invalid]
|
||||
proposal = generate_dungeon_find_proposal(split_build, entrance_regions, True, world, player)
|
||||
split = any(x for x in independents if x not in entrance_regions)
|
||||
proposal = generate_dungeon_find_proposal(split_build, entrance_regions, split, world, player)
|
||||
# record split proposals
|
||||
builder.valid_proposal[name] = proposal
|
||||
builder.exception_list = list(sector_list)
|
||||
@@ -3302,6 +3349,32 @@ def check_for_valid_layout(builder, sector_list, builder_info):
|
||||
return len(unreached_doors) == 0, unreached_doors
|
||||
|
||||
|
||||
def find_independent_entrances(entrance_regions, world, player):
|
||||
independents = set()
|
||||
for region in entrance_regions:
|
||||
portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region.name == region), None)
|
||||
if portal:
|
||||
if portal.destination:
|
||||
continue
|
||||
elif len(entrance_regions) > 1:
|
||||
p_region = portal.door.entrance.connected_region
|
||||
access_region = next(x.parent_region for x in p_region.entrances
|
||||
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld])
|
||||
if access_region.name in world.inaccessible_regions[player]:
|
||||
continue
|
||||
else:
|
||||
r = world.get_region(region, player)
|
||||
access_region = next(x.parent_region for x in r.entrances
|
||||
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]
|
||||
or x.parent_region.name == 'Sewer Drop')
|
||||
if access_region.name == 'Sewer Drop':
|
||||
access_region = next(x.parent_region for x in access_region.entrances)
|
||||
if access_region.name in world.inaccessible_regions[player]:
|
||||
continue
|
||||
independents.add(region)
|
||||
return independents
|
||||
|
||||
|
||||
def resolve_equations(builder, sector_list):
|
||||
unreached_doors = defaultdict(list)
|
||||
equations = {x: y for x, y in copy_door_equations(builder, sector_list).items() if len(y) > 0}
|
||||
|
||||
20
Dungeons.py
20
Dungeons.py
@@ -375,13 +375,6 @@ flexible_starts = {
|
||||
'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 = {
|
||||
'Hyrule Castle': 'Small Key (Escape)',
|
||||
'Eastern Palace': 'Small Key (Eastern Palace)',
|
||||
@@ -414,6 +407,19 @@ dungeon_bigs = {
|
||||
'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 = {
|
||||
'Hyrule Castle': 'in Hyrule Castle',
|
||||
'Eastern Palace': 'in Eastern Palace',
|
||||
|
||||
@@ -2120,10 +2120,11 @@ def connect_doors(world, doors, targets, player):
|
||||
"""This works inplace"""
|
||||
random.shuffle(doors)
|
||||
random.shuffle(targets)
|
||||
while doors:
|
||||
door = doors.pop()
|
||||
target = targets.pop()
|
||||
placing = min(len(doors), len(targets))
|
||||
for door, target in zip(doors, targets):
|
||||
connect_entrance(world, door, target, player)
|
||||
doors[:] = doors[placing:]
|
||||
targets[:] = targets[placing:]
|
||||
|
||||
|
||||
def skull_woods_shuffle(world, player):
|
||||
|
||||
173
Fill.py
173
Fill.py
@@ -1,4 +1,6 @@
|
||||
import RaceRandom as random
|
||||
import collections
|
||||
import itertools
|
||||
import logging
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
@@ -211,7 +213,7 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool =
|
||||
and valid_key_placement(item_to_place, location, itempool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world):
|
||||
spot_to_fill = location
|
||||
break
|
||||
elif item_to_place.smallkey or item_to_place.bigkey:
|
||||
if item_to_place.smallkey or item_to_place.bigkey:
|
||||
location.item = None
|
||||
|
||||
if spot_to_fill is None:
|
||||
@@ -221,7 +223,10 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool =
|
||||
if world.accessibility[item_to_place.player] != 'none':
|
||||
logging.getLogger('').warning('Not all items placed. Game beatable anyway. (Could not place %s)' % item_to_place)
|
||||
continue
|
||||
raise FillError('No more spots to place %s' % item_to_place)
|
||||
spot_to_fill = last_ditch_placement(item_to_place, locations, world, maximum_exploration_state,
|
||||
base_state, itempool, keys_in_itempool, single_player_placement)
|
||||
if spot_to_fill is None:
|
||||
raise FillError('No more spots to place %s' % item_to_place)
|
||||
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
track_outside_keys(item_to_place, spot_to_fill, world)
|
||||
@@ -250,9 +255,7 @@ def valid_key_placement(item, location, itempool, world):
|
||||
def track_outside_keys(item, location, world):
|
||||
if not item.smallkey:
|
||||
return
|
||||
item_dungeon = item.name.split('(')[1][:-1]
|
||||
if item_dungeon == 'Escape':
|
||||
item_dungeon = 'Hyrule Castle'
|
||||
item_dungeon = item.dungeon
|
||||
if location.player == item.player:
|
||||
loc_dungeon = location.parent_region.dungeon
|
||||
if loc_dungeon and loc_dungeon.name == item_dungeon:
|
||||
@@ -260,6 +263,80 @@ def track_outside_keys(item, location, world):
|
||||
world.key_logic[item.player][item_dungeon].outside_keys += 1
|
||||
|
||||
|
||||
def last_ditch_placement(item_to_place, locations, world, state, base_state, itempool,
|
||||
keys_in_itempool=None, single_player_placement=False):
|
||||
def location_preference(loc):
|
||||
if not loc.item.advancement:
|
||||
return 1
|
||||
if loc.item.type and loc.item.type != 'Sword':
|
||||
if loc.item.type in ['Map', 'Compass']:
|
||||
return 2
|
||||
else:
|
||||
return 3
|
||||
return 4
|
||||
|
||||
if item_to_place.type == 'Crystal':
|
||||
possible_swaps = [x for x in state.locations_checked if x.item.type == 'Crystal']
|
||||
else:
|
||||
possible_swaps = [x for x in state.locations_checked
|
||||
if x.item.type not in ['Event', 'Crystal'] and not x.forced_item]
|
||||
swap_locations = sorted(possible_swaps, key=location_preference)
|
||||
|
||||
for location in swap_locations:
|
||||
old_item = location.item
|
||||
new_pool = list(itempool) + [old_item]
|
||||
new_spot = find_spot_for_item(item_to_place, [location], world, base_state, new_pool,
|
||||
keys_in_itempool, single_player_placement)
|
||||
if new_spot:
|
||||
restore_item = new_spot.item
|
||||
new_spot.item = item_to_place
|
||||
swap_spot = find_spot_for_item(old_item, locations, world, base_state, itempool,
|
||||
keys_in_itempool, single_player_placement)
|
||||
if swap_spot:
|
||||
logging.getLogger('').debug(f'Swapping {old_item} for {item_to_place}')
|
||||
world.push_item(swap_spot, old_item, False)
|
||||
swap_spot.event = True
|
||||
locations.remove(swap_spot)
|
||||
locations.append(new_spot)
|
||||
return new_spot
|
||||
else:
|
||||
new_spot.item = restore_item
|
||||
else:
|
||||
location.item = old_item
|
||||
return None
|
||||
|
||||
|
||||
def find_spot_for_item(item_to_place, locations, world, base_state, pool,
|
||||
keys_in_itempool=None, single_player_placement=False):
|
||||
def sweep_from_pool():
|
||||
new_state = base_state.copy()
|
||||
for item in pool:
|
||||
new_state.collect(item, True)
|
||||
new_state.sweep_for_events()
|
||||
return new_state
|
||||
for location in locations:
|
||||
maximum_exploration_state = sweep_from_pool()
|
||||
perform_access_check = True
|
||||
old_item = None
|
||||
if world.accessibility[item_to_place.player] == 'none':
|
||||
perform_access_check = not world.has_beaten_game(maximum_exploration_state, item_to_place.player) if single_player_placement else not world.has_beaten_game(maximum_exploration_state)
|
||||
|
||||
if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there
|
||||
old_item = location.item
|
||||
location.item = item_to_place
|
||||
test_state = maximum_exploration_state.copy()
|
||||
test_state.stale[item_to_place.player] = True
|
||||
else:
|
||||
test_state = maximum_exploration_state
|
||||
if (not single_player_placement or location.player == item_to_place.player) \
|
||||
and location.can_fill(test_state, item_to_place, perform_access_check) \
|
||||
and valid_key_placement(item_to_place, location, pool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world):
|
||||
return location
|
||||
if item_to_place.smallkey or item_to_place.bigkey:
|
||||
location.item = old_item
|
||||
return None
|
||||
|
||||
|
||||
def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None):
|
||||
# If not passed in, then get a shuffled list of locations to fill in
|
||||
if not fill_locations:
|
||||
@@ -420,9 +497,8 @@ def sell_keys(world, player):
|
||||
|
||||
def balance_multiworld_progression(world):
|
||||
state = CollectionState(world)
|
||||
checked_locations = []
|
||||
unchecked_locations = world.get_locations().copy()
|
||||
random.shuffle(unchecked_locations)
|
||||
checked_locations = set()
|
||||
unchecked_locations = set(world.get_locations())
|
||||
|
||||
reachable_locations_count = {}
|
||||
for player in range(1, world.players + 1):
|
||||
@@ -430,7 +506,7 @@ def balance_multiworld_progression(world):
|
||||
|
||||
def get_sphere_locations(sphere_state, locations):
|
||||
sphere_state.sweep_for_events(key_only=True, locations=locations)
|
||||
return [loc for loc in locations if sphere_state.can_reach(loc) and sphere_state.not_flooding_a_key(sphere_state.world, loc)]
|
||||
return {loc for loc in locations if sphere_state.can_reach(loc) and sphere_state.not_flooding_a_key(sphere_state.world, loc)}
|
||||
|
||||
while True:
|
||||
sphere_locations = get_sphere_locations(state, unchecked_locations)
|
||||
@@ -441,38 +517,42 @@ def balance_multiworld_progression(world):
|
||||
if checked_locations:
|
||||
threshold = max(reachable_locations_count.values()) - 20
|
||||
|
||||
balancing_players = [player for player, reachables in reachable_locations_count.items() if reachables < threshold]
|
||||
if balancing_players is not None and len(balancing_players) > 0:
|
||||
balancing_players = {player for player, reachables in reachable_locations_count.items() if reachables < threshold}
|
||||
if balancing_players:
|
||||
balancing_state = state.copy()
|
||||
balancing_unchecked_locations = unchecked_locations.copy()
|
||||
balancing_reachables = reachable_locations_count.copy()
|
||||
balancing_sphere = sphere_locations.copy()
|
||||
candidate_items = []
|
||||
candidate_items = collections.defaultdict(set)
|
||||
while True:
|
||||
for location in balancing_sphere:
|
||||
if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
|
||||
balancing_state.collect(location.item, True, location)
|
||||
if location.item.player in balancing_players and not location.locked:
|
||||
candidate_items.append(location)
|
||||
player = location.item.player
|
||||
if player in balancing_players and not location.locked and location.player != player:
|
||||
candidate_items[player].add(location)
|
||||
balancing_sphere = get_sphere_locations(balancing_state, balancing_unchecked_locations)
|
||||
for location in balancing_sphere:
|
||||
balancing_unchecked_locations.remove(location)
|
||||
balancing_reachables[location.player] += 1
|
||||
if world.has_beaten_game(balancing_state) or all([reachables >= threshold for reachables in balancing_reachables.values()]):
|
||||
if world.has_beaten_game(balancing_state) or all(reachables >= threshold for reachables in balancing_reachables.values()):
|
||||
break
|
||||
elif not balancing_sphere:
|
||||
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
|
||||
|
||||
unlocked_locations = [l for l in unchecked_locations if l not in balancing_unchecked_locations]
|
||||
unlocked_locations = collections.defaultdict(set)
|
||||
for l in unchecked_locations:
|
||||
if l not in balancing_unchecked_locations:
|
||||
unlocked_locations[l.player].add(l)
|
||||
items_to_replace = []
|
||||
for player in balancing_players:
|
||||
locations_to_test = [l for l in unlocked_locations if l.player == player]
|
||||
# only replace items that end up in another player's world
|
||||
items_to_test = [l for l in candidate_items if l.item.player == player and l.player != player]
|
||||
locations_to_test = unlocked_locations[player]
|
||||
items_to_test = candidate_items[player]
|
||||
while items_to_test:
|
||||
testing = items_to_test.pop()
|
||||
reducing_state = state.copy()
|
||||
for location in [*[l for l in items_to_replace if l.item.player == player], *items_to_test]:
|
||||
for location in itertools.chain((l for l in items_to_replace if l.item.player == player),
|
||||
items_to_test):
|
||||
reducing_state.collect(location.item, True, location)
|
||||
|
||||
reducing_state.sweep_for_events(locations=locations_to_test)
|
||||
@@ -486,33 +566,44 @@ def balance_multiworld_progression(world):
|
||||
items_to_replace.append(testing)
|
||||
|
||||
replaced_items = False
|
||||
replacement_locations = [l for l in checked_locations if not l.event and not l.locked]
|
||||
# sort then shuffle to maintain deterministic behaviour,
|
||||
# while allowing use of set for better algorithm growth behaviour elsewhere
|
||||
replacement_locations = sorted((l for l in checked_locations if not l.event and not l.locked),
|
||||
key=lambda loc: (loc.name, loc.player))
|
||||
random.shuffle(replacement_locations)
|
||||
items_to_replace.sort(key=lambda item: (item.name, item.player))
|
||||
random.shuffle(items_to_replace)
|
||||
while replacement_locations and items_to_replace:
|
||||
new_location = replacement_locations.pop()
|
||||
old_location = items_to_replace.pop()
|
||||
for new_location in replacement_locations:
|
||||
if (new_location.can_fill(state, old_location.item, False) and
|
||||
old_location.can_fill(state, new_location.item, False)):
|
||||
replacement_locations.remove(new_location)
|
||||
new_location.item, old_location.item = old_location.item, new_location.item
|
||||
if world.shopsanity[new_location.player]:
|
||||
check_shop_swap(new_location)
|
||||
if world.shopsanity[old_location.player]:
|
||||
check_shop_swap(old_location)
|
||||
new_location.event, old_location.event = True, False
|
||||
logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, "
|
||||
f"displacing {old_location.item} into {old_location}")
|
||||
state.collect(new_location.item, True, new_location)
|
||||
replaced_items = True
|
||||
break
|
||||
else:
|
||||
logging.warning(f"Could not Progression Balance {old_location.item}")
|
||||
|
||||
while not new_location.can_fill(state, old_location.item, False) or (new_location.item and not old_location.can_fill(state, new_location.item, False)):
|
||||
replacement_locations.insert(0, new_location)
|
||||
new_location = replacement_locations.pop()
|
||||
|
||||
new_location.item, old_location.item = old_location.item, new_location.item
|
||||
if world.shopsanity[new_location.player]:
|
||||
check_shop_swap(new_location)
|
||||
if world.shopsanity[old_location.player]:
|
||||
check_shop_swap(old_location)
|
||||
new_location.event, old_location.event = True, False
|
||||
state.collect(new_location.item, True, new_location)
|
||||
replaced_items = True
|
||||
if replaced_items:
|
||||
for location in get_sphere_locations(state, [l for l in unlocked_locations if l.player in balancing_players]):
|
||||
unlocked = {fresh for player in balancing_players for fresh in unlocked_locations[player]}
|
||||
for location in get_sphere_locations(state, unlocked):
|
||||
unchecked_locations.remove(location)
|
||||
reachable_locations_count[location.player] += 1
|
||||
sphere_locations.append(location)
|
||||
sphere_locations.add(location)
|
||||
|
||||
for location in sphere_locations:
|
||||
if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
|
||||
state.collect(location.item, True, location)
|
||||
checked_locations.extend(sphere_locations)
|
||||
checked_locations |= sphere_locations
|
||||
|
||||
if world.has_beaten_game(state):
|
||||
break
|
||||
@@ -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]:
|
||||
wallet[player] += income
|
||||
rooms_visited[player].add(room)
|
||||
if checked_locations:
|
||||
if checked_locations or len(unchecked_locations) == 0:
|
||||
if world.has_beaten_game(state):
|
||||
done = True
|
||||
continue
|
||||
@@ -661,11 +752,11 @@ def balance_money_progression(world):
|
||||
solvent = set()
|
||||
insolvent = set()
|
||||
for player in range(1, world.players+1):
|
||||
if wallet[player] >= sphere_costs[player] > 0:
|
||||
if wallet[player] >= sphere_costs[player] >= 0:
|
||||
solvent.add(player)
|
||||
if sphere_costs[player] > 0 and sphere_costs[player] > wallet[player]:
|
||||
insolvent.add(player)
|
||||
if len(solvent) == 0:
|
||||
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])
|
||||
difference = sphere_costs[target_player]-wallet[target_player]
|
||||
logger.debug(f'Money balancing needed: Player {target_player} short {difference}')
|
||||
@@ -705,7 +796,7 @@ def balance_money_progression(world):
|
||||
for player in solvent:
|
||||
wallet[player] -= sphere_costs[player]
|
||||
for location in locked_by_money[player]:
|
||||
if location == 'Kiki':
|
||||
if isinstance(location, str) and location == 'Kiki':
|
||||
kiki_paid[player] = True
|
||||
else:
|
||||
state.collect(location.item, True, location)
|
||||
|
||||
@@ -347,11 +347,11 @@ def generate_itempool(world, player):
|
||||
# rather than making all hearts/heart pieces progression items (which slows down generation considerably)
|
||||
# We mark one random heart container as an advancement item (or 4 heart pieces in expert mode)
|
||||
if world.difficulty[player] in ['normal', 'hard'] and not (world.custom and world.customitemarray[player]["heartcontainer"] == 0):
|
||||
[item for item in items if item.name == 'Boss Heart Container'][0].advancement = True
|
||||
next(item for item in items if item.name == 'Boss Heart Container').advancement = True
|
||||
elif world.difficulty[player] in ['expert'] and not (world.custom and world.customitemarray[player]["heartpiece"] < 4):
|
||||
adv_heart_pieces = [item for item in items if item.name == 'Piece of Heart'][0:4]
|
||||
for hp in adv_heart_pieces:
|
||||
hp.advancement = True
|
||||
adv_heart_pieces = (item for item in items if item.name == 'Piece of Heart')
|
||||
for i in range(4):
|
||||
next(adv_heart_pieces).advancement = True
|
||||
|
||||
beeweights = {'0': {None: 100},
|
||||
'1': {None: 75, 'trap': 25},
|
||||
|
||||
@@ -2,9 +2,9 @@ import itertools
|
||||
import logging
|
||||
from collections import defaultdict, deque
|
||||
|
||||
from BaseClasses import DoorType
|
||||
from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType
|
||||
from Regions import dungeon_events
|
||||
from Dungeons import dungeon_keys, dungeon_bigs, default_key_counts
|
||||
from Dungeons import dungeon_keys, dungeon_bigs, dungeon_prize
|
||||
from DungeonGenerator import ExplorationState, special_big_key_doors
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ class KeyLayout(object):
|
||||
self.all_locations = set()
|
||||
self.item_locations = set()
|
||||
|
||||
self.found_doors = set()
|
||||
self.prize_relevant = False
|
||||
# bk special?
|
||||
# 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.all_locations = set()
|
||||
self.item_locations = set()
|
||||
self.prize_relevant = False
|
||||
|
||||
|
||||
class KeyLogic(object):
|
||||
@@ -54,10 +57,11 @@ class KeyLogic(object):
|
||||
self.location_rules = {}
|
||||
self.outside_keys = 0
|
||||
self.dungeon = dungeon_name
|
||||
self.sm_doors = {}
|
||||
|
||||
def check_placement(self, unplaced_keys, big_key_loc=None):
|
||||
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
|
||||
if big_key_loc:
|
||||
for rule_a, rule_b in itertools.combinations(self.placement_rules, 2):
|
||||
@@ -65,6 +69,15 @@ class KeyLogic(object):
|
||||
return False
|
||||
return True
|
||||
|
||||
def reset(self):
|
||||
self.door_rules.clear()
|
||||
self.bk_restricted.clear()
|
||||
self.bk_locked.clear()
|
||||
self.sm_restricted.clear()
|
||||
self.bk_doors.clear()
|
||||
self.bk_chests.clear()
|
||||
self.placement_rules.clear()
|
||||
|
||||
|
||||
class DoorRules(object):
|
||||
|
||||
@@ -79,6 +92,8 @@ class DoorRules(object):
|
||||
self.small_location = None
|
||||
self.opposite = None
|
||||
|
||||
self.new_rules = {} # keyed by type, or type+lock_item -> number
|
||||
|
||||
|
||||
class LocationRule(object):
|
||||
def __init__(self):
|
||||
@@ -101,6 +116,7 @@ class PlacementRule(object):
|
||||
self.needed_keys_w_bk = None
|
||||
self.needed_keys_wo_bk = None
|
||||
self.check_locations_w_bk = None
|
||||
self.special_bk_avail = False
|
||||
self.check_locations_wo_bk = None
|
||||
self.bk_relevant = True
|
||||
self.key_reduced = False
|
||||
@@ -112,6 +128,10 @@ class PlacementRule(object):
|
||||
rule_locations = rule.check_locations_wo_bk if rule_blocked else rule.check_locations_w_bk
|
||||
if check_locations is None or rule_locations is None:
|
||||
return False
|
||||
if not bk_blocked and big_key_loc not in check_locations: # bk is not available, so rule doesn't apply
|
||||
return False
|
||||
if not rule_blocked and big_key_loc not in rule_locations: # bk is not available, so rule doesn't apply
|
||||
return False
|
||||
check_locations = check_locations - {big_key_loc}
|
||||
rule_locations = rule_locations - {big_key_loc}
|
||||
threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk
|
||||
@@ -134,13 +154,23 @@ class PlacementRule(object):
|
||||
left -= rule_needed
|
||||
return False
|
||||
|
||||
def is_satisfiable(self, outside_keys, unplaced_keys):
|
||||
def is_satisfiable(self, outside_keys, unplaced_keys, big_key_loc):
|
||||
bk_blocked = False
|
||||
if self.bk_conditional_set:
|
||||
for loc in self.bk_conditional_set:
|
||||
if loc.item and loc.item.bigkey:
|
||||
bk_blocked = True
|
||||
break
|
||||
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
|
||||
if not bk_blocked and check_locations is None:
|
||||
return True
|
||||
@@ -170,6 +200,8 @@ class KeyCounter(object):
|
||||
self.important_location = False
|
||||
self.other_locations = {}
|
||||
self.important_locations = {}
|
||||
self.prize_doors_opened = False
|
||||
self.prize_received = False
|
||||
|
||||
def used_smalls_loc(self, reserve=0):
|
||||
return max(self.used_keys + reserve - len(self.key_only_locations), 0)
|
||||
@@ -209,8 +241,19 @@ def calc_max_chests(builder, key_layout, world, player):
|
||||
|
||||
|
||||
def analyze_dungeon(key_layout, world, player):
|
||||
key_layout.key_logic.reset()
|
||||
key_layout.key_counters = create_key_counters(key_layout, world, player)
|
||||
key_logic = key_layout.key_logic
|
||||
for door in key_layout.proposal:
|
||||
if isinstance(door, tuple):
|
||||
key_logic.sm_doors[door[0]] = door[1]
|
||||
key_logic.sm_doors[door[1]] = door[0]
|
||||
else:
|
||||
if door.dest and door.type != DoorType.SpiralStairs:
|
||||
key_logic.sm_doors[door] = door.dest
|
||||
key_logic.sm_doors[door.dest] = door
|
||||
else:
|
||||
key_logic.sm_doors[door] = None
|
||||
|
||||
find_bk_locked_sections(key_layout, world, player)
|
||||
key_logic.bk_chests.update(find_big_chest_locations(key_layout.all_chest_locations))
|
||||
@@ -218,7 +261,9 @@ def analyze_dungeon(key_layout, world, player):
|
||||
if world.retro[player] and world.mode[player] != 'standard':
|
||||
return
|
||||
|
||||
original_key_counter = find_counter({}, False, key_layout)
|
||||
original_key_counter = find_counter({}, False, key_layout, False)
|
||||
if key_layout.big_key_special and forced_big_key_avail(original_key_counter.other_locations) is not None:
|
||||
original_key_counter = find_counter({}, True, key_layout, False)
|
||||
queue = deque([(None, original_key_counter)])
|
||||
doors_completed = set()
|
||||
visited_cid = set()
|
||||
@@ -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
|
||||
child_queue = deque()
|
||||
for child in key_counter.child_doors.keys():
|
||||
if not child.bigKey or not key_layout.big_key_special or big_avail:
|
||||
if can_open_door_by_counter(child, key_counter, key_layout, world, player):
|
||||
odd_counter = create_odd_key_counter(child, key_counter, key_layout, world, player)
|
||||
empty_flag = empty_counter(odd_counter)
|
||||
child_queue.append((child, odd_counter, empty_flag))
|
||||
while len(child_queue) > 0:
|
||||
child, odd_counter, empty_flag = child_queue.popleft()
|
||||
if not child.bigKey and child not in doors_completed:
|
||||
best_counter = find_best_counter(child, odd_counter, key_counter, key_layout, world, player, False, empty_flag)
|
||||
rule = create_rule(best_counter, key_counter, key_layout, world, player)
|
||||
prize_flag = key_counter.prize_doors_opened
|
||||
if child in key_layout.flat_prop and child not in doors_completed:
|
||||
best_counter = find_best_counter(child, key_layout, odd_counter, False, empty_flag)
|
||||
rule = create_rule(best_counter, key_counter, world, player)
|
||||
create_worst_case_rule(rule, best_counter, world, player)
|
||||
check_for_self_lock_key(rule, child, best_counter, key_layout, world, player)
|
||||
bk_restricted_rules(rule, child, odd_counter, empty_flag, key_counter, key_layout, world, player)
|
||||
key_logic.door_rules[child.name] = rule
|
||||
elif not child.bigKey and child not in doors_completed:
|
||||
prize_flag = True
|
||||
doors_completed.add(child)
|
||||
next_counter = find_next_counter(child, key_counter, key_layout)
|
||||
next_counter = find_next_counter(child, key_counter, key_layout, prize_flag)
|
||||
ctr_id = cid(next_counter, key_layout)
|
||||
if ctr_id not in visited_cid:
|
||||
queue.append((child, next_counter))
|
||||
visited_cid.add(ctr_id)
|
||||
check_rules(original_key_counter, key_layout, world, player)
|
||||
# todo: why is this commented out?
|
||||
# check_rules(original_key_counter, key_layout, world, player)
|
||||
|
||||
# Flip bk rules if more restrictive, to prevent placing a big key in a softlocking location
|
||||
for rule in key_logic.door_rules.values():
|
||||
@@ -274,6 +324,8 @@ def create_exhaustive_placement_rules(key_layout, world, player):
|
||||
key_logic = key_layout.key_logic
|
||||
max_ctr = find_max_counter(key_layout)
|
||||
for code, key_counter in key_layout.key_counters.items():
|
||||
if skip_key_counter_due_to_prize(key_layout, key_counter):
|
||||
continue # we have the prize, we are not concerned about this case
|
||||
accessible_loc = set()
|
||||
accessible_loc.update(key_counter.free_locations)
|
||||
accessible_loc.update(key_counter.key_only_locations)
|
||||
@@ -294,7 +346,9 @@ def create_exhaustive_placement_rules(key_layout, world, player):
|
||||
else:
|
||||
placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player)
|
||||
rule.check_locations_w_bk = accessible_loc
|
||||
check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc)
|
||||
if key_layout.big_key_special:
|
||||
rule.special_bk_avail = forced_big_key_avail(key_counter.important_locations) is not None
|
||||
# check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc)
|
||||
else:
|
||||
if big_key_progress(key_counter) and only_sm_doors(key_counter):
|
||||
create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, accessible_loc, min_keys, world, player)
|
||||
@@ -308,6 +362,10 @@ def create_exhaustive_placement_rules(key_layout, world, player):
|
||||
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):
|
||||
if len(blocked_loc) == 1 and world.accessibility[player] != '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
|
||||
|
||||
|
||||
# this rule is suspect - commented out usages for now
|
||||
def check_sm_restriction_needed(key_layout, max_ctr, rule, blocked):
|
||||
if rule.needed_keys_w_bk == key_layout.max_chests + len(max_ctr.key_only_locations):
|
||||
key_layout.key_logic.sm_restricted.update(blocked.difference(max_ctr.key_only_locations))
|
||||
@@ -413,7 +472,8 @@ def refine_placement_rules(key_layout, max_ctr):
|
||||
rule_b = temp
|
||||
if rule_a.bk_conditional_set and rule_b.check_locations_w_bk:
|
||||
common_needed = min(rule_a.needed_keys_wo_bk, rule_b.needed_keys_w_bk)
|
||||
if len(rule_b.check_locations_w_bk & rule_a.check_locations_wo_bk) < common_needed:
|
||||
common_locs = len(rule_b.check_locations_w_bk & rule_a.check_locations_wo_bk)
|
||||
if (common_needed - common_locs) * 2 > key_layout.max_chests:
|
||||
key_logic.bk_restricted.update(rule_a.bk_conditional_set)
|
||||
rules_to_remove.append(rule_a)
|
||||
changed = True
|
||||
@@ -478,7 +538,7 @@ def create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, a
|
||||
else:
|
||||
placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player)
|
||||
rule.check_locations_w_bk = accessible_loc
|
||||
check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc)
|
||||
# check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc)
|
||||
key_logic.placement_rules.append(rule)
|
||||
adjust_locations_rules(key_logic, rule, accessible_loc, key_layout, key_counter, max_ctr)
|
||||
|
||||
@@ -538,6 +598,8 @@ def relative_empty_counter(odd_counter, key_counter):
|
||||
return False
|
||||
if len(set(odd_counter.free_locations).difference(key_counter.free_locations)) > 0:
|
||||
return False
|
||||
if len(set(odd_counter.other_locations).difference(key_counter.other_locations)) > 0:
|
||||
return False
|
||||
# important only
|
||||
if len(set(odd_counter.important_locations).difference(key_counter.important_locations)) > 0:
|
||||
return False
|
||||
@@ -594,33 +656,50 @@ def unique_child_door_2(child, key_counter):
|
||||
return True
|
||||
|
||||
|
||||
def find_best_counter(door, odd_counter, key_counter, key_layout, world, player, skip_bk, empty_flag): # try to waste as many keys as possible?
|
||||
ignored_doors = {door, door.dest} if door is not None else {}
|
||||
finished = False
|
||||
opened_doors = dict(key_counter.open_doors)
|
||||
bk_opened = key_counter.big_key_opened
|
||||
# new_counter = key_counter
|
||||
last_counter = key_counter
|
||||
while not finished:
|
||||
door_set = find_potential_open_doors(last_counter, ignored_doors, key_layout, skip_bk)
|
||||
if door_set is None or len(door_set) == 0:
|
||||
finished = True
|
||||
continue
|
||||
for new_door in door_set:
|
||||
proposed_doors = {**opened_doors, **dict.fromkeys([new_door, new_door.dest])}
|
||||
bk_open = bk_opened or new_door.bigKey
|
||||
new_counter = find_counter(proposed_doors, bk_open, key_layout)
|
||||
bk_open = new_counter.big_key_opened
|
||||
# this means the new_door invalidates the door / leads to the same stuff
|
||||
if not empty_flag and relative_empty_counter(odd_counter, new_counter):
|
||||
ignored_doors.add(new_door)
|
||||
elif empty_flag or key_wasted(new_door, door, last_counter, new_counter, key_layout, world, player):
|
||||
last_counter = new_counter
|
||||
opened_doors = proposed_doors
|
||||
bk_opened = bk_open
|
||||
else:
|
||||
ignored_doors.add(new_door)
|
||||
return last_counter
|
||||
# def find_best_counter(door, odd_counter, key_counter, key_layout, world, player, skip_bk, empty_flag): # try to waste as many keys as possible?
|
||||
# ignored_doors = {door, door.dest} if door is not None else {}
|
||||
# finished = False
|
||||
# opened_doors = dict(key_counter.open_doors)
|
||||
# bk_opened = key_counter.big_key_opened
|
||||
# # new_counter = key_counter
|
||||
# last_counter = key_counter
|
||||
# while not finished:
|
||||
# door_set = find_potential_open_doors(last_counter, ignored_doors, key_layout, skip_bk)
|
||||
# if door_set is None or len(door_set) == 0:
|
||||
# finished = True
|
||||
# continue
|
||||
# for new_door in door_set:
|
||||
# proposed_doors = {**opened_doors, **dict.fromkeys([new_door, new_door.dest])}
|
||||
# bk_open = bk_opened or new_door.bigKey
|
||||
# new_counter = find_counter(proposed_doors, bk_open, key_layout)
|
||||
# bk_open = new_counter.big_key_opened
|
||||
# # this means the new_door invalidates the door / leads to the same stuff
|
||||
# if not empty_flag and relative_empty_counter(odd_counter, new_counter):
|
||||
# ignored_doors.add(new_door)
|
||||
# elif empty_flag or key_wasted(new_door, door, last_counter, new_counter, key_layout, world, player):
|
||||
# last_counter = new_counter
|
||||
# opened_doors = proposed_doors
|
||||
# bk_opened = bk_open
|
||||
# else:
|
||||
# ignored_doors.add(new_door)
|
||||
# return last_counter
|
||||
|
||||
|
||||
def find_best_counter(door, key_layout, odd_counter, skip_bk, empty_flag):
|
||||
best, best_ctr, locations = 0, None, 0
|
||||
for code, counter in key_layout.key_counters.items():
|
||||
if door not in counter.open_doors:
|
||||
if best_ctr is None or counter.used_keys > best or (counter.used_keys == best and count_locations(counter) > locations):
|
||||
if not skip_bk or not counter.big_key_opened:
|
||||
if empty_flag or not relative_empty_counter(odd_counter, counter):
|
||||
best = counter.used_keys
|
||||
best_ctr = counter
|
||||
locations = count_locations(counter)
|
||||
return best_ctr
|
||||
|
||||
|
||||
def count_locations(ctr):
|
||||
return len(ctr.free_locations) + len(ctr.key_only_locations) + len(ctr.other_locations) + len(ctr.important_locations)
|
||||
|
||||
|
||||
def find_worst_counter(door, odd_counter, key_counter, key_layout, skip_bk): # try to waste as many keys as possible?
|
||||
@@ -638,7 +717,7 @@ def find_worst_counter(door, odd_counter, key_counter, key_layout, skip_bk): #
|
||||
for new_door in door_set:
|
||||
proposed_doors = {**opened_doors, **dict.fromkeys([new_door, new_door.dest])}
|
||||
bk_open = bk_opened or new_door.bigKey
|
||||
new_counter = find_counter(proposed_doors, bk_open, key_layout)
|
||||
new_counter = find_counter(proposed_doors, bk_open, key_layout, key_counter.prize_doors_opened)
|
||||
bk_open = new_counter.big_key_opened
|
||||
if not new_door.bigKey and progressive_ctr(new_counter, last_counter) and relative_empty_counter_2(odd_counter, new_counter):
|
||||
ignored_doors.add(new_door)
|
||||
@@ -690,7 +769,7 @@ def key_wasted(new_door, old_door, old_counter, new_counter, key_layout, world,
|
||||
for new_child in new_children:
|
||||
proposed_doors = {**opened_doors, **dict.fromkeys([new_child, new_child.dest])}
|
||||
bk_open = bk_opened or new_door.bigKey
|
||||
new_counter = find_counter(proposed_doors, bk_open, key_layout)
|
||||
new_counter = find_counter(proposed_doors, bk_open, key_layout, current_counter.prize_doors_opened)
|
||||
if key_wasted(new_child, old_door, current_counter, new_counter, key_layout, world, player):
|
||||
wasted_keys += 1
|
||||
if new_avail - wasted_keys < old_avail:
|
||||
@@ -698,10 +777,11 @@ def key_wasted(new_door, old_door, old_counter, new_counter, key_layout, world,
|
||||
return False
|
||||
|
||||
|
||||
def find_next_counter(new_door, old_counter, key_layout):
|
||||
def find_next_counter(new_door, old_counter, key_layout, prize_flag=None):
|
||||
proposed_doors = {**old_counter.open_doors, **dict.fromkeys([new_door, new_door.dest])}
|
||||
bk_open = old_counter.big_key_opened or new_door.bigKey
|
||||
return find_counter(proposed_doors, bk_open, key_layout)
|
||||
prize_flag = prize_flag if prize_flag else old_counter.prize_doors_opened
|
||||
return find_counter(proposed_doors, bk_open, key_layout, prize_flag)
|
||||
|
||||
|
||||
def check_special_locations(locations):
|
||||
@@ -717,7 +797,7 @@ def calc_avail_keys(key_counter, world, player):
|
||||
return raw_avail - key_counter.used_keys
|
||||
|
||||
|
||||
def create_rule(key_counter, prev_counter, key_layout, world, player):
|
||||
def create_rule(key_counter, prev_counter, world, player):
|
||||
# prev_chest_keys = available_chest_small_keys(prev_counter, world)
|
||||
# prev_avail = prev_chest_keys + len(prev_counter.key_only_locations)
|
||||
chest_keys = available_chest_small_keys(key_counter, world, player)
|
||||
@@ -736,6 +816,11 @@ def create_rule(key_counter, prev_counter, key_layout, world, player):
|
||||
return DoorRules(rule_num, is_valid)
|
||||
|
||||
|
||||
def create_worst_case_rule(rules, key_counter, world, player):
|
||||
required_keys = key_counter.used_keys + 1 # this makes more sense, if key_counter has wasted all keys
|
||||
rules.new_rules[KeyRuleType.WorstCase] = required_keys
|
||||
|
||||
|
||||
def check_for_self_lock_key(rule, door, parent_counter, key_layout, world, player):
|
||||
if world.accessibility[player] != 'locations':
|
||||
counter = find_inverted_counter(door, parent_counter, key_layout, world, player)
|
||||
@@ -744,11 +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:
|
||||
rule.allow_small = True
|
||||
rule.small_location = next(iter(counter.free_locations))
|
||||
rule.new_rules[KeyRuleType.AllowSmall] = rule.new_rules[KeyRuleType.WorstCase] - 1
|
||||
|
||||
|
||||
def find_inverted_counter(door, parent_counter, key_layout, world, player):
|
||||
# open all doors in counter
|
||||
counter = open_all_counter(parent_counter, key_layout, door=door)
|
||||
counter = open_all_counter(parent_counter, key_layout, world, player, door=door)
|
||||
max_counter = find_max_counter(key_layout)
|
||||
# find the difference
|
||||
inverted_counter = KeyCounter(key_layout.max_chests)
|
||||
@@ -764,7 +850,7 @@ def find_inverted_counter(door, parent_counter, key_layout, world, player):
|
||||
return inverted_counter
|
||||
|
||||
|
||||
def open_all_counter(parent_counter, key_layout, door=None, skipBk=False):
|
||||
def open_all_counter(parent_counter, key_layout, world, player, door=None, skipBk=False):
|
||||
changed = True
|
||||
counter = parent_counter
|
||||
proposed_doors = dict.fromkeys(parent_counter.open_doors.keys())
|
||||
@@ -776,14 +862,12 @@ def open_all_counter(parent_counter, key_layout, door=None, skipBk=False):
|
||||
if skipBk:
|
||||
if not child.bigKey:
|
||||
doors_to_open[child] = None
|
||||
elif not child.bigKey or not key_layout.big_key_special or counter.big_key_opened:
|
||||
elif can_open_door_by_counter(child, counter, key_layout, world, player):
|
||||
doors_to_open[child] = None
|
||||
if len(doors_to_open.keys()) > 0:
|
||||
proposed_doors = {**proposed_doors, **doors_to_open}
|
||||
bk_hint = counter.big_key_opened
|
||||
for d in doors_to_open.keys():
|
||||
bk_hint = bk_hint or d.bigKey
|
||||
counter = find_counter(proposed_doors, bk_hint, key_layout)
|
||||
bk_hint = counter.big_key_opened or any(d.bigKey for d in doors_to_open.keys())
|
||||
counter = find_counter(proposed_doors, bk_hint, key_layout, True)
|
||||
changed = True
|
||||
return counter
|
||||
|
||||
@@ -804,7 +888,7 @@ def open_some_counter(parent_counter, key_layout, ignored_doors):
|
||||
bk_hint = counter.big_key_opened
|
||||
for d in doors_to_open.keys():
|
||||
bk_hint = bk_hint or d.bigKey
|
||||
counter = find_counter(proposed_doors, bk_hint, key_layout)
|
||||
counter = find_counter(proposed_doors, bk_hint, key_layout, parent_counter.prize_doors_opened)
|
||||
changed = True
|
||||
return counter
|
||||
|
||||
@@ -845,16 +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):
|
||||
if key_counter.big_key_opened:
|
||||
return
|
||||
best_counter = find_best_counter(door, odd_counter, key_counter, key_layout, world, player, True, empty_flag)
|
||||
bk_rule = create_rule(best_counter, key_counter, key_layout, world, player)
|
||||
best_counter = find_best_counter(door, key_layout, odd_counter, True, empty_flag)
|
||||
bk_rule = create_rule(best_counter, key_counter, world, player)
|
||||
if bk_rule.small_key_num >= rule.small_key_num:
|
||||
return
|
||||
door_open = find_next_counter(door, best_counter, key_layout)
|
||||
ignored_doors = dict_intersection(best_counter.child_doors, door_open.child_doors)
|
||||
dest_ignored = []
|
||||
for door in ignored_doors.keys():
|
||||
if door.dest not in ignored_doors:
|
||||
dest_ignored.append(door.dest)
|
||||
for d in ignored_doors.keys():
|
||||
if d.dest not in ignored_doors:
|
||||
dest_ignored.append(d.dest)
|
||||
ignored_doors = {**ignored_doors, **dict.fromkeys(dest_ignored)}
|
||||
post_counter = open_some_counter(door_open, key_layout, ignored_doors.keys())
|
||||
unique_loc = dict_difference(post_counter.free_locations, best_counter.free_locations)
|
||||
@@ -862,8 +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
|
||||
rule.alternate_small_key = bk_rule.small_key_num
|
||||
rule.alternate_big_key_loc.update(unique_loc)
|
||||
# elif not bk_rule.is_valid:
|
||||
# key_layout.key_logic.bk_restricted.update(unique_loc)
|
||||
if not door.bigKey:
|
||||
rule.new_rules[(KeyRuleType.Lock, key_layout.key_logic.bk_name)] = best_counter.used_keys + 1
|
||||
|
||||
|
||||
def find_worst_counter_wo_bk(small_key_num, accessible_set, door, odd_ctr, key_counter, key_layout):
|
||||
@@ -887,12 +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
|
||||
|
||||
|
||||
def open_a_door(door, child_state, flat_proposal):
|
||||
def open_a_door(door, child_state, flat_proposal, world, player):
|
||||
if door.bigKey or door.name in special_big_key_doors:
|
||||
child_state.big_key_opened = True
|
||||
child_state.avail_doors.extend(child_state.big_doors)
|
||||
child_state.opened_doors.extend(set([d.door for d in child_state.big_doors]))
|
||||
child_state.big_doors.clear()
|
||||
elif door in child_state.prize_door_set:
|
||||
child_state.prize_doors_opened = True
|
||||
for exp_door in child_state.prize_doors:
|
||||
new_region = exp_door.door.entrance.parent_region
|
||||
child_state.visit_region(new_region, key_checks=True)
|
||||
child_state.add_all_doors_check_keys(new_region, flat_proposal, world, player)
|
||||
child_state.prize_doors.clear()
|
||||
else:
|
||||
child_state.opened_doors.append(door)
|
||||
doors_to_open = [x for x in child_state.small_doors if x.door == door]
|
||||
@@ -935,6 +1026,7 @@ def only_sm_doors(key_counter):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
# doesn't count dest doors
|
||||
def count_unique_small_doors(key_counter, proposal):
|
||||
cnt = 0
|
||||
@@ -949,7 +1041,7 @@ def count_unique_small_doors(key_counter, proposal):
|
||||
|
||||
|
||||
def exist_relevant_big_doors(key_counter, key_layout):
|
||||
bk_counter = find_counter(key_counter.open_doors, True, key_layout, False)
|
||||
bk_counter = find_counter(key_counter.open_doors, True, key_layout, key_counter.prize_doors_opened, False)
|
||||
if bk_counter is not None:
|
||||
diff = dict_difference(bk_counter.free_locations, key_counter.free_locations)
|
||||
if len(diff) > 0:
|
||||
@@ -982,14 +1074,6 @@ def filter_big_chest(locations):
|
||||
return [x for x in locations if '- Big Chest' not in x.name]
|
||||
|
||||
|
||||
def count_locations_exclude_logic(locations, key_logic):
|
||||
cnt = 0
|
||||
for loc in locations:
|
||||
if not location_is_bk_locked(loc, key_logic) and not loc.forced_item and not prize_or_event(loc):
|
||||
cnt += 1
|
||||
return cnt
|
||||
|
||||
|
||||
def location_is_bk_locked(loc, key_logic):
|
||||
return loc in key_logic.bk_chests or loc in key_logic.bk_locked
|
||||
|
||||
@@ -998,18 +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']
|
||||
|
||||
|
||||
def count_free_locations(state):
|
||||
def boss_unavail(loc, world, player):
|
||||
# todo: ambrosia
|
||||
# return world.bossdrops[player] == 'ambrosia' and "- Boss" in loc.name
|
||||
return False
|
||||
|
||||
|
||||
def blind_boss_unavail(loc, state, world, player):
|
||||
if loc.name == "Thieves' Town - Boss":
|
||||
# todo: check attic
|
||||
return (loc.parent_region.dungeon.boss.name == 'Blind' and
|
||||
(not any(x for x in state.found_locations if x.name == 'Suspicious Maiden') or
|
||||
(world.get_region('Thieves Attic Window', player).dungeon.name == 'Thieves Town' and
|
||||
not any(x for x in state.found_locations if x.name == 'Attic Cracked Floor'))))
|
||||
return False
|
||||
|
||||
|
||||
def count_free_locations(state, world, player):
|
||||
cnt = 0
|
||||
for loc in state.found_locations:
|
||||
if not prize_or_event(loc) and not loc.forced_item:
|
||||
if (not prize_or_event(loc) and not loc.forced_item and not boss_unavail(loc, world, player)
|
||||
and not blind_boss_unavail(loc, state, world, player)):
|
||||
cnt += 1
|
||||
return cnt
|
||||
|
||||
|
||||
def count_locations_exclude_big_chest(state):
|
||||
def count_locations_exclude_big_chest(state, world, player):
|
||||
cnt = 0
|
||||
for loc in state.found_locations:
|
||||
if '- Big Chest' not in loc.name and not loc.forced_item and not prize_or_event(loc):
|
||||
if ('- Big Chest' not in loc.name and not loc.forced_item and not boss_unavail(loc, world, player)
|
||||
and not prize_or_event(loc) and not blind_boss_unavail(loc, state, world, player)):
|
||||
cnt += 1
|
||||
return cnt
|
||||
|
||||
@@ -1197,7 +1299,7 @@ def check_rules_deep(original_counter, key_layout, world, player):
|
||||
elif not door.bigKey:
|
||||
can_open = True
|
||||
if can_open:
|
||||
can_progress = smalls_opened or not big_maybe_not_found
|
||||
can_progress = (big_avail or not big_maybe_not_found) if door.bigKey else smalls_opened
|
||||
next_counter = find_next_counter(door, counter, key_layout)
|
||||
c_id = cid(next_counter, key_layout)
|
||||
if c_id not in completed:
|
||||
@@ -1265,6 +1367,13 @@ def check_bk_special(regions, world, player):
|
||||
return False
|
||||
|
||||
|
||||
def forced_big_key_avail(locations):
|
||||
for loc in locations:
|
||||
if loc.forced_big_key():
|
||||
return loc
|
||||
return None
|
||||
|
||||
|
||||
# Soft lock stuff
|
||||
def validate_key_layout(key_layout, world, player):
|
||||
# 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.big_key_special = check_bk_special(key_layout.sector.regions, world, player)
|
||||
for region in key_layout.start_regions:
|
||||
state.visit_region(region, key_checks=True)
|
||||
state.add_all_doors_check_keys(region, flat_proposal, world, player)
|
||||
dungeon_entrance, portal_door = find_outside_connection(region)
|
||||
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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
return True # I think that's the end
|
||||
# todo: fix state to separate out these types
|
||||
ttl_locations = count_free_locations(state) if state.big_key_opened else count_locations_exclude_big_chest(state)
|
||||
if state.big_key_opened:
|
||||
ttl_locations = count_free_locations(state, world, player)
|
||||
else:
|
||||
ttl_locations = count_locations_exclude_big_chest(state, world, player)
|
||||
ttl_small_key_only = count_small_key_only_locations(state)
|
||||
available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player)
|
||||
available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player)
|
||||
@@ -1301,14 +1421,16 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa
|
||||
if smalls_done and bk_done:
|
||||
return False
|
||||
else:
|
||||
# todo: pretty sure you should OR these paths together, maybe when there's one location and it can
|
||||
# either be small or big key
|
||||
if smalls_avail and available_small_locations > 0:
|
||||
for exp_door in state.small_doors:
|
||||
state_copy = state.copy()
|
||||
open_a_door(exp_door.door, state_copy, flat_proposal)
|
||||
open_a_door(exp_door.door, state_copy, flat_proposal, world, player)
|
||||
state_copy.used_smalls += 1
|
||||
if state_copy.used_smalls > ttl_small_key_only:
|
||||
state_copy.used_locations += 1
|
||||
code = state_id(state_copy, flat_proposal)
|
||||
code = validate_id(state_copy, flat_proposal)
|
||||
if code not in checked_states.keys():
|
||||
valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal,
|
||||
state, available_small_locations, world, player)
|
||||
@@ -1319,10 +1441,23 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa
|
||||
return False
|
||||
if not state.big_key_opened and (available_big_locations >= num_bigs > 0 or (found_forced_bk and num_bigs > 0)):
|
||||
state_copy = state.copy()
|
||||
open_a_door(state.big_doors[0].door, state_copy, flat_proposal)
|
||||
open_a_door(state.big_doors[0].door, state_copy, flat_proposal, world, player)
|
||||
if not found_forced_bk:
|
||||
state_copy.used_locations += 1
|
||||
code = state_id(state_copy, flat_proposal)
|
||||
code = validate_id(state_copy, flat_proposal)
|
||||
if code not in checked_states.keys():
|
||||
valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal,
|
||||
state, available_small_locations, world, player)
|
||||
checked_states[code] = valid
|
||||
else:
|
||||
valid = checked_states[code]
|
||||
if not valid:
|
||||
return False
|
||||
# todo: feel like you only open these if the boss is available???
|
||||
if not state.prize_doors_opened and key_layout.prize_relevant:
|
||||
state_copy = state.copy()
|
||||
open_a_door(next(iter(state_copy.prize_door_set)), state_copy, flat_proposal, world, player)
|
||||
code = validate_id(state_copy, flat_proposal)
|
||||
if code not in checked_states.keys():
|
||||
valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal,
|
||||
state, available_small_locations, world, player)
|
||||
@@ -1341,7 +1476,7 @@ def invalid_self_locking_key(key_layout, state, prev_state, prev_avail, world, p
|
||||
state_copy = state.copy()
|
||||
while len(new_bk_doors) > 0:
|
||||
for door in new_bk_doors:
|
||||
open_a_door(door.door, state_copy, key_layout.flat_prop)
|
||||
open_a_door(door.door, state_copy, key_layout.flat_prop, world, player)
|
||||
new_bk_doors = set(state_copy.big_doors).difference(set(prev_state.big_doors))
|
||||
expand_key_state(state_copy, key_layout.flat_prop, world, player)
|
||||
new_locations = set(state_copy.found_locations).difference(set(prev_state.found_locations))
|
||||
@@ -1373,30 +1508,55 @@ def cnt_avail_small_locations(free_locations, key_only, state, world, player):
|
||||
return state.key_locations - state.used_smalls
|
||||
|
||||
|
||||
def cnt_avail_small_locations_by_ctr(free_locations, counter, layout, world, player):
|
||||
if not world.keyshuffle[player] and not world.retro[player]:
|
||||
bk_adj = 1 if counter.big_key_opened and not layout.big_key_special else 0
|
||||
avail_chest_keys = min(free_locations - bk_adj, layout.max_chests)
|
||||
return max(0, avail_chest_keys + len(counter.key_only_locations) - counter.used_keys)
|
||||
return layout.max_chests + len(counter.key_only_locations) - counter.used_keys
|
||||
|
||||
|
||||
def cnt_avail_big_locations(ttl_locations, state, world, player):
|
||||
if not world.bigkeyshuffle[player]:
|
||||
return max(0, ttl_locations - state.used_locations) if not state.big_key_special else 0
|
||||
return 1 if not state.big_key_special else 0
|
||||
|
||||
|
||||
def cnt_avail_big_locations_by_ctr(ttl_locations, counter, layout, world, player):
|
||||
if not world.bigkeyshuffle[player]:
|
||||
bk_adj = 1 if counter.big_key_opened and not layout.big_key_special else 0
|
||||
used_locations = max(0, counter.used_keys - len(counter.key_only_locations)) + bk_adj
|
||||
return max(0, ttl_locations - used_locations) if not layout.big_key_special else 0
|
||||
return 1 if not layout.big_key_special else 0
|
||||
|
||||
|
||||
def create_key_counters(key_layout, world, player):
|
||||
key_counters = {}
|
||||
key_layout.found_doors.clear()
|
||||
flat_proposal = key_layout.flat_prop
|
||||
state = ExplorationState(dungeon=key_layout.sector.name)
|
||||
if world.doorShuffle[player] == 'vanilla':
|
||||
state.key_locations = 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:
|
||||
builder = world.dungeon_layouts[player][key_layout.sector.name]
|
||||
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 location in region.locations:
|
||||
if location.forced_big_key():
|
||||
state.big_key_special = True
|
||||
special_region = region
|
||||
for region in key_layout.start_regions:
|
||||
state.visit_region(region, key_checks=True)
|
||||
state.add_all_doors_check_keys(region, flat_proposal, world, player)
|
||||
dungeon_entrance, portal_door = find_outside_connection(region)
|
||||
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)
|
||||
code = state_id(state, key_layout.flat_prop)
|
||||
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:
|
||||
next_key_counter, parent_state = queue.popleft()
|
||||
for door in next_key_counter.child_doors:
|
||||
key_layout.found_doors.add(door)
|
||||
if door.dest in flat_proposal and door.type != DoorType.SpiralStairs:
|
||||
key_layout.found_doors.add(door.dest)
|
||||
child_state = parent_state.copy()
|
||||
if door.bigKey or door.name in special_big_key_doors:
|
||||
key_layout.key_logic.bk_doors.add(door)
|
||||
# open the door, if possible
|
||||
if not door.bigKey or not child_state.big_key_special or child_state.visited_at_all(special_region):
|
||||
open_a_door(door, child_state, flat_proposal)
|
||||
if can_open_door(door, child_state, world, player):
|
||||
open_a_door(door, child_state, flat_proposal, world, player)
|
||||
expand_key_state(child_state, flat_proposal, world, player)
|
||||
code = state_id(child_state, key_layout.flat_prop)
|
||||
if code not in key_counters.keys():
|
||||
@@ -1419,9 +1582,52 @@ def create_key_counters(key_layout, world, player):
|
||||
return key_counters
|
||||
|
||||
|
||||
def find_outside_connection(region):
|
||||
portal = next((x for x in region.entrances if ' Portal' in x.parent_region.name), None)
|
||||
if portal:
|
||||
dungeon_entrance = next(x for x in portal.parent_region.entrances
|
||||
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld])
|
||||
portal_entrance = next(x for x in portal.parent_region.entrances if x.parent_region == region)
|
||||
return dungeon_entrance, portal_entrance.door
|
||||
return None, None
|
||||
|
||||
|
||||
def can_open_door(door, state, world, player):
|
||||
if state.big_key_opened:
|
||||
ttl_locations = count_free_locations(state, world, player)
|
||||
else:
|
||||
ttl_locations = count_locations_exclude_big_chest(state, world, player)
|
||||
if door.smallKey:
|
||||
ttl_small_key_only = count_small_key_only_locations(state)
|
||||
available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player)
|
||||
return available_small_locations > 0
|
||||
elif door.bigKey:
|
||||
available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player)
|
||||
found_forced_bk = state.found_forced_bk()
|
||||
return not state.big_key_opened and (available_big_locations > 0 or found_forced_bk)
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def can_open_door_by_counter(door, counter: KeyCounter, layout, world, player):
|
||||
if counter.big_key_opened:
|
||||
ttl_locations = len(counter.free_locations)
|
||||
else:
|
||||
ttl_locations = len([x for x in counter.free_locations if '- Big Chest' not in x.name])
|
||||
|
||||
if door.smallKey:
|
||||
# ttl_small_key_only = len(counter.key_only_locations)
|
||||
return cnt_avail_small_locations_by_ctr(ttl_locations, counter, layout, world, player) > 0
|
||||
elif door.bigKey:
|
||||
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):
|
||||
key_counter = KeyCounter(key_layout.max_chests)
|
||||
key_counter.child_doors.update(dict.fromkeys(unique_doors(state.small_doors+state.big_doors)))
|
||||
key_counter.child_doors.update(dict.fromkeys(unique_doors(state.small_doors+state.big_doors+state.prize_doors)))
|
||||
for loc in state.found_locations:
|
||||
if important_location(loc, world, player):
|
||||
key_counter.important_location = True
|
||||
@@ -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.used_keys = count_unique_sm_doors(state.opened_doors)
|
||||
key_counter.big_key_opened = state.big_key_opened
|
||||
if len(state.prize_door_set) > 0 and state.prize_doors_opened:
|
||||
key_counter.prize_doors_opened = True
|
||||
if any(x for x in key_counter.important_locations if '- Prize' in x.name):
|
||||
key_counter.prize_received = True
|
||||
return key_counter
|
||||
|
||||
|
||||
@@ -1488,11 +1698,23 @@ def state_id(state, flat_proposal):
|
||||
s_id = '1' if state.big_key_opened else '0'
|
||||
for d in flat_proposal:
|
||||
s_id += '1' if d in state.opened_doors else '0'
|
||||
if len(state.prize_door_set) > 0:
|
||||
s_id += '1' if state.prize_doors_opened else '0'
|
||||
return s_id
|
||||
|
||||
|
||||
def find_counter(opened_doors, bk_hint, key_layout, raise_on_error=True):
|
||||
counter = find_counter_hint(opened_doors, bk_hint, key_layout)
|
||||
def validate_id(state, flat_proposal):
|
||||
s_id = '1' if state.big_key_opened else '0'
|
||||
for d in flat_proposal:
|
||||
s_id += '1' if d in state.opened_doors else '0'
|
||||
if len(state.prize_door_set) > 0:
|
||||
s_id += '1' if state.prize_doors_opened else '0'
|
||||
s_id += str(state.used_locations)
|
||||
return s_id
|
||||
|
||||
|
||||
def find_counter(opened_doors, bk_hint, key_layout, prize_flag, raise_on_error=True):
|
||||
counter = find_counter_hint(opened_doors, bk_hint, key_layout, prize_flag)
|
||||
if counter is not None:
|
||||
return counter
|
||||
more_doors = []
|
||||
@@ -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():
|
||||
more_doors.append(door.dest)
|
||||
if len(more_doors) > len(opened_doors.keys()):
|
||||
counter = find_counter_hint(dict.fromkeys(more_doors), bk_hint, key_layout)
|
||||
counter = find_counter_hint(dict.fromkeys(more_doors), bk_hint, key_layout, prize_flag)
|
||||
if counter is not None:
|
||||
return counter
|
||||
if raise_on_error:
|
||||
raise Exception('Unable to find door permutation. Init CID: %s' % counter_id(opened_doors, bk_hint, key_layout.flat_prop))
|
||||
cid = counter_id(opened_doors, bk_hint, key_layout.flat_prop, key_layout.prize_relevant, prize_flag)
|
||||
raise Exception(f'Unable to find door permutation. Init CID: {cid}')
|
||||
return None
|
||||
|
||||
|
||||
def find_counter_hint(opened_doors, bk_hint, key_layout):
|
||||
cid = counter_id(opened_doors, bk_hint, key_layout.flat_prop)
|
||||
def find_counter_hint(opened_doors, bk_hint, key_layout, prize_flag):
|
||||
cid = counter_id(opened_doors, bk_hint, key_layout.flat_prop, key_layout.prize_relevant, prize_flag)
|
||||
if cid in key_layout.key_counters.keys():
|
||||
return key_layout.key_counters[cid]
|
||||
if not bk_hint:
|
||||
cid = counter_id(opened_doors, True, key_layout.flat_prop)
|
||||
cid = counter_id(opened_doors, True, key_layout.flat_prop, key_layout.prize_relevant, prize_flag)
|
||||
if cid in key_layout.key_counters.keys():
|
||||
return key_layout.key_counters[cid]
|
||||
return None
|
||||
|
||||
|
||||
def find_max_counter(key_layout):
|
||||
max_counter = find_counter_hint(dict.fromkeys(key_layout.flat_prop), False, key_layout)
|
||||
max_counter = find_counter_hint(dict.fromkeys(key_layout.found_doors), False, key_layout, True)
|
||||
if max_counter is None:
|
||||
raise Exception("Max Counter is none - something is amiss")
|
||||
if len(max_counter.child_doors) > 0:
|
||||
max_counter = find_counter_hint(dict.fromkeys(key_layout.flat_prop), True, key_layout)
|
||||
max_counter = find_counter_hint(dict.fromkeys(key_layout.found_doors), True, key_layout, True)
|
||||
return max_counter
|
||||
|
||||
|
||||
def counter_id(opened_doors, bk_unlocked, flat_proposal):
|
||||
def counter_id(opened_doors, bk_unlocked, flat_proposal, prize_relevant, prize_flag):
|
||||
s_id = '1' if bk_unlocked else '0'
|
||||
for d in flat_proposal:
|
||||
s_id += '1' if d in opened_doors.keys() else '0'
|
||||
if prize_relevant:
|
||||
s_id += '1' if prize_flag else '0'
|
||||
return s_id
|
||||
|
||||
|
||||
def cid(counter, key_layout):
|
||||
return counter_id(counter.open_doors, counter.big_key_opened, key_layout.flat_prop)
|
||||
return counter_id(counter.open_doors, counter.big_key_opened, key_layout.flat_prop,
|
||||
key_layout.prize_relevant, counter.prize_doors_opened)
|
||||
|
||||
|
||||
# class SoftLockException(Exception):
|
||||
@@ -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_keys = sum(1 for i in found_locations if i.item is not None and i.item.name == smallkey_name and i.item.player == player) + \
|
||||
len(counter.key_only_locations) + keys_outside
|
||||
if key_layout.prize_relevant:
|
||||
found_prize = any(x for x in counter.important_locations if '- Prize' in x.name)
|
||||
if not found_prize and 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 \
|
||||
found_keys > counter.used_keys and any(not d.bigKey for d in counter.child_doors)
|
||||
found_keys > counter.used_keys and any(not d.bigKey for d in counter.child_doors) or \
|
||||
self_locked_child_door(key_layout, counter) or \
|
||||
(key_layout.prize_relevant and not counter.prize_doors_opened and found_prize)
|
||||
if not can_progress:
|
||||
missing_locations = set(max_counter.free_locations.keys()).difference(found_locations)
|
||||
missing_items = [l for l in missing_locations if l.item is None or (l.item.name != smallkey_name and l.item.name != bigkey_name) or "- Boss" in l.name]
|
||||
@@ -1762,3 +1998,11 @@ def validate_key_placement(key_layout, world, player):
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def self_locked_child_door(key_layout, counter):
|
||||
if len(counter.child_doors) == 1:
|
||||
door = next(iter(counter.child_doors.keys()))
|
||||
return door.smallKey and key_layout.key_logic.door_rules[door.name].allow_small
|
||||
return False
|
||||
|
||||
|
||||
|
||||
33
Main.py
33
Main.py
@@ -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 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
|
||||
|
||||
@@ -244,7 +244,7 @@ def main(args, seed=None, fish=None):
|
||||
balance_multiworld_progression(world)
|
||||
|
||||
# if we only check for beatable, we can do this sanity check first before creating the rom
|
||||
if not world.can_beat_game():
|
||||
if not world.can_beat_game(log_error=True):
|
||||
raise RuntimeError(world.fish.translate("cli","cli","cannot.beat.game"))
|
||||
|
||||
for player in range(1, world.players+1):
|
||||
@@ -402,6 +402,8 @@ def copy_world(world):
|
||||
ret.mixed_travel = world.mixed_travel.copy()
|
||||
ret.standardize_palettes = world.standardize_palettes.copy()
|
||||
|
||||
ret.exp_cache = world.exp_cache.copy()
|
||||
|
||||
for player in range(1, world.players + 1):
|
||||
if world.mode[player] != 'inverted':
|
||||
create_regions(ret, player)
|
||||
@@ -461,6 +463,7 @@ def copy_world(world):
|
||||
# these need to be modified properly by set_rules
|
||||
new_location.access_rule = lambda state: True
|
||||
new_location.item_rule = lambda state: True
|
||||
new_location.forced_item = location.forced_item
|
||||
|
||||
# copy remaining itempool. No item in itempool should have an assigned location
|
||||
for item in world.itempool:
|
||||
@@ -538,11 +541,11 @@ def create_playthrough(world):
|
||||
while sphere_candidates:
|
||||
state.sweep_for_events(key_only=True)
|
||||
|
||||
sphere = []
|
||||
sphere = set()
|
||||
# build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
|
||||
for location in sphere_candidates:
|
||||
if state.can_reach(location) and state.not_flooding_a_key(world, location):
|
||||
sphere.append(location)
|
||||
sphere.add(location)
|
||||
|
||||
for location in sphere:
|
||||
sphere_candidates.remove(location)
|
||||
@@ -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
|
||||
for num, sphere in reversed(list(enumerate(collection_spheres))):
|
||||
to_delete = []
|
||||
to_delete = set()
|
||||
for location in sphere:
|
||||
# we remove the item at location and check if game is still beatable
|
||||
logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player)
|
||||
old_item = location.item
|
||||
location.item = None
|
||||
# todo: this is not very efficient, but I'm not sure how else to do it for this backwards logic
|
||||
# world.clear_exp_cache()
|
||||
if world.can_beat_game(state_cache[num]):
|
||||
to_delete.append(location)
|
||||
# logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is not required')
|
||||
to_delete.add(location)
|
||||
else:
|
||||
# still required, got to keep it around
|
||||
# logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is required')
|
||||
location.item = old_item
|
||||
|
||||
# cull entries in spheres for spoiler walkthrough at end
|
||||
for location in to_delete:
|
||||
sphere.remove(location)
|
||||
sphere -= to_delete
|
||||
|
||||
# second phase, sphere 0
|
||||
for item in [i for i in world.precollected_items if i.advancement]:
|
||||
@@ -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
|
||||
# to build up the correct spheres
|
||||
|
||||
required_locations = [item for sphere in collection_spheres for item in sphere]
|
||||
required_locations = {item for sphere in collection_spheres for item in sphere}
|
||||
state = CollectionState(world)
|
||||
collection_spheres = []
|
||||
while required_locations:
|
||||
@@ -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))
|
||||
if not sphere:
|
||||
raise RuntimeError(world.fish.translate("cli","cli","cannot.reach.required"))
|
||||
if world.has_beaten_game(state):
|
||||
required_locations.clear()
|
||||
else:
|
||||
raise RuntimeError(world.fish.translate("cli","cli","cannot.reach.required"))
|
||||
|
||||
# store the required locations for statistical analysis
|
||||
old_world.required_locations = [(location.name, location.player) for sphere in collection_spheres for location in sphere]
|
||||
@@ -630,7 +639,7 @@ def create_playthrough(world):
|
||||
old_world.spoiler.paths = dict()
|
||||
for player in range(1, world.players + 1):
|
||||
old_world.spoiler.paths.update({location.gen_name(): get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere if location.player == player})
|
||||
for _, path in dict(old_world.spoiler.paths).items():
|
||||
for path in dict(old_world.spoiler.paths).values():
|
||||
if any(exit == 'Pyramid Fairy' for (_, exit) in path):
|
||||
if world.mode[player] != 'inverted':
|
||||
old_world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state, world.get_region('Big Bomb Shop', player))
|
||||
@@ -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))
|
||||
|
||||
# we can finally output our playthrough
|
||||
old_world.spoiler.playthrough = OrderedDict([("0", [str(item) for item in world.precollected_items if item.advancement])])
|
||||
old_world.spoiler.playthrough = {"0": [str(item) for item in world.precollected_items if item.advancement]}
|
||||
for i, sphere in enumerate(collection_spheres):
|
||||
old_world.spoiler.playthrough[str(i + 1)] = {location.gen_name(): str(location.item) for location in sphere}
|
||||
|
||||
@@ -31,6 +31,7 @@ def main():
|
||||
parser.add_argument('--rom')
|
||||
parser.add_argument('--enemizercli')
|
||||
parser.add_argument('--outputpath')
|
||||
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
|
||||
for player in range(1, multiargs.multi + 1):
|
||||
parser.add_argument(f'--p{player}', help=argparse.SUPPRESS)
|
||||
args = parser.parse_args()
|
||||
@@ -63,6 +64,7 @@ def main():
|
||||
erargs.race = True
|
||||
erargs.outputname = seedname
|
||||
erargs.outputpath = args.outputpath
|
||||
erargs.loglevel = args.loglevel
|
||||
|
||||
if args.rom:
|
||||
erargs.rom = args.rom
|
||||
|
||||
5
Rom.py
5
Rom.py
@@ -102,8 +102,7 @@ class LocalRom(object):
|
||||
self.buffer[address] = value
|
||||
|
||||
def write_bytes(self, startaddress, values):
|
||||
for i, value in enumerate(values):
|
||||
self.write_byte(startaddress + i, value)
|
||||
self.buffer[startaddress:startaddress + len(values)] = values
|
||||
|
||||
def write_to_file(self, file):
|
||||
with open(file, 'wb') as outfile:
|
||||
@@ -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]
|
||||
if len(pots) > 0:
|
||||
write_int16(rom, pot_item_room_table_lookup + 2*i, n)
|
||||
rom.write_bytes(n, itertools.chain(*((pot.x,pot.y,pot.item) for pot in pots)))
|
||||
rom.write_bytes(n, list(itertools.chain.from_iterable(((pot.x, pot.y, pot.item) for pot in pots))))
|
||||
n += 3*len(pots) + 2
|
||||
rom.write_bytes(n - 2, [0xFF,0xFF])
|
||||
else:
|
||||
|
||||
47
Rules.py
47
Rules.py
@@ -3,7 +3,7 @@ import logging
|
||||
from collections import deque
|
||||
|
||||
import OverworldGlitchRules
|
||||
from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier
|
||||
from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier, KeyRuleType
|
||||
from RoomData import DoorKind
|
||||
from OverworldGlitchRules import overworld_glitches_rules
|
||||
|
||||
@@ -1941,14 +1941,8 @@ bunny_impassible_doors = {
|
||||
def add_key_logic_rules(world, player):
|
||||
key_logic = world.key_logic[player]
|
||||
for d_name, d_logic in key_logic.items():
|
||||
for door_name, keys in d_logic.door_rules.items():
|
||||
spot = world.get_entrance(door_name, player)
|
||||
if not world.retro[player] or world.mode[player] != 'standard' or not retro_in_hc(spot):
|
||||
rule = create_advanced_key_rule(d_logic, player, keys)
|
||||
if keys.opposite:
|
||||
rule = or_rule(rule, create_advanced_key_rule(d_logic, player, keys.opposite))
|
||||
add_rule(spot, rule)
|
||||
|
||||
for door_name, rule in d_logic.door_rules.items():
|
||||
add_rule(world.get_entrance(door_name, player), eval_small_key_door(door_name, d_name, player))
|
||||
for location in d_logic.bk_restricted:
|
||||
if not location.forced_item:
|
||||
forbid_item(location, d_logic.bk_name, player)
|
||||
@@ -1956,8 +1950,9 @@ def add_key_logic_rules(world, player):
|
||||
forbid_item(location, d_logic.small_key_name, player)
|
||||
for door in d_logic.bk_doors:
|
||||
add_rule(world.get_entrance(door.name, player), create_rule(d_logic.bk_name, player))
|
||||
for chest in d_logic.bk_chests:
|
||||
add_rule(world.get_location(chest.name, player), create_rule(d_logic.bk_name, player))
|
||||
if len(d_logic.bk_doors) > 0 or len(d_logic.bk_chests) > 1:
|
||||
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]:
|
||||
for d_name, layout in world.key_layout[player].items():
|
||||
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))
|
||||
|
||||
|
||||
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):
|
||||
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
|
||||
import os
|
||||
import re
|
||||
import operator as op
|
||||
import subprocess
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from collections import defaultdict
|
||||
from functools import reduce
|
||||
|
||||
|
||||
def int16_as_bytes(value):
|
||||
@@ -116,6 +118,28 @@ def make_new_base2current(old_rom='Zelda no Densetsu - Kamigami no Triforce (Jap
|
||||
return "New Rom Hash: " + basemd5.hexdigest()
|
||||
|
||||
|
||||
def kth_combination(k, l, r):
|
||||
if r == 0:
|
||||
return []
|
||||
elif len(l) == r:
|
||||
return l
|
||||
else:
|
||||
i = ncr(len(l)-1, r-1)
|
||||
if k < i:
|
||||
return l[0:1] + kth_combination(k, l[1:], r-1)
|
||||
else:
|
||||
return kth_combination(k-i, l[1:], r)
|
||||
|
||||
|
||||
def ncr(n, r):
|
||||
if r == 0:
|
||||
return 1
|
||||
r = min(r, n-r)
|
||||
numerator = reduce(op.mul, range(n, n-r, -1), 1)
|
||||
denominator = reduce(op.mul, range(1, r+1), 1)
|
||||
return numerator / denominator
|
||||
|
||||
|
||||
entrance_offsets = {
|
||||
'Sanctuary': 0x2,
|
||||
'HC West': 0x3,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"cli": {
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"app.title": "ALttP Door Randomizer Version %s - Seed: %s, Code: %s",
|
||||
"app.title": "ALttP Door Randomizer Version %s : --seed %s --code %s",
|
||||
"version": "Version",
|
||||
"seed": "Seed",
|
||||
"player": "Player",
|
||||
|
||||
Reference in New Issue
Block a user