From f2ea4b64420cb671aedc44f916bb4198a79e9c4a Mon Sep 17 00:00:00 2001 From: Kevin Cathcart Date: Sat, 28 Oct 2017 18:34:37 -0400 Subject: [PATCH] Implement Key-sanity Still need to add documentation for this mode. --- BaseClasses.py | 58 ++++++++++++++++++++++++++----------------- EntranceRandomizer.py | 4 +++ Gui.py | 5 ++++ Main.py | 11 ++++---- Plando.py | 2 +- Regions.py | 2 ++ Rom.py | 25 +++++++++++++++---- 7 files changed, 73 insertions(+), 34 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 8831e879..949cfdb3 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -6,7 +6,7 @@ from collections import OrderedDict class World(object): - def __init__(self, shuffle, logic, mode, difficulty, goal, algorithm, place_dungeon_items, check_beatable_only, shuffle_ganon, quickswap, fastmenu): + def __init__(self, shuffle, logic, mode, difficulty, goal, algorithm, place_dungeon_items, check_beatable_only, shuffle_ganon, quickswap, fastmenu, keysanity): self.shuffle = shuffle self.logic = logic self.mode = mode @@ -49,8 +49,13 @@ class World(object): self.can_access_trock_eyebridge = None self.quickswap = quickswap self.fastmenu = fastmenu + self.keysanity = keysanity self.spoiler = Spoiler(self) + def intialize_regions(self): + for region in self.regions: + region.world = self + def get_region(self, regionname): if isinstance(regionname, Region): return regionname @@ -221,7 +226,8 @@ class World(object): algorithm = ['freshness', 'flood', 'vt21', 'vt22', 'vt25', 'vt26'].index(self.algorithm) beatableonly = 1 if self.check_beatable_only else 0 shuffleganon = 1 if self.shuffle_ganon else 0 - return logic | (beatableonly << 1) | (dungeonitems << 2) | (shuffleganon << 3) | (goal << 4) | (shuffle << 7) | (difficulty << 11) | (algorithm << 13) | (mode << 16) + keysanity = 1 if self.keysanity else 0 + return logic | (beatableonly << 1) | (dungeonitems << 2) | (shuffleganon << 3) | (goal << 4) | (shuffle << 7) | (difficulty << 11) | (algorithm << 13) | (mode << 16) | (keysanity << 18) class CollectionState(object): @@ -448,6 +454,7 @@ class Region(object): self.exits = [] self.locations = [] self.dungeon = None + self.world = None self.spot_type = 'Region' self.hint_text = 'Hyrule' self.recursion_count = 0 @@ -457,16 +464,17 @@ class Region(object): if state.can_reach(entrance): return True return False - + def can_fill(self, item): - if item.key or item.map or item.compass: + is_dungeon_item = item.key or item.map or item.compass + 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 self.dungeon and self.dungeon.is_dungeon_item(item): return True else: return False - + return True - def __str__(self): return str(self.__unicode__()) @@ -509,6 +517,7 @@ class Entrance(object): def __unicode__(self): return '%s' % self.name + class Dungeon(object): def __init__(self, name, regions, big_key, small_keys, dungeon_items): @@ -517,24 +526,25 @@ class Dungeon(object): self.big_key = big_key self.small_keys = small_keys self.dungeon_items = dungeon_items - + @property def keys(self): return self.small_keys + ([self.big_key] if self.big_key else []) - + @property def all_items(self): - return self.dungeon_items+self.keys - + return self.dungeon_items + self.keys + def is_dungeon_item(self, item): return item.name in [dungeon_item.name for dungeon_item in self.all_items] - + def __str__(self): return str(self.__unicode__()) def __unicode__(self): return '%s' % self.name + class Location(object): def __init__(self, name='', address=None, crystal=False, hint_text=None, parent=None): @@ -554,7 +564,7 @@ class Location(object): def item_rule(self, item): return True - + def can_fill(self, item): return self.parent_region.can_fill(item) and self.item_rule(item) @@ -570,7 +580,7 @@ class Location(object): return '%s' % self.name -class Item(object): +class Item(object): def __init__(self, name='', advancement=False, priority=False, type=None, code=None, altar_hint=None, altar_credit=None, sickkid_credit=None, zora_credit=None, witch_credit=None, fluteboy_credit=None): self.name = name @@ -585,20 +595,20 @@ class Item(object): self.fluteboy_credit_text = fluteboy_credit self.code = code self.location = None - - @property + + @property def key(self): return self.type == 'SmallKey' or self.type == 'BigKey' - - @property + + @property def crystal(self): return self.type == 'Crystal' - - @property + + @property def map(self): return self.type == 'Map' - - @property + + @property def compass(self): return self.type == 'Compass' @@ -642,7 +652,8 @@ class Spoiler(object): 'completeable': not self.world.check_beatable_only, 'dungeonitems': self.world.place_dungeon_items, 'quickswap': self.world.quickswap, - 'fastmenu': self.world.fastmenu} + 'fastmenu': self.world.fastmenu, + 'keysanity': self.world.keysanity} def to_json(self): self.parse_data() @@ -666,7 +677,8 @@ class Spoiler(object): outfile.write('All Locations Accessible: %s\n' % ('Yes' if self.metadata['completeable'] else 'No, some locations may be unreachable')) outfile.write('Maps and Compasses in Dungeons: %s\n' % ('Yes' if self.metadata['dungeonitems'] else 'No')) outfile.write('L\\R Quickswap enabled: %s\n' % ('Yes' if self.metadata['quickswap'] else 'No')) - outfile.write('Fastmenu enabled: %s' % ('Yes' if self.metadata['fastmenu'] else 'No')) + outfile.write('Fastmenu enabled: %s\n' % ('Yes' if self.metadata['fastmenu'] else 'No')) + outfile.write('Keysanity enabled: %s' % ('Yes' if self.metadata['keysanity'] else 'No')) if self.entrances: outfile.write('\n\nEntrances:\n\n') outfile.write('\n'.join(['%s %s %s' % (entry['entrance'], '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', entry['exit']) for entry in self.entrances])) diff --git a/EntranceRandomizer.py b/EntranceRandomizer.py index 9022ff46..f824a4ee 100644 --- a/EntranceRandomizer.py +++ b/EntranceRandomizer.py @@ -126,6 +126,10 @@ if __name__ == '__main__': ''', type=int) parser.add_argument('--fastmenu', help='Enable instant menu', action='store_true') parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', 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('--nodungeonitems', help='''\ Remove Maps and Compasses from Itempool, replacing them by empty slots. diff --git a/Gui.py b/Gui.py index cc180359..d2171e08 100644 --- a/Gui.py +++ b/Gui.py @@ -21,6 +21,8 @@ def guiMain(args=None): quickSwapCheckbutton = Checkbutton(checkBoxFrame, text="Enabled L/R Item quickswapping", variable=quickSwapVar) fastMenuVar = IntVar() fastMenuCheckbutton = Checkbutton(checkBoxFrame, text="Enable instant menu", variable=fastMenuVar) + keysanityVar = IntVar() + keysanityCheckbutton = Checkbutton(checkBoxFrame, text="Keysanity (keys anywhere)", variable=keysanityVar) dungeonItemsVar = IntVar() dungeonItemsCheckbutton = Checkbutton(checkBoxFrame, text="Place Dungeon Items (Compasses/Maps)", onvalue=0, offvalue=1, variable=dungeonItemsVar) beatableOnlyVar = IntVar() @@ -32,6 +34,7 @@ def guiMain(args=None): suppressRomCheckbutton.pack(expand=True, anchor=W) quickSwapCheckbutton.pack(expand=True, anchor=W) fastMenuCheckbutton.pack(expand=True, anchor=W) + keysanityCheckbutton.pack(expand=True, anchor=W) dungeonItemsCheckbutton.pack(expand=True, anchor=W) beatableOnlyCheckbutton.pack(expand=True, anchor=W) shuffleGanonCheckbutton.pack(expand=True, anchor=W) @@ -160,6 +163,7 @@ def guiMain(args=None): guiargs.heartbeep = heartbeepVar.get() guiargs.create_spoiler = bool(createSpoilerVar.get()) guiargs.suppress_rom = bool(suppressRomVar.get()) + guiargs.keysanity = bool(keysanityVar.get()) guiargs.nodungeonitems = bool(dungeonItemsVar.get()) guiargs.beatableonly = bool(beatableOnlyVar.get()) guiargs.fastmenu = bool(fastMenuVar.get()) @@ -198,6 +202,7 @@ 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) if args.nodungeonitems: dungeonItemsVar.set(int(not args.nodungeonitems)) beatableOnlyVar.set(int(args.beatableonly)) diff --git a/Main.py b/Main.py index 5155e17b..bec89f49 100644 --- a/Main.py +++ b/Main.py @@ -28,7 +28,7 @@ def main(args, seed=None): start = time.clock() # initialize the world - world = World(args.shuffle, args.logic, args.mode, args.difficulty, args.goal, args.algorithm, not args.nodungeonitems, args.beatableonly, args.shuffleganon, args.quickswap, args.fastmenu) + world = World(args.shuffle, args.logic, args.mode, args.difficulty, args.goal, args.algorithm, not args.nodungeonitems, args.beatableonly, args.shuffleganon, args.quickswap, args.fastmenu, args.keysanity) logger = logging.getLogger('') if seed is None: random.seed(None) @@ -91,7 +91,7 @@ def main(args, seed=None): else: sprite = None - outfilebase = 'ER_%s_%s-%s-%s_%s-%s%s%s%s_%s' % (world.logic, world.difficulty, world.mode, world.goal, world.shuffle, world.algorithm, "-fastmenu" if world.fastmenu else "","-quickswap" if world.quickswap else "", "-shuffleganon" if world.shuffle_ganon else "", world.seed) + outfilebase = 'ER_%s_%s-%s-%s_%s-%s%s%s%s%s_%s' % (world.logic, world.difficulty, world.mode, world.goal, world.shuffle, world.algorithm, "-keysanity" if world.keysanity else "", "-fastmenu" if world.fastmenu else "","-quickswap" if world.quickswap else "", "-shuffleganon" if world.shuffle_ganon else "", world.seed) if not args.suppress_rom: if args.jsonout: @@ -202,7 +202,7 @@ def generate_itempool(world): def copy_world(world): # ToDo: Not good yet - ret = World(world.shuffle, world.logic, world.mode, world.difficulty, world.goal, world.algorithm, world.place_dungeon_items, world.check_beatable_only, world.shuffle_ganon, world.quickswap, world.fastmenu) + ret = World(world.shuffle, world.logic, world.mode, world.difficulty, world.goal, world.algorithm, world.place_dungeon_items, world.check_beatable_only, world.shuffle_ganon, world.quickswap, world.fastmenu, world.keysanity) ret.required_medallions = list(world.required_medallions) ret.swamp_patch_required = world.swamp_patch_required ret.ganon_at_pyramid = world.ganon_at_pyramid @@ -256,14 +256,15 @@ def create_playthrough(world): raise RuntimeError('Cannot beat game. Something went terribly wrong here!') # get locations containing progress items - prog_locations = [location for location in world.get_locations() if location.item is not None and location.item.advancement] + prog_locations = [location for location in world.get_locations() if location.item is not None and (location.item.advancement or (location.item.key and world.keysanity))] collection_spheres = [] state = CollectionState(world) sphere_candidates = list(prog_locations) logging.getLogger('').debug('Building up collection spheres.') while sphere_candidates: - state.sweep_for_events(key_only=True) + if not world.keysanity: + 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 diff --git a/Plando.py b/Plando.py index a28cf771..9d765c4b 100644 --- a/Plando.py +++ b/Plando.py @@ -29,7 +29,7 @@ def main(args, seed=None): start = time.clock() # initialize the world - world = World('vanilla', 'noglitches', 'standard', 'normal', 'ganon', 'freshness', False, False, False, args.quickswap, args.fastmenu) + world = World('vanilla', 'noglitches', 'standard', 'normal', 'ganon', 'freshness', False, False, False, args.quickswap, args.fastmenu, False) logger = logging.getLogger('') hasher = hashlib.md5() diff --git a/Regions.py b/Regions.py index b9a3fd8e..a9535fa5 100644 --- a/Regions.py +++ b/Regions.py @@ -262,6 +262,8 @@ def create_regions(world): create_region('Bottom of Pyramid', None, ['Pyramid Exit']), create_region('Pyramid Ledge', None, ['Pyramid Entrance', 'Pyramid Drop']) ] + + world.intialize_regions() def create_region(name, locations=None, exits=None): diff --git a/Rom.py b/Rom.py index dc3b87f7..39e693ae 100644 --- a/Rom.py +++ b/Rom.py @@ -64,7 +64,7 @@ class LocalRom(object): patchedmd5 = hashlib.md5() patchedmd5.update(self.buffer) if not 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.') + 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 write_crc(self): # this does not seem to work @@ -97,9 +97,15 @@ def patch_rom(world, rom, hashtable, beep='normal', sprite=None): # patch music music_addresses = dungeon_music_addresses[location.name] - music = 0x11 if 'Pendant' in location.item.name else 0x16 + if world.keysanity: + 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 # patch entrances for region in world.regions: @@ -308,9 +314,18 @@ def patch_rom(world, rom, hashtable, beep='normal', sprite=None): rom.write_byte(0x18003E, 0x02) # make ganon invincible until all dungeons are beat elif world.goal in ['crystals']: rom.write_byte(0x18003E, 0x04) # make ganon invincible until all crystals - rom.write_byte(0x18016A, 0x00) # disable free roaming item text boxes - rom.write_byte(0x18003B, 0x00) # disable maps showing crystals on overworld - rom.write_byte(0x18003C, 0x00) # disable compasses showing dungeon count + 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 + + # 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: + rom.write_byte(0x18003C, 0x01) #show on pickup + else: + rom.write_byte(0x18003C, 0x00) + + rom.write_byte(0x180045, 0x01 if world.keysanity else 0x00) # free roaming items in menu digging_game_rng = random.randint(1, 30) # set rng for digging game rom.write_byte(0x180020, digging_game_rng) rom.write_byte(0xEFD95, digging_game_rng)