from BaseClasses import World, CollectionState, Item from Regions import create_regions from EntranceShuffle import link_entrances from Rom import patch_rom, LocalRom, JsonRom from Rules import set_rules from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive from Items import ItemFactory from Fill import distribute_items_cutoff, distribute_items_staleness, distribute_items_restrictive, fill_restrictive, flood_items from collections import OrderedDict from ItemList import generate_itempool import random import time import logging import json __version__ = '0.4.7-dev' logic_hash = [118, 17, 154, 187, 209, 19, 0, 97, 63, 62, 164, 160, 155, 28, 136, 220, 251, 76, 55, 109, 174, 36, 82, 140, 87, 226, 26, 150, 200, 115, 6, 238, 85, 229, 49, 141, 66, 199, 112, 212, 182, 98, 249, 54, 201, 161, 148, 126, 179, 5, 47, 162, 108, 152, 67, 203, 239, 15, 211, 132, 198, 124, 221, 81, 217, 191, 177, 37, 145, 216, 84, 56, 65, 190, 163, 138, 186, 157, 9, 23, 189, 8, 188, 69, 204, 29, 22, 114, 79, 175, 59, 202, 107, 231, 96, 91, 45, 64, 228, 2, 43, 74, 89, 205, 246, 123, 166, 83, 219, 248, 117, 241, 94, 60, 227, 20, 35, 18, 1, 252, 250, 110, 137, 58, 42, 102, 106, 93, 101, 105, 193, 77, 39, 119, 223, 73, 51, 218, 78, 100, 21, 247, 41, 214, 170, 185, 237, 130, 12, 24, 92, 180, 16, 178, 235, 4, 240, 158, 57, 197, 133, 88, 142, 234, 147, 196, 146, 224, 139, 207, 31, 232, 243, 3, 121, 210, 167, 99, 13, 44, 70, 213, 168, 244, 153, 127, 171, 233, 172, 75, 34, 236, 113, 25, 149, 134, 53, 222, 122, 80, 195, 254, 27, 169, 255, 242, 143, 159, 225, 135, 230, 151, 48, 33, 72, 10, 95, 103, 253, 184, 52, 125, 206, 144, 128, 32, 61, 176, 215, 50, 194, 40, 183, 173, 131, 46, 111, 90, 192, 208, 86, 181, 68, 104, 129, 116, 165, 156, 11, 14, 120, 30, 71, 245, 7, 38] def main(args, seed=None): start = time.clock() # initialize the world world = World(args.shuffle, args.logic, args.mode, args.difficulty, args.timer, args.progressive, 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) world.seed = random.randint(0, 999999999) else: world.seed = int(seed) random.seed(world.seed) logger.info('ALttP Entrance Randomizer Version %s - Seed: %s\n\n' % (__version__, world.seed)) create_regions(world) create_dungeons(world) logger.info('Shuffling the World about.') link_entrances(world) logger.info('Calculating Access Rules.') set_rules(world) logger.info('Generating Item Pool.') generate_itempool(world) logger.info('Placing Dungeon Items.') shuffled_locations = None if args.algorithm == 'vt26' or args.keysanity: shuffled_locations = world.get_unfilled_locations() random.shuffle(shuffled_locations) fill_dungeons_restrictive(world, shuffled_locations) else: fill_dungeons(world) logger.info('Fill the world.') if args.algorithm == 'flood': flood_items(world) # different algo, biased towards early game progress items elif args.algorithm == 'vt21': distribute_items_cutoff(world, 1) elif args.algorithm == 'vt22': distribute_items_cutoff(world, 0.66) elif args.algorithm == 'freshness': distribute_items_staleness(world) elif args.algorithm == 'vt25': distribute_items_restrictive(world, 0) elif args.algorithm == 'vt26': distribute_items_restrictive(world, random.randint(0, 15), shuffled_locations) elif args.algorithm == 'balanced': distribute_items_restrictive(world, random.randint(0, 15)) logger.info('Calculating playthrough.') create_playthrough(world) logger.info('Patching ROM.') if args.sprite is not None: sprite = bytearray(open(args.sprite, 'rb').read()) else: sprite = None outfilebase = 'ER_%s_%s-%s-%s%s_%s-%s%s%s%s%s%s_%s' % (world.logic, world.difficulty, world.mode, world.goal, "" if world.timer in ['none', 'display'] else "-" + world.timer, world.shuffle, world.algorithm, "-keysanity" if world.keysanity else "", "-progressives_" + world.progressive if world.progressive in ['off', 'random'] 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: rom = JsonRom() else: rom = LocalRom(args.rom) patch_rom(world, rom, bytearray(logic_hash), args.heartbeep, sprite) if args.jsonout: print(json.dumps({'patch': rom.patches, 'spoiler': world.spoiler.to_json()})) else: rom.write_to_file(args.jsonout or '%s.sfc' % outfilebase) if args.create_spoiler and not args.jsonout: world.spoiler.to_file('%s_Spoiler.txt' % outfilebase) logger.info('Done. Enjoy.') logger.debug('Total Time: %s' % (time.clock() - start)) return world def copy_world(world): # ToDo: Not good yet ret = World(world.shuffle, world.logic, world.mode, world.difficulty, world.timer, world.progressive, 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 ret.treasure_hunt_count = world.treasure_hunt_count ret.treasure_hunt_icon = world.treasure_hunt_icon ret.sewer_light_cone = world.sewer_light_cone ret.light_world_light_cone = world.light_world_light_cone ret.dark_world_light_cone = world.dark_world_light_cone ret.seed = world.seed ret.can_access_trock_eyebridge = world.can_access_trock_eyebridge create_regions(ret) create_dungeons(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.priority, location.item.type) ret.get_location(location.name).item = item item.location = ret.get_location(location.name) if location.event: ret.get_location(location.name).event = True # 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.priority, item.type)) # copy progress items in state ret.state.prog_items = list(world.state.prog_items) set_rules(ret) return ret def create_playthrough(world): # create a copy as we will modify it old_world = world world = copy_world(world) # in treasure hunt and pedestal goals, ganon is invincible if world.goal in ['pedestal', 'triforcehunt']: world.get_location('Ganon').item = None # if we only check for beatable, we can do this sanity check first before writing down spheres if world.check_beatable_only and not world.can_beat_game(): 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 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: 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 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, True) collection_spheres.append(sphere) logging.getLogger('').debug('Calculated sphere %i, containing %i of %i progress items.' % (len(collection_spheres), len(sphere), len(prog_locations))) if not sphere: logging.getLogger('').debug('The following items could not be reached: %s' % ['%s at %s' % (location.item.name, location.name) for location in sphere_candidates]) if not world.check_beatable_only: raise RuntimeError('Not all progression items reachable. Something went terribly wrong here.') else: break # 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 logging.getLogger('').debug('Checking if %s is required to beat the game.' % location.item.name) 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] # store the required locations for statistical analysis old_world.required_locations = [location.name for sphere in collection_spheres for location in sphere] # we can finally output our playthrough old_world.spoiler.playthrough = OrderedDict([(str(i + 1), {str(location): str(location.item) for location in sphere}) for i, sphere in enumerate(collection_spheres)])