Combinatoric approach revised (KLA1)
Backported some fixes
This commit is contained in:
309
BaseClasses.py
309
BaseClasses.py
@@ -458,9 +458,10 @@ class World(object):
|
||||
|
||||
class CollectionState(object):
|
||||
|
||||
def __init__(self, parent):
|
||||
self.prog_items = Counter()
|
||||
def __init__(self, parent, skip_init=False):
|
||||
self.world = parent
|
||||
if not skip_init:
|
||||
self.prog_items = Counter()
|
||||
self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)}
|
||||
self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)}
|
||||
self.events = []
|
||||
@@ -469,6 +470,14 @@ class CollectionState(object):
|
||||
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.ghost_keys = Counter()
|
||||
self.dungeon_limits = None
|
||||
|
||||
def update_reachable_regions(self, player):
|
||||
self.stale[player] = False
|
||||
@@ -479,66 +488,261 @@ 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)]
|
||||
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:
|
||||
while len(queue) > 0:
|
||||
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):
|
||||
if not self.should_visit(new_region, rrp, crystal_state, player):
|
||||
bc.pop(connection, None)
|
||||
elif connection.can_reach(self):
|
||||
bc.pop(connection, None)
|
||||
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]
|
||||
|
||||
rrp[new_region] = new_crystal_state
|
||||
|
||||
for exit in new_region.exits:
|
||||
door = exit.door
|
||||
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))
|
||||
bc[conn] = door_crystal_state
|
||||
queue.append((conn, door_crystal_state))
|
||||
elif door is None:
|
||||
queue.append((exit, new_crystal_state))
|
||||
# note: no door in dungeon indicates what exactly? (always traversable)?
|
||||
queue.append((conn, new_crystal_state))
|
||||
else:
|
||||
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))
|
||||
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))
|
||||
|
||||
# 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
|
||||
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) and not self.world.retro[player]: # todo: retro
|
||||
door = connection.door
|
||||
dungeon_name = connection.parent_region.dungeon.name # todo: universal
|
||||
key_logic = self.world.key_logic[player][dungeon_name]
|
||||
if door.name not in self.reached_doors[player]:
|
||||
self.door_counter[player][0][dungeon_name] += 1
|
||||
self.reached_doors[player].add(door.name)
|
||||
if key_logic.sm_doors[door]:
|
||||
self.reached_doors[player].add(key_logic.sm_doors[door].name)
|
||||
if not connection.can_reach(self):
|
||||
checklist = self.dungeons_to_check[player][dungeon_name]
|
||||
checklist[connection.name] = (connection, crystal_state)
|
||||
elif door.name not in self.opened_doors[player]:
|
||||
opened_doors = self.opened_doors[player]
|
||||
door = connection.door
|
||||
if door.name not in opened_doors:
|
||||
self.door_counter[player][1][dungeon_name] += 1
|
||||
opened_doors.add(door.name)
|
||||
key_logic = self.world.key_logic[player][dungeon_name]
|
||||
if key_logic.sm_doors[door]:
|
||||
opened_doors.add(key_logic.sm_doors[door].name)
|
||||
|
||||
def should_visit(self, new_region, rrp, crystal_state, player):
|
||||
if not new_region:
|
||||
return False
|
||||
if self.dungeon_limits and not self.possibly_connected_to_dungeon(new_region, player):
|
||||
return False
|
||||
if new_region not in rrp:
|
||||
return True
|
||||
if new_region.type != RegionType.Dungeon:
|
||||
return False
|
||||
return (rrp[new_region] & crystal_state) != crystal_state
|
||||
|
||||
def possibly_connected_to_dungeon(self, new_region, player):
|
||||
if new_region.dungeon:
|
||||
return new_region.dungeon.name in self.dungeon_limits
|
||||
else:
|
||||
return new_region.name in self.world.inaccessible_regions[player]
|
||||
|
||||
@staticmethod
|
||||
def valid_crystal(door, new_crystal_state):
|
||||
return (not door.crystal or door.crystal == CrystalBarrier.Either or new_crystal_state == CrystalBarrier.Either
|
||||
or new_crystal_state == door.crystal)
|
||||
|
||||
def check_key_doors_in_dungeons(self, rrp, player):
|
||||
for dungeon_name, checklist in self.dungeons_to_check[player].items():
|
||||
init_door_candidates = self.should_explore_child_state(self, dungeon_name, player)
|
||||
key_total = self.prog_items[(dungeon_keys[dungeon_name], player)] # todo: universal
|
||||
remaining_keys = key_total - self.door_counter[player][1][dungeon_name]
|
||||
if not init_door_candidates or remaining_keys == 0:
|
||||
continue
|
||||
dungeon_doors = {x.name for x in self.world.key_logic[player][dungeon_name].sm_doors.keys()}
|
||||
|
||||
def valid_d_door(x):
|
||||
return x in dungeon_doors
|
||||
|
||||
child_states = deque()
|
||||
child_states.append(self)
|
||||
visited_opened_doors = set()
|
||||
visited_opened_doors.add(frozenset(self.opened_doors[player]))
|
||||
terminal_states, done, common_regions, common_bc, common_doors = [], False, {}, {}, set()
|
||||
while not done:
|
||||
terminal_states.clear()
|
||||
while len(child_states) > 0:
|
||||
next_child = child_states.popleft()
|
||||
door_candidates = CollectionState.should_explore_child_state(next_child, dungeon_name, player)
|
||||
if door_candidates:
|
||||
for chosen_door in door_candidates:
|
||||
child_state = next_child.copy()
|
||||
child_queue = deque()
|
||||
child_state.door_counter[player][1][dungeon_name] += 1
|
||||
if isinstance(chosen_door, tuple):
|
||||
child_state.opened_doors[player].add(chosen_door[0])
|
||||
child_state.opened_doors[player].add(chosen_door[1])
|
||||
if chosen_door[0] in checklist:
|
||||
child_queue.append(checklist[chosen_door[0]])
|
||||
if chosen_door[1] in checklist:
|
||||
child_queue.append(checklist[chosen_door[1]])
|
||||
else:
|
||||
child_state.opened_doors[player].add(chosen_door)
|
||||
if chosen_door in checklist:
|
||||
child_queue.append(checklist[chosen_door])
|
||||
if child_state.opened_doors[player] not in visited_opened_doors:
|
||||
done = False
|
||||
while not done:
|
||||
rrp_ = child_state.reachable_regions[player]
|
||||
bc_ = child_state.blocked_connections[player]
|
||||
self.dungeon_limits = [dungeon_name]
|
||||
child_state.traverse_world(child_queue, rrp_, bc_, player)
|
||||
new_events = child_state.sweep_for_events_once()
|
||||
child_state.stale[player] = False
|
||||
if new_events:
|
||||
for conn in bc_:
|
||||
if conn.parent_region.dungeon and conn.parent_region.dungeon.name == dungeon_name:
|
||||
child_queue.append((conn, bc_[conn]))
|
||||
done = not new_events
|
||||
visited_opened_doors.add(frozenset(child_state.opened_doors[player]))
|
||||
child_states.append(child_state)
|
||||
else:
|
||||
terminal_states.append(next_child)
|
||||
common_regions, common_doors, first = {}, set(), True
|
||||
for term_state in terminal_states:
|
||||
t_rrp = term_state.reachable_regions[player]
|
||||
if first:
|
||||
first = False
|
||||
common_regions = {x: y for x, y in t_rrp.items() if x not in rrp or y != rrp[x]}
|
||||
common_doors = {x for x in term_state.opened_doors[player] - self.opened_doors[player]
|
||||
if valid_d_door(x)}
|
||||
else:
|
||||
cm_rrp = {x: y for x, y in t_rrp.items() if x not in rrp or y != rrp[x]}
|
||||
common_regions = {k: self.comb_crys(v, cm_rrp[k]) for k, v in common_regions.items()
|
||||
if k in cm_rrp and self.crys_agree(v, cm_rrp[k])}
|
||||
common_doors &= {x for x in term_state.opened_doors[player] - self.opened_doors[player]
|
||||
if valid_d_door(x)}
|
||||
done = len(child_states) == 0
|
||||
|
||||
terminal_queue = deque()
|
||||
for door in common_doors:
|
||||
self.opened_doors[player].add(door)
|
||||
if door in checklist:
|
||||
terminal_queue.append(checklist[door])
|
||||
if self.find_door_pair(player, dungeon_name, door) not in self.opened_doors[player]:
|
||||
self.door_counter[player][1][dungeon_name] += 1
|
||||
|
||||
self.dungeon_limits = [dungeon_name]
|
||||
rrp_ = self.reachable_regions[player]
|
||||
bc_ = self.blocked_connections[player]
|
||||
self.traverse_world(terminal_queue, rrp_, bc_, player)
|
||||
self.dungeon_limits = None
|
||||
|
||||
rrp = self.reachable_regions[player]
|
||||
missing_regions = {x: y for x, y in common_regions.items() if x not in rrp}
|
||||
for k in missing_regions:
|
||||
rrp[k] = missing_regions[k]
|
||||
checklist.clear()
|
||||
|
||||
@staticmethod
|
||||
def comb_crys(a, b):
|
||||
return a if a == b or a != CrystalBarrier.Either else b
|
||||
|
||||
@staticmethod
|
||||
def crys_agree(a, b):
|
||||
return a == b or a == CrystalBarrier.Either or b == CrystalBarrier.Either
|
||||
|
||||
def find_door_pair(self, player, dungeon_name, name):
|
||||
for door in self.world.key_logic[player][dungeon_name].sm_doors.keys():
|
||||
if door.name == name:
|
||||
paired_door = self.world.key_logic[player][dungeon_name].sm_doors[door]
|
||||
return paired_door.name if paired_door else None
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def should_explore_child_state(state, dungeon_name, player):
|
||||
small_key_name = dungeon_keys[dungeon_name] # todo: universal
|
||||
key_total = state.prog_items[(small_key_name, player)] + state.ghost_keys[(small_key_name, player)]
|
||||
remaining_keys = key_total - state.door_counter[player][1][dungeon_name]
|
||||
unopened_doors = state.door_counter[player][0][dungeon_name] - state.door_counter[player][1][dungeon_name]
|
||||
if remaining_keys > 0 and unopened_doors > 0:
|
||||
key_logic = state.world.key_logic[player][dungeon_name] # todo: universal
|
||||
door_candidates, skip = [], set()
|
||||
for door, paired in key_logic.sm_doors.items():
|
||||
if door.name in state.reached_doors[player] and door.name not in state.opened_doors[player]:
|
||||
if door.name not in skip:
|
||||
if paired:
|
||||
door_candidates.append((door.name, paired.name))
|
||||
skip.add(paired.name)
|
||||
else:
|
||||
door_candidates.append(door.name)
|
||||
return door_candidates
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def print_rrp(rrp):
|
||||
logger = logging.getLogger('')
|
||||
logger.debug('RRP Checking')
|
||||
for region, packet in rrp.items():
|
||||
new_crystal_state, logic, path = packet
|
||||
logger.debug(f'\nRegion: {region.name} (CS: {str(new_crystal_state)})')
|
||||
for i in range(0, len(logic)):
|
||||
logger.debug(f'{logic[i]}')
|
||||
logger.debug(f'{",".join(str(x) for x in path[i])}')
|
||||
|
||||
def copy(self):
|
||||
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)}
|
||||
# todo: verify if this isn't copied deep enough
|
||||
ret.dungeons_to_check = {
|
||||
player: defaultdict(dict, {name: copy.copy(checklist)
|
||||
for name, checklist in self.dungeons_to_check[player].items()})
|
||||
for player in range(1, self.world.players + 1)}
|
||||
ret.ghost_keys = self.ghost_keys.copy()
|
||||
return ret
|
||||
|
||||
def can_reach(self, spot, resolution_hint=None, player=None):
|
||||
@@ -556,6 +760,19 @@ class CollectionState(object):
|
||||
|
||||
return spot.can_reach(self)
|
||||
|
||||
def sweep_for_events_once(self, key_only=False, locations=None):
|
||||
if locations is None:
|
||||
locations = self.world.get_filled_locations()
|
||||
checked_locations = set([l for l in locations if l in self.locations_checked])
|
||||
reachable_events = [location for location in locations if location.event and
|
||||
(not key_only or (not self.world.keyshuffle[location.item.player] and location.item.smallkey) or (not self.world.bigkeyshuffle[location.item.player] and location.item.bigkey))
|
||||
and location.can_reach(self)]
|
||||
reachable_events = self._do_not_flood_the_keys(reachable_events)
|
||||
for event in reachable_events:
|
||||
if event not in checked_locations:
|
||||
self.events.append((event.name, event.player))
|
||||
self.collect(event.item, True, event)
|
||||
return len(reachable_events) > len(checked_locations)
|
||||
|
||||
def sweep_for_events(self, key_only=False, locations=None):
|
||||
# this may need improvement
|
||||
@@ -603,6 +820,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']
|
||||
@@ -1806,6 +2030,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__())
|
||||
|
||||
@@ -2196,6 +2429,21 @@ 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)'
|
||||
}
|
||||
|
||||
class PotItem(FastEnum):
|
||||
Nothing = 0x0
|
||||
@@ -2346,3 +2594,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(Enum):
|
||||
WorstCase = 0
|
||||
AllowSmall = 1
|
||||
Lock = 2
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import 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):
|
||||
@@ -212,8 +211,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
|
||||
@@ -1576,28 +1575,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)
|
||||
|
||||
15
Dungeons.py
15
Dungeons.py
@@ -375,21 +375,6 @@ flexible_starts = {
|
||||
'Skull Woods': ['Skull Left Drop', 'Skull Pinball']
|
||||
}
|
||||
|
||||
dungeon_keys = {
|
||||
'Hyrule Castle': 'Small Key (Escape)',
|
||||
'Eastern Palace': 'Small Key (Eastern Palace)',
|
||||
'Desert Palace': 'Small Key (Desert Palace)',
|
||||
'Tower of Hera': 'Small Key (Tower of Hera)',
|
||||
'Agahnims Tower': 'Small Key (Agahnims Tower)',
|
||||
'Palace of Darkness': 'Small Key (Palace of Darkness)',
|
||||
'Swamp Palace': 'Small Key (Swamp Palace)',
|
||||
'Skull Woods': 'Small Key (Skull Woods)',
|
||||
'Thieves Town': 'Small Key (Thieves Town)',
|
||||
'Ice Palace': 'Small Key (Ice Palace)',
|
||||
'Misery Mire': 'Small Key (Misery Mire)',
|
||||
'Turtle Rock': 'Small Key (Turtle Rock)',
|
||||
'Ganons Tower': 'Small Key (Ganons Tower)'
|
||||
}
|
||||
|
||||
dungeon_bigs = {
|
||||
'Hyrule Castle': 'Big Key (Escape)',
|
||||
|
||||
14
Fill.py
14
Fill.py
@@ -199,6 +199,7 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool =
|
||||
|
||||
spot_to_fill = None
|
||||
|
||||
valid_locations = []
|
||||
for location in locations:
|
||||
if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there
|
||||
location.item = item_to_place
|
||||
@@ -209,11 +210,16 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool =
|
||||
if (not single_player_placement or location.player == item_to_place.player)\
|
||||
and location.can_fill(test_state, item_to_place, perform_access_check)\
|
||||
and valid_key_placement(item_to_place, location, itempool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world):
|
||||
# todo: optimization: break instead of cataloging all valid locations
|
||||
if not spot_to_fill:
|
||||
spot_to_fill = location
|
||||
break
|
||||
elif item_to_place.smallkey or item_to_place.bigkey:
|
||||
valid_locations.append(location)
|
||||
|
||||
if item_to_place.smallkey or item_to_place.bigkey:
|
||||
location.item = None
|
||||
|
||||
logging.getLogger('').debug(f'{item_to_place} valid placement at {len(valid_locations)} locations')
|
||||
|
||||
if spot_to_fill is None:
|
||||
# we filled all reachable spots. Maybe the game can be beaten anyway?
|
||||
unplaced_items.insert(0, item_to_place)
|
||||
@@ -250,9 +256,7 @@ def valid_key_placement(item, location, itempool, world):
|
||||
def track_outside_keys(item, location, world):
|
||||
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:
|
||||
|
||||
@@ -2,9 +2,9 @@ import itertools
|
||||
import logging
|
||||
from collections import defaultdict, deque
|
||||
|
||||
from BaseClasses import DoorType
|
||||
from BaseClasses import DoorType, dungeon_keys, KeyRuleType
|
||||
from Regions import dungeon_events
|
||||
from Dungeons import dungeon_keys, dungeon_bigs
|
||||
from Dungeons import dungeon_bigs
|
||||
from DungeonGenerator import ExplorationState, special_big_key_doors
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ class KeyLayout(object):
|
||||
self.all_locations = set()
|
||||
self.item_locations = set()
|
||||
|
||||
self.found_doors = set()
|
||||
# bk special?
|
||||
# bk required? True if big chests or big doors exists
|
||||
|
||||
@@ -54,6 +55,7 @@ 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:
|
||||
@@ -65,6 +67,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 +90,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):
|
||||
@@ -209,8 +222,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))
|
||||
@@ -247,8 +271,9 @@ def analyze_dungeon(key_layout, world, player):
|
||||
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)
|
||||
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
|
||||
@@ -258,7 +283,8 @@ def analyze_dungeon(key_layout, world, player):
|
||||
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():
|
||||
@@ -294,7 +320,7 @@ 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)
|
||||
# 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)
|
||||
@@ -320,6 +346,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))
|
||||
@@ -478,7 +505,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 +565,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 +623,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?
|
||||
@@ -717,7 +763,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 +782,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)
|
||||
@@ -845,16 +896,16 @@ def big_key_drop_available(key_counter):
|
||||
def bk_restricted_rules(rule, door, odd_counter, empty_flag, key_counter, key_layout, world, player):
|
||||
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 +913,8 @@ def bk_restricted_rules(rule, door, odd_counter, empty_flag, key_counter, key_la
|
||||
if len(unique_loc) > 0: # and bk_rule.is_valid
|
||||
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):
|
||||
@@ -935,6 +986,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
|
||||
@@ -1197,7 +1249,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:
|
||||
@@ -1381,6 +1433,7 @@ def cnt_avail_big_locations(ttl_locations, state, world, player):
|
||||
|
||||
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':
|
||||
@@ -1403,6 +1456,9 @@ 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)
|
||||
@@ -1520,11 +1576,11 @@ def find_counter_hint(opened_doors, bk_hint, key_layout):
|
||||
|
||||
|
||||
def find_max_counter(key_layout):
|
||||
max_counter = find_counter_hint(dict.fromkeys(key_layout.flat_prop), False, key_layout)
|
||||
max_counter = find_counter_hint(dict.fromkeys(key_layout.found_doors), False, key_layout)
|
||||
if max_counter is None:
|
||||
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)
|
||||
return max_counter
|
||||
|
||||
|
||||
|
||||
1
Main.py
1
Main.py
@@ -448,6 +448,7 @@ def copy_world(world):
|
||||
# these need to be modified properly by set_rules
|
||||
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:
|
||||
|
||||
48
Rules.py
48
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
|
||||
|
||||
@@ -1939,14 +1939,10 @@ 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))
|
||||
if rule.allow_small:
|
||||
set_always_allow(rule.small_location, allow_self_locking_small(d_logic, player))
|
||||
for location in d_logic.bk_restricted:
|
||||
if not location.forced_item:
|
||||
forbid_item(location, d_logic.bk_name, player)
|
||||
@@ -1954,6 +1950,7 @@ def add_key_logic_rules(world, player):
|
||||
forbid_item(location, d_logic.small_key_name, player)
|
||||
for door in d_logic.bk_doors:
|
||||
add_rule(world.get_entrance(door.name, player), create_rule(d_logic.bk_name, player))
|
||||
if len(d_logic.bk_doors) > 0 or len(d_logic.bk_chests) > 1:
|
||||
for chest in d_logic.bk_chests:
|
||||
add_rule(world.get_location(chest.name, player), create_rule(d_logic.bk_name, player))
|
||||
if world.retro[player]:
|
||||
@@ -1963,6 +1960,39 @@ def add_key_logic_rules(world, player):
|
||||
add_rule(door.entrance, create_key_rule('Small Key (Universal)', player, 1))
|
||||
|
||||
|
||||
def allow_self_locking_small(logic, player):
|
||||
return lambda state, item: item.player == player and logic.small_key_name == item.name
|
||||
|
||||
|
||||
def eval_small_key_door_main(state, door_name, dungeon, player):
|
||||
if state.is_door_open(door_name, player):
|
||||
return True
|
||||
key_logic = state.world.key_logic[player][dungeon]
|
||||
door_rule = key_logic.door_rules[door_name]
|
||||
door_openable = False
|
||||
for ruleType, number in door_rule.new_rules.items():
|
||||
if door_openable:
|
||||
return True
|
||||
if ruleType == KeyRuleType.WorstCase:
|
||||
door_openable |= state.has_sm_key(key_logic.small_key_name, player, number)
|
||||
elif ruleType == KeyRuleType.AllowSmall:
|
||||
if door_rule.small_location.item and door_rule.small_location.item.name == key_logic.small_key_name:
|
||||
return True # always okay if allow small is on
|
||||
elif isinstance(ruleType, tuple):
|
||||
lock, lock_item = ruleType
|
||||
# this doesn't track logical locks yet, i.e. hammer locks the item and hammer is there, but the item isn't
|
||||
for loc in door_rule.alternate_big_key_loc:
|
||||
spot = state.world.get_location(loc, player)
|
||||
if spot.item and spot.item.name == lock_item:
|
||||
door_openable |= state.has_sm_key(key_logic.small_key_name, player, number)
|
||||
break
|
||||
return door_openable
|
||||
|
||||
|
||||
def eval_small_key_door(door_name, dungeon, player):
|
||||
return lambda state: eval_small_key_door_main(state, door_name, dungeon, player)
|
||||
|
||||
|
||||
def retro_in_hc(spot):
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user