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):
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

View File

@@ -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)

View File

@@ -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
View File

@@ -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:

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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,