from collections import OrderedDict from itertools import zip_longest import json import logging import random import time from BaseClasses import World, CollectionState, Item from Regions import create_regions, mark_light_world_regions from EntranceShuffle import link_entrances from Rom import patch_rom, Sprite, LocalRom, JsonRom from Rules import set_rules from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive from Fill import distribute_items_cutoff, distribute_items_staleness, distribute_items_restrictive, flood_items from ItemList import generate_itempool, difficulties from Utils import output_path __version__ = '0.6.0' logic_hash = [26, 76, 4, 144, 72, 105, 234, 233, 12, 184, 95, 94, 100, 13, 15, 174, 186, 135, 130, 189, 246, 254, 123, 245, 85, 241, 101, 129, 70, 255, 55, 248, 43, 146, 23, 179, 243, 208, 230, 176, 9, 88, 239, 226, 222, 203, 244, 183, 205, 74, 44, 5, 122, 220, 206, 47, 221, 125, 138, 155, 98, 79, 238, 119, 30, 24, 159, 39, 253, 27, 33, 218, 62, 82, 200, 28, 141, 191, 93, 22, 192, 54, 227, 108, 48, 78, 242, 166, 60, 250, 75, 145, 49, 212, 41, 25, 127, 89, 178, 157, 19, 158, 177, 231, 207, 66, 172, 17, 133, 61, 109, 86, 57, 143, 142, 219, 148, 209, 181, 87, 163, 40, 81, 114, 240, 103, 31, 175, 237, 185, 18, 173, 168, 45, 216, 106, 161, 16, 151, 139, 104, 134, 110, 21, 32, 131, 118, 182, 215, 67, 3, 73, 171, 71, 150, 147, 223, 247, 42, 132, 107, 149, 232, 153, 10, 201, 156, 225, 116, 194, 187, 204, 46, 165, 124, 92, 7, 0, 251, 126, 162, 80, 90, 154, 252, 197, 188, 52, 137, 117, 198, 63, 167, 38, 136, 96, 58, 11, 1, 115, 229, 224, 37, 112, 170, 59, 68, 196, 36, 64, 91, 213, 14, 180, 190, 164, 8, 56, 214, 77, 202, 193, 97, 84, 152, 83, 236, 211, 20, 217, 2, 228, 140, 69, 121, 111, 113, 128, 210, 51, 53, 6, 235, 34, 102, 29, 120, 35, 50, 65, 160, 249, 99, 169, 199, 195] 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.disablemusic, args.keysanity, args.retro, args.custom, args.customitemarray) 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) world.difficulty_requirements = difficulties[world.difficulty] create_regions(world) create_dungeons(world) logger.info('Shuffling the World about.') link_entrances(world) mark_light_world_regions(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 in ['balanced', '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, gt_filler(world), shuffled_locations) elif args.algorithm == 'balanced': distribute_items_restrictive(world, gt_filler(world)) logger.info('Calculating playthrough.') create_playthrough(world) logger.info('Patching ROM.') if args.sprite is not None: if isinstance(args.sprite, Sprite): sprite = args.sprite else: sprite = Sprite(args.sprite) else: sprite = None outfilebase = 'ER_%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 "", "-retro" if world.retro else "", "-prog_" + world.progressive if world.progressive in ['off', 'random'] 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, args.heartcolor, sprite) if args.jsonout: print(json.dumps({'patch': rom.patches, 'spoiler': world.spoiler.to_json()})) else: rom.write_to_file(args.jsonout or output_path('%s.sfc' % outfilebase)) if args.create_spoiler and not args.jsonout: world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase)) logger.info('Done. Enjoy.') logger.debug('Total Time: %s', time.clock() - start) return world def gt_filler(world): if world.goal == 'triforcehunt': return random.randint(15, 50) return random.randint(0, 15) 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.disable_music, world.keysanity, world.retro, world.custom, world.customitemarray) 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 ret.can_take_damage = world.can_take_damage ret.difficulty_requirements = world.difficulty_requirements ret.fix_fake_world = world.fix_fake_world ret.lamps_needed_for_dark_rooms = world.lamps_needed_for_dark_rooms create_regions(ret) create_dungeons(ret) # connect copied world for region in world.regions: copied_region = ret.get_region(region.name) copied_region.is_light_world = region.is_light_world copied_region.is_dark_world = region.is_dark_world for entrance in region.entrances: ret.get_entrance(entrance.name).connect(copied_region) # 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_filled_locations() if location.item.advancement] state_cache = [None] 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, location) collection_spheres.append(sphere) state_cache.append(state.copy()) 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 num, sphere in reversed(list(enumerate(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) if world.can_beat_game(state_cache[num]): 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. Unfortunately # the previous pruning stage could potentially have made certain items dependant on others # in the same or later sphere (because the location had 2 ways to access but the item originally # used to access it was deemed not required.) So we need to do one final sphere collection pass # to build up the correct spheres required_locations = [item for sphere in collection_spheres for item in sphere] state = CollectionState(world) collection_spheres = [] while required_locations: if not world.keysanity: state.sweep_for_events(key_only=True) sphere = list(filter(state.can_reach, required_locations)) for location in sphere: required_locations.remove(location) state.collect(location.item, True, location) collection_spheres.append(sphere) logging.getLogger('').debug('Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere), len(required_locations)) if not sphere: raise RuntimeError('Not all required items reachable. Something went terribly wrong here.') # store the required locations for statistical analysis old_world.required_locations = [location.name for sphere in collection_spheres for location in sphere] def flist_to_iter(node): while node: value, node = node yield value def get_path(state, region): reversed_path_as_flist = state.path.get(region, (region, None)) string_path_flat = reversed(list(map(str, flist_to_iter(reversed_path_as_flist)))) # Now we combine the flat string list into (region, exit) pairs pathsiter = iter(string_path_flat) pathpairs = zip_longest(pathsiter, pathsiter) return list(pathpairs) old_world.spoiler.paths = {location.name : get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere} if any(exit == 'Pyramid Fairy' for path in old_world.spoiler.paths.values() for (_, exit) in path): old_world.spoiler.paths['Big Bomb Shop'] = get_path(state, world.get_region('Big Bomb Shop')) # 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)])