From f374c637c3e41d44275ba3bd32f677e227785c06 Mon Sep 17 00:00:00 2001 From: LLCoolDave Date: Tue, 16 May 2017 21:23:47 +0200 Subject: [PATCH] Fixed remaining key logic, correctly implemented sewers. Added support for other game modes, create playthrough. --- BaseClasses.py | 91 +++++++++++++++++---- EntranceShuffle.py | 14 +++- Main.py | 193 +++++++++++++++++++++++++++++++++++++++++---- Regions.py | 11 ++- Rules.py | 65 +++++++++------ 5 files changed, 313 insertions(+), 61 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 5d4a7c25..b8051e36 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1,9 +1,15 @@ import copy +import logging class World(object): - def __init__(self): + def __init__(self, shuffle, logic, mode, difficulty, goal): + self.shuffle = shuffle + self.logic = logic + self.mode = mode + self.difficulty = difficulty + self.goal = goal self.regions = [] self.itempool = [] self.state = CollectionState(self) @@ -66,7 +72,7 @@ class World(object): if collect: self.state.collect(item) - print('Placed %s at %s' % (item, location)) + logging.getLogger('').debug('Placed %s at %s' % (item, location)) else: raise RuntimeError('Cannot assign item %s to location %s.' % (item, location)) @@ -96,11 +102,34 @@ class World(object): temp_state.collect(item) return len(self.get_placeable_locations()) < len(self.get_placeable_locations(temp_state)) + def can_beat_game(self): + prog_locations = [location for location in self.get_locations() if location.item is not None and location.item.advancement] + + state = CollectionState(self) + while prog_locations: + sphere = [] + # build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres + for location in prog_locations: + if state.can_reach(location): + if location.item.name == 'Triforce': + return True + sphere.append(location) + + if not sphere: + # ran out of places and did not find triforce yet, quit + return False + + for location in sphere: + prog_locations.remove(location) + state.collect(location.item) + + return False + class CollectionState(object): def __init__(self, parent, has_everything=False): - self.prog_items = set() + self.prog_items = [] self.world = parent self.has_everything = has_everything self.changed = False @@ -233,32 +262,68 @@ class CollectionState(object): if self.has('Golden Sword'): return elif self.has('Tempered Sword'): - self.prog_items.add('Golden Sword') + self.prog_items.append('Golden Sword') self.changed = True elif self.has('Master Sword'): - self.prog_items.add('Tempered Sword') + self.prog_items.append('Tempered Sword') self.changed = True elif self.has('Fighter Sword'): - self.prog_items.add('Master Sword') + self.prog_items.append('Master Sword') self.changed = True else: - self.prog_items.add('Fighter Sword') + self.prog_items.append('Fighter Sword') self.changed = True elif 'Glove' in item.name: if self.has('Titans Mitts'): return elif self.has('Power Glove'): - self.prog_items.add('Titans Mitts') + self.prog_items.append('Titans Mitts') self.changed = True else: - self.prog_items.add('Power Glove') + self.prog_items.append('Power Glove') self.changed = True return if item.advancement: - self.prog_items.add(item.name) + self.prog_items.append(item.name) self.changed = True + def remove(self, item): + if item.advancement: + to_remove = item.name + if to_remove.startswith('Progressive '): + if 'Sword' in to_remove: + if self.has('Golden Sword'): + to_remove = 'Golden Sword' + elif self.has('Tempered Sword'): + to_remove = 'Tempered Sword' + elif self.has('Master Sword'): + to_remove = 'Master Sword' + elif self.has('Fighter Sword'): + to_remove = 'Fighter Sword' + else: + to_remove = None + elif 'Glove' in item.name: + if self.has('Titans Mitts'): + to_remove = 'Titans Mitts' + elif self.has('Power Glove'): + to_remove = 'Power Glove' + else: + to_remove = None + + if to_remove is not None: + try: + self.prog_items.remove(to_remove) + except IndexError: + return + + # invalidate caches, nothing can be trusted anymore now + self.region_cache = {} + self.location_cache = {} + self.entrance_cache = {} + self.recursion_cache = [] + self.changed = False + def __getattr__(self, item): if item.startswith('can_reach_'): return self.can_reach(item[10]) @@ -340,10 +405,7 @@ class Location(object): return True def item_rule(self, item): - if item.name != 'Triforce': - return True - else: - return False + return True def can_reach(self, state): if self.parent_region: @@ -372,4 +434,3 @@ class Item(object): def __unicode__(self): return '%s' % self.name - diff --git a/EntranceShuffle.py b/EntranceShuffle.py index ea788a5d..113a9abf 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1,10 +1,10 @@ -def link_entrances(world, shuffle): +def link_entrances(world): # setup mandatory connections for exitname, regionname in mandatory_connections: connect(world, exitname, regionname) # if we do not shuffle, set default connections - if shuffle=='Default': + if world.shuffle == 'Default': for exitname, regionname in default_connections: connect(world, exitname, regionname) return @@ -27,6 +27,11 @@ mandatory_connections = [('Zoras River', 'Zoras River'), ('Lumberjack Tree (top to bottom)', 'Lumberjack Tree (bottom)'), ('Desert Palace Stairs', 'Desert Palace Stairs'), ('Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (North) Spot'), + ('Throne Room', 'Sewers (Dark)'), + ('Sewers Door', 'Sewers'), + ('Sanctuary Push Door', 'Sanctuary'), + ('Sewer Drop', 'Sewers'), + ('Sewers Back Door', 'Sewers (Dark)'), ('Aghanim 1', 'Aghanim 1'), ('Flute Spot 1', 'Death Mountain'), ('Spectacle Rock Cave Drop', 'Spectacle Rock Cave (Bottom)'), @@ -174,8 +179,9 @@ default_connections = [("Thiefs Hut", "Thiefs Hut"), ('Hyrule Castle Exit (West)', 'Hyrule Castle Ledge'), ('Hyrule Castle Exit (East)', 'Hyrule Castle Ledge'), ('Aghanims Tower', 'Aghanims Tower'), - ('Sanctuary', 'Hyrule Castle'), # this set of two exits can be randomized together! - ('Sanctuary Grave', 'Hyrule Castle'), + ('Sanctuary', 'Sanctuary'), + ('Sanctuary Grave', 'Sewer Drop'), + ('Sanctuary Exit', 'Light World'), ('Old Man Cave (West)', 'Old Man Cave'), ('Old Man Cave (East)', 'Old Man Cave'), diff --git a/Main.py b/Main.py index d45b3826..e1d371cd 100644 --- a/Main.py +++ b/Main.py @@ -1,4 +1,4 @@ -from BaseClasses import World +from BaseClasses import World, CollectionState from Regions import create_regions from EntranceShuffle import link_entrances from Rules import set_rules @@ -7,19 +7,21 @@ from Items import * import random import cProfile import time +import logging def main(seed=None, shuffle='Default', logic='no-glitches', mode='standard', difficulty='normal', goal='defeat ganon'): # initialize the world - world = World() + world = World(shuffle, logic, mode, difficulty, goal) create_regions(world) random.seed(seed) - link_entrances(world, shuffle) - set_rules(world, logic, mode) - generate_itempool(world, difficulty, goal) + link_entrances(world) + set_rules(world) + generate_itempool(world) distribute_items(world) + # flood_items(world) # different algo, biased towards early game progress items return world @@ -56,6 +58,10 @@ def distribute_items(world): if candidate_item_to_place is not None: item_to_place = candidate_item_to_place else: + # we placed all available progress items. Maybe the game can be beaten anyway? + if world.can_beat_game(): + logging.getLogger('').warning('Not all locations reachable. Game beatable anyway.') + break raise RuntimeError('No more progress items left to place.') spot_to_fill = None @@ -65,21 +71,80 @@ def distribute_items(world): break if spot_to_fill is None: + # we filled all reachable spots. Maybe the game can be beaten anyway? + if world.can_beat_game(): + logging.getLogger('').warning('Not all items placed. Game beatable anyway.') + break raise RuntimeError('No more spots to place %s' % item_to_place) world.push_item(spot_to_fill, item_to_place, True) itempool.remove(item_to_place) fill_locations.remove(spot_to_fill) - print('Unplaced items: %s - Unfilled Locations: %s' % (itempool, fill_locations)) + logging.getLogger('').debug('Unplaced items: %s - Unfilled Locations: %s' % (itempool, fill_locations)) -def generate_itempool(world, difficulty, goal): - if difficulty != 'normal' or goal != 'defeat ganon': +def flood_items(world): + # get items to distribute + random.shuffle(world.itempool) + itempool = world.itempool + progress_done = False + + # fill world from top of itempool while we can + while not progress_done: + location_list = world.get_unfilled_locations() + random.shuffle(location_list) + spot_to_fill = None + for location in location_list: + if world.state.can_reach(location): + spot_to_fill = location + break + + if spot_to_fill: + item = itempool.pop(0) + world.push_item(spot_to_fill, item, True) + continue + + # ran out of spots, check if we need to step in and correct things + if len(world.get_reachable_locations()) == len(world.get_locations()): + progress_done = True + continue + + # need to place a progress item instead of an already placed item, find candidate + item_to_place = None + candidate_item_to_place = None + for item in itempool: + if item.advancement: + candidate_item_to_place = item + if world.unlocks_new_location(item): + item_to_place = item + break + + # we might be in a situation where all new locations require multiple items to reach. If that is the case, just place any advancement item we've found and continue trying + if item_to_place is None: + if candidate_item_to_place is not None: + item_to_place = candidate_item_to_place + else: + raise RuntimeError('No more progress items left to place.') + + # find item to replace with progress item + 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.key and 'Map' not in location.item.name and 'Compass' not in location.item.name: + # safe to replace + replace_item = location.item + replace_item.location = None + itempool.append(replace_item) + world.push_item(location, item_to_place, True) + itempool.remove(item_to_place) + break + + +def generate_itempool(world): + if world.difficulty != 'normal' or world.goal not in ['defeat ganon', 'pedestal', 'all dungeons'] or world.mode not in ['open', 'standard']: raise NotImplementedError('Not supported yet') - # Push the two fixed items - world.push_item('Uncle', ProgressiveSword()) world.push_item('Ganon', Triforce(), False) # set up item pool @@ -134,6 +199,22 @@ def generate_itempool(world, difficulty, goal): Rupees20(), Rupees20(), Rupees20(), Rupees20(), Rupees20(), Rupees20(), Rupees20(), Rupees20(), Rupees20(), Rupees20(), Rupees20(), Rupees20() ] + if world.mode == 'standard': + world.push_item('Uncle', ProgressiveSword()) + else: + world.itempool.append(ProgressiveSword()) + + if world.goal == 'pedestal': + world.push_item('Altar', Triforce()) + items = list(world.itempool) + random.shuffle(items) + for item in items: + if not item.advancement: + # save to remove + world.itempool.remove(item) + break + # ToDo what to do if EVERYTHING is a progress item? + if random.randint(0, 3) == 0: world.itempool.append(QuarterMagic()) else: @@ -156,15 +237,95 @@ def generate_itempool(world, difficulty, goal): # push dungeon items fill_dungeons(world) -#profiler = cProfile.Profile() -#profiler.enable() +def copy_world(world): + # ToDo: Not good yet + ret = World(world.shuffle, world.logic, world.mode, world.difficulty, world.goal) + ret.required_medallions = list(world.required_medallions) + create_regions(ret) + set_rules(ret) + + # connect copied world + for region in world.regions: + for entrance in region.entrances: + ret.get_entrance(entrance.name).connect(ret.get_region(region.name)) + + # fill locations + for location in world.get_locations(): + if location.item is not None: + item = Item(location.item.name, location.item.advancement, location.item.key) + ret.get_location(location.name).item = item + item.location = ret.get_location(location.name) + + # copy remaining itempool. No item in itempool should have an assigned location + for item in world.itempool: + ret.itempool.append(Item(item.name, item.advancement, item.key)) + + # copy progress items in state + ret.state.prog_items = list(world.state.prog_items) + + return ret + + +def create_playthrough(world): + # create a copy as we will modify it + world = copy_world(world) + + # get locations containing progress items + prog_locations = [location for location in world.get_locations() if location.item is not None and location.item.advancement] + + collection_spheres = [] + state = CollectionState(world) + sphere_candidates = list(prog_locations) + while sphere_candidates: + sphere = [] + # build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres + for location in sphere_candidates: + if state.can_reach(location): + sphere.append(location) + + for location in sphere: + sphere_candidates.remove(location) + state.collect(location.item) + + collection_spheres.append(sphere) + + # in the second phase, we cull each sphere such that the game is still beatable, reducing each range of influence to the bare minimum required inside it + for sphere in reversed(collection_spheres): + to_delete = [] + for location in sphere: + # we remove the item at location and check if game is still beatable + old_item = location.item + location.item = None + state.remove(old_item) + world._item_cache = {} # need to invalidate + if world.can_beat_game(): + to_delete.append(location) + else: + # still required, got to keep it around + location.item = old_item + + # cull entries in spheres for spoiler walkthrough at end + for location in to_delete: + sphere.remove(location) + + # we are now down to just the required progress items in collection_spheres in a minimum number of spheres. As a cleanup, we right trim empty spheres (can happen if we have multiple triforces) + collection_spheres = [sphere for sphere in collection_spheres if sphere] + + # we can finally output our playthrough + return ''.join(['%s: {\n%s}\n' % (i + 1, ''.join([' %s: %s\n' % (location, location.item) for location in sphere])) for i, sphere in enumerate(collection_spheres)]) + + +profiler = cProfile.Profile() + +profiler.enable() tally = {} -iterations = 300 +iterations = 10 start = time.clock() for i in range(iterations): print('Seed %s\n\n' % i) - w = main() + w = main(mode='open') + print(create_playthrough(w)) for location in w.get_locations(): if location.item is not None: old_sk, old_bk, old_prog = tally.get(location.name, (0, 0, 0)) @@ -183,6 +344,6 @@ print('\n\n\n') for location, stats in tally.items(): print('%s, %s, %s, %s, %s, %s, %s, %s' % (location, stats[0], stats[0]/float(iterations), stats[1], stats[1]/float(iterations), stats[2], stats[2]/float(iterations), 0 if iterations - stats[0] - stats[1] == 0 else stats[2]/float(iterations - stats[0] - stats[1]))) -#profiler.disable() -#profiler.print_stats() +profiler.disable() +profiler.print_stats() diff --git a/Regions.py b/Regions.py index e021e3ae..1894cacb 100644 --- a/Regions.py +++ b/Regions.py @@ -56,10 +56,13 @@ def create_regions(world): '[dungeon-L1-1F] Eastern Palace - Big Key Room', '[dungeon-L1-1F] Eastern Palace - Map Room', 'Armos - Heart Container', 'Armos - Pendant']), create_region('Master Sword Meadow', ['Altar']), create_region('Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Aghanims Tower']), - create_region('Hyrule Castle', ['[dungeon-C-B1] Hyrule Castle - Boomerang Room', '[dungeon-C-B1] Hyrule Castle - Map Room', '[dungeon-C-B1] Hyrule Castle - Next To Zelda', - '[dungeon-C-B1] Escape - First B1 Room', '[dungeon-C-B1] Escape - Final Basement Room [left chest]', '[dungeon-C-B1] Escape - Final Basement Room [middle chest]', - '[dungeon-C-B1] Escape - Final Basement Room [right chest]', '[dungeon-C-1F] Sanctuary'], - ['Hyrule Castle Exit (East)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (South)']), + create_region('Hyrule Castle', ['[dungeon-C-B1] Hyrule Castle - Boomerang Room', '[dungeon-C-B1] Hyrule Castle - Map Room', '[dungeon-C-B1] Hyrule Castle - Next To Zelda'], + ['Hyrule Castle Exit (East)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (South)', 'Throne Room']), + create_region('Sewer Drop', None, ['Sewer Drop']), # This exists only to be referenced for access checks + create_region('Sewers (Dark)', ['[dungeon-C-B1] Escape - First B1 Room'], ['Sewers Door']), + create_region('Sewers', ['[dungeon-C-B1] Escape - Final Basement Room [left chest]', '[dungeon-C-B1] Escape - Final Basement Room [middle chest]', + '[dungeon-C-B1] Escape - Final Basement Room [right chest]'], ['Sanctuary Push Door', 'Sewers Back Door']), + create_region('Sanctuary', ['[dungeon-C-1F] Sanctuary'], ['Sanctuary Exit']), create_region('Aghanims Tower', ['[dungeon-A1-2F] Hyrule Castle Tower - 2 Knife Guys Room', '[dungeon-A1-3F] Hyrule Castle Tower - Maze Room'], ['Aghanim 1']), create_region('Aghanim 1', None, ['Top of Pyramid']), create_region('Old Man Cave', ['Old Mountain Man'], ['Old Man Cave Exit']), diff --git a/Rules.py b/Rules.py index 71fac8ae..985912ee 100644 --- a/Rules.py +++ b/Rules.py @@ -1,11 +1,22 @@ -def set_rules(world, logic, mode): +def set_rules(world): global_rules(world) - if logic == 'no-glitches' and mode in ['open', 'standard']: - no_glitches_rules(world, mode) + if world.logic == 'no-glitches': + no_glitches_rules(world) else: raise NotImplementedError('Not implemented yet') + if world.mode == 'open': + open_rules(world) + elif world.mode == 'standard': + standard_rules(world) + else: + raise NotImplementedError('Not implemented yet') + + if world.goal == 'all dungeons': + # require altar for ganon to enforce getting everything + add_rule(world.get_location('Ganon'), lambda state: state.can_reach('Altar', 'Location')) + def set_rule(spot, rule): spot.access_rule = rule @@ -111,9 +122,8 @@ def global_rules(world): set_rule(world.get_entrance('Turtle Rock'), lambda state: state.has_Pearl() and state.has_sword() and state.has_turtle_rock_medallion()) # sword required to cast magic (!) set_rule(world.get_location('[cave-013] Mimic Cave'), lambda state: state.has('Hammer')) - set_rule(world.get_location('[dungeon-C-B1] Escape - Final Basement Room [left chest]'), lambda state: state.can_lift_rocks()) # ToDo fix this up for shuffling, need access to drop into escape - set_rule(world.get_location('[dungeon-C-B1] Escape - Final Basement Room [middle chest]'), lambda state: state.can_lift_rocks()) - set_rule(world.get_location('[dungeon-C-B1] Escape - Final Basement Room [right chest]'), lambda state: state.can_lift_rocks()) + set_rule(world.get_entrance('Sewers Door'), lambda state: state.can_collect('Small Key (Escape)')) + set_rule(world.get_entrance('Sewers Back Door'), lambda state: state.can_collect('Small Key (Escape)')) set_rule(world.get_location('[dungeon-L1-1F] Eastern Palace - Big Chest'), lambda state: state.can_collect('Big Key (Eastern Palace)')) set_rule(world.get_location('Armos - Heart Container'), lambda state: state.has('Bow') and state.can_collect('Big Key (Eastern Palace)')) @@ -151,24 +161,25 @@ def global_rules(world): forbid_item(world.get_location(location), 'Big Key (Swamp Palace)') set_rule(world.get_entrance('Thieves Town Big Key Door'), lambda state: state.can_collect('Big Key (Thieves Town)')) - set_rule(world.get_entrance('Blind Fight'), lambda state: state.has_blunt_weapon() or state.has('Cane of Somaria')) + set_rule(world.get_entrance('Blind Fight'), lambda state: state.can_collect('Small Key (Thieves Town)') and (state.has_blunt_weapon() or state.has('Cane of Somaria'))) set_rule(world.get_location('[dungeon-D4-B2] Thieves Town - Big Chest'), lambda state: state.can_collect('Small Key (Thieves Town)') and state.has('Hammer')) set_rule(world.get_location('[dungeon-D4-1F] Thieves Town - Room above Boss'), lambda state: state.can_collect('Small Key (Thieves Town)')) for location in ['[dungeon-D4-1F] Thieves Town - Room above Boss', '[dungeon-D4-B2] Thieves Town - Big Chest', '[dungeon-D4-B2] Thieves Town - Chest next to Blind', 'Blind - Heart Container']: forbid_item(world.get_location(location), 'Big Key (Thieves Town)') set_rule(world.get_location('[dungeon-D3-B1] Skull Woods - Big Chest'), lambda state: state.can_collect('Big Key (Skull Woods)')) - set_rule(world.get_entrance('Skull Woods Torch Room'), lambda state: state.can_collect('Small Key (Skull Woods)', 3) and state.has('Fire Rod') and - (state.has_blunt_weapon() or state.has('Bottle') or state.has('Half Magic') or state.has('Quarter Magic'))) + set_rule(world.get_entrance('Skull Woods Torch Room'), lambda state: state.can_collect('Small Key (Skull Woods)', 3) and state.has('Fire Rod') and state.has_sword()) # sword required for curtain for location in ['[dungeon-D3-B1] Skull Woods - Big Chest']: forbid_item(world.get_location(location), 'Big Key (Skull Woods)') - set_rule(world.get_entrance('Ice Palace Entrance Room'), lambda state: state.has('Fire Rod') or state.has('Bombos')) # ToDo Rework key logic for Ice Palace for non-bombjump routes, will impose more restrictions + set_rule(world.get_entrance('Ice Palace Entrance Room'), lambda state: state.has('Fire Rod') or state.has('Bombos')) set_rule(world.get_location('[dungeon-D5-B5] Ice Palace - Big Chest'), lambda state: state.can_collect('Big Key (Ice Palace)')) - set_rule(world.get_entrance('Ice Palace (Kholdstare)'), lambda state: state.can_lift_rocks() and state.has('Hammer')) - set_rule(world.get_entrance('Ice Palace (East)'), lambda state: state.has('Hookshot')) # as one can be stupid and waste all keys but the guaranteed one on the East Wing, The Small Key Door access can never be required + set_rule(world.get_entrance('Ice Palace (Kholdstare)'), lambda state: state.can_lift_rocks() and state.has('Hammer') and state.can_collect('Big Key (Ice Palace)') and state.can_collect('Small Key (Ice Palace)', 2)) + set_rule(world.get_entrance('Ice Palace (East)'), lambda state: state.has('Hookshot') or (state.can_collect('Small Key(Ice Palace)', 1) and ((state.world.get_location('[dungeon-D5-B3] Ice Palace - Spike Room').item is not None and state.world.get_location('[dungeon-D5-B3] Ice Palace - Spike Room').item.name in ['Big Key (Ice Palace)']) or + (state.world.get_location('[dungeon-D5-B1] Ice Palace - Big Key Room').item is not None and state.world.get_location('[dungeon-D5-B1] Ice Palace - Big Key Room').item.name in ['Big Key (Ice Palace)']) or + (state.world.get_location('[dungeon-D5-B2] Ice Palace - Map Room').item is not None and state.world.get_location('[dungeon-D5-B2] Ice Palace - Map Room').item.name in ['Big Key (Ice Palace)'])))) # if you do ipbj and waste SKs in the basement, you have to BJ over the hookshot room to fix your mess potentially. This seems fair set_rule(world.get_entrance('Ice Palace (East Top)'), lambda state: state.can_lift_rocks() and state.has('Hammer')) - for location in ['[dungeon-D5-B5] Ice Palace - Big Chest']: + for location in ['[dungeon-D5-B5] Ice Palace - Big Chest', 'Kholdstare - Heart Container']: forbid_item(world.get_location(location), 'Big Key (Ice Palace)') set_rule(world.get_entrance('Misery Mire Entrance Gap'), lambda state: state.has_Boots() or state.has('Hookshot')) @@ -201,7 +212,6 @@ def global_rules(world): '[dungeon-D7-B2] Turtle Rock - Eye Bridge Room [bottom right chest]', '[dungeon-D7-B2] Turtle Rock - Eye Bridge Room [top left chest]', '[dungeon-D7-B2] Turtle Rock - Eye Bridge Room [top right chest]']: # ToDo Big Key can be elsewhere if we have an entrance shuffle forbid_item(world.get_location(location), 'Big Key (Turtle Rock)') - # this key logic sucks ToDo set_rule(world.get_entrance('Dark Palace Bonk Wall'), lambda state: state.has('Bow')) set_rule(world.get_entrance('Dark Palace Hammer Peg Drop'), lambda state: state.has('Hammer')) set_rule(world.get_entrance('Dark Palace Bridge Room'), lambda state: state.can_collect('Small Key (Palace of Darkness)', 1)) # If we can reach any other small key door, we already have back door access to this area @@ -238,8 +248,7 @@ def global_rules(world): set_rule(world.get_location('Ganon'), lambda state: state.has_beam_sword() and state.has_fire_source() and (state.has('Tempered Sword') or state.has('Golden Sword') or state.has('Silver Arrows') or state.has('Lamp') or state.has('Bottle') or state.has('Half Magic') or state.has('Quarter Magic'))) # need to light torch a sufficient amount of times -def no_glitches_rules(world, mode): - # overworld requirements +def no_glitches_rules(world): set_rule(world.get_entrance('Zoras River'), lambda state: state.has('Flippers') or state.can_lift_rocks()) set_rule(world.get_entrance('Hobo Bridge'), lambda state: state.has('Flippers')) add_rule(world.get_entrance('Ice Palace'), lambda state: state.has_Pearl() and state.has('Flippers')) @@ -253,7 +262,7 @@ def no_glitches_rules(world, mode): set_rule(world.get_location('[dungeon-D1-B1] Dark Palace - Dark Room [right chest]'), lambda state: state.has('Lamp')) add_rule(world.get_entrance('Ganons Tower (Hookshot Room)'), lambda state: state.has('Hookshot')) - if mode == 'open': + if world.mode == 'open': add_rule(world.get_entrance('Aghanim 1'), lambda state: state.has('Lamp')) set_rule(world.get_location('Old Mountain Man'), lambda state: state.has('Lamp')) set_rule(world.get_entrance('Old Man Cave Exit'), lambda state: state.has('Lamp')) @@ -261,8 +270,20 @@ def no_glitches_rules(world, mode): add_rule(world.get_location('Armos - Heart Container'), lambda state: state.has('Lamp')) add_rule(world.get_location('Armos - Pendant'), lambda state: state.has('Lamp')) add_rule(world.get_location('[dungeon-C-B1] Escape - First B1 Room'), lambda state: state.has('Lamp')) - elif mode == 'standard': - add_rule(world.get_location('[dungeon-C-B1] Escape - Final Basement Room [left chest]'), lambda state: state.can_collect('Small Key (Escape)')) - add_rule(world.get_location('[dungeon-C-B1] Escape - Final Basement Room [middle chest]'), lambda state: state.can_collect('Small Key (Escape)')) - add_rule(world.get_location('[dungeon-C-B1] Escape - Final Basement Room [right chest]'), lambda state: state.can_collect('Small Key (Escape)')) - add_rule(world.get_location('[dungeon-C-1F] Sanctuary'), lambda state: state.can_collect('Small Key (Escape)')) + + +def open_rules(world): + pass + + +def standard_rules(world): + # easiest way to enforce key placement not relevant for open + forbid_item(world.get_location('[dungeon-C-B1] Escape - Final Basement Room [left chest]'), 'Small Key (Escape)') + forbid_item(world.get_location('[dungeon-C-B1] Escape - Final Basement Room [middle chest]'), 'Small Key (Escape)') + forbid_item(world.get_location('[dungeon-C-B1] Escape - Final Basement Room [right chest]'), 'Small Key (Escape)') + forbid_item(world.get_location('[dungeon-C-1F] Sanctuary'), 'Small Key (Escape)') + add_rule(world.get_location('[dungeon-C-B1] Escape - Final Basement Room [left chest]'), lambda state: state.can_reach('Sewer Drop')) + add_rule(world.get_location('[dungeon-C-B1] Escape - Final Basement Room [middle chest]'), lambda state: state.can_reach('Sewer Drop')) + add_rule(world.get_location('[dungeon-C-B1] Escape - Final Basement Room [right chest]'), lambda state: state.can_reach('Sewer Drop')) + add_rule(world.get_location('[dungeon-C-B1] Escape - First B1 Room'), lambda state: state.can_reach('Sewer Drop') or (state.world.get_location('[dungeon-C-B1] Escape - First B1 Room').item is not None and state.world.get_location('[dungeon-C-B1] Escape - First B1 Room').item.name in ['Small Key (Escape)'])) # you could skip this chest and be unable to go back until you can drop into escape +