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

View File

@ -113,14 +113,13 @@ def fill_dungeons(world):
continue continue
# next place dungeon items # next place dungeon items
if world.place_dungeon_items: for dungeon_item in dungeon_items:
for dungeon_item in dungeon_items: di_location = dungeon_locations.pop()
di_location = dungeon_locations.pop() world.push_item(di_location, dungeon_item, False)
world.push_item(di_location, dungeon_item, False)
def get_dungeon_item_pool(world): 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): def fill_dungeons_restrictive(world, shuffled_locations):
all_state_base = world.get_all_state() all_state_base = world.get_all_state()
@ -135,16 +134,18 @@ def fill_dungeons_restrictive(world, shuffled_locations):
pinball_room.locked = True pinball_room.locked = True
shuffled_locations.remove(pinball_room) shuffled_locations.remove(pinball_room)
if world.keysanity: # with shuffled dungeon items they are distributed as part of the normal item pool
#in keysanity dungeon items are distributed as part of the normal item pool for item in world.get_items():
for item in world.get_items(): if (item.smallkey and world.keyshuffle) or (item.bigkey and world.bigkeyshuffle):
if item.key: all_state_base.collect(item, True)
item.advancement = True item.advancement = True
elif item.map or item.compass: elif (item.map and world.mapshuffle) or (item.compass and world.compassshuffle):
item.priority = True item.priority = True
return
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 in the order Big Key, Small Key, Other before placing dungeon items
sort_order = {"BigKey": 3, "SmallKey": 2} 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 Graveyard Cave
Mimic 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: 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('--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('--disablemusic', help='Disables game music.', action='store_true')
parser.add_argument('--keysanity', help='''\ parser.add_argument('--mapshuffle', help='Maps are no longer restricted to their dungeons, but can be anywhere', action='store_true')
Keys (and other dungeon items) are no longer restricted to parser.add_argument('--compassshuffle', help='Compasses are no longer restricted to their dungeons, but can be anywhere', action='store_true')
their dungeons, but can be anywhere parser.add_argument('--keyshuffle', help='Small Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true')
''', 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='''\ parser.add_argument('--retro', help='''\
Keys are universal, shooting arrows costs rupees, Keys are universal, shooting arrows costs rupees,
and a few other little things make this more like Zelda-1. and a few other little things make this more like Zelda-1.
''', action='store_true') ''', action='store_true')
parser.add_argument('--custom', default=False, help='Not supported.') parser.add_argument('--custom', default=False, help='Not supported.')
parser.add_argument('--customitemarray', 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='''\ parser.add_argument('--accessibility', default='items', const='items', nargs='?', choices=['items', 'locations', 'none'], help='''\
Select Item/Location Accessibility. (default: %(default)s) Select Item/Location Accessibility. (default: %(default)s)
Items: You can reach all unique inventory items. No guarantees about 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) random.shuffle(fill_locations)
fill_locations.reverse() fill_locations.reverse()
# Make sure the escape small key is placed first in standard keysanity to prevent running out of spots # Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots
if world.keysanity and world.mode == 'standard': if world.keyshuffle and world.mode == 'standard':
progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' else 0) progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' else 0)
fill_restrictive(world, world.state, fill_locations, progitempool) fill_restrictive(world, world.state, fill_locations, progitempool)
@ -312,7 +312,7 @@ def flood_items(world):
location_list = world.get_reachable_locations() location_list = world.get_reachable_locations()
random.shuffle(location_list) random.shuffle(location_list)
for location in 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 # safe to replace
replace_item = location.item replace_item = location.item
replace_item.location = None replace_item.location = None
@ -332,8 +332,7 @@ def balance_multiworld_progression(world):
reachable_locations_count[player] = 0 reachable_locations_count[player] = 0
def get_sphere_locations(sphere_state, locations): 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)] return [loc for loc in locations if sphere_state.can_reach(loc)]
while True: while True:
@ -354,7 +353,7 @@ def balance_multiworld_progression(world):
candidate_items = [] candidate_items = []
while True: while True:
for location in balancing_sphere: 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) balancing_state.collect(location.item, True, location)
if location.item.player in balancing_players and not location.locked: if location.item.player in balancing_players and not location.locked:
candidate_items.append(location) candidate_items.append(location)
@ -364,11 +363,14 @@ def balance_multiworld_progression(world):
balancing_reachables[location.player] += 1 balancing_reachables[location.player] += 1
if world.has_beaten_game(balancing_state) or all([reachables >= threshold for reachables in balancing_reachables.values()]): if world.has_beaten_game(balancing_state) or all([reachables >= threshold for reachables in balancing_reachables.values()]):
break 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] unlocked_locations = [l for l in unchecked_locations if l not in balancing_unchecked_locations]
items_to_replace = [] items_to_replace = []
for player in balancing_players: for player in balancing_players:
locations_to_test = [l for l in unlocked_locations if l.player == player] 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] items_to_test = [l for l in candidate_items if l.item.player == player and l.player != player]
while items_to_test: while items_to_test:
testing = items_to_test.pop() testing = items_to_test.pop()
@ -392,7 +394,7 @@ def balance_multiworld_progression(world):
new_location = replacement_locations.pop() new_location = replacement_locations.pop()
old_location = items_to_replace.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) replacement_locations.insert(0, new_location)
new_location = replacement_locations.pop() new_location = replacement_locations.pop()
@ -407,9 +409,11 @@ def balance_multiworld_progression(world):
sphere_locations.append(location) sphere_locations.append(location)
for location in sphere_locations: 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) state.collect(location.item, True, location)
checked_locations.extend(sphere_locations) checked_locations.extend(sphere_locations)
if world.has_beaten_game(state): if world.has_beaten_game(state):
break 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) quickSwapCheckbutton = Checkbutton(checkBoxFrame, text="Enabled L/R Item quickswapping", variable=quickSwapVar)
openpyramidVar = IntVar() openpyramidVar = IntVar()
openpyramidCheckbutton = Checkbutton(checkBoxFrame, text="Pre-open Pyramid Hole", variable=openpyramidVar) openpyramidCheckbutton = Checkbutton(checkBoxFrame, text="Pre-open Pyramid Hole", variable=openpyramidVar)
keysanityVar = IntVar() mcsbshuffleFrame = Frame(checkBoxFrame)
keysanityCheckbutton = Checkbutton(checkBoxFrame, text="Keysanity (keys anywhere)", variable=keysanityVar) 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() retroVar = IntVar()
retroCheckbutton = Checkbutton(checkBoxFrame, text="Retro mode (universal keys)", variable=retroVar) 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() disableMusicVar = IntVar()
disableMusicCheckbutton = Checkbutton(checkBoxFrame, text="Disable game music", variable=disableMusicVar) disableMusicCheckbutton = Checkbutton(checkBoxFrame, text="Disable game music", variable=disableMusicVar)
shuffleGanonVar = IntVar() shuffleGanonVar = IntVar()
@ -84,9 +90,13 @@ def guiMain(args=None):
suppressRomCheckbutton.pack(expand=True, anchor=W) suppressRomCheckbutton.pack(expand=True, anchor=W)
quickSwapCheckbutton.pack(expand=True, anchor=W) quickSwapCheckbutton.pack(expand=True, anchor=W)
openpyramidCheckbutton.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) retroCheckbutton.pack(expand=True, anchor=W)
dungeonItemsCheckbutton.pack(expand=True, anchor=W)
disableMusicCheckbutton.pack(expand=True, anchor=W) disableMusicCheckbutton.pack(expand=True, anchor=W)
shuffleGanonCheckbutton.pack(expand=True, anchor=W) shuffleGanonCheckbutton.pack(expand=True, anchor=W)
hintsCheckbutton.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.create_spoiler = bool(createSpoilerVar.get())
guiargs.suppress_rom = bool(suppressRomVar.get()) guiargs.suppress_rom = bool(suppressRomVar.get())
guiargs.openpyramid = bool(openpyramidVar.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.retro = bool(retroVar.get())
guiargs.nodungeonitems = bool(dungeonItemsVar.get())
guiargs.quickswap = bool(quickSwapVar.get()) guiargs.quickswap = bool(quickSwapVar.get())
guiargs.disablemusic = bool(disableMusicVar.get()) guiargs.disablemusic = bool(disableMusicVar.get())
guiargs.shuffleganon = bool(shuffleGanonVar.get()) guiargs.shuffleganon = bool(shuffleGanonVar.get())
@ -1160,10 +1172,11 @@ def guiMain(args=None):
# load values from commandline args # load values from commandline args
createSpoilerVar.set(int(args.create_spoiler)) createSpoilerVar.set(int(args.create_spoiler))
suppressRomVar.set(int(args.suppress_rom)) 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) retroVar.set(args.retro)
if args.nodungeonitems:
dungeonItemsVar.set(int(not args.nodungeonitems))
quickSwapVar.set(int(args.quickswap)) quickSwapVar.set(int(args.quickswap))
disableMusicVar.set(int(args.disablemusic)) disableMusicVar.set(int(args.disablemusic))
if args.count: if args.count:

View File

@ -221,8 +221,11 @@ def generate_itempool(world, player):
if treasure_hunt_icon is not None: if treasure_hunt_icon is not None:
world.treasure_hunt_icon = treasure_hunt_icon 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) # 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) # 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() start = time.process_time()
# initialize the world # 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('') logger = logging.getLogger('')
if seed is None: if seed is None:
random.seed(None) random.seed(None)
@ -34,6 +34,19 @@ def main(args, seed=None):
world.seed = int(seed) world.seed = int(seed)
random.seed(world.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_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.crystals_needed_for_gt = random.randint(0, 7) if args.crystals_gt == 'random' else int(args.crystals_gt)
world.open_pyramid = args.openpyramid world.open_pyramid = args.openpyramid
@ -83,7 +96,7 @@ def main(args, seed=None):
logger.info('Placing Dungeon Items.') logger.info('Placing Dungeon Items.')
shuffled_locations = None 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() shuffled_locations = world.get_unfilled_locations()
random.shuffle(shuffled_locations) random.shuffle(shuffled_locations)
fill_dungeons_restrictive(world, shuffled_locations) fill_dungeons_restrictive(world, shuffled_locations)
@ -124,7 +137,7 @@ def main(args, seed=None):
player_names = parse_names_string(args.names) player_names = parse_names_string(args.names)
outfileprefix = 'ER_%s_' % world.seed 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 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) 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): def copy_world(world):
# ToDo: Not good yet # 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.required_medallions = world.required_medallions.copy()
ret.swamp_patch_required = world.swamp_patch_required.copy() ret.swamp_patch_required = world.swamp_patch_required.copy()
ret.ganon_at_pyramid = world.ganon_at_pyramid.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.difficulty_requirements = world.difficulty_requirements
ret.fix_fake_world = world.fix_fake_world ret.fix_fake_world = world.fix_fake_world
ret.lamps_needed_for_dark_rooms = world.lamps_needed_for_dark_rooms 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_ganon = world.crystals_needed_for_ganon
ret.crystals_needed_for_gt = world.crystals_needed_for_gt ret.crystals_needed_for_gt = world.crystals_needed_for_gt
@ -318,8 +335,7 @@ def create_playthrough(world):
sphere_candidates = list(prog_locations) sphere_candidates = list(prog_locations)
logging.getLogger('').debug('Building up collection spheres.') logging.getLogger('').debug('Building up collection spheres.')
while sphere_candidates: while sphere_candidates:
if not world.keysanity: state.sweep_for_events(key_only=True)
state.sweep_for_events(key_only=True)
sphere = [] sphere = []
# build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres # 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) state = CollectionState(world)
collection_spheres = [] collection_spheres = []
while required_locations: 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)) 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) item = ItemFactory(itemstr.strip(), 1)
if item is not None: if item is not None:
world.push_item(location, item) world.push_item(location, item)
if item.key: if item.smallkey or item.bigkey:
location.event = True location.event = True
elif '<=>' in line: elif '<=>' in line:
entrance, exit = line.split('<=>', 1) entrance, exit = line.split('<=>', 1)

View File

@ -121,7 +121,7 @@ Does not invoke a timer.
### Display ### Display
Displays a timer on-screen but does not alter the item pool. 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 ### 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. 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 These settings allow dungeon specific items to be distributed anywhere in the world and not just in their native dungeon.
in their native dungeon. Small Keys dropped by enemies or found in pots are not affected. The chest in southeast Skull Woods that Small Keys dropped by enemies or found in pots are not affected. The chest in southeast Skull Woods that is traditionally
is traditionally a guaranteed Small Key still is. These items will be distributed according to the v26/balanced algorithm, but a guaranteed Small Key still is. These items will be distributed according to the v26/balanced algorithm, but the rest
the rest of the itempool will respect the algorithm setting. Music for dungeons is randomized so it cannot be used as a tell 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. for which dungeons contain pendants and crystals; finding a Map for a dungeon will allow the overworld map to display its prize.
## Retro ## 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) 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 --retro
@ -433,13 +433,6 @@ Enable Keysanity (default: False)
Enable Retro mode (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}] --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 # Keys in their native dungeon should use the orignal item code for keys
if location.parent_region.dungeon: if location.parent_region.dungeon:
dungeon = 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 is not None and dungeon.is_dungeon_item(location.item):
if location.item.type == "BigKey": if location.item.bigkey:
itemid = 0x32 itemid = 0x32
if location.item.type == "SmallKey": if location.item.smallkey:
itemid = 0x24 itemid = 0x24
if location.item and location.item.player != player: if location.item and location.item.player != player:
if location.player_address is not None: if location.player_address is not None:
@ -462,15 +462,15 @@ def patch_rom(world, player, rom):
# patch music # patch music
music_addresses = dungeon_music_addresses[location.name] music_addresses = dungeon_music_addresses[location.name]
if world.keysanity: if world.mapshuffle:
music = random.choice([0x11, 0x16]) music = random.choice([0x11, 0x16])
else: else:
music = 0x11 if 'Pendant' in location.item.name else 0x16 music = 0x11 if 'Pendant' in location.item.name else 0x16
for music_address in music_addresses: for music_address in music_addresses:
rom.write_byte(music_address, music) rom.write_byte(music_address, music)
if world.keysanity: if world.mapshuffle:
rom.write_byte(0x155C9, random.choice([0x11, 0x16])) # Randomize GT music too in keysanity mode rom.write_byte(0x155C9, random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle
# patch entrance/exits/holes # patch entrance/exits/holes
for region in world.regions: for region in world.regions:
@ -811,7 +811,7 @@ def patch_rom(world, player, rom):
ERtimeincrease = 10 ERtimeincrease = 10
else: else:
ERtimeincrease = 20 ERtimeincrease = 20
if world.keysanity: if world.keyshuffle or world.bigkeyshuffle or world.mapshuffle:
ERtimeincrease = ERtimeincrease + 15 ERtimeincrease = ERtimeincrease + 15
if world.clock_mode == 'off': if world.clock_mode == 'off':
rom.write_bytes(0x180190, [0x00, 0x00, 0x00]) # turn off clock mode 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(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(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(0x18016A, 0x10 | ((0x01 if world.keyshuffle else 0x00)
rom.write_byte(0x18003B, 0x01 if world.keysanity else 0x00) # maps showing crystals on overworld | (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 # compasses showing dungeon count
if world.clock_mode != 'off': if world.clock_mode != 'off':
rom.write_byte(0x18003C, 0x00) # Currently must be off if timer is on, because they use same HUD location 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 rom.write_byte(0x18003C, 0x01) # show on pickup
else: else:
rom.write_byte(0x18003C, 0x00) 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 # Map reveals
reveal_bytes = { reveal_bytes = {
@ -958,8 +964,8 @@ def patch_rom(world, player, rom):
return reveal_bytes.get(location.parent_region.dungeon.name, 0x0000) return reveal_bytes.get(location.parent_region.dungeon.name, 0x0000)
return 0x0000 return 0x0000
write_int16(rom, 0x18017A, get_reveal_bytes('Green Pendant') if world.keysanity else 0x0000) # Sahasrahla 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.keysanity else 0x0000) # Bomb Shop 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(0x180172, 0x01 if world.retro else 0x00) # universal keys
rom.write_byte(0x180175, 0x01 if world.retro else 0x00) # rupee bow 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. # 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() items_to_hint = RelevantItems.copy()
if world.keysanity: if world.keyshuffle:
items_to_hint.extend(KeysanityItems) items_to_hint.extend(SmallKeys)
if world.bigkeyshuffle:
items_to_hint.extend(BigKeys)
random.shuffle(items_to_hint) random.shuffle(items_to_hint)
hint_count = 5 if world.shuffle not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 8 hint_count = 5 if world.shuffle not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 8
while hint_count > 0: while hint_count > 0:
@ -2022,28 +2030,30 @@ RelevantItems = ['Bow',
'Magic Upgrade (1/4)' 'Magic Upgrade (1/4)'
] ]
KeysanityItems = ['Small Key (Eastern Palace)', SmallKeys = ['Small Key (Eastern Palace)',
'Big Key (Eastern Palace)', 'Small Key (Escape)',
'Small Key (Escape)', 'Small Key (Desert Palace)',
'Small Key (Desert Palace)', 'Small Key (Tower of Hera)',
'Big Key (Desert Palace)', 'Small Key (Agahnims Tower)',
'Small Key (Tower of Hera)', 'Small Key (Palace of Darkness)',
'Big Key (Tower of Hera)', 'Small Key (Thieves Town)',
'Small Key (Agahnims Tower)', 'Small Key (Swamp Palace)',
'Small Key (Palace of Darkness)', 'Small Key (Skull Woods)',
'Big Key (Palace of Darkness)', 'Small Key (Ice Palace)',
'Small Key (Thieves Town)', 'Small Key (Misery Mire)',
'Big Key (Thieves Town)', 'Small Key (Turtle Rock)',
'Small Key (Swamp Palace)', 'Small Key (Ganons Tower)',
'Big Key (Swamp Palace)', ]
'Small Key (Skull Woods)',
'Big Key (Skull Woods)', BigKeys = ['Big Key (Eastern Palace)',
'Small Key (Ice Palace)', 'Big Key (Desert Palace)',
'Big Key (Ice Palace)', 'Big Key (Tower of Hera)',
'Small Key (Misery Mire)', 'Big Key (Palace of Darkness)',
'Big Key (Misery Mire)', 'Big Key (Thieves Town)',
'Small Key (Turtle Rock)', 'Big Key (Swamp Palace)',
'Big Key (Turtle Rock)', 'Big Key (Skull Woods)',
'Small Key (Ganons Tower)', 'Big Key (Ice Palace)',
'Big Key (Ganons Tower)' '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', 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 - Bottom Right', 'Turtle Rock - Eye Bridge - Top Left',
'Turtle Rock - Eye Bridge - Top Right'] 'Turtle Rock - Eye Bridge - Top Right']
if not world.keysanity: if not world.keyshuffle:
non_big_key_locations += ['Turtle Rock - Big Key Chest'] non_big_key_locations += ['Turtle Rock - Big Key Chest']
else: 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)) 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', 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 - Bottom Right', 'Turtle Rock - Eye Bridge - Top Left',
'Turtle Rock - Eye Bridge - Top Right'] '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'] non_big_key_locations += ['Turtle Rock - Big Key Chest', 'Turtle Rock - Chain Chomps']
# set big key restrictions # set big key restrictions