Merge commit 'c4ae75bc2b866e4a304614200eac678fd4da4aee' into multidoors
This commit is contained in:
@@ -7,7 +7,7 @@ from Rom import LocalRom, apply_rom_settings
|
||||
|
||||
|
||||
def adjust(args):
|
||||
start = time.perf_counter()
|
||||
start = time.process_time()
|
||||
logger = logging.getLogger('')
|
||||
logger.info('Patching ROM.')
|
||||
|
||||
@@ -26,6 +26,6 @@ def adjust(args):
|
||||
rom.write_to_file(output_path('%s.sfc' % outfilebase))
|
||||
|
||||
logger.info('Done. Enjoy.')
|
||||
logger.debug('Total Time: %s', time.perf_counter() - start)
|
||||
logger.debug('Total Time: %s', time.process_time() - start)
|
||||
|
||||
return args
|
||||
|
||||
@@ -6,7 +6,6 @@ from collections import OrderedDict, deque
|
||||
|
||||
from EntranceShuffle import door_addresses
|
||||
from _vendor.collections_extended import bag
|
||||
from EntranceShuffle import door_addresses
|
||||
from Utils import int16_as_bytes
|
||||
from Tables import normal_offset_table, spiral_offset_table
|
||||
from RoomData import Room
|
||||
@@ -122,7 +121,7 @@ class World(object):
|
||||
except KeyError:
|
||||
for region in self.regions:
|
||||
if region.name == regionname and region.player == player:
|
||||
assert not region.world # this should only happen before initialization
|
||||
assert not region.world # this should only happen before initialization
|
||||
return region
|
||||
raise RuntimeError('No such region %s for player %d' % (regionname, player))
|
||||
|
||||
|
||||
@@ -640,6 +640,14 @@ def cross_dungeon(world, player):
|
||||
paths = determine_required_paths(world, player)
|
||||
check_required_paths(paths, world, player)
|
||||
|
||||
hc = world.get_dungeon('Hyrule Castle', player)
|
||||
del hc.dungeon_items[0] # removes map
|
||||
hc.dungeon_items.append(ItemFactory('Compass (Escape)', player))
|
||||
at = world.get_dungeon('Agahnims Tower', player)
|
||||
at.dungeon_items.append(ItemFactory('Compass (Agahnims Tower)', player))
|
||||
gt = world.get_dungeon('Ganons Tower', player)
|
||||
del gt.dungeon_items[0] # removes map
|
||||
|
||||
start = time.process_time()
|
||||
total_keys = remaining = 29
|
||||
total_candidates = 0
|
||||
@@ -703,6 +711,7 @@ def cross_dungeon(world, player):
|
||||
queue = collections.deque(sorted(queue, key=lambda b: b.combo_size))
|
||||
else:
|
||||
logger.info('Cross Dungeon: Increase failed for %s', name)
|
||||
builder.key_doors_num -= 1
|
||||
builder.flex = 0
|
||||
logger.info('Cross Dungeon: Keys unable to assign in pool %s', remaining)
|
||||
|
||||
@@ -1185,7 +1194,7 @@ def add_inaccessible_doors(world, player):
|
||||
create_door(world, player, 'Death Mountain Return Cave (West)', 'Death Mountain Return Ledge')
|
||||
if 'Desert Palace Lone Stairs' in world.inaccessible_regions[player]:
|
||||
create_door(world, player, 'Desert Palace Entrance (East)', 'Desert Palace Lone Stairs')
|
||||
if world.mode == 'standard' and 'Hyrule Castle Ledge' in world.inaccessible_regions[player]:
|
||||
if world.mode[player] == 'standard' and 'Hyrule Castle Ledge' in world.inaccessible_regions[player]:
|
||||
create_door(world, player, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Ledge')
|
||||
create_door(world, player, 'Hyrule Castle Entrance (West)', 'Hyrule Castle Ledge')
|
||||
|
||||
@@ -2020,36 +2029,18 @@ default_one_way_connections = [
|
||||
]
|
||||
|
||||
# For crossed
|
||||
default_dungeon_sets = [
|
||||
['Hyrule Castle Lobby', 'Hyrule Castle West Lobby', 'Hyrule Castle East Lobby', 'Sewers Rat Path', 'Sanctuary',
|
||||
'Hyrule Dungeon Cellblock'],
|
||||
['Eastern Lobby', 'Eastern Boss'],
|
||||
['Desert Back Lobby', 'Desert Boss', 'Desert Main Lobby', 'Desert West Lobby', 'Desert East Lobby'],
|
||||
['Hera Lobby', 'Hera Boss'],
|
||||
['Tower Lobby', 'Tower Agahnim 1'],
|
||||
['PoD Lobby', 'PoD Boss'],
|
||||
['Swamp Lobby', 'Swamp Boss'],
|
||||
['Skull 1 Lobby', 'Skull Pinball', 'Skull Left Drop', 'Skull Pot Circle', 'Skull 2 East Lobby',
|
||||
'Skull 2 West Lobby', 'Skull Back Drop', 'Skull 3 Lobby', 'Skull Boss'],
|
||||
['Thieves Lobby', 'Thieves Attic Window', 'Thieves Blind\'s Cell', 'Thieves Boss'],
|
||||
['Ice Lobby', 'Ice Boss'],
|
||||
['Mire Lobby', 'Mire Boss'],
|
||||
['TR Main Lobby', 'TR Boss', 'TR Eye Bridge', 'TR Big Chest Entrance', 'TR Lazy Eyes'],
|
||||
['GT Lobby', 'GT Agahnim 2']
|
||||
]
|
||||
|
||||
dungeon_x_idx_to_name = {
|
||||
0: 'Hyrule Castle',
|
||||
1: 'Eastern Palace',
|
||||
2: 'Desert Palace',
|
||||
3: 'Tower of Hera',
|
||||
4: 'Agahnims Tower',
|
||||
5: 'Palace of Darkness',
|
||||
6: 'Swamp Palace',
|
||||
7: 'Skull Woods',
|
||||
8: 'Thieves Town',
|
||||
9: 'Ice Palace',
|
||||
10: 'Misery Mire',
|
||||
11: 'Turtle Rock',
|
||||
12: 'Ganons Tower'
|
||||
compass_data = { # offset from 0x122e17, sram storage, write offset from 1037b9, 0 = jmp or # of nops
|
||||
'Hyrule Castle': (0x1, 0xc0, 0x16, 0),
|
||||
'Eastern Palace': (0x1C, 0xc1, 0x28, 0),
|
||||
'Desert Palace': (0x35, 0xc2, 0x4a, 0),
|
||||
'Agahnims Tower': (0x51, 0xc3, 0x5c, 0),
|
||||
'Swamp Palace': (0x6A, 0xc4, 0x7e, 0),
|
||||
'Palace of Darkness': (0x83, 0xc5, 0xa4, 0),
|
||||
'Misery Mire': (0x9C, 0xc6, 0xca, 0),
|
||||
'Skull Woods': (0xB5, 0xc7, 0xf0, 0),
|
||||
'Ice Palace': (0xD0, 0xc8, 0x102, 0),
|
||||
'Tower of Hera': (0xEB, 0xc9, 0x114, 0),
|
||||
'Thieves Town': (0x106, 0xca, 0x138, 0),
|
||||
'Turtle Rock': (0x11F, 0xcb, 0x15e, 0),
|
||||
'Ganons Tower': (0x13A, 0xcc, 0x170, 2)
|
||||
}
|
||||
|
||||
@@ -1105,10 +1105,13 @@ def weighted_random_locations(dungeon_map, free_location_sectors):
|
||||
|
||||
|
||||
def minimal_locations(dungeon_name):
|
||||
# bump to 5 if maps do something useful for all these dungeons
|
||||
if dungeon_name == 'Hyrule Castle':
|
||||
return 3
|
||||
return 4 # bk + compass + 2 others
|
||||
if dungeon_name == 'Agahnims Tower':
|
||||
return 2 # bump to 3 once compasses work
|
||||
return 4
|
||||
if dungeon_name == 'Ganons Tower':
|
||||
return 4
|
||||
# reduce gt to 4 once compasses work
|
||||
return 5
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ def parse_arguments(argv, no_defaults=False):
|
||||
slightly biased to placing progression items with
|
||||
less restrictions.
|
||||
''')
|
||||
parser.add_argument('--shuffle', default=defval('full'), const='full', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeonsfull', 'dungeonssimple'],
|
||||
parser.add_argument('--shuffle', default=defval('vanilla'), const='full', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeonsfull', 'dungeonssimple'],
|
||||
help='''\
|
||||
Select Entrance Shuffling Algorithm. (default: %(default)s)
|
||||
Full: Mix cave and dungeon entrances freely while limiting
|
||||
@@ -173,7 +173,7 @@ def parse_arguments(argv, no_defaults=False):
|
||||
The dungeon variants only mix up dungeons and keep the rest of
|
||||
the overworld vanilla.
|
||||
''')
|
||||
parser.add_argument('--door_shuffle', default=defval('vanilla'), const='vanilla', nargs='?', choices=['vanilla', 'basic', 'crossed', 'experimental'],
|
||||
parser.add_argument('--door_shuffle', default=defval('basic'), const='vanilla', nargs='?', choices=['vanilla', 'basic', 'crossed', 'experimental'],
|
||||
help='''\
|
||||
Select Door Shuffling Algorithm. (default: %(default)s)
|
||||
Basic: Doors are mixed within a single dungeon.
|
||||
|
||||
3
Items.py
3
Items.py
@@ -125,10 +125,11 @@ item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher cla
|
||||
'Map (Tower of Hera)': (False, True, 'Map', 0x75, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Tower of Hera'),
|
||||
'Small Key (Escape)': (False, False, 'SmallKey', 0xA0, 'A small key to the castle', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Hyrule Castle'),
|
||||
'Big Key (Escape)': (False, False, 'BigKey', 0x9F, 'A big key to the castle', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Hyrule Castle'),
|
||||
'Compass (Escape)': (False, True, 'Compass', 0x8F, 'Now you can find no boss!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Hyrule Castle'),
|
||||
'Compass (Escape)': (False, True, 'Compass', 0x8F, 'Now you can find no boss!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds null again', 'a compass to Hyrule Castle'),
|
||||
'Map (Escape)': (False, True, 'Map', 0x7F, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Hyrule Castle'),
|
||||
'Small Key (Agahnims Tower)': (False, False, 'SmallKey', 0xA4, 'A small key to Agahnim', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Castle Tower'),
|
||||
'Big Key (Agahnims Tower)': (False, False, 'BigKey', 0x9B, 'A big key to Agahnim', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Castle Tower'),
|
||||
'Compass (Agahnims Tower)': (False, True, 'Compass', 0x8B, 'Now you can find Aga1!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds null again', 'a compass to Castle Tower'),
|
||||
'Small Key (Palace of Darkness)': (False, False, 'SmallKey', 0xA6, 'A small key to darkness', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Palace of Darkness'),
|
||||
'Big Key (Palace of Darkness)': (False, False, 'BigKey', 0x99, 'A big key to darkness', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Palace of Darkness'),
|
||||
'Compass (Palace of Darkness)': (False, True, 'Compass', 0x89, 'Now you can find Helmasaur King!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Palace of Darkness'),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import collections
|
||||
from collections import defaultdict
|
||||
from collections import defaultdict, deque
|
||||
|
||||
from Regions import dungeon_events
|
||||
from Dungeons import dungeon_keys, dungeon_bigs
|
||||
@@ -108,42 +107,61 @@ def analyze_dungeon(key_layout, world, player):
|
||||
key_logic.bk_chests.update(find_big_chest_locations(key_layout.all_chest_locations))
|
||||
|
||||
original_key_counter = find_counter({}, False, key_layout)
|
||||
queue = collections.deque([(None, original_key_counter)])
|
||||
queue = deque([(None, original_key_counter)])
|
||||
doors_completed = set()
|
||||
visited_cid = set()
|
||||
visited_cid.add(cid(original_key_counter, key_layout))
|
||||
|
||||
while len(queue) > 0:
|
||||
queue = collections.deque(sorted(queue, key=queue_sorter))
|
||||
queue = deque(sorted(queue, key=queue_sorter))
|
||||
parent_door, key_counter = queue.popleft()
|
||||
chest_keys = available_chest_small_keys(key_counter, world, player)
|
||||
raw_avail = chest_keys + len(key_counter.key_only_locations)
|
||||
available = raw_avail - key_counter.used_keys
|
||||
possible_smalls = count_unique_small_doors(key_counter, key_layout.flat_prop)
|
||||
avail_bigs = exist_relevant_big_doors(key_counter, key_layout)
|
||||
non_big_locs = count_locations_big_optional(key_counter.free_locations)
|
||||
if not key_counter.big_key_opened:
|
||||
if chest_keys == count_locations_big_optional(key_counter.free_locations) and available <= possible_smalls and not avail_bigs:
|
||||
if chest_keys == non_big_locs and available <= possible_smalls and not avail_bigs:
|
||||
key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations))
|
||||
if not key_counter.big_key_opened and big_chest_in_locations(key_counter.free_locations):
|
||||
key_logic.sm_restricted.update(find_big_chest_locations(key_counter.free_locations))
|
||||
# todo: detect forced subsequent keys - see keypuzzles
|
||||
# try to relax the rules here? - smallest requirement that doesn't force a softlock
|
||||
child_queue = collections.deque()
|
||||
child_queue = deque()
|
||||
smallest_rule = None
|
||||
for child in key_counter.child_doors.keys():
|
||||
if not child.bigKey or not key_layout.big_key_special or key_counter.big_key_opened:
|
||||
odd_counter = create_odd_key_counter(child, key_counter, key_layout, world, player)
|
||||
if not empty_counter(odd_counter) and child not in doors_completed:
|
||||
child_queue.append((child, odd_counter))
|
||||
empty_flag = empty_counter(odd_counter)
|
||||
child_queue.append((child, odd_counter, empty_flag))
|
||||
if child in doors_completed and child in key_logic.door_rules.keys():
|
||||
rule = key_logic.door_rules[child]
|
||||
if smallest_rule is None or rule.small_key_num < smallest_rule:
|
||||
smallest_rule = rule.small_key_num
|
||||
while len(child_queue) > 0:
|
||||
child, odd_counter = child_queue.popleft()
|
||||
if not child.bigKey:
|
||||
best_counter = find_best_counter(child, odd_counter, key_counter, key_layout, world, player, False)
|
||||
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)
|
||||
if smallest_rule is None or rule.small_key_num < smallest_rule:
|
||||
smallest_rule = rule.small_key_num
|
||||
check_for_self_lock_key(rule, child, best_counter, key_layout, world, player)
|
||||
bk_restricted_rules(rule, child, odd_counter, 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
|
||||
doors_completed.add(child)
|
||||
next_counter = find_next_counter(child, key_counter, key_layout)
|
||||
queue.append((child, next_counter))
|
||||
check_rules(original_key_counter, key_layout)
|
||||
ctr_id = cid(next_counter, key_layout)
|
||||
if ctr_id not in visited_cid:
|
||||
queue.append((child, next_counter))
|
||||
visited_cid.add(ctr_id)
|
||||
possible_smalls_collected = len(key_counter.key_only_locations) + non_big_locs
|
||||
if not key_counter.big_key_opened:
|
||||
if smallest_rule is not None and smallest_rule >= possible_smalls_collected and not avail_bigs:
|
||||
key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations))
|
||||
if not key_counter.big_key_opened and big_chest_in_locations(key_counter.free_locations):
|
||||
key_logic.sm_restricted.update(find_big_chest_locations(key_counter.free_locations))
|
||||
check_rules(original_key_counter, key_layout, world, player)
|
||||
|
||||
|
||||
def count_key_drops(sector):
|
||||
@@ -170,6 +188,8 @@ def queue_sorter_2(queue_item):
|
||||
|
||||
|
||||
def find_bk_locked_sections(key_layout, world):
|
||||
if key_layout.big_key_special:
|
||||
return
|
||||
key_counters = key_layout.key_counters
|
||||
key_logic = key_layout.key_logic
|
||||
|
||||
@@ -217,7 +237,7 @@ def unique_child_door(child, key_counter):
|
||||
return True
|
||||
|
||||
|
||||
def find_best_counter(door, odd_counter, key_counter, key_layout, world, player, skip_bk): # 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 {}
|
||||
finished = False
|
||||
opened_doors = dict(key_counter.open_doors)
|
||||
@@ -235,15 +255,14 @@ def find_best_counter(door, odd_counter, key_counter, key_layout, world, player,
|
||||
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 relative_empty_counter(odd_counter, new_counter):
|
||||
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:
|
||||
if not key_wasted(new_door, last_counter, new_counter, key_layout, world, player):
|
||||
ignored_doors.add(new_door)
|
||||
else:
|
||||
last_counter = new_counter
|
||||
opened_doors = proposed_doors
|
||||
bk_opened = bk_open
|
||||
ignored_doors.add(new_door)
|
||||
return last_counter
|
||||
|
||||
|
||||
@@ -266,18 +285,20 @@ def find_potential_open_doors(key_counter, ignored_doors, key_layout, skip_bk):
|
||||
return small_doors + big_doors
|
||||
|
||||
|
||||
def key_wasted(new_door, old_counter, new_counter, key_layout, world, player):
|
||||
def key_wasted(new_door, old_door, old_counter, new_counter, key_layout, world, player):
|
||||
if new_door.bigKey: # big keys are not wastes - it uses up a location
|
||||
return True
|
||||
chest_keys = available_chest_small_keys(old_counter, world, player)
|
||||
old_avail = chest_keys + len(old_counter.key_only_locations) - old_counter.used_keys
|
||||
old_key_diff = len(old_counter.key_only_locations) - old_counter.used_keys
|
||||
old_avail = chest_keys + old_key_diff
|
||||
new_chest_keys = available_chest_small_keys(new_counter, world, player)
|
||||
new_avail = new_chest_keys + len(new_counter.key_only_locations) - new_counter.used_keys
|
||||
if new_avail < old_avail:
|
||||
new_key_diff = len(new_counter.key_only_locations) - new_counter.used_keys
|
||||
new_avail = new_chest_keys + new_key_diff
|
||||
if new_key_diff < old_key_diff or new_avail < old_avail:
|
||||
return True
|
||||
if new_avail == old_avail:
|
||||
old_children = old_counter.child_doors.keys()
|
||||
new_children = [x for x in new_counter.child_doors.keys() if x not in old_children and x.dest not in old_children]
|
||||
new_children = [x for x in new_counter.child_doors.keys() if x != old_door and x.dest != old_door and (not x.bigKey or x not in old_children)]
|
||||
current_counter = new_counter
|
||||
opened_doors = dict(current_counter.open_doors)
|
||||
bk_opened = current_counter.big_key_opened
|
||||
@@ -285,7 +306,7 @@ def key_wasted(new_door, old_counter, new_counter, key_layout, world, player):
|
||||
proposed_doors = {**opened_doors, **dict.fromkeys([new_child, new_child.dest])}
|
||||
bk_open = bk_opened or new_door.bigKey
|
||||
new_counter = find_counter(proposed_doors, bk_open, key_layout)
|
||||
if key_wasted(new_child, current_counter, new_counter, key_layout, world, player):
|
||||
if key_wasted(new_child, old_door, current_counter, new_counter, key_layout, world, player):
|
||||
return True # waste is possible
|
||||
return False
|
||||
|
||||
@@ -314,15 +335,12 @@ def create_rule(key_counter, prev_counter, key_layout, world, player):
|
||||
# prev_avail = prev_chest_keys + len(prev_counter.key_only_locations)
|
||||
chest_keys = available_chest_small_keys(key_counter, world, player)
|
||||
key_gain = len(key_counter.key_only_locations) - len(prev_counter.key_only_locations)
|
||||
raw_avail = chest_keys + len(key_counter.key_only_locations)
|
||||
available = raw_avail - key_counter.used_keys
|
||||
possible_smalls = count_unique_small_doors(key_counter, key_layout.flat_prop)
|
||||
required_keys = min(available, possible_smalls) + key_counter.used_keys
|
||||
# required_keys = key_counter.used_keys + 1 # this sometimes makes more sense
|
||||
# if prev_avail < required_keys:
|
||||
# required_keys = prev_avail + prev_counter.used_keys
|
||||
# return DoorRules(required_keys)
|
||||
# else:
|
||||
# previous method
|
||||
# raw_avail = chest_keys + len(key_counter.key_only_locations)
|
||||
# available = raw_avail - key_counter.used_keys
|
||||
# possible_smalls = count_unique_small_doors(key_counter, key_layout.flat_prop)
|
||||
# required_keys = min(available, possible_smalls) + key_counter.used_keys
|
||||
required_keys = key_counter.used_keys + 1 # this makes more sense, if key_counter has wasted all keys
|
||||
adj_chest_keys = min(chest_keys, required_keys)
|
||||
needed_chests = required_keys - len(key_counter.key_only_locations)
|
||||
unneeded_chests = min(key_gain, adj_chest_keys - needed_chests)
|
||||
@@ -349,6 +367,7 @@ def find_inverted_counter(door, parent_counter, key_layout, world, player):
|
||||
inverted_counter.free_locations = dict_difference(max_counter.free_locations, counter.free_locations)
|
||||
inverted_counter.key_only_locations = dict_difference(max_counter.key_only_locations, counter.key_only_locations)
|
||||
# child doors? used_keys?
|
||||
# inverted_counter.child_doors = dict_difference(max_counter.child_doors, counter.child_doors)
|
||||
inverted_counter.open_doors = dict_difference(max_counter.open_doors, counter.open_doors)
|
||||
inverted_counter.other_locations = dict_difference(max_counter.other_locations, counter.other_locations)
|
||||
for loc in inverted_counter.other_locations:
|
||||
@@ -417,10 +436,21 @@ def available_chest_small_keys(key_counter, world, player):
|
||||
return key_counter.max_chests
|
||||
|
||||
|
||||
def bk_restricted_rules(rule, door, odd_counter, key_counter, key_layout, world, player):
|
||||
def available_chest_small_keys_logic(key_counter, world, player, sm_restricted):
|
||||
if not world.keyshuffle[player] and not world.retro[player]:
|
||||
cnt = 0
|
||||
for loc in key_counter.free_locations:
|
||||
if loc not in sm_restricted and (key_counter.big_key_opened or '- Big Chest' not in loc.name):
|
||||
cnt += 1
|
||||
return min(cnt, key_counter.max_chests)
|
||||
else:
|
||||
return key_counter.max_chests
|
||||
|
||||
|
||||
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)
|
||||
best_counter = find_best_counter(door, odd_counter, key_counter, key_layout, world, player, True, empty_flag)
|
||||
bk_number = create_rule(best_counter, key_counter, key_layout, world, player).small_key_num
|
||||
if bk_number == rule.small_key_num:
|
||||
return
|
||||
@@ -497,6 +527,9 @@ def exist_relevant_big_doors(key_counter, key_layout):
|
||||
diff = dict_difference(bk_counter.key_only_locations, key_counter.key_only_locations)
|
||||
if len(diff) > 0:
|
||||
return True
|
||||
diff = dict_difference(bk_counter.child_doors, key_counter.child_doors)
|
||||
if len(diff) > 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -553,14 +586,14 @@ def flatten_pair_list(paired_list):
|
||||
return flat_list
|
||||
|
||||
|
||||
def check_rules(original_counter, key_layout):
|
||||
def check_rules(original_counter, key_layout, world, player):
|
||||
all_key_only = set()
|
||||
key_only_map = {}
|
||||
queue = collections.deque([(None, original_counter, original_counter.key_only_locations)])
|
||||
queue = deque([(None, original_counter, original_counter.key_only_locations)])
|
||||
completed = set()
|
||||
completed.add(cid(original_counter, key_layout))
|
||||
while len(queue) > 0:
|
||||
queue = collections.deque(sorted(queue, key=queue_sorter_2))
|
||||
queue = deque(sorted(queue, key=queue_sorter_2))
|
||||
access_door, counter, key_only_loc = queue.popleft()
|
||||
for loc in key_only_loc:
|
||||
if loc not in all_key_only:
|
||||
@@ -606,6 +639,7 @@ def check_rules(original_counter, key_layout):
|
||||
if check_non_bk:
|
||||
adjust_key_location_mins(key_layout, min_rule_non_bk, lambda r: r.small_key_num if r.alternate_small_key is None else r.alternate_small_key,
|
||||
lambda r, v: r if r.alternate_small_key is None else setattr(r, 'alternate_small_key', v))
|
||||
check_rules_deep(original_counter, key_layout, world, player)
|
||||
|
||||
|
||||
def adjust_key_location_mins(key_layout, min_rules, getter, setter):
|
||||
@@ -632,6 +666,108 @@ def adjust_key_location_mins(key_layout, min_rules, getter, setter):
|
||||
setter(rule, collected_keys)
|
||||
|
||||
|
||||
def check_rules_deep(original_counter, key_layout, world, player):
|
||||
key_logic = key_layout.key_logic
|
||||
big_locations = {x for x in key_layout.all_chest_locations if x not in key_logic.bk_restricted}
|
||||
queue = deque([original_counter])
|
||||
completed = set()
|
||||
completed.add(cid(original_counter, key_layout))
|
||||
last_counter = None
|
||||
bail = 0
|
||||
while len(queue) > 0:
|
||||
counter = queue.popleft()
|
||||
if counter == last_counter:
|
||||
bail += 1
|
||||
if bail > 10:
|
||||
raise Exception('Key logic issue, during deep rule check: %s' % key_layout.sector.name)
|
||||
else:
|
||||
bail = 0
|
||||
last_counter = counter
|
||||
chest_keys = available_chest_small_keys_logic(counter, world, player, key_logic.sm_restricted)
|
||||
big_avail = counter.big_key_opened
|
||||
big_maybe_not_found = not counter.big_key_opened
|
||||
if not key_layout.big_key_special and not big_avail:
|
||||
for location in counter.free_locations:
|
||||
if location not in key_logic.bk_restricted:
|
||||
big_avail = True
|
||||
break
|
||||
outstanding_big_locs = {x for x in big_locations if x not in counter.free_locations}
|
||||
if big_maybe_not_found:
|
||||
if len(outstanding_big_locs) == 0:
|
||||
big_maybe_not_found = False
|
||||
big_uses_chest = big_avail and not key_layout.big_key_special
|
||||
collected_alt = len(counter.key_only_locations) + chest_keys
|
||||
if big_uses_chest and chest_keys == count_locations_big_optional(counter.free_locations, counter.big_key_opened):
|
||||
chest_keys -= 1
|
||||
collected = len(counter.key_only_locations) + chest_keys
|
||||
can_progress = len(counter.child_doors) == 0
|
||||
smalls_opened = False
|
||||
small_rules = []
|
||||
for door in counter.child_doors.keys():
|
||||
can_open = False
|
||||
if door.bigKey and big_avail:
|
||||
can_open = True
|
||||
elif door.name in key_logic.door_rules.keys():
|
||||
rule = key_logic.door_rules[door.name]
|
||||
small_rules.append(rule)
|
||||
if rule_satisfied(rule, collected, collected_alt, outstanding_big_locs, chest_keys, key_layout):
|
||||
can_open = True
|
||||
smalls_opened = True
|
||||
elif not door.bigKey:
|
||||
can_open = True
|
||||
if can_open:
|
||||
can_progress = smalls_opened or not big_maybe_not_found
|
||||
next_counter = find_next_counter(door, counter, key_layout)
|
||||
c_id = cid(next_counter, key_layout)
|
||||
if c_id not in completed:
|
||||
completed.add(c_id)
|
||||
queue.append(next_counter)
|
||||
if not can_progress:
|
||||
if len(small_rules) > 0: # zero could be indicative of a problem, but also, the big key is now required
|
||||
reduce_rules(small_rules, collected, collected_alt)
|
||||
queue.append(counter) # run it through again
|
||||
else:
|
||||
raise Exception('Possible problem with generation or bk rules')
|
||||
|
||||
|
||||
def rule_satisfied(rule, collected, collected_alt, outstanding_big_locs, chest_keys, key_layout):
|
||||
if collected >= rule.small_key_num:
|
||||
return True
|
||||
if rule.allow_small and collected >= rule.small_key_num-1 and chest_keys < key_layout.max_chests:
|
||||
return True
|
||||
rule_diff = outstanding_big_locs.difference(rule.alternate_big_key_loc)
|
||||
if rule.alternate_small_key is not None and len(rule_diff) == 0 and collected >= rule.alternate_small_key:
|
||||
return True
|
||||
if collected_alt > collected:
|
||||
if collected_alt >= rule.small_key_num:
|
||||
return True
|
||||
if rule.allow_small and collected_alt >= rule.small_key_num-1 and chest_keys+1 < key_layout.max_chests:
|
||||
return True
|
||||
if rule.alternate_small_key is not None and len(rule_diff) == 0 and collected_alt >= rule.alternate_small_key:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def reduce_rules(small_rules, collected, collected_alt):
|
||||
smallest_rules = []
|
||||
min_num = None
|
||||
for rule in small_rules:
|
||||
if min_num is None or rule.small_key_num <= min_num:
|
||||
if min_num is not None and rule.small_key_num < min_num:
|
||||
min_num = rule.small_key_num
|
||||
smallest_rules.clear()
|
||||
elif min_num is None:
|
||||
min_num = rule.small_key_num
|
||||
smallest_rules.append(rule)
|
||||
for rule in smallest_rules:
|
||||
if rule.allow_small: # we are already reducing it
|
||||
rule.allow_small = False
|
||||
if min_num > collected_alt > collected:
|
||||
rule.small_key_num = collected_alt
|
||||
else:
|
||||
rule.small_key_num = collected
|
||||
|
||||
|
||||
# Soft lock stuff
|
||||
def validate_key_layout(key_layout, world, player):
|
||||
flat_proposal = key_layout.flat_prop
|
||||
@@ -688,7 +824,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa
|
||||
def cnt_avail_small_locations(key_layout, ttl_locations, state, world, player):
|
||||
if not world.keyshuffle[player] and not world.retro[player]:
|
||||
return min(ttl_locations - state.used_locations, state.key_locations - state.used_smalls)
|
||||
return key_layout.max_chests + state.key_locations - state.used_smalls
|
||||
return state.key_locations - state.used_smalls
|
||||
|
||||
|
||||
def cnt_avail_big_locations(ttl_locations, state, world, player):
|
||||
@@ -709,7 +845,7 @@ def create_key_counters(key_layout, world, player):
|
||||
expand_key_state(state, flat_proposal, world, player)
|
||||
code = state_id(state, key_layout.flat_prop)
|
||||
key_counters[code] = create_key_counter(state, key_layout, world, player)
|
||||
queue = collections.deque([(key_counters[code], state)])
|
||||
queue = deque([(key_counters[code], state)])
|
||||
while len(queue) > 0:
|
||||
next_key_counter, parent_state = queue.popleft()
|
||||
for door in next_key_counter.child_doors:
|
||||
@@ -820,7 +956,10 @@ def find_counter_hint(opened_doors, bk_hint, key_layout):
|
||||
|
||||
|
||||
def find_max_counter(key_layout):
|
||||
return find_counter_hint(dict.fromkeys(key_layout.flat_prop), False, key_layout)
|
||||
max_counter = find_counter_hint(dict.fromkeys(key_layout.flat_prop), False, key_layout)
|
||||
if len(max_counter.child_doors) > 0:
|
||||
max_counter = find_counter_hint(dict.fromkeys(key_layout.flat_prop), True, key_layout)
|
||||
return max_counter
|
||||
|
||||
|
||||
def counter_id(opened_doors, bk_unlocked, flat_proposal):
|
||||
@@ -861,12 +1000,12 @@ def validate_vanilla_key_logic(world, player):
|
||||
|
||||
|
||||
def val_hyrule(key_logic, world, player):
|
||||
val_rule(key_logic.door_rules['Sewers Secret Room Key Door S'], 3)
|
||||
val_rule(key_logic.door_rules['Sewers Dark Cross Key Door N'], 3)
|
||||
val_rule(key_logic.door_rules['Sewers Secret Room Key Door S'], 2)
|
||||
val_rule(key_logic.door_rules['Sewers Dark Cross Key Door N'], 2)
|
||||
val_rule(key_logic.door_rules['Hyrule Dungeon Map Room Key Door S'], 2)
|
||||
# why is allow_small actually false? - because chest key is forced elsewhere?
|
||||
val_rule(key_logic.door_rules['Hyrule Dungeon Armory Interior Key Door N'], 3, True, 'Hyrule Castle - Zelda\'s Chest')
|
||||
# val_rule(key_logic.door_rules['Hyrule Dungeon Armory Interior Key Door N'], 4)
|
||||
# val_rule(key_logic.door_rules['Hyrule Dungeon Armory Interior Key Door N'], 3, True, 'Hyrule Castle - Zelda\'s Chest')
|
||||
val_rule(key_logic.door_rules['Hyrule Dungeon Armory Interior Key Door N'], 4)
|
||||
|
||||
|
||||
def val_eastern(key_logic, world, player):
|
||||
@@ -951,7 +1090,8 @@ def val_ice(key_logic, world, player):
|
||||
|
||||
def val_mire(key_logic, world, player):
|
||||
mire_west_wing = {'Misery Mire - Big Key Chest', 'Misery Mire - Compass Chest'}
|
||||
val_rule(key_logic.door_rules['Mire Spikes NW'], 5) # todo: is sometimes 3 or 5? best_counter order matters
|
||||
val_rule(key_logic.door_rules['Mire Spikes NW'], 3) # todo: is sometimes 3 or 5? best_counter order matters
|
||||
# val_rule(key_logic.door_rules['Mire Spike Barrier NE'], 4) # kind of a waste mostly
|
||||
val_rule(key_logic.door_rules['Mire Hub WS'], 5, False, None, 3, mire_west_wing)
|
||||
val_rule(key_logic.door_rules['Mire Conveyor Crystal WS'], 6, False, None, 4, mire_west_wing)
|
||||
assert world.get_location('Misery Mire - Boss', player) in key_logic.bk_restricted
|
||||
@@ -981,11 +1121,11 @@ def val_ganons(key_logic, world, player):
|
||||
compass_room = {'Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right'}
|
||||
gt_middle = {'Ganons Tower - Big Key Room - Left', 'Ganons Tower - Big Key Chest', 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest'}
|
||||
val_rule(key_logic.door_rules['GT Double Switch EN'], 6, False, None, 4, rando_room.union({'Ganons Tower - Firesnake Room'}))
|
||||
val_rule(key_logic.door_rules['GT Hookshot ES'], 8, True, 'Ganons Tower - Map Chest', 5, {'Ganons Tower - Map Chest'})
|
||||
val_rule(key_logic.door_rules['GT Tile Room EN'], 7, False, None, 5, compass_room)
|
||||
val_rule(key_logic.door_rules['GT Firesnake Room SW'], 8, False, None, 5, rando_room)
|
||||
val_rule(key_logic.door_rules['GT Conveyor Star Pits EN'], 8, False, None, 6, gt_middle) # should be 7?
|
||||
val_rule(key_logic.door_rules['GT Mini Helmasaur Room WN'], 6) # not sure about 6 this...
|
||||
val_rule(key_logic.door_rules['GT Hookshot ES'], 7, False, 'Ganons Tower - Map Chest', 5, {'Ganons Tower - Map Chest'})
|
||||
val_rule(key_logic.door_rules['GT Tile Room EN'], 6, False, None, 5, compass_room)
|
||||
val_rule(key_logic.door_rules['GT Firesnake Room SW'], 7, False, None, 5, rando_room)
|
||||
val_rule(key_logic.door_rules['GT Conveyor Star Pits EN'], 6, False, None, 5, gt_middle) # should be 7?
|
||||
val_rule(key_logic.door_rules['GT Mini Helmasaur Room WN'], 6) # not sure about this 6...
|
||||
val_rule(key_logic.door_rules['GT Crystal Circles SW'], 8)
|
||||
assert world.get_location('Ganons Tower - Mini Helmasaur Room - Left', player) in key_logic.bk_restricted
|
||||
assert world.get_location('Ganons Tower - Mini Helmasaur Room - Right', player) in key_logic.bk_restricted
|
||||
|
||||
@@ -20,7 +20,7 @@ from Main import create_playthrough
|
||||
__version__ = '0.2-dev'
|
||||
|
||||
def main(args):
|
||||
start_time = time.perf_counter()
|
||||
start_time = time.process_time()
|
||||
|
||||
# initialize the world
|
||||
world = World(1, 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, False, False, False, None, False)
|
||||
@@ -86,7 +86,7 @@ def main(args):
|
||||
world.spoiler.to_file('%s_Spoiler.txt' % outfilebase)
|
||||
|
||||
logger.info('Done. Enjoy.')
|
||||
logger.debug('Total Time: %s', time.perf_counter() - start_time)
|
||||
logger.debug('Total Time: %s', time.process_time() - start_time)
|
||||
|
||||
return world
|
||||
|
||||
|
||||
400
README.md
400
README.md
@@ -32,178 +32,6 @@ Doors are shuffled only within a single dungeon.
|
||||
|
||||
Doors are shuffled between dungeons as well.
|
||||
|
||||
This mode removes all swords from the itempool. Otherwise just like open.
|
||||
|
||||
Special notes:
|
||||
|
||||
- The Medallions to open Misery Mire and Turtle Rock can be used without a sword if you stand on the symbol.
|
||||
- The curtains in Skull Woods and Hyrule Castle Tower that normally require a sword to cut have been removed.
|
||||
- Ganon takes damage from the Hammer.
|
||||
- The magic barrier to Hyrule Castle Tower can be broken with a Hammer.
|
||||
- The Hammer can be used to activate the Ether and Bombos tablets.
|
||||
|
||||
### Inverted
|
||||
|
||||
This mode is similar to Open but requires the Moon Pearl in order to not transform into a bunny in the Light World and the Sanctuary spawn point is moved to the Dark Sanctuary
|
||||
|
||||
Special Notes:
|
||||
|
||||
- Link's House is shuffled freely. The Dark Sanctuary is shuffled within West Dark World.
|
||||
- There are a number of overworld changes to account for the inability to mirror from the Light World to the Dark World.
|
||||
- Legacy shuffles are not implemente for this mode.
|
||||
|
||||
## Game Logic
|
||||
This determines the Item Requirements for each location.
|
||||
|
||||
### No Glitches
|
||||
|
||||
The game can be completed without knowing how to perform glitches of any kind.
|
||||
|
||||
### Minor Glitches
|
||||
|
||||
May require Fake Flippers, Bunny Revival.
|
||||
|
||||
### No Logic
|
||||
|
||||
Items are placed without regard for progression or the seed being possible. Major glitches are likely required.
|
||||
|
||||
## Game Goal
|
||||
|
||||
### Ganon
|
||||
|
||||
Standard game completion requiring you to collect the 7 crystals, defeat Agahnim 2 and then beat Ganon.
|
||||
|
||||
### Pedestal
|
||||
|
||||
Places the Triforce at the Master Sword Pedestal. Ganon cannot be damaged.
|
||||
|
||||
### All Dungeons
|
||||
|
||||
Ganon cannot be damaged until all dungeons (including Hyrule Castle Tower and Ganons Tower) are cleared.
|
||||
|
||||
### Triforce Hunt
|
||||
|
||||
Triforce Pieces are added to the item pool, and some number of them being found will trigger game completion. Ganon cannot be damaged.
|
||||
By default 30 Triforce Pieces are placed while 20 are needed to beat the game. Both values can be adjusted with the custom item pool feature.
|
||||
|
||||
### Crystals
|
||||
|
||||
Standard game completion requiring you to collect the 7 crystals and then beat Ganon.
|
||||
|
||||
This is only noticeably different if the the Ganon shuffle option is enabled.
|
||||
|
||||
## Game Difficulty
|
||||
|
||||
### Normal
|
||||
|
||||
This is the default setting that has an item pool most similar to the original
|
||||
The Legend of Zelda: A Link to the Past.
|
||||
|
||||
### Hard
|
||||
|
||||
This setting reduces the availability of a variety of minor helpful items, most notably
|
||||
limiting the player to two bottles, a Tempered Sword, and Blue Mail. Several minor game
|
||||
mechanics are adjusted to increase difficulty, most notably weakening potions and preventing
|
||||
the player from having fairies in bottles.
|
||||
|
||||
### Expert
|
||||
|
||||
This setting is a more extreme version of the Hard setting. Potions are further nerfed, the item
|
||||
pool is less helpful, and the player can find no armor, only a Master Sword, and only a single bottle.
|
||||
|
||||
## Timer Setting
|
||||
|
||||
### None
|
||||
|
||||
Does not invoke a timer.
|
||||
|
||||
### Display
|
||||
|
||||
Displays a timer on-screen but does not alter the item pool.
|
||||
This will prevent the dungeon item count feature in Easy and Compass shuffle from working.
|
||||
|
||||
### Timed
|
||||
|
||||
Displays a count-up timer on screen that can be reduced with Green Clocks and Blue Clocks or
|
||||
increased with Red Clocks found in chests that will be added to the itempool.
|
||||
|
||||
### Timed-OHKO
|
||||
|
||||
Displays a countdown timer on screen that, when it hits zero, will put the player into a one hit
|
||||
knockout state until more time is added to the clock via some of the Green Clocks that will be added
|
||||
to the itempool.
|
||||
|
||||
### OHKO
|
||||
|
||||
The player will be in a one hit knockout state the entire game. This is the same as Timed-OHKO except
|
||||
without the Clock items and the timer permanently at zero.
|
||||
|
||||
### Timed-countdown
|
||||
|
||||
Displays a countdown timer on screen that can be increased with Green Clocks and Blue Clocks or
|
||||
decreased with Red Clocks found in chests that will be added to the itempool. The goal of this mode
|
||||
is to finish the game without the timer reaching zero, but the game will continue uninterrupted if
|
||||
the player runs out of time.
|
||||
|
||||
## Progressive equipment
|
||||
|
||||
Determines if Sword, Shield, and gloves are progressive (upgrading in sequence) or not.
|
||||
|
||||
### On (Default)
|
||||
|
||||
This setting makes swords, shields, armor, and gloves progressive. The first of any type of equipment found
|
||||
by the player will be the lowest level item, and each subsequent find of a category will upgrade that type of
|
||||
equipment.
|
||||
|
||||
### Off
|
||||
|
||||
This setting makes swords, shields, armor, and gloves non-progressive. All of the items of these types will be
|
||||
randomly placed in chests, and the player could find them in any order and thus instantly receive high level equipment.
|
||||
Downgrades are not possible; finding a lower level piece of equipment than what is already in the player's possession
|
||||
will simply do nothing.
|
||||
|
||||
### Random
|
||||
|
||||
This setting makes swords, shields, armor, and gloves randomly either progressive or not. Each category is independently
|
||||
randomized.
|
||||
|
||||
## Item Distribution Algorithm
|
||||
|
||||
Determines how the items are shuffled.
|
||||
|
||||
### Balanced
|
||||
This is a variation of VT26 that aims to strike a balance between the overworld heavy VT25 and the dungeon heavy VT26 algorithm.
|
||||
It does this by reshuffling the remaining locations after placing dungeon items.
|
||||
|
||||
### VT26
|
||||
Items and locations are shuffled like in VT25, and dungeon items are now placed using the same algorithm. When Ganon is not
|
||||
shuffled it includes a slight deliberate bias against having too many desireable items in Ganon's Tower to help counterbalance
|
||||
the sheer number of chests in that single location.
|
||||
|
||||
### VT25
|
||||
Items and locations are shuffled and placed from the top of the lists. The only thing preventing an item from being placed into a spot
|
||||
is if is absolutely impossible to be there given the previous made placement choices. Leads to very uniform but guaranteed solvable distributions.
|
||||
|
||||
### VT22
|
||||
The ordinary VT v8.22 algorithm. Fixes issues in placement in VT21 by discarding all previously skipped and unfilled locations
|
||||
after 2/3 of the progression items were placed to prevent stale late game locations from soaking up the same items all the time.
|
||||
|
||||
### VT21
|
||||
The ordinary VT v8.21 algorithm. Unbiased placement of items into unlocked locations, placing items that unlock new locations first.
|
||||
May lead to distributions that seem a bit wonky (high likelyhood of ice rod in Turtle Rock, for instance)
|
||||
|
||||
### Flood
|
||||
Pushes out items starting from Link's House and is slightly biased to placing progression items with less restrictions. Use for relatively simple distributions.
|
||||
|
||||
### Freshness
|
||||
Alternative approach to VT22 to improve on VT21 flaws. Locations that are skipped because they are currently unreachable increase in
|
||||
staleness, decreasing the likelihood of receiving a progress item.
|
||||
|
||||
## Entrance Shuffle Algorithm
|
||||
|
||||
Determines how locations are shuffled. In all modes other than Insanity and the similar legacy versions, holes shuffle as a pair with the connecting cave and the front
|
||||
two sections of Skull Woods remain confined to the general Skull Woods area. Link's house is never shuffled as a design decision.
|
||||
|
||||
### Vanilla
|
||||
|
||||
Doors are not shuffled.
|
||||
@@ -212,56 +40,6 @@ Doors are not shuffled.
|
||||
|
||||
Used for development testing. This will be removed in a future version. Use at your own risk. Might play like a plando.
|
||||
|
||||
### Restricted
|
||||
|
||||
Uses dungeon shuffling from Simple but freely connects remaining entrances. Caves and dungeons with multiple entrances will be confined to one world.
|
||||
|
||||
### Full
|
||||
|
||||
Mixes cave and dungeon entrances freely. Caves and dungeons with multiple entrances will be confined to one world.
|
||||
|
||||
### Crossed
|
||||
|
||||
Mixes cave and dungeon entrances freely, but now connector caves and dungeons can link Light World and Dark World.
|
||||
|
||||
### Insanity
|
||||
|
||||
Decouples entrances and exits from each other and shuffles them freely. Caves that were single entrance in vanilla still can only exit to the same location from which they were entered.
|
||||
|
||||
### Legacy Variants
|
||||
|
||||
Similar to the base shuffles, but the distinction between single entrance and multi-entrance caves from older versions of the randomizer is maintained.
|
||||
Madness_Legacy is the more similar to the modern Insanity. Insanity_Legacy has fake worlds and guaranteed Moon Pearl and Magic Mirror for a very different experience.
|
||||
|
||||
### Dungeon Variants
|
||||
|
||||
The dungeon variants only mix up dungeons and keep the rest of the overworld vanilla.
|
||||
|
||||
## Heartbeep Sound Rate
|
||||
|
||||
Select frequency of beeps when on low health. Can completely disable them.
|
||||
|
||||
## Heart Color
|
||||
|
||||
Select the color of Link's hearts.
|
||||
|
||||
## Menu Speed
|
||||
|
||||
A setting that lets the player set the rate at which the menu opens and closes.
|
||||
|
||||
## Create Spoiler Log
|
||||
|
||||
Output a Spoiler File.
|
||||
|
||||
## Do not Create Patched Rom
|
||||
|
||||
If set, will not produce a patched rom as output. Useful in conjunction with the spoiler log option to batch
|
||||
generate spoilers for statistical analysis.
|
||||
|
||||
## Enable L/R button quickswapping
|
||||
|
||||
Use to enable quick item swap with L/R buttons. Press L and R together to switch the state of items like the Mushroom/Powder pair.
|
||||
|
||||
## Map/Compass/Small Key/Big Key shuffle (aka Keysanity)
|
||||
|
||||
These settings allow dungeon specific items to be distributed anywhere in the world and not just in their native dungeon.
|
||||
@@ -280,26 +58,6 @@ Container and a Bottle being filled with Blue Potion, and one of the four swords
|
||||
well. The five caves that are removed for these will be randomly selected single entrance caves that did not contain any items or any shops.
|
||||
In further concert with the Bow changes, all arrows under pots, in chests, and elsewhere in the seed will be replaced with rupees.
|
||||
|
||||
## Place Dungeon Items
|
||||
|
||||
If not set, Compasses and Maps are removed from the dungeon item pools and replaced by empty chests that may end up anywhere in the world.
|
||||
This may lead to different amount of itempool items being placed in a dungeon than you are used to.
|
||||
|
||||
## Include Ganon's Tower and Pyramid Hole in Shuffle pool
|
||||
|
||||
If set, Ganon's Tower is included in the dungeon shuffle pool and the Pyramid Hole/Exit pair is included in the Holes shuffle pool. Ganon can not be defeated until the primary goal is fulfilled.
|
||||
This setting removes any bias against Ganon's Tower that some algorithms may have.
|
||||
|
||||
## Include Helpful Hints
|
||||
|
||||
If set, the 15 telepathic tiles and 5 storytellers scattered about Hyrule will give helpful hints about various items and entrances. An exact breakdown of the hint
|
||||
distribution is provided as an included text file.
|
||||
|
||||
## Use Custom Item Pool
|
||||
|
||||
If set, the item pool normally associated with your difficulty setting is replaced by the item pool specified in the custom tab. This feature is only supported when the randomizer is run
|
||||
via the GUI; attempting to set this via the command line does nothing.
|
||||
|
||||
## Seed
|
||||
|
||||
Can be used to set a seed number to generate. Using the same seed with same settings on the same version of the entrance randomizer will always yield an identical output.
|
||||
@@ -321,161 +79,3 @@ Show the help message and exit.
|
||||
```
|
||||
|
||||
For specifying the door shuffle you want as above. (default: basic)
|
||||
Output a Spoiler File (default: False)
|
||||
|
||||
```
|
||||
--logic [{noglitches,minorglitches,nologic}]
|
||||
```
|
||||
|
||||
Select the game logic (default: noglitches)
|
||||
|
||||
```
|
||||
--mode [{standard,open,swordless,inverted}]
|
||||
```
|
||||
|
||||
Select the game mode. (default: open)
|
||||
|
||||
```
|
||||
--goal [{ganon,pedestal,dungeons,triforcehunt,crystals}]
|
||||
```
|
||||
|
||||
Select the game completion goal. (default: ganon)
|
||||
|
||||
```
|
||||
--difficulty [{normal,hard,expert}]
|
||||
```
|
||||
|
||||
Select the game difficulty. Affects available itempool. (default: normal)
|
||||
|
||||
```
|
||||
--item_functionality [{normal,hard,expert}]
|
||||
```
|
||||
|
||||
Select limits on item functionality to increase difficulty. (default: normal)
|
||||
|
||||
```
|
||||
--timer [{none,display,timed,timed-ohko,ohko,timed-countdown}]
|
||||
```
|
||||
|
||||
Select the timer setting. (default: none)
|
||||
|
||||
```
|
||||
--progressive [{on,off,random}]
|
||||
```
|
||||
|
||||
Select the setting for progressive equipment. (default: on)
|
||||
|
||||
```
|
||||
--algorithm [{freshness,flood,vt21,vt22,vt25,vt26,balanced}]
|
||||
```
|
||||
|
||||
Select item distribution algorithm. (default: balanced)
|
||||
|
||||
```
|
||||
--shuffle [{default,simple,restricted,full,crossed,insanity,restricted_legacy,full_legacy,madness_legacy,insanity_legacy,dungeonsfull,dungeonssimple}]
|
||||
```
|
||||
|
||||
Select entrance shuffle algorithm. (default: full)
|
||||
|
||||
```
|
||||
--rom ROM
|
||||
```
|
||||
|
||||
Path to a Japanese 1.0 A Link to the Past Rom. (default: Zelda no Densetsu - Kamigami no Triforce (Japan).sfc)
|
||||
|
||||
```
|
||||
--loglevel [{error,info,warning,debug}]
|
||||
```
|
||||
|
||||
Select level of logging for output. (default: info)
|
||||
|
||||
```
|
||||
--seed SEED
|
||||
```
|
||||
|
||||
Define seed number to generate. (default: None)
|
||||
|
||||
```
|
||||
--count COUNT
|
||||
```
|
||||
|
||||
Set the count option (default: None)
|
||||
|
||||
```
|
||||
--quickswap
|
||||
```
|
||||
|
||||
Use to enable quick item swap with L/R buttons. (default: False)
|
||||
|
||||
```
|
||||
--fastmenu [{normal,instant,double,triple,quadruple,half}]
|
||||
```
|
||||
|
||||
Alters the rate at which the menu opens and closes. (default: normal)
|
||||
|
||||
|
||||
```
|
||||
--disablemusic
|
||||
```
|
||||
|
||||
Disables game music, resulting in the game sound being just the SFX. (default: False)
|
||||
|
||||
```
|
||||
--mapshuffle --compassshuffle --keyshuffle --bigkeyshuffle
|
||||
```
|
||||
|
||||
Respectively enable Map/Compass/SmallKey/BigKey shuffle (default: False)
|
||||
|
||||
```
|
||||
--retro
|
||||
```
|
||||
|
||||
Enable Retro mode (default: False)
|
||||
|
||||
```
|
||||
--heartbeep [{normal,half,quarter,off}]
|
||||
```
|
||||
|
||||
Select frequency of beeps when on low health. (default: normal)
|
||||
|
||||
```
|
||||
--heartcolor [{red,blue,green,yellow,random}]
|
||||
```
|
||||
|
||||
Select the color of Link\'s heart meter. (default: red)
|
||||
|
||||
```
|
||||
--sprite SPRITE
|
||||
```
|
||||
|
||||
Use to select a different sprite sheet to use for Link. Path to a binary file of length 0x7000 containing the sprite data stored at address 0x80000 in the rom. (default: None)
|
||||
|
||||
```
|
||||
--accessibility [{items,locations,none}]
|
||||
```
|
||||
|
||||
Sets the item/location accessibility rules. (default: items)
|
||||
|
||||
```
|
||||
--hints
|
||||
```
|
||||
|
||||
Enables helpful hints from storytellers and telepathic tiles (default: False)
|
||||
|
||||
```
|
||||
--no-shuffleganon
|
||||
```
|
||||
|
||||
Disables the "Include Ganon's Tower and Pyramid Hole in Shuffle pool" option. (default: Enabled)
|
||||
|
||||
```
|
||||
--suppress_rom
|
||||
```
|
||||
|
||||
Enables the "Do not Create Patched Rom" option. (default: False)
|
||||
|
||||
```
|
||||
--gui
|
||||
```
|
||||
|
||||
Open the graphical user interface. Preloads selections with set command line parameters.
|
||||
|
||||
71
Rom.py
71
Rom.py
@@ -10,17 +10,18 @@ import sys
|
||||
import subprocess
|
||||
|
||||
from BaseClasses import CollectionState, ShopType, Region, Location, Item, DoorType
|
||||
from DoorShuffle import compass_data
|
||||
from Dungeons import dungeon_music_addresses
|
||||
from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable
|
||||
from Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, Blind_texts, BombShop2_texts, junk_texts
|
||||
from Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts, LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names
|
||||
from Utils import output_path, local_path, int16_as_bytes, int32_as_bytes, snes_to_pc
|
||||
from Items import ItemFactory
|
||||
from Items import ItemFactory, item_table
|
||||
from EntranceShuffle import door_addresses, exit_ids
|
||||
|
||||
|
||||
JAP10HASH = '03a63945398191337e896e5771f77173'
|
||||
# RANDOMIZERBASEHASH = '1907d4caccffe60fc69940cfa11b2dab'
|
||||
RANDOMIZERBASEHASH = 'a5ae693acf449264533429d4dbd217e4'
|
||||
|
||||
|
||||
class JsonRom(object):
|
||||
@@ -68,6 +69,7 @@ class JsonRom(object):
|
||||
h.update(json.dumps([self.patches]).encode('utf-8'))
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
class LocalRom(object):
|
||||
|
||||
def __init__(self, file, patch=True):
|
||||
@@ -109,10 +111,10 @@ class LocalRom(object):
|
||||
self.write_bytes(int(baseaddress), values)
|
||||
|
||||
# verify md5
|
||||
# patchedmd5 = hashlib.md5()
|
||||
# patchedmd5.update(self.buffer)
|
||||
# if RANDOMIZERBASEHASH != patchedmd5.hexdigest():
|
||||
# raise RuntimeError('Provided Base Rom unsuitable for patching. Please provide a JAP(1.0) "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" rom to use as a base.')
|
||||
patchedmd5 = hashlib.md5()
|
||||
patchedmd5.update(self.buffer)
|
||||
if RANDOMIZERBASEHASH != patchedmd5.hexdigest():
|
||||
raise RuntimeError('Provided Base Rom unsuitable for patching. Please provide a JAP(1.0) "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" rom to use as a base.')
|
||||
|
||||
def merge_enemizer_patches(self, patches):
|
||||
self.buffer.extend(bytearray([0x00] * (0x400000 - len(self.buffer))))
|
||||
@@ -583,6 +585,10 @@ def patch_rom(world, player, rom, enemized):
|
||||
rom.write_byte(0x151f1, 2)
|
||||
rom.write_byte(0x15270, 2)
|
||||
rom.write_byte(0x1597b, 2)
|
||||
if compass_code_good(rom):
|
||||
update_compasses(rom, world, player)
|
||||
else:
|
||||
logging.getLogger('').warning('Randomizer rom update! Compasses in crossed are borken')
|
||||
for door in world.doors:
|
||||
if door.dest is not None and door.player == player and door.type in [DoorType.Normal, DoorType.SpiralStairs]:
|
||||
rom.write_bytes(door.getAddress(), door.dest.getTarget(door.toggle))
|
||||
@@ -2059,6 +2065,59 @@ def patch_shuffled_dark_sanc(world, rom, player):
|
||||
write_int16s(rom, 0x180253, [vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x])
|
||||
rom.write_bytes(0x180262, [unknown_1, unknown_2, 0x00])
|
||||
|
||||
# 24AE17 and 20B7B9
|
||||
compass_r_addr = 0x122e17 # a9 90 24 8f 9a c7 7e
|
||||
compass_w_addr = 0x1037b9 # e2 20 ad 0c 04 c9 00 d0
|
||||
|
||||
|
||||
def compass_code_good(rom):
|
||||
if isinstance(rom, LocalRom):
|
||||
# a990248f9ac77e
|
||||
if rom.buffer[compass_r_addr] != 0xa9 or rom.buffer[compass_r_addr+1] != 0x90 or rom.buffer[compass_r_addr+2] != 0x24:
|
||||
return False
|
||||
if rom.buffer[compass_r_addr+3] != 0x8f or rom.buffer[compass_r_addr+4] != 0x9a or rom.buffer[compass_r_addr+5] != 0xc7:
|
||||
return False
|
||||
if rom.buffer[compass_w_addr] != 0xe2 or rom.buffer[compass_w_addr+1] != 0x20 or rom.buffer[compass_w_addr+2] != 0xad:
|
||||
return False
|
||||
if rom.buffer[compass_w_addr+3] != 0x0c or rom.buffer[compass_w_addr+4] != 0x04 or rom.buffer[compass_w_addr+5] != 0xc9:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def update_compasses(rom, world, player):
|
||||
layouts = world.dungeon_layouts[player]
|
||||
for name, builder in layouts.items():
|
||||
digit_offset, sram_byte, write_offset, jmp_nop_flag = compass_data[name]
|
||||
digit1 = builder.location_cnt // 10
|
||||
digit2 = builder.location_cnt % 10
|
||||
rom.write_byte(compass_r_addr+digit_offset, 0x90+digit1)
|
||||
rom.write_byte(compass_r_addr+digit_offset+7, 0x90+digit2)
|
||||
|
||||
# read compass count code
|
||||
start_address = compass_r_addr+digit_offset+15
|
||||
# lda $7ef4(sb); jmp $adbe (or 0x122dbe is the next instruction 2dbe=bead)
|
||||
rom.write_bytes(start_address, [0xaf, sram_byte, 0xf4, 0x7e, 0x4c, 0xbe, 0xad])
|
||||
|
||||
# write compass count code
|
||||
write_address = compass_w_addr+write_offset
|
||||
# lda $7ef4(sb); inc; sta $7ef4(sb)
|
||||
rom.write_bytes(write_address, [0xaf, sram_byte, 0xf4, 0x7e, 0x1a, 0x8f, sram_byte, 0xf4, 0x7e])
|
||||
if jmp_nop_flag == 0:
|
||||
rom.write_bytes(write_address+9, [0x4c, 0x3f, 0xb9]) # jmp $b93f (or 0x10393f is the next instruction 393f=3fb9)
|
||||
else:
|
||||
for i in range(0, jmp_nop_flag):
|
||||
rom.write_byte(write_address+9+i, 0xea) # nop
|
||||
|
||||
|
||||
InconvenientEntrances = {'Turtle Rock': 'Turtle Rock Main',
|
||||
'Misery Mire': 'Misery Mire',
|
||||
'Ice Palace': 'Ice Palace',
|
||||
'Skull Woods Final Section': 'The back of Skull Woods',
|
||||
'Death Mountain Return Cave (West)': 'The SW DM foothills cave',
|
||||
'Mimic Cave': 'Mimic Ledge',
|
||||
'Dark World Hammer Peg Cave': 'The rows of pegs',
|
||||
'Pyramid Fairy': 'The crack on the pyramid'
|
||||
}
|
||||
InconvenientDungeonEntrances = {'Turtle Rock': 'Turtle Rock Main',
|
||||
'Misery Mire': 'Misery Mire',
|
||||
'Ice Palace': 'Ice Palace',
|
||||
|
||||
4
Rules.py
4
Rules.py
@@ -1511,13 +1511,13 @@ def create_key_rule_allow_small(small_key_name, player, keys, location):
|
||||
|
||||
def create_key_rule_bk_exception(small_key_name, big_key_name, player, keys, bk_keys, bk_locs):
|
||||
chest_names = [x.name for x in bk_locs]
|
||||
return lambda state: state.has_key(small_key_name, player, keys) or (item_in_locations(state, big_key_name, player, zip(chest_names, [player] * len(chest_names))) and state.has_key(small_key_name, player, bk_keys))
|
||||
return lambda state: (state.has_key(small_key_name, player, keys) and not item_in_locations(state, big_key_name, player, zip(chest_names, [player] * len(chest_names)))) or (item_in_locations(state, big_key_name, player, zip(chest_names, [player] * len(chest_names))) and state.has_key(small_key_name, player, bk_keys))
|
||||
|
||||
|
||||
def create_key_rule_bk_exception_or_allow(small_key_name, big_key_name, player, keys, location, bk_keys, bk_locs):
|
||||
loc = location.name
|
||||
chest_names = [x.name for x in bk_locs]
|
||||
return lambda state: state.has_key(small_key_name, player, keys) or (item_name(state, loc, player) in [(small_key_name, player)] and state.has_key(small_key_name, player, keys-1)) or (item_in_locations(state, big_key_name, player, zip(chest_names, [player] * len(chest_names))) and state.has_key(small_key_name, player, bk_keys))
|
||||
return lambda state: (state.has_key(small_key_name, player, keys) and not item_in_locations(state, big_key_name, player, zip(chest_names, [player] * len(chest_names)))) or (item_name(state, loc, player) in [(small_key_name, player)] and state.has_key(small_key_name, player, keys-1)) or (item_in_locations(state, big_key_name, player, zip(chest_names, [player] * len(chest_names))) and state.has_key(small_key_name, player, bk_keys))
|
||||
|
||||
|
||||
def create_advanced_key_rule(key_logic, player, rule):
|
||||
|
||||
4
Utils.py
4
Utils.py
@@ -228,4 +228,6 @@ def print_wiki_doors(d_regions, world, player):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
read_entrance_data(old_rom='C:\\Users\\Randall\\Documents\\kwyn\\orig\\z3.sfc')
|
||||
pass
|
||||
# make_new_base2current()
|
||||
# read_entrance_data(old_rom='')
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user