Combinatoric approach revised (KLA1)

Backported some fixes
This commit is contained in:
aerinon
2021-06-29 16:34:28 -06:00
parent 62fff1f6e1
commit b21564d5aa
8 changed files with 490 additions and 158 deletions

View File

@@ -458,9 +458,10 @@ class World(object):
class CollectionState(object): class CollectionState(object):
def __init__(self, parent): def __init__(self, parent, skip_init=False):
self.prog_items = Counter()
self.world = parent self.world = parent
if not skip_init:
self.prog_items = Counter()
self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)} self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)}
self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)} self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)}
self.events = [] self.events = []
@@ -469,6 +470,14 @@ class CollectionState(object):
self.stale = {player: True for player in range(1, parent.players + 1)} self.stale = {player: True for player in range(1, parent.players + 1)}
for item in parent.precollected_items: for item in parent.precollected_items:
self.collect(item, True) self.collect(item, True)
# reached vs. opened in the counter
self.door_counter = {player: (Counter(), Counter()) for player in range(1, parent.players + 1)}
self.reached_doors = {player: set() for player in range(1, parent.players + 1)}
self.opened_doors = {player: set() for player in range(1, parent.players + 1)}
self.dungeons_to_check = {player: defaultdict(dict) for player in range(1, parent.players + 1)}
self.ghost_keys = Counter()
self.dungeon_limits = None
def update_reachable_regions(self, player): def update_reachable_regions(self, player):
self.stale[player] = False self.stale[player] = False
@@ -479,66 +488,261 @@ class CollectionState(object):
start = self.world.get_region('Menu', player) start = self.world.get_region('Menu', player)
if not start in rrp: if not start in rrp:
rrp[start] = CrystalBarrier.Orange rrp[start] = CrystalBarrier.Orange
for exit in start.exits: for conn in start.exits:
bc[exit] = CrystalBarrier.Orange bc[conn] = CrystalBarrier.Orange
queue = deque(self.blocked_connections[player].items()) queue = deque(self.blocked_connections[player].items())
self.traverse_world(queue, rrp, bc, player)
unresolved_events = [x for y in self.reachable_regions[player] for x in y.locations
if x.event and x.item and (x.item.smallkey or x.item.bigkey or x.item.advancement)
and x not in self.locations_checked and x.can_reach(self)]
if len(unresolved_events) == 0:
self.check_key_doors_in_dungeons(rrp, player)
def traverse_world(self, queue, rrp, bc, player):
# run BFS on all connections, and keep track of those blocked by missing items # run BFS on all connections, and keep track of those blocked by missing items
while True: while len(queue) > 0:
try:
connection, crystal_state = queue.popleft() connection, crystal_state = queue.popleft()
new_region = connection.connected_region new_region = connection.connected_region
if new_region is None or new_region in rrp and (new_region.type != RegionType.Dungeon or (rrp[new_region] & crystal_state) == crystal_state): if not self.should_visit(new_region, rrp, crystal_state, player):
bc.pop(connection, None) bc.pop(connection, None)
elif connection.can_reach(self): elif connection.can_reach(self):
bc.pop(connection, None)
if new_region.type == RegionType.Dungeon: if new_region.type == RegionType.Dungeon:
new_crystal_state = crystal_state new_crystal_state = crystal_state
for exit in new_region.exits:
door = exit.door
if door is not None and door.crystal == CrystalBarrier.Either and door.entrance.can_reach(self):
new_crystal_state = CrystalBarrier.Either
break
if new_region in rrp: if new_region in rrp:
new_crystal_state |= rrp[new_region] new_crystal_state |= rrp[new_region]
rrp[new_region] = new_crystal_state rrp[new_region] = new_crystal_state
for conn in new_region.exits:
for exit in new_region.exits: door = conn.door
door = exit.door
if door is not None and not door.blocked: if door is not None and not door.blocked:
if self.valid_crystal(door, new_crystal_state):
door_crystal_state = door.crystal if door.crystal else new_crystal_state door_crystal_state = door.crystal if door.crystal else new_crystal_state
bc[exit] = door_crystal_state bc[conn] = door_crystal_state
queue.append((exit, door_crystal_state)) queue.append((conn, door_crystal_state))
elif door is None: elif door is None:
queue.append((exit, new_crystal_state)) # note: no door in dungeon indicates what exactly? (always traversable)?
queue.append((conn, new_crystal_state))
else: else:
new_crystal_state = CrystalBarrier.Orange new_crystal_state = CrystalBarrier.Orange
rrp[new_region] = new_crystal_state rrp[new_region] = new_crystal_state
bc.pop(connection, None) for conn in new_region.exits:
for exit in new_region.exits: bc[conn] = new_crystal_state
bc[exit] = new_crystal_state queue.append((conn, new_crystal_state))
queue.append((exit, new_crystal_state))
self.path[new_region] = (new_region.name, self.path.get(connection, None)) self.path[new_region] = (new_region.name, self.path.get(connection, None))
# Retry connections if the new region can unblock them # Retry connections if the new region can unblock them
if new_region.name in indirect_connections: if new_region.name in indirect_connections:
new_entrance = self.world.get_entrance(indirect_connections[new_region.name], player) new_entrance = self.world.get_entrance(indirect_connections[new_region.name], player)
if new_entrance in bc and new_entrance not in queue and new_entrance.parent_region in rrp: if new_entrance in bc and new_entrance.parent_region in rrp:
queue.append((new_entrance, rrp[new_entrance.parent_region])) new_crystal_state = rrp[new_entrance.parent_region]
except IndexError: if (new_entrance, new_crystal_state) not in queue:
break queue.append((new_entrance, new_crystal_state))
# else those connections that are not accessible yet
if self.is_small_door(connection) and not self.world.retro[player]: # todo: retro
door = connection.door
dungeon_name = connection.parent_region.dungeon.name # todo: universal
key_logic = self.world.key_logic[player][dungeon_name]
if door.name not in self.reached_doors[player]:
self.door_counter[player][0][dungeon_name] += 1
self.reached_doors[player].add(door.name)
if key_logic.sm_doors[door]:
self.reached_doors[player].add(key_logic.sm_doors[door].name)
if not connection.can_reach(self):
checklist = self.dungeons_to_check[player][dungeon_name]
checklist[connection.name] = (connection, crystal_state)
elif door.name not in self.opened_doors[player]:
opened_doors = self.opened_doors[player]
door = connection.door
if door.name not in opened_doors:
self.door_counter[player][1][dungeon_name] += 1
opened_doors.add(door.name)
key_logic = self.world.key_logic[player][dungeon_name]
if key_logic.sm_doors[door]:
opened_doors.add(key_logic.sm_doors[door].name)
def should_visit(self, new_region, rrp, crystal_state, player):
if not new_region:
return False
if self.dungeon_limits and not self.possibly_connected_to_dungeon(new_region, player):
return False
if new_region not in rrp:
return True
if new_region.type != RegionType.Dungeon:
return False
return (rrp[new_region] & crystal_state) != crystal_state
def possibly_connected_to_dungeon(self, new_region, player):
if new_region.dungeon:
return new_region.dungeon.name in self.dungeon_limits
else:
return new_region.name in self.world.inaccessible_regions[player]
@staticmethod
def valid_crystal(door, new_crystal_state):
return (not door.crystal or door.crystal == CrystalBarrier.Either or new_crystal_state == CrystalBarrier.Either
or new_crystal_state == door.crystal)
def check_key_doors_in_dungeons(self, rrp, player):
for dungeon_name, checklist in self.dungeons_to_check[player].items():
init_door_candidates = self.should_explore_child_state(self, dungeon_name, player)
key_total = self.prog_items[(dungeon_keys[dungeon_name], player)] # todo: universal
remaining_keys = key_total - self.door_counter[player][1][dungeon_name]
if not init_door_candidates or remaining_keys == 0:
continue
dungeon_doors = {x.name for x in self.world.key_logic[player][dungeon_name].sm_doors.keys()}
def valid_d_door(x):
return x in dungeon_doors
child_states = deque()
child_states.append(self)
visited_opened_doors = set()
visited_opened_doors.add(frozenset(self.opened_doors[player]))
terminal_states, done, common_regions, common_bc, common_doors = [], False, {}, {}, set()
while not done:
terminal_states.clear()
while len(child_states) > 0:
next_child = child_states.popleft()
door_candidates = CollectionState.should_explore_child_state(next_child, dungeon_name, player)
if door_candidates:
for chosen_door in door_candidates:
child_state = next_child.copy()
child_queue = deque()
child_state.door_counter[player][1][dungeon_name] += 1
if isinstance(chosen_door, tuple):
child_state.opened_doors[player].add(chosen_door[0])
child_state.opened_doors[player].add(chosen_door[1])
if chosen_door[0] in checklist:
child_queue.append(checklist[chosen_door[0]])
if chosen_door[1] in checklist:
child_queue.append(checklist[chosen_door[1]])
else:
child_state.opened_doors[player].add(chosen_door)
if chosen_door in checklist:
child_queue.append(checklist[chosen_door])
if child_state.opened_doors[player] not in visited_opened_doors:
done = False
while not done:
rrp_ = child_state.reachable_regions[player]
bc_ = child_state.blocked_connections[player]
self.dungeon_limits = [dungeon_name]
child_state.traverse_world(child_queue, rrp_, bc_, player)
new_events = child_state.sweep_for_events_once()
child_state.stale[player] = False
if new_events:
for conn in bc_:
if conn.parent_region.dungeon and conn.parent_region.dungeon.name == dungeon_name:
child_queue.append((conn, bc_[conn]))
done = not new_events
visited_opened_doors.add(frozenset(child_state.opened_doors[player]))
child_states.append(child_state)
else:
terminal_states.append(next_child)
common_regions, common_doors, first = {}, set(), True
for term_state in terminal_states:
t_rrp = term_state.reachable_regions[player]
if first:
first = False
common_regions = {x: y for x, y in t_rrp.items() if x not in rrp or y != rrp[x]}
common_doors = {x for x in term_state.opened_doors[player] - self.opened_doors[player]
if valid_d_door(x)}
else:
cm_rrp = {x: y for x, y in t_rrp.items() if x not in rrp or y != rrp[x]}
common_regions = {k: self.comb_crys(v, cm_rrp[k]) for k, v in common_regions.items()
if k in cm_rrp and self.crys_agree(v, cm_rrp[k])}
common_doors &= {x for x in term_state.opened_doors[player] - self.opened_doors[player]
if valid_d_door(x)}
done = len(child_states) == 0
terminal_queue = deque()
for door in common_doors:
self.opened_doors[player].add(door)
if door in checklist:
terminal_queue.append(checklist[door])
if self.find_door_pair(player, dungeon_name, door) not in self.opened_doors[player]:
self.door_counter[player][1][dungeon_name] += 1
self.dungeon_limits = [dungeon_name]
rrp_ = self.reachable_regions[player]
bc_ = self.blocked_connections[player]
self.traverse_world(terminal_queue, rrp_, bc_, player)
self.dungeon_limits = None
rrp = self.reachable_regions[player]
missing_regions = {x: y for x, y in common_regions.items() if x not in rrp}
for k in missing_regions:
rrp[k] = missing_regions[k]
checklist.clear()
@staticmethod
def comb_crys(a, b):
return a if a == b or a != CrystalBarrier.Either else b
@staticmethod
def crys_agree(a, b):
return a == b or a == CrystalBarrier.Either or b == CrystalBarrier.Either
def find_door_pair(self, player, dungeon_name, name):
for door in self.world.key_logic[player][dungeon_name].sm_doors.keys():
if door.name == name:
paired_door = self.world.key_logic[player][dungeon_name].sm_doors[door]
return paired_door.name if paired_door else None
return None
@staticmethod
def should_explore_child_state(state, dungeon_name, player):
small_key_name = dungeon_keys[dungeon_name] # todo: universal
key_total = state.prog_items[(small_key_name, player)] + state.ghost_keys[(small_key_name, player)]
remaining_keys = key_total - state.door_counter[player][1][dungeon_name]
unopened_doors = state.door_counter[player][0][dungeon_name] - state.door_counter[player][1][dungeon_name]
if remaining_keys > 0 and unopened_doors > 0:
key_logic = state.world.key_logic[player][dungeon_name] # todo: universal
door_candidates, skip = [], set()
for door, paired in key_logic.sm_doors.items():
if door.name in state.reached_doors[player] and door.name not in state.opened_doors[player]:
if door.name not in skip:
if paired:
door_candidates.append((door.name, paired.name))
skip.add(paired.name)
else:
door_candidates.append(door.name)
return door_candidates
return None
@staticmethod
def print_rrp(rrp):
logger = logging.getLogger('')
logger.debug('RRP Checking')
for region, packet in rrp.items():
new_crystal_state, logic, path = packet
logger.debug(f'\nRegion: {region.name} (CS: {str(new_crystal_state)})')
for i in range(0, len(logic)):
logger.debug(f'{logic[i]}')
logger.debug(f'{",".join(str(x) for x in path[i])}')
def copy(self): def copy(self):
ret = CollectionState(self.world) ret = CollectionState(self.world, skip_init=True)
ret.prog_items = self.prog_items.copy() ret.prog_items = self.prog_items.copy()
ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in range(1, self.world.players + 1)} ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in range(1, self.world.players + 1)}
ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in range(1, self.world.players + 1)} ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in range(1, self.world.players + 1)}
ret.events = copy.copy(self.events) ret.events = copy.copy(self.events)
ret.path = copy.copy(self.path) ret.path = copy.copy(self.path)
ret.locations_checked = copy.copy(self.locations_checked) ret.locations_checked = copy.copy(self.locations_checked)
ret.stale = {player: self.stale[player] for player in range(1, self.world.players + 1)}
ret.door_counter = {player: (copy.copy(self.door_counter[player][0]), copy.copy(self.door_counter[player][1]))
for player in range(1, self.world.players + 1)}
ret.reached_doors = {player: copy.copy(self.reached_doors[player]) for player in range(1, self.world.players + 1)}
ret.opened_doors = {player: copy.copy(self.opened_doors[player]) for player in range(1, self.world.players + 1)}
# todo: verify if this isn't copied deep enough
ret.dungeons_to_check = {
player: defaultdict(dict, {name: copy.copy(checklist)
for name, checklist in self.dungeons_to_check[player].items()})
for player in range(1, self.world.players + 1)}
ret.ghost_keys = self.ghost_keys.copy()
return ret return ret
def can_reach(self, spot, resolution_hint=None, player=None): def can_reach(self, spot, resolution_hint=None, player=None):
@@ -556,6 +760,19 @@ class CollectionState(object):
return spot.can_reach(self) return spot.can_reach(self)
def sweep_for_events_once(self, key_only=False, locations=None):
if locations is None:
locations = self.world.get_filled_locations()
checked_locations = set([l for l in locations if l in self.locations_checked])
reachable_events = [location for location in locations if location.event and
(not key_only or (not self.world.keyshuffle[location.item.player] and location.item.smallkey) or (not self.world.bigkeyshuffle[location.item.player] and location.item.bigkey))
and location.can_reach(self)]
reachable_events = self._do_not_flood_the_keys(reachable_events)
for event in reachable_events:
if event not in checked_locations:
self.events.append((event.name, event.player))
self.collect(event.item, True, event)
return len(reachable_events) > len(checked_locations)
def sweep_for_events(self, key_only=False, locations=None): def sweep_for_events(self, key_only=False, locations=None):
# this may need improvement # this may need improvement
@@ -603,6 +820,13 @@ class CollectionState(object):
or not self.location_can_be_flooded(flood_location)) or not self.location_can_be_flooded(flood_location))
return True return True
@staticmethod
def is_small_door(connection):
return connection and connection.door and connection.door.smallKey
def is_door_open(self, door_name, player):
return door_name in self.opened_doors[player]
@staticmethod @staticmethod
def location_can_be_flooded(location): def location_can_be_flooded(location):
return location.parent_region.name in ['Swamp Trench 1 Alcove', 'Swamp Trench 2 Alcove'] return location.parent_region.name in ['Swamp Trench 1 Alcove', 'Swamp Trench 2 Alcove']
@@ -1806,6 +2030,15 @@ class Item(object):
def compass(self): def compass(self):
return self.type == 'Compass' return self.type == 'Compass'
@property
def dungeon(self):
if not self.smallkey and not self.bigkey and not self.map and not self.compass:
return None
item_dungeon = self.name.split('(')[1][:-1]
if item_dungeon == 'Escape':
item_dungeon = 'Hyrule Castle'
return item_dungeon
def __str__(self): def __str__(self):
return str(self.__unicode__()) return str(self.__unicode__())
@@ -2196,6 +2429,21 @@ dungeon_names = [
'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace', 'Misery Mire', 'Turtle Rock', 'Ganons Tower' 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace', 'Misery Mire', 'Turtle Rock', 'Ganons Tower'
] ]
dungeon_keys = {
'Hyrule Castle': 'Small Key (Escape)',
'Eastern Palace': 'Small Key (Eastern Palace)',
'Desert Palace': 'Small Key (Desert Palace)',
'Tower of Hera': 'Small Key (Tower of Hera)',
'Agahnims Tower': 'Small Key (Agahnims Tower)',
'Palace of Darkness': 'Small Key (Palace of Darkness)',
'Swamp Palace': 'Small Key (Swamp Palace)',
'Skull Woods': 'Small Key (Skull Woods)',
'Thieves Town': 'Small Key (Thieves Town)',
'Ice Palace': 'Small Key (Ice Palace)',
'Misery Mire': 'Small Key (Misery Mire)',
'Turtle Rock': 'Small Key (Turtle Rock)',
'Ganons Tower': 'Small Key (Ganons Tower)'
}
class PotItem(FastEnum): class PotItem(FastEnum):
Nothing = 0x0 Nothing = 0x0
@@ -2346,3 +2594,10 @@ class Settings(object):
args.enemy_health[p] = r(e_health)[(settings[7] & 0xE0) >> 5] args.enemy_health[p] = r(e_health)[(settings[7] & 0xE0) >> 5]
args.enemy_damage[p] = r(e_dmg)[(settings[7] & 0x18) >> 3] args.enemy_damage[p] = r(e_dmg)[(settings[7] & 0x18) >> 3]
args.shufflepots[p] = True if settings[7] & 0x4 else False args.shufflepots[p] = True if settings[7] & 0x4 else False
@unique
class KeyRuleType(Enum):
WorstCase = 0
AllowSmall = 1
Lock = 2

View File

@@ -1,22 +1,21 @@
import random import random
from collections import defaultdict, deque from collections import defaultdict, deque
import logging import logging
import operator as op
import time import time
from enum import unique, Flag from enum import unique, Flag
from typing import DefaultDict, Dict, List from typing import DefaultDict, Dict, List
from functools import reduce from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo, dungeon_keys
from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo
from Doors import reset_portals from Doors import reset_portals
from Dungeons import dungeon_regions, region_starts, standard_starts, split_region_starts from Dungeons import dungeon_regions, region_starts, standard_starts, split_region_starts
from Dungeons import dungeon_bigs, dungeon_keys, dungeon_hints from Dungeons import dungeon_bigs, dungeon_hints
from Items import ItemFactory from Items import ItemFactory
from RoomData import DoorKind, PairedDoor, reset_rooms from RoomData import DoorKind, PairedDoor, reset_rooms
from DungeonGenerator import ExplorationState, convert_regions, generate_dungeon, pre_validate, determine_required_paths, drop_entrances from DungeonGenerator import ExplorationState, convert_regions, generate_dungeon, pre_validate, determine_required_paths, drop_entrances
from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances
from DungeonGenerator import dungeon_portals, dungeon_drops, GenerationException from DungeonGenerator import dungeon_portals, dungeon_drops, GenerationException
from KeyDoorShuffle import analyze_dungeon, validate_vanilla_key_logic, build_key_layout, validate_key_layout from KeyDoorShuffle import analyze_dungeon, build_key_layout, validate_key_layout
from Utils import ncr, kth_combination
def link_doors(world, player): def link_doors(world, player):
@@ -212,8 +211,8 @@ def vanilla_key_logic(world, player):
analyze_dungeon(key_layout, world, player) analyze_dungeon(key_layout, world, player)
world.key_logic[player][builder.name] = key_layout.key_logic world.key_logic[player][builder.name] = key_layout.key_logic
log_key_logic(builder.name, key_layout.key_logic) log_key_logic(builder.name, key_layout.key_logic)
if world.shuffle[player] == 'vanilla' and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]: # if world.shuffle[player] == 'vanilla' and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]:
validate_vanilla_key_logic(world, player) # validate_vanilla_key_logic(world, player)
# some useful functions # some useful functions
@@ -1576,28 +1575,6 @@ def find_key_door_candidates(region, checked, world, player):
return candidates, checked_doors return candidates, checked_doors
def kth_combination(k, l, r):
if r == 0:
return []
elif len(l) == r:
return l
else:
i = ncr(len(l)-1, r-1)
if k < i:
return l[0:1] + kth_combination(k, l[1:], r-1)
else:
return kth_combination(k-i, l[1:], r)
def ncr(n, r):
if r == 0:
return 1
r = min(r, n-r)
numerator = reduce(op.mul, range(n, n-r, -1), 1)
denominator = reduce(op.mul, range(1, r+1), 1)
return numerator / denominator
def reassign_key_doors(builder, world, player): def reassign_key_doors(builder, world, player):
logger = logging.getLogger('') logger = logging.getLogger('')
logger.debug('Key doors for %s', builder.name) logger.debug('Key doors for %s', builder.name)

View File

@@ -375,21 +375,6 @@ flexible_starts = {
'Skull Woods': ['Skull Left Drop', 'Skull Pinball'] 'Skull Woods': ['Skull Left Drop', 'Skull Pinball']
} }
dungeon_keys = {
'Hyrule Castle': 'Small Key (Escape)',
'Eastern Palace': 'Small Key (Eastern Palace)',
'Desert Palace': 'Small Key (Desert Palace)',
'Tower of Hera': 'Small Key (Tower of Hera)',
'Agahnims Tower': 'Small Key (Agahnims Tower)',
'Palace of Darkness': 'Small Key (Palace of Darkness)',
'Swamp Palace': 'Small Key (Swamp Palace)',
'Skull Woods': 'Small Key (Skull Woods)',
'Thieves Town': 'Small Key (Thieves Town)',
'Ice Palace': 'Small Key (Ice Palace)',
'Misery Mire': 'Small Key (Misery Mire)',
'Turtle Rock': 'Small Key (Turtle Rock)',
'Ganons Tower': 'Small Key (Ganons Tower)'
}
dungeon_bigs = { dungeon_bigs = {
'Hyrule Castle': 'Big Key (Escape)', 'Hyrule Castle': 'Big Key (Escape)',

14
Fill.py
View File

@@ -199,6 +199,7 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool =
spot_to_fill = None spot_to_fill = None
valid_locations = []
for location in locations: for location in locations:
if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there
location.item = item_to_place location.item = item_to_place
@@ -209,11 +210,16 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool =
if (not single_player_placement or location.player == item_to_place.player)\ if (not single_player_placement or location.player == item_to_place.player)\
and location.can_fill(test_state, item_to_place, perform_access_check)\ and location.can_fill(test_state, item_to_place, perform_access_check)\
and valid_key_placement(item_to_place, location, itempool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world): and valid_key_placement(item_to_place, location, itempool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world):
# todo: optimization: break instead of cataloging all valid locations
if not spot_to_fill:
spot_to_fill = location spot_to_fill = location
break valid_locations.append(location)
elif item_to_place.smallkey or item_to_place.bigkey:
if item_to_place.smallkey or item_to_place.bigkey:
location.item = None location.item = None
logging.getLogger('').debug(f'{item_to_place} valid placement at {len(valid_locations)} locations')
if spot_to_fill is None: if spot_to_fill is None:
# we filled all reachable spots. Maybe the game can be beaten anyway? # we filled all reachable spots. Maybe the game can be beaten anyway?
unplaced_items.insert(0, item_to_place) unplaced_items.insert(0, item_to_place)
@@ -250,9 +256,7 @@ def valid_key_placement(item, location, itempool, world):
def track_outside_keys(item, location, world): def track_outside_keys(item, location, world):
if not item.smallkey: if not item.smallkey:
return return
item_dungeon = item.name.split('(')[1][:-1] item_dungeon = item.dungeon
if item_dungeon == 'Escape':
item_dungeon = 'Hyrule Castle'
if location.player == item.player: if location.player == item.player:
loc_dungeon = location.parent_region.dungeon loc_dungeon = location.parent_region.dungeon
if loc_dungeon and loc_dungeon.name == item_dungeon: if loc_dungeon and loc_dungeon.name == item_dungeon:

View File

@@ -2,9 +2,9 @@ import itertools
import logging import logging
from collections import defaultdict, deque from collections import defaultdict, deque
from BaseClasses import DoorType from BaseClasses import DoorType, dungeon_keys, KeyRuleType
from Regions import dungeon_events from Regions import dungeon_events
from Dungeons import dungeon_keys, dungeon_bigs from Dungeons import dungeon_bigs
from DungeonGenerator import ExplorationState, special_big_key_doors from DungeonGenerator import ExplorationState, special_big_key_doors
@@ -25,6 +25,7 @@ class KeyLayout(object):
self.all_locations = set() self.all_locations = set()
self.item_locations = set() self.item_locations = set()
self.found_doors = set()
# bk special? # bk special?
# bk required? True if big chests or big doors exists # bk required? True if big chests or big doors exists
@@ -54,6 +55,7 @@ class KeyLogic(object):
self.location_rules = {} self.location_rules = {}
self.outside_keys = 0 self.outside_keys = 0
self.dungeon = dungeon_name self.dungeon = dungeon_name
self.sm_doors = {}
def check_placement(self, unplaced_keys, big_key_loc=None): def check_placement(self, unplaced_keys, big_key_loc=None):
for rule in self.placement_rules: for rule in self.placement_rules:
@@ -65,6 +67,15 @@ class KeyLogic(object):
return False return False
return True return True
def reset(self):
self.door_rules.clear()
self.bk_restricted.clear()
self.bk_locked.clear()
self.sm_restricted.clear()
self.bk_doors.clear()
self.bk_chests.clear()
self.placement_rules.clear()
class DoorRules(object): class DoorRules(object):
@@ -79,6 +90,8 @@ class DoorRules(object):
self.small_location = None self.small_location = None
self.opposite = None self.opposite = None
self.new_rules = {} # keyed by type, or type+lock_item -> number
class LocationRule(object): class LocationRule(object):
def __init__(self): def __init__(self):
@@ -209,8 +222,19 @@ def calc_max_chests(builder, key_layout, world, player):
def analyze_dungeon(key_layout, world, player): def analyze_dungeon(key_layout, world, player):
key_layout.key_logic.reset()
key_layout.key_counters = create_key_counters(key_layout, world, player) key_layout.key_counters = create_key_counters(key_layout, world, player)
key_logic = key_layout.key_logic key_logic = key_layout.key_logic
for door in key_layout.proposal:
if isinstance(door, tuple):
key_logic.sm_doors[door[0]] = door[1]
key_logic.sm_doors[door[1]] = door[0]
else:
if door.dest and door.type != DoorType.SpiralStairs:
key_logic.sm_doors[door] = door.dest
key_logic.sm_doors[door.dest] = door
else:
key_logic.sm_doors[door] = None
find_bk_locked_sections(key_layout, world, player) find_bk_locked_sections(key_layout, world, player)
key_logic.bk_chests.update(find_big_chest_locations(key_layout.all_chest_locations)) key_logic.bk_chests.update(find_big_chest_locations(key_layout.all_chest_locations))
@@ -247,8 +271,9 @@ def analyze_dungeon(key_layout, world, player):
while len(child_queue) > 0: while len(child_queue) > 0:
child, odd_counter, empty_flag = child_queue.popleft() child, odd_counter, empty_flag = child_queue.popleft()
if not child.bigKey and child not in doors_completed: if not child.bigKey and child not in doors_completed:
best_counter = find_best_counter(child, odd_counter, key_counter, key_layout, world, player, False, empty_flag) best_counter = find_best_counter(child, key_layout, odd_counter, False, empty_flag)
rule = create_rule(best_counter, key_counter, key_layout, world, player) rule = create_rule(best_counter, key_counter, world, player)
create_worst_case_rule(rule, best_counter, world, player)
check_for_self_lock_key(rule, child, best_counter, key_layout, world, player) check_for_self_lock_key(rule, child, best_counter, key_layout, world, player)
bk_restricted_rules(rule, child, odd_counter, empty_flag, key_counter, key_layout, world, player) bk_restricted_rules(rule, child, odd_counter, empty_flag, key_counter, key_layout, world, player)
key_logic.door_rules[child.name] = rule key_logic.door_rules[child.name] = rule
@@ -258,7 +283,8 @@ def analyze_dungeon(key_layout, world, player):
if ctr_id not in visited_cid: if ctr_id not in visited_cid:
queue.append((child, next_counter)) queue.append((child, next_counter))
visited_cid.add(ctr_id) visited_cid.add(ctr_id)
check_rules(original_key_counter, key_layout, world, player) # todo: why is this commented out?
# check_rules(original_key_counter, key_layout, world, player)
# Flip bk rules if more restrictive, to prevent placing a big key in a softlocking location # Flip bk rules if more restrictive, to prevent placing a big key in a softlocking location
for rule in key_logic.door_rules.values(): for rule in key_logic.door_rules.values():
@@ -294,7 +320,7 @@ def create_exhaustive_placement_rules(key_layout, world, player):
else: else:
placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player) placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player)
rule.check_locations_w_bk = accessible_loc rule.check_locations_w_bk = accessible_loc
check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc) # check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc)
else: else:
if big_key_progress(key_counter) and only_sm_doors(key_counter): if big_key_progress(key_counter) and only_sm_doors(key_counter):
create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, accessible_loc, min_keys, world, player) create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, accessible_loc, min_keys, world, player)
@@ -320,6 +346,7 @@ def placement_self_lock_adjustment(rule, max_ctr, blocked_loc, ctr, world, playe
rule.needed_keys_w_bk -= 1 rule.needed_keys_w_bk -= 1
# this rule is suspect - commented out usages for now
def check_sm_restriction_needed(key_layout, max_ctr, rule, blocked): def check_sm_restriction_needed(key_layout, max_ctr, rule, blocked):
if rule.needed_keys_w_bk == key_layout.max_chests + len(max_ctr.key_only_locations): if rule.needed_keys_w_bk == key_layout.max_chests + len(max_ctr.key_only_locations):
key_layout.key_logic.sm_restricted.update(blocked.difference(max_ctr.key_only_locations)) key_layout.key_logic.sm_restricted.update(blocked.difference(max_ctr.key_only_locations))
@@ -478,7 +505,7 @@ def create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, a
else: else:
placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player) placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player)
rule.check_locations_w_bk = accessible_loc rule.check_locations_w_bk = accessible_loc
check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc) # check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc)
key_logic.placement_rules.append(rule) key_logic.placement_rules.append(rule)
adjust_locations_rules(key_logic, rule, accessible_loc, key_layout, key_counter, max_ctr) adjust_locations_rules(key_logic, rule, accessible_loc, key_layout, key_counter, max_ctr)
@@ -538,6 +565,8 @@ def relative_empty_counter(odd_counter, key_counter):
return False return False
if len(set(odd_counter.free_locations).difference(key_counter.free_locations)) > 0: if len(set(odd_counter.free_locations).difference(key_counter.free_locations)) > 0:
return False return False
if len(set(odd_counter.other_locations).difference(key_counter.other_locations)) > 0:
return False
# important only # important only
if len(set(odd_counter.important_locations).difference(key_counter.important_locations)) > 0: if len(set(odd_counter.important_locations).difference(key_counter.important_locations)) > 0:
return False return False
@@ -594,33 +623,50 @@ def unique_child_door_2(child, key_counter):
return True return True
def find_best_counter(door, odd_counter, key_counter, key_layout, world, player, skip_bk, empty_flag): # try to waste as many keys as possible? # def find_best_counter(door, odd_counter, key_counter, key_layout, world, player, skip_bk, empty_flag): # try to waste as many keys as possible?
ignored_doors = {door, door.dest} if door is not None else {} # ignored_doors = {door, door.dest} if door is not None else {}
finished = False # finished = False
opened_doors = dict(key_counter.open_doors) # opened_doors = dict(key_counter.open_doors)
bk_opened = key_counter.big_key_opened # bk_opened = key_counter.big_key_opened
# new_counter = key_counter # # new_counter = key_counter
last_counter = key_counter # last_counter = key_counter
while not finished: # while not finished:
door_set = find_potential_open_doors(last_counter, ignored_doors, key_layout, skip_bk) # door_set = find_potential_open_doors(last_counter, ignored_doors, key_layout, skip_bk)
if door_set is None or len(door_set) == 0: # if door_set is None or len(door_set) == 0:
finished = True # finished = True
continue # continue
for new_door in door_set: # for new_door in door_set:
proposed_doors = {**opened_doors, **dict.fromkeys([new_door, new_door.dest])} # proposed_doors = {**opened_doors, **dict.fromkeys([new_door, new_door.dest])}
bk_open = bk_opened or new_door.bigKey # bk_open = bk_opened or new_door.bigKey
new_counter = find_counter(proposed_doors, bk_open, key_layout) # new_counter = find_counter(proposed_doors, bk_open, key_layout)
bk_open = new_counter.big_key_opened # bk_open = new_counter.big_key_opened
# this means the new_door invalidates the door / leads to the same stuff # # this means the new_door invalidates the door / leads to the same stuff
if not empty_flag and relative_empty_counter(odd_counter, new_counter): # if not empty_flag and relative_empty_counter(odd_counter, new_counter):
ignored_doors.add(new_door) # ignored_doors.add(new_door)
elif empty_flag or key_wasted(new_door, door, last_counter, new_counter, key_layout, world, player): # elif empty_flag or key_wasted(new_door, door, last_counter, new_counter, key_layout, world, player):
last_counter = new_counter # last_counter = new_counter
opened_doors = proposed_doors # opened_doors = proposed_doors
bk_opened = bk_open # bk_opened = bk_open
else: # else:
ignored_doors.add(new_door) # ignored_doors.add(new_door)
return last_counter # return last_counter
def find_best_counter(door, key_layout, odd_counter, skip_bk, empty_flag):
best, best_ctr, locations = 0, None, 0
for code, counter in key_layout.key_counters.items():
if door not in counter.open_doors:
if best_ctr is None or counter.used_keys > best or (counter.used_keys == best and count_locations(counter) > locations):
if not skip_bk or not counter.big_key_opened:
if empty_flag or not relative_empty_counter(odd_counter, counter):
best = counter.used_keys
best_ctr = counter
locations = count_locations(counter)
return best_ctr
def count_locations(ctr):
return len(ctr.free_locations) + len(ctr.key_only_locations) + len(ctr.other_locations) + len(ctr.important_locations)
def find_worst_counter(door, odd_counter, key_counter, key_layout, skip_bk): # try to waste as many keys as possible? def find_worst_counter(door, odd_counter, key_counter, key_layout, skip_bk): # try to waste as many keys as possible?
@@ -717,7 +763,7 @@ def calc_avail_keys(key_counter, world, player):
return raw_avail - key_counter.used_keys return raw_avail - key_counter.used_keys
def create_rule(key_counter, prev_counter, key_layout, world, player): def create_rule(key_counter, prev_counter, world, player):
# prev_chest_keys = available_chest_small_keys(prev_counter, world) # prev_chest_keys = available_chest_small_keys(prev_counter, world)
# prev_avail = prev_chest_keys + len(prev_counter.key_only_locations) # prev_avail = prev_chest_keys + len(prev_counter.key_only_locations)
chest_keys = available_chest_small_keys(key_counter, world, player) chest_keys = available_chest_small_keys(key_counter, world, player)
@@ -736,6 +782,11 @@ def create_rule(key_counter, prev_counter, key_layout, world, player):
return DoorRules(rule_num, is_valid) return DoorRules(rule_num, is_valid)
def create_worst_case_rule(rules, key_counter, world, player):
required_keys = key_counter.used_keys + 1 # this makes more sense, if key_counter has wasted all keys
rules.new_rules[KeyRuleType.WorstCase] = required_keys
def check_for_self_lock_key(rule, door, parent_counter, key_layout, world, player): def check_for_self_lock_key(rule, door, parent_counter, key_layout, world, player):
if world.accessibility[player] != 'locations': if world.accessibility[player] != 'locations':
counter = find_inverted_counter(door, parent_counter, key_layout, world, player) counter = find_inverted_counter(door, parent_counter, key_layout, world, player)
@@ -845,16 +896,16 @@ def big_key_drop_available(key_counter):
def bk_restricted_rules(rule, door, odd_counter, empty_flag, key_counter, key_layout, world, player): def bk_restricted_rules(rule, door, odd_counter, empty_flag, key_counter, key_layout, world, player):
if key_counter.big_key_opened: if key_counter.big_key_opened:
return return
best_counter = find_best_counter(door, odd_counter, key_counter, key_layout, world, player, True, empty_flag) best_counter = find_best_counter(door, key_layout, odd_counter, True, empty_flag)
bk_rule = create_rule(best_counter, key_counter, key_layout, world, player) bk_rule = create_rule(best_counter, key_counter, world, player)
if bk_rule.small_key_num >= rule.small_key_num: if bk_rule.small_key_num >= rule.small_key_num:
return return
door_open = find_next_counter(door, best_counter, key_layout) door_open = find_next_counter(door, best_counter, key_layout)
ignored_doors = dict_intersection(best_counter.child_doors, door_open.child_doors) ignored_doors = dict_intersection(best_counter.child_doors, door_open.child_doors)
dest_ignored = [] dest_ignored = []
for door in ignored_doors.keys(): for d in ignored_doors.keys():
if door.dest not in ignored_doors: if d.dest not in ignored_doors:
dest_ignored.append(door.dest) dest_ignored.append(d.dest)
ignored_doors = {**ignored_doors, **dict.fromkeys(dest_ignored)} ignored_doors = {**ignored_doors, **dict.fromkeys(dest_ignored)}
post_counter = open_some_counter(door_open, key_layout, ignored_doors.keys()) post_counter = open_some_counter(door_open, key_layout, ignored_doors.keys())
unique_loc = dict_difference(post_counter.free_locations, best_counter.free_locations) unique_loc = dict_difference(post_counter.free_locations, best_counter.free_locations)
@@ -862,8 +913,8 @@ def bk_restricted_rules(rule, door, odd_counter, empty_flag, key_counter, key_la
if len(unique_loc) > 0: # and bk_rule.is_valid if len(unique_loc) > 0: # and bk_rule.is_valid
rule.alternate_small_key = bk_rule.small_key_num rule.alternate_small_key = bk_rule.small_key_num
rule.alternate_big_key_loc.update(unique_loc) rule.alternate_big_key_loc.update(unique_loc)
# elif not bk_rule.is_valid: if not door.bigKey:
# key_layout.key_logic.bk_restricted.update(unique_loc) rule.new_rules[(KeyRuleType.Lock, key_layout.key_logic.bk_name)] = best_counter.used_keys + 1
def find_worst_counter_wo_bk(small_key_num, accessible_set, door, odd_ctr, key_counter, key_layout): def find_worst_counter_wo_bk(small_key_num, accessible_set, door, odd_ctr, key_counter, key_layout):
@@ -935,6 +986,7 @@ def only_sm_doors(key_counter):
return False return False
return True return True
# doesn't count dest doors # doesn't count dest doors
def count_unique_small_doors(key_counter, proposal): def count_unique_small_doors(key_counter, proposal):
cnt = 0 cnt = 0
@@ -1197,7 +1249,7 @@ def check_rules_deep(original_counter, key_layout, world, player):
elif not door.bigKey: elif not door.bigKey:
can_open = True can_open = True
if can_open: if can_open:
can_progress = smalls_opened or not big_maybe_not_found can_progress = (big_avail or not big_maybe_not_found) if door.bigKey else smalls_opened
next_counter = find_next_counter(door, counter, key_layout) next_counter = find_next_counter(door, counter, key_layout)
c_id = cid(next_counter, key_layout) c_id = cid(next_counter, key_layout)
if c_id not in completed: if c_id not in completed:
@@ -1381,6 +1433,7 @@ def cnt_avail_big_locations(ttl_locations, state, world, player):
def create_key_counters(key_layout, world, player): def create_key_counters(key_layout, world, player):
key_counters = {} key_counters = {}
key_layout.found_doors.clear()
flat_proposal = key_layout.flat_prop flat_proposal = key_layout.flat_prop
state = ExplorationState(dungeon=key_layout.sector.name) state = ExplorationState(dungeon=key_layout.sector.name)
if world.doorShuffle[player] == 'vanilla': if world.doorShuffle[player] == 'vanilla':
@@ -1403,6 +1456,9 @@ def create_key_counters(key_layout, world, player):
while len(queue) > 0: while len(queue) > 0:
next_key_counter, parent_state = queue.popleft() next_key_counter, parent_state = queue.popleft()
for door in next_key_counter.child_doors: for door in next_key_counter.child_doors:
key_layout.found_doors.add(door)
if door.dest in flat_proposal and door.type != DoorType.SpiralStairs:
key_layout.found_doors.add(door.dest)
child_state = parent_state.copy() child_state = parent_state.copy()
if door.bigKey or door.name in special_big_key_doors: if door.bigKey or door.name in special_big_key_doors:
key_layout.key_logic.bk_doors.add(door) key_layout.key_logic.bk_doors.add(door)
@@ -1520,11 +1576,11 @@ def find_counter_hint(opened_doors, bk_hint, key_layout):
def find_max_counter(key_layout): def find_max_counter(key_layout):
max_counter = find_counter_hint(dict.fromkeys(key_layout.flat_prop), False, key_layout) max_counter = find_counter_hint(dict.fromkeys(key_layout.found_doors), False, key_layout)
if max_counter is None: if max_counter is None:
raise Exception("Max Counter is none - something is amiss") raise Exception("Max Counter is none - something is amiss")
if len(max_counter.child_doors) > 0: if len(max_counter.child_doors) > 0:
max_counter = find_counter_hint(dict.fromkeys(key_layout.flat_prop), True, key_layout) max_counter = find_counter_hint(dict.fromkeys(key_layout.found_doors), True, key_layout)
return max_counter return max_counter

View File

@@ -448,6 +448,7 @@ def copy_world(world):
# these need to be modified properly by set_rules # these need to be modified properly by set_rules
new_location.access_rule = lambda state: True new_location.access_rule = lambda state: True
new_location.item_rule = lambda state: True new_location.item_rule = lambda state: True
new_location.forced_item = location.forced_item
# copy remaining itempool. No item in itempool should have an assigned location # copy remaining itempool. No item in itempool should have an assigned location
for item in world.itempool: for item in world.itempool:

View File

@@ -3,7 +3,7 @@ import logging
from collections import deque from collections import deque
import OverworldGlitchRules import OverworldGlitchRules
from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier, KeyRuleType
from RoomData import DoorKind from RoomData import DoorKind
from OverworldGlitchRules import overworld_glitches_rules from OverworldGlitchRules import overworld_glitches_rules
@@ -1939,14 +1939,10 @@ bunny_impassible_doors = {
def add_key_logic_rules(world, player): def add_key_logic_rules(world, player):
key_logic = world.key_logic[player] key_logic = world.key_logic[player]
for d_name, d_logic in key_logic.items(): for d_name, d_logic in key_logic.items():
for door_name, keys in d_logic.door_rules.items(): for door_name, rule in d_logic.door_rules.items():
spot = world.get_entrance(door_name, player) add_rule(world.get_entrance(door_name, player), eval_small_key_door(door_name, d_name, player))
if not world.retro[player] or world.mode[player] != 'standard' or not retro_in_hc(spot): if rule.allow_small:
rule = create_advanced_key_rule(d_logic, player, keys) set_always_allow(rule.small_location, allow_self_locking_small(d_logic, player))
if keys.opposite:
rule = or_rule(rule, create_advanced_key_rule(d_logic, player, keys.opposite))
add_rule(spot, rule)
for location in d_logic.bk_restricted: for location in d_logic.bk_restricted:
if not location.forced_item: if not location.forced_item:
forbid_item(location, d_logic.bk_name, player) forbid_item(location, d_logic.bk_name, player)
@@ -1954,6 +1950,7 @@ def add_key_logic_rules(world, player):
forbid_item(location, d_logic.small_key_name, player) forbid_item(location, d_logic.small_key_name, player)
for door in d_logic.bk_doors: for door in d_logic.bk_doors:
add_rule(world.get_entrance(door.name, player), create_rule(d_logic.bk_name, player)) add_rule(world.get_entrance(door.name, player), create_rule(d_logic.bk_name, player))
if len(d_logic.bk_doors) > 0 or len(d_logic.bk_chests) > 1:
for chest in d_logic.bk_chests: for chest in d_logic.bk_chests:
add_rule(world.get_location(chest.name, player), create_rule(d_logic.bk_name, player)) add_rule(world.get_location(chest.name, player), create_rule(d_logic.bk_name, player))
if world.retro[player]: if world.retro[player]:
@@ -1963,6 +1960,39 @@ def add_key_logic_rules(world, player):
add_rule(door.entrance, create_key_rule('Small Key (Universal)', player, 1)) add_rule(door.entrance, create_key_rule('Small Key (Universal)', player, 1))
def allow_self_locking_small(logic, player):
return lambda state, item: item.player == player and logic.small_key_name == item.name
def eval_small_key_door_main(state, door_name, dungeon, player):
if state.is_door_open(door_name, player):
return True
key_logic = state.world.key_logic[player][dungeon]
door_rule = key_logic.door_rules[door_name]
door_openable = False
for ruleType, number in door_rule.new_rules.items():
if door_openable:
return True
if ruleType == KeyRuleType.WorstCase:
door_openable |= state.has_sm_key(key_logic.small_key_name, player, number)
elif ruleType == KeyRuleType.AllowSmall:
if door_rule.small_location.item and door_rule.small_location.item.name == key_logic.small_key_name:
return True # always okay if allow small is on
elif isinstance(ruleType, tuple):
lock, lock_item = ruleType
# this doesn't track logical locks yet, i.e. hammer locks the item and hammer is there, but the item isn't
for loc in door_rule.alternate_big_key_loc:
spot = state.world.get_location(loc, player)
if spot.item and spot.item.name == lock_item:
door_openable |= state.has_sm_key(key_logic.small_key_name, player, number)
break
return door_openable
def eval_small_key_door(door_name, dungeon, player):
return lambda state: eval_small_key_door_main(state, door_name, dungeon, player)
def retro_in_hc(spot): def retro_in_hc(spot):
return spot.parent_region.dungeon.name == 'Hyrule Castle' if spot.parent_region.dungeon else False return spot.parent_region.dungeon.name == 'Hyrule Castle' if spot.parent_region.dungeon else False

View File

@@ -1,10 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os import os
import re import re
import operator as op
import subprocess import subprocess
import sys import sys
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from collections import defaultdict from collections import defaultdict
from functools import reduce
def int16_as_bytes(value): def int16_as_bytes(value):
@@ -116,6 +118,28 @@ def make_new_base2current(old_rom='Zelda no Densetsu - Kamigami no Triforce (Jap
return "New Rom Hash: " + basemd5.hexdigest() return "New Rom Hash: " + basemd5.hexdigest()
def kth_combination(k, l, r):
if r == 0:
return []
elif len(l) == r:
return l
else:
i = ncr(len(l)-1, r-1)
if k < i:
return l[0:1] + kth_combination(k, l[1:], r-1)
else:
return kth_combination(k-i, l[1:], r)
def ncr(n, r):
if r == 0:
return 1
r = min(r, n-r)
numerator = reduce(op.mul, range(n, n-r, -1), 1)
denominator = reduce(op.mul, range(1, r+1), 1)
return numerator / denominator
entrance_offsets = { entrance_offsets = {
'Sanctuary': 0x2, 'Sanctuary': 0x2,
'HC West': 0x3, 'HC West': 0x3,