Implement Key-sanity

Still need to add documentation for this mode.
This commit is contained in:
Kevin Cathcart 2017-10-28 18:34:37 -04:00
parent 623e6637ff
commit f2ea4b6442
7 changed files with 73 additions and 34 deletions

View File

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

View File

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

5
Gui.py
View File

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

11
Main.py
View File

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

View File

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

View File

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

25
Rom.py
View File

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