Replace keysanity with map/compass/key/bk shuffle

This commit is contained in:
Bonta-kun 2019-12-13 22:37:52 +01:00
parent 6ca08a0fa4
commit fc9d1b501b
12 changed files with 177 additions and 126 deletions

View File

@ -8,7 +8,7 @@ from Utils import int16_as_bytes
class World(object):
def __init__(self, players, shuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer, progressive, goal, algorithm, place_dungeon_items, accessibility, shuffle_ganon, quickswap, fastmenu, disable_music, keysanity, retro, custom, customitemarray, boss_shuffle, hints):
def __init__(self, players, shuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer, progressive, goal, algorithm, accessibility, shuffle_ganon, quickswap, fastmenu, disable_music, retro, custom, customitemarray, boss_shuffle, hints):
self.players = players
self.shuffle = shuffle
self.logic = logic
@ -35,7 +35,6 @@ class World(object):
self._entrance_cache = {}
self._location_cache = {}
self.required_locations = []
self.place_dungeon_items = place_dungeon_items # configurable in future
self.shuffle_bonk_prizes = False
self.swamp_patch_required = {player: False for player in range(1, players + 1)}
self.powder_patch_required = {player: False for player in range(1, players + 1)}
@ -65,7 +64,10 @@ class World(object):
self.quickswap = quickswap
self.fastmenu = fastmenu
self.disable_music = disable_music
self.keysanity = keysanity
self.mapshuffle = False
self.compassshuffle = False
self.keyshuffle = False
self.bigkeyshuffle = False
self.retro = retro
self.custom = custom
self.customitemarray = customitemarray
@ -175,7 +177,7 @@ class World(object):
elif item.name.startswith('Bottle'):
if ret.bottle_count(item.player) < self.difficulty_requirements.progressive_bottle_limit:
ret.prog_items.add((item.name, item.player))
elif item.advancement or item.key:
elif item.advancement or item.smallkey or item.bigkey:
ret.prog_items.add((item.name, item.player))
for item in self.itempool:
@ -352,12 +354,14 @@ class CollectionState(object):
def sweep_for_events(self, key_only=False, locations=None):
# this may need improvement
if locations is None:
locations = self.world.get_filled_locations()
new_locations = True
checked_locations = 0
while new_locations:
if locations is None:
locations = self.world.get_filled_locations()
reachable_events = [location for location in locations if location.event and (not key_only or location.item.key) and location.can_reach(self)]
reachable_events = [location for location in locations if location.event and
(not key_only or (not self.world.keyshuffle and location.item.smallkey) or (not self.world.bigkeyshuffle and location.item.bigkey))
and location.can_reach(self)]
for event in reachable_events:
if (event.name, event.player) not in self.events:
self.events.append((event.name, event.player))
@ -677,9 +681,12 @@ class Region(object):
return False
def can_fill(self, item):
is_dungeon_item = item.key or item.map or item.compass
inside_dungeon_item = ((item.smallkey and not self.world.keyshuffle)
or (item.bigkey and not self.world.bigkeyshuffle)
or (item.map and not self.world.mapshuffle)
or (item.compass and not self.world.compassshuffle))
sewer_hack = self.world.mode == 'standard' and item.name == 'Small Key (Escape)'
if sewer_hack or (is_dungeon_item and not self.world.keysanity):
if sewer_hack or inside_dungeon_item:
return self.dungeon and self.dungeon.is_dungeon_item(item) and item.player == self.player
return True
@ -838,14 +845,18 @@ class Item(object):
self.location = None
self.player = player
@property
def key(self):
return self.type == 'SmallKey' or self.type == 'BigKey'
@property
def crystal(self):
return self.type == 'Crystal'
@property
def smallkey(self):
return self.type == 'SmallKey'
@property
def bigkey(self):
return self.type == 'BigKey'
@property
def map(self):
return self.type == 'Map'
@ -1036,7 +1047,10 @@ class Spoiler(object):
'item_functionality': self.world.difficulty_adjustments,
'accessibility': self.world.accessibility,
'hints': self.world.hints,
'keysanity': self.world.keysanity,
'mapshuffle': self.world.mapshuffle,
'compassshuffle': self.world.compassshuffle,
'keyshuffle': self.world.keyshuffle,
'bigkeyshuffle': self.world.bigkeyshuffle,
'players': self.world.players
}
@ -1068,10 +1082,12 @@ class Spoiler(object):
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'])
outfile.write('Filling Algorithm: %s\n' % self.world.algorithm)
outfile.write('Accessibility: %s\n' % self.metadata['accessibility'])
outfile.write('Maps and Compasses in Dungeons: %s\n' % ('Yes' if self.world.place_dungeon_items else 'No'))
outfile.write('L\\R Quickswap enabled: %s\n' % ('Yes' if self.world.quickswap else 'No'))
outfile.write('Menu speed: %s\n' % self.world.fastmenu)
outfile.write('Keysanity enabled: %s\n' % ('Yes' if self.metadata['keysanity'] else 'No'))
outfile.write('Map shuffle: %s\n' % ('Yes' if self.metadata['mapshuffle'] else 'No'))
outfile.write('Compass shuffle: %s\n' % ('Yes' if self.metadata['compassshuffle'] else 'No'))
outfile.write('Small Key shuffle: %s\n' % ('Yes' if self.metadata['keyshuffle'] else 'No'))
outfile.write('Big Key shuffle: %s\n' % ('Yes' if self.metadata['bigkeyshuffle'] else 'No'))
outfile.write('Players: %d' % self.world.players)
if self.entrances:
outfile.write('\n\nEntrances:\n\n')

View File

@ -113,14 +113,13 @@ def fill_dungeons(world):
continue
# next place dungeon items
if world.place_dungeon_items:
for dungeon_item in dungeon_items:
di_location = dungeon_locations.pop()
world.push_item(di_location, dungeon_item, False)
for dungeon_item in dungeon_items:
di_location = dungeon_locations.pop()
world.push_item(di_location, dungeon_item, False)
def get_dungeon_item_pool(world):
return [item for dungeon in world.dungeons for item in dungeon.all_items if item.key or world.place_dungeon_items]
return [item for dungeon in world.dungeons for item in dungeon.all_items]
def fill_dungeons_restrictive(world, shuffled_locations):
all_state_base = world.get_all_state()
@ -135,16 +134,18 @@ def fill_dungeons_restrictive(world, shuffled_locations):
pinball_room.locked = True
shuffled_locations.remove(pinball_room)
if world.keysanity:
#in keysanity dungeon items are distributed as part of the normal item pool
for item in world.get_items():
if item.key:
item.advancement = True
elif item.map or item.compass:
item.priority = True
return
# with shuffled dungeon items they are distributed as part of the normal item pool
for item in world.get_items():
if (item.smallkey and world.keyshuffle) or (item.bigkey and world.bigkeyshuffle):
all_state_base.collect(item, True)
item.advancement = True
elif (item.map and world.mapshuffle) or (item.compass and world.compassshuffle):
item.priority = True
dungeon_items = get_dungeon_item_pool(world)
dungeon_items = [item for item in get_dungeon_item_pool(world) if ((item.smallkey and not world.keyshuffle)
or (item.bigkey and not world.bigkeyshuffle)
or (item.map and not world.mapshuffle)
or (item.compass and not world.compassshuffle))]
# sort in the order Big Key, Small Key, Other before placing dungeon items
sort_order = {"BigKey": 3, "SmallKey": 2}

View File

@ -85,7 +85,7 @@ In the vanilla, dungeonssimple, and dungeonsfull shuffles, the following two loc
Graveyard Cave
Mimic Cave
Valuable Items are simply all items that are shown on the pause subscreen (Y, B, or A sections) minus Silver Arrows and plus Triforce Pieces, Magic Upgrades (1/2 or 1/4), and the Single Arrow. If keysanity is being used, you can additionally get hints for Small Keys or Big Keys but not hints for Maps or Compasses.
Valuable Items are simply all items that are shown on the pause subscreen (Y, B, or A sections) minus Silver Arrows and plus Triforce Pieces, Magic Upgrades (1/2 or 1/4), and the Single Arrow. If key shuffle is being used, you can additionally get hints for Small Keys or Big Keys but not hints for Maps or Compasses.
While the exact verbage of location names and item names can be found in the source code, here's a copy for reference:

View File

@ -198,20 +198,16 @@ def start():
''')
parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true')
parser.add_argument('--disablemusic', help='Disables game music.', action='store_true')
parser.add_argument('--keysanity', help='''\
Keys (and other dungeon items) are no longer restricted to
their dungeons, but can be anywhere
''', action='store_true')
parser.add_argument('--mapshuffle', help='Maps are no longer restricted to their dungeons, but can be anywhere', action='store_true')
parser.add_argument('--compassshuffle', help='Compasses are no longer restricted to their dungeons, but can be anywhere', action='store_true')
parser.add_argument('--keyshuffle', help='Small Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true')
parser.add_argument('--bigkeyshuffle', help='Big Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true')
parser.add_argument('--retro', help='''\
Keys are universal, shooting arrows costs rupees,
and a few other little things make this more like Zelda-1.
''', action='store_true')
parser.add_argument('--custom', default=False, help='Not supported.')
parser.add_argument('--customitemarray', default=False, help='Not supported.')
parser.add_argument('--nodungeonitems', help='''\
Remove Maps and Compasses from Itempool, replacing them by
empty slots.
''', action='store_true')
parser.add_argument('--accessibility', default='items', const='items', nargs='?', choices=['items', 'locations', 'none'], help='''\
Select Item/Location Accessibility. (default: %(default)s)
Items: You can reach all unique inventory items. No guarantees about

20
Fill.py
View File

@ -240,8 +240,8 @@ def distribute_items_restrictive(world, gftower_trash_count=0, fill_locations=No
random.shuffle(fill_locations)
fill_locations.reverse()
# Make sure the escape small key is placed first in standard keysanity to prevent running out of spots
if world.keysanity and world.mode == 'standard':
# Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots
if world.keyshuffle and world.mode == 'standard':
progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' else 0)
fill_restrictive(world, world.state, fill_locations, progitempool)
@ -312,7 +312,7 @@ def flood_items(world):
location_list = world.get_reachable_locations()
random.shuffle(location_list)
for location in location_list:
if location.item is not None and not location.item.advancement and not location.item.priority and not location.item.key:
if location.item is not None and not location.item.advancement and not location.item.priority and not location.item.smallkey and not location.item.bigkey:
# safe to replace
replace_item = location.item
replace_item.location = None
@ -332,8 +332,7 @@ def balance_multiworld_progression(world):
reachable_locations_count[player] = 0
def get_sphere_locations(sphere_state, locations):
if not world.keysanity:
sphere_state.sweep_for_events(key_only=True, locations=locations)
sphere_state.sweep_for_events(key_only=True, locations=locations)
return [loc for loc in locations if sphere_state.can_reach(loc)]
while True:
@ -354,7 +353,7 @@ def balance_multiworld_progression(world):
candidate_items = []
while True:
for location in balancing_sphere:
if location.event:
if location.event and (world.keyshuffle or not location.item.smallkey) and (world.bigkeyshuffle or not location.item.bigkey):
balancing_state.collect(location.item, True, location)
if location.item.player in balancing_players and not location.locked:
candidate_items.append(location)
@ -364,11 +363,14 @@ def balance_multiworld_progression(world):
balancing_reachables[location.player] += 1
if world.has_beaten_game(balancing_state) or all([reachables >= threshold for reachables in balancing_reachables.values()]):
break
elif not balancing_sphere:
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
unlocked_locations = [l for l in unchecked_locations if l not in balancing_unchecked_locations]
items_to_replace = []
for player in balancing_players:
locations_to_test = [l for l in unlocked_locations if l.player == player]
# only replace items that end up in another player's world
items_to_test = [l for l in candidate_items if l.item.player == player and l.player != player]
while items_to_test:
testing = items_to_test.pop()
@ -392,7 +394,7 @@ def balance_multiworld_progression(world):
new_location = replacement_locations.pop()
old_location = items_to_replace.pop()
while not new_location.can_fill(state, old_location.item):
while not new_location.can_fill(state, old_location.item, False) or (new_location.item and not old_location.can_fill(state, new_location.item, False)):
replacement_locations.insert(0, new_location)
new_location = replacement_locations.pop()
@ -407,9 +409,11 @@ def balance_multiworld_progression(world):
sphere_locations.append(location)
for location in sphere_locations:
if location.event and (world.keysanity or not location.item.key):
if location.event and (world.keyshuffle or not location.item.smallkey) and (world.bigkeyshuffle or not location.item.bigkey):
state.collect(location.item, True, location)
checked_locations.extend(sphere_locations)
if world.has_beaten_game(state):
break
elif not sphere_locations:
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')

35
Gui.py
View File

@ -63,12 +63,18 @@ def guiMain(args=None):
quickSwapCheckbutton = Checkbutton(checkBoxFrame, text="Enabled L/R Item quickswapping", variable=quickSwapVar)
openpyramidVar = IntVar()
openpyramidCheckbutton = Checkbutton(checkBoxFrame, text="Pre-open Pyramid Hole", variable=openpyramidVar)
keysanityVar = IntVar()
keysanityCheckbutton = Checkbutton(checkBoxFrame, text="Keysanity (keys anywhere)", variable=keysanityVar)
mcsbshuffleFrame = Frame(checkBoxFrame)
mcsbLabel = Label(mcsbshuffleFrame, text="Shuffle: ")
mapshuffleVar = IntVar()
mapshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="Maps", variable=mapshuffleVar)
compassshuffleVar = IntVar()
compassshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="Compasses", variable=compassshuffleVar)
keyshuffleVar = IntVar()
keyshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="Keys", variable=keyshuffleVar)
bigkeyshuffleVar = IntVar()
bigkeyshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="BigKeys", variable=bigkeyshuffleVar)
retroVar = IntVar()
retroCheckbutton = Checkbutton(checkBoxFrame, text="Retro mode (universal keys)", variable=retroVar)
dungeonItemsVar = IntVar()
dungeonItemsCheckbutton = Checkbutton(checkBoxFrame, text="Place Dungeon Items (Compasses/Maps)", onvalue=0, offvalue=1, variable=dungeonItemsVar)
disableMusicVar = IntVar()
disableMusicCheckbutton = Checkbutton(checkBoxFrame, text="Disable game music", variable=disableMusicVar)
shuffleGanonVar = IntVar()
@ -84,9 +90,13 @@ def guiMain(args=None):
suppressRomCheckbutton.pack(expand=True, anchor=W)
quickSwapCheckbutton.pack(expand=True, anchor=W)
openpyramidCheckbutton.pack(expand=True, anchor=W)
keysanityCheckbutton.pack(expand=True, anchor=W)
mcsbshuffleFrame.pack(expand=True, anchor=W)
mcsbLabel.grid(row=0, column=0)
mapshuffleCheckbutton.grid(row=0, column=1)
compassshuffleCheckbutton.grid(row=0, column=2)
keyshuffleCheckbutton.grid(row=0, column=3)
bigkeyshuffleCheckbutton.grid(row=0, column=4)
retroCheckbutton.pack(expand=True, anchor=W)
dungeonItemsCheckbutton.pack(expand=True, anchor=W)
disableMusicCheckbutton.pack(expand=True, anchor=W)
shuffleGanonCheckbutton.pack(expand=True, anchor=W)
hintsCheckbutton.pack(expand=True, anchor=W)
@ -385,9 +395,11 @@ def guiMain(args=None):
guiargs.create_spoiler = bool(createSpoilerVar.get())
guiargs.suppress_rom = bool(suppressRomVar.get())
guiargs.openpyramid = bool(openpyramidVar.get())
guiargs.keysanity = bool(keysanityVar.get())
guiargs.mapshuffle = bool(mapshuffleVar.get())
guiargs.compassshuffle = bool(compassshuffleVar.get())
guiargs.keyshuffle = bool(keyshuffleVar.get())
guiargs.bigkeyshuffle = bool(bigkeyshuffleVar.get())
guiargs.retro = bool(retroVar.get())
guiargs.nodungeonitems = bool(dungeonItemsVar.get())
guiargs.quickswap = bool(quickSwapVar.get())
guiargs.disablemusic = bool(disableMusicVar.get())
guiargs.shuffleganon = bool(shuffleGanonVar.get())
@ -1160,10 +1172,11 @@ def guiMain(args=None):
# load values from commandline args
createSpoilerVar.set(int(args.create_spoiler))
suppressRomVar.set(int(args.suppress_rom))
keysanityVar.set(args.keysanity)
mapshuffleVar.set(args.mapshuffle)
compassshuffleVar.set(args.compassshuffle)
keyshuffleVar.set(args.keyshuffle)
bigkeyshuffleVar.set(args.bigkeyshuffle)
retroVar.set(args.retro)
if args.nodungeonitems:
dungeonItemsVar.set(int(not args.nodungeonitems))
quickSwapVar.set(int(args.quickswap))
disableMusicVar.set(int(args.disablemusic))
if args.count:

View File

@ -221,8 +221,11 @@ def generate_itempool(world, player):
if treasure_hunt_icon is not None:
world.treasure_hunt_icon = treasure_hunt_icon
if world.keysanity:
world.itempool.extend([item for item in get_dungeon_item_pool(world) if item.player == player])
world.itempool.extend([item for item in get_dungeon_item_pool(world) if item.player == player
and ((item.smallkey and world.keyshuffle)
or (item.bigkey and world.bigkeyshuffle)
or (item.map and world.mapshuffle)
or (item.compass and world.compassshuffle))])
# logic has some branches where having 4 hearts is one possible requirement (of several alternatives)
# rather than making all hearts/heart pieces progression items (which slows down generation considerably)

31
Main.py
View File

@ -25,7 +25,7 @@ def main(args, seed=None):
start = time.process_time()
# initialize the world
world = World(args.multi, args.shuffle, args.logic, args.mode, args.swords, args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm, not args.nodungeonitems, args.accessibility, args.shuffleganon, args.quickswap, args.fastmenu, args.disablemusic, args.keysanity, args.retro, args.custom, args.customitemarray, args.shufflebosses, args.hints)
world = World(args.multi, args.shuffle, args.logic, args.mode, args.swords, args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm, args.accessibility, args.shuffleganon, args.quickswap, args.fastmenu, args.disablemusic, args.retro, args.custom, args.customitemarray, args.shufflebosses, args.hints)
logger = logging.getLogger('')
if seed is None:
random.seed(None)
@ -34,6 +34,19 @@ def main(args, seed=None):
world.seed = int(seed)
random.seed(world.seed)
world.mapshuffle = args.mapshuffle
world.compassshuffle = args.compassshuffle
world.keyshuffle = args.keyshuffle
world.bigkeyshuffle = args.bigkeyshuffle
mcsb_name = ''
if all([world.mapshuffle, world.compassshuffle, world.keyshuffle, world.bigkeyshuffle]):
mcsb_name = '-keysanity'
elif [world.mapshuffle, world.compassshuffle, world.keyshuffle, world.bigkeyshuffle].count(True) == 1:
mcsb_name = '-mapshuffle' if world.mapshuffle else '-compassshuffle' if world.compassshuffle else '-keyshuffle' if world.keyshuffle else '-bigkeyshuffle'
elif any([world.mapshuffle, world.compassshuffle, world.keyshuffle, world.bigkeyshuffle]):
mcsb_name = '-%s%s%s%sshuffle' % ('M' if world.mapshuffle else '', 'C' if world.compassshuffle else '', 'S' if world.keyshuffle else '', 'B' if world.bigkeyshuffle else '')
world.crystals_needed_for_ganon = random.randint(0, 7) if args.crystals_ganon == 'random' else int(args.crystals_ganon)
world.crystals_needed_for_gt = random.randint(0, 7) if args.crystals_gt == 'random' else int(args.crystals_gt)
world.open_pyramid = args.openpyramid
@ -83,7 +96,7 @@ def main(args, seed=None):
logger.info('Placing Dungeon Items.')
shuffled_locations = None
if args.algorithm in ['balanced', 'vt26'] or args.keysanity:
if args.algorithm in ['balanced', 'vt26'] or args.mapshuffle or args.compassshuffle or args.keyshuffle or args.bigkeyshuffle:
shuffled_locations = world.get_unfilled_locations()
random.shuffle(shuffled_locations)
fill_dungeons_restrictive(world, shuffled_locations)
@ -124,7 +137,7 @@ def main(args, seed=None):
player_names = parse_names_string(args.names)
outfileprefix = 'ER_%s_' % world.seed
outfilesuffix = '%s_%s-%s-%s-%s%s_%s-%s%s%s%s%s' % (world.logic, world.difficulty, world.difficulty_adjustments, world.mode, world.goal, "" if world.timer in ['none', 'display'] else "-" + world.timer, world.shuffle, world.algorithm, "-keysanity" if world.keysanity else "", "-retro" if world.retro else "", "-prog_" + world.progressive if world.progressive in ['off', 'random'] else "", "-nohints" if not world.hints else "")
outfilesuffix = '%s_%s-%s-%s-%s%s_%s-%s%s%s%s%s' % (world.logic, world.difficulty, world.difficulty_adjustments, world.mode, world.goal, "" if world.timer in ['none', 'display'] else "-" + world.timer, world.shuffle, world.algorithm, mcsb_name, "-retro" if world.retro else "", "-prog_" + world.progressive if world.progressive in ['off', 'random'] else "", "-nohints" if not world.hints else "")
outfilebase = outfileprefix + outfilesuffix
use_enemizer = args.enemizercli and (args.shufflebosses != 'none' or args.shuffleenemies or args.enemy_health != 'default' or args.enemy_health != 'default' or args.enemy_damage or args.shufflepalette or args.shufflepots)
@ -198,7 +211,7 @@ def gt_filler(world):
def copy_world(world):
# ToDo: Not good yet
ret = World(world.players, world.shuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.place_dungeon_items, world.accessibility, world.shuffle_ganon, world.quickswap, world.fastmenu, world.disable_music, world.keysanity, world.retro, world.custom, world.customitemarray, world.boss_shuffle, world.hints)
ret = World(world.players, world.shuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.accessibility, world.shuffle_ganon, world.quickswap, world.fastmenu, world.disable_music, world.retro, world.custom, world.customitemarray, world.boss_shuffle, world.hints)
ret.required_medallions = world.required_medallions.copy()
ret.swamp_patch_required = world.swamp_patch_required.copy()
ret.ganon_at_pyramid = world.ganon_at_pyramid.copy()
@ -218,6 +231,10 @@ def copy_world(world):
ret.difficulty_requirements = world.difficulty_requirements
ret.fix_fake_world = world.fix_fake_world
ret.lamps_needed_for_dark_rooms = world.lamps_needed_for_dark_rooms
ret.mapshuffle = world.mapshuffle
ret.compassshuffle = world.compassshuffle
ret.keyshuffle = world.keyshuffle
ret.bigkeyshuffle = world.bigkeyshuffle
ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon
ret.crystals_needed_for_gt = world.crystals_needed_for_gt
@ -318,8 +335,7 @@ def create_playthrough(world):
sphere_candidates = list(prog_locations)
logging.getLogger('').debug('Building up collection spheres.')
while sphere_candidates:
if not world.keysanity:
state.sweep_for_events(key_only=True)
state.sweep_for_events(key_only=True)
sphere = []
# build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
@ -372,8 +388,7 @@ def create_playthrough(world):
state = CollectionState(world)
collection_spheres = []
while required_locations:
if not world.keysanity:
state.sweep_for_events(key_only=True)
state.sweep_for_events(key_only=True)
sphere = list(filter(state.can_reach, required_locations))

View File

@ -174,7 +174,7 @@ def fill_world(world, plando, text_patches):
item = ItemFactory(itemstr.strip(), 1)
if item is not None:
world.push_item(location, item)
if item.key:
if item.smallkey or item.bigkey:
location.event = True
elif '<=>' in line:
entrance, exit = line.split('<=>', 1)

View File

@ -121,7 +121,7 @@ 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 Keysanity from working.
This will prevent the dungeon item count feature in Easy and Compass shuffle from working.
### Timed
@ -264,12 +264,12 @@ generate spoilers for statistical analysis.
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.
## Keysanity
## Map/Compass/Small Key/Big Key shuffle (aka Keysanity)
This setting allows dungeon specific items (Small Key, Big Key, Map, Compass) to be distributed anywhere in the world and not just
in their native dungeon. Small Keys dropped by enemies or found in pots are not affected. The chest in southeast Skull Woods that
is traditionally a guaranteed Small Key still is. These items will be distributed according to the v26/balanced algorithm, but
the rest of the itempool will respect the algorithm setting. Music for dungeons is randomized so it cannot be used as a tell
These settings allow dungeon specific items to be distributed anywhere in the world and not just in their native dungeon.
Small Keys dropped by enemies or found in pots are not affected. The chest in southeast Skull Woods that is traditionally
a guaranteed Small Key still is. These items will be distributed according to the v26/balanced algorithm, but the rest
of the itempool will respect the algorithm setting. Music for dungeons is randomized so it cannot be used as a tell
for which dungeons contain pendants and crystals; finding a Map for a dungeon will allow the overworld map to display its prize.
## Retro
@ -422,10 +422,10 @@ Alters the rate at which the menu opens and closes. (default: normal)
Disables game music, resulting in the game sound being just the SFX. (default: False)
```
--keysanity
--mapshuffle --compassshuffle --keyshuffle --bigkeyshuffle
```
Enable Keysanity (default: False)
Respectively enable Map/Compass/SmallKey/BigKey shuffle (default: False)
```
--retro
@ -433,13 +433,6 @@ Enable Keysanity (default: False)
Enable Retro mode (default: False)
```
--nodungeonitems
```
If 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. (default: False)
```
--heartbeep [{normal,half,quarter,off}]
```

90
Rom.py
View File

@ -444,10 +444,10 @@ def patch_rom(world, player, rom):
# Keys in their native dungeon should use the orignal item code for keys
if location.parent_region.dungeon:
dungeon = location.parent_region.dungeon
if location.item is not None and location.item.key and dungeon.is_dungeon_item(location.item):
if location.item.type == "BigKey":
if location.item is not None and dungeon.is_dungeon_item(location.item):
if location.item.bigkey:
itemid = 0x32
if location.item.type == "SmallKey":
if location.item.smallkey:
itemid = 0x24
if location.item and location.item.player != player:
if location.player_address is not None:
@ -462,15 +462,15 @@ def patch_rom(world, player, rom):
# patch music
music_addresses = dungeon_music_addresses[location.name]
if world.keysanity:
if world.mapshuffle:
music = random.choice([0x11, 0x16])
else:
music = 0x11 if 'Pendant' in location.item.name else 0x16
for music_address in music_addresses:
rom.write_byte(music_address, music)
if world.keysanity:
rom.write_byte(0x155C9, random.choice([0x11, 0x16])) # Randomize GT music too in keysanity mode
if world.mapshuffle:
rom.write_byte(0x155C9, random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle
# patch entrance/exits/holes
for region in world.regions:
@ -811,7 +811,7 @@ def patch_rom(world, player, rom):
ERtimeincrease = 10
else:
ERtimeincrease = 20
if world.keysanity:
if world.keyshuffle or world.bigkeyshuffle or world.mapshuffle:
ERtimeincrease = ERtimeincrease + 15
if world.clock_mode == 'off':
rom.write_bytes(0x180190, [0x00, 0x00, 0x00]) # turn off clock mode
@ -922,18 +922,24 @@ def patch_rom(world, player, rom):
rom.write_byte(0x18005F, world.crystals_needed_for_ganon)
rom.write_byte(0x18008A, 0x01 if world.mode == "standard" else 0x00) # block HC upstairs doors in rain state in standard mode
rom.write_byte(0x18016A, 0x01 if world.keysanity else 0x00) # free roaming item text boxes
rom.write_byte(0x18003B, 0x01 if world.keysanity else 0x00) # maps showing crystals on overworld
rom.write_byte(0x18016A, 0x10 | ((0x01 if world.keyshuffle else 0x00)
| (0x02 if world.compassshuffle else 0x00)
| (0x04 if world.mapshuffle else 0x00)
| (0x08 if world.bigkeyshuffle else 0x00))) # free roaming item text boxes
rom.write_byte(0x18003B, 0x01 if world.mapshuffle else 0x00) # maps showing crystals on overworld
# compasses showing dungeon count
if world.clock_mode != 'off':
rom.write_byte(0x18003C, 0x00) # Currently must be off if timer is on, because they use same HUD location
elif world.keysanity:
elif world.compassshuffle:
rom.write_byte(0x18003C, 0x01) # show on pickup
else:
rom.write_byte(0x18003C, 0x00)
rom.write_byte(0x180045, 0xFF if world.keysanity else 0x00) # free roaming items in menu
rom.write_byte(0x180045, ((0x01 if world.keyshuffle else 0x00)
| (0x02 if world.bigkeyshuffle else 0x00)
| (0x04 if world.compassshuffle else 0x00)
| (0x08 if world.mapshuffle else 0x00))) # free roaming items in menu
# Map reveals
reveal_bytes = {
@ -958,8 +964,8 @@ def patch_rom(world, player, rom):
return reveal_bytes.get(location.parent_region.dungeon.name, 0x0000)
return 0x0000
write_int16(rom, 0x18017A, get_reveal_bytes('Green Pendant') if world.keysanity else 0x0000) # Sahasrahla reveal
write_int16(rom, 0x18017C, get_reveal_bytes('Crystal 5')|get_reveal_bytes('Crystal 6') if world.keysanity else 0x0000) # Bomb Shop Reveal
write_int16(rom, 0x18017A, get_reveal_bytes('Green Pendant') if world.mapshuffle else 0x0000) # Sahasrahla reveal
write_int16(rom, 0x18017C, get_reveal_bytes('Crystal 5')|get_reveal_bytes('Crystal 6') if world.mapshuffle else 0x0000) # Bomb Shop Reveal
rom.write_byte(0x180172, 0x01 if world.retro else 0x00) # universal keys
rom.write_byte(0x180175, 0x01 if world.retro else 0x00) # rupee bow
@ -1440,8 +1446,10 @@ def write_strings(rom, world, player):
# Lastly we write hints to show where certain interesting items are. It is done the way it is to re-use the silver code and also to give one hint per each type of item regardless of how many exist. This supports many settings well.
items_to_hint = RelevantItems.copy()
if world.keysanity:
items_to_hint.extend(KeysanityItems)
if world.keyshuffle:
items_to_hint.extend(SmallKeys)
if world.bigkeyshuffle:
items_to_hint.extend(BigKeys)
random.shuffle(items_to_hint)
hint_count = 5 if world.shuffle not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 8
while hint_count > 0:
@ -2022,28 +2030,30 @@ RelevantItems = ['Bow',
'Magic Upgrade (1/4)'
]
KeysanityItems = ['Small Key (Eastern Palace)',
'Big Key (Eastern Palace)',
'Small Key (Escape)',
'Small Key (Desert Palace)',
'Big Key (Desert Palace)',
'Small Key (Tower of Hera)',
'Big Key (Tower of Hera)',
'Small Key (Agahnims Tower)',
'Small Key (Palace of Darkness)',
'Big Key (Palace of Darkness)',
'Small Key (Thieves Town)',
'Big Key (Thieves Town)',
'Small Key (Swamp Palace)',
'Big Key (Swamp Palace)',
'Small Key (Skull Woods)',
'Big Key (Skull Woods)',
'Small Key (Ice Palace)',
'Big Key (Ice Palace)',
'Small Key (Misery Mire)',
'Big Key (Misery Mire)',
'Small Key (Turtle Rock)',
'Big Key (Turtle Rock)',
'Small Key (Ganons Tower)',
'Big Key (Ganons Tower)'
]
SmallKeys = ['Small Key (Eastern Palace)',
'Small Key (Escape)',
'Small Key (Desert Palace)',
'Small Key (Tower of Hera)',
'Small Key (Agahnims Tower)',
'Small Key (Palace of Darkness)',
'Small Key (Thieves Town)',
'Small Key (Swamp Palace)',
'Small Key (Skull Woods)',
'Small Key (Ice Palace)',
'Small Key (Misery Mire)',
'Small Key (Turtle Rock)',
'Small Key (Ganons Tower)',
]
BigKeys = ['Big Key (Eastern Palace)',
'Big Key (Desert Palace)',
'Big Key (Tower of Hera)',
'Big Key (Palace of Darkness)',
'Big Key (Thieves Town)',
'Big Key (Swamp Palace)',
'Big Key (Skull Woods)',
'Big Key (Ice Palace)',
'Big Key (Misery Mire)',
'Big Key (Turtle Rock)',
'Big Key (Ganons Tower)'
]

View File

@ -804,7 +804,7 @@ def set_trock_key_rules(world, player):
non_big_key_locations += ['Turtle Rock - Crystaroller Room', 'Turtle Rock - Eye Bridge - Bottom Left',
'Turtle Rock - Eye Bridge - Bottom Right', 'Turtle Rock - Eye Bridge - Top Left',
'Turtle Rock - Eye Bridge - Top Right']
if not world.keysanity:
if not world.keyshuffle:
non_big_key_locations += ['Turtle Rock - Big Key Chest']
else:
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 2) if item_in_locations(state, 'Big Key (Turtle Rock)', player, [('Turtle Rock - Compass Chest', player), ('Turtle Rock - Roller Room - Left', player), ('Turtle Rock - Roller Room - Right', player)]) else state.has_key('Small Key (Turtle Rock)', player, 4))
@ -814,7 +814,7 @@ def set_trock_key_rules(world, player):
non_big_key_locations += ['Turtle Rock - Crystaroller Room', 'Turtle Rock - Eye Bridge - Bottom Left',
'Turtle Rock - Eye Bridge - Bottom Right', 'Turtle Rock - Eye Bridge - Top Left',
'Turtle Rock - Eye Bridge - Top Right']
if not world.keysanity:
if not world.keyshuffle:
non_big_key_locations += ['Turtle Rock - Big Key Chest', 'Turtle Rock - Chain Chomps']
# set big key restrictions