350 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			350 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
	
from BaseClasses import World, CollectionState
 | 
						|
from Regions import create_regions
 | 
						|
from EntranceShuffle import link_entrances
 | 
						|
from Rules import set_rules
 | 
						|
from Dungeons import fill_dungeons
 | 
						|
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(shuffle, logic, mode, difficulty, goal)
 | 
						|
    create_regions(world)
 | 
						|
 | 
						|
    random.seed(seed)
 | 
						|
 | 
						|
    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
 | 
						|
 | 
						|
 | 
						|
def distribute_items(world):
 | 
						|
    # get list of locations to fill in
 | 
						|
    fill_locations = world.get_unfilled_locations()
 | 
						|
    random.shuffle(fill_locations)
 | 
						|
 | 
						|
    # get items to distribute
 | 
						|
    random.shuffle(world.itempool)
 | 
						|
    itempool = world.itempool
 | 
						|
 | 
						|
    progress_done = False
 | 
						|
 | 
						|
    while itempool and fill_locations:
 | 
						|
        candidate_item_to_place = None
 | 
						|
        item_to_place = None
 | 
						|
        for item in itempool:
 | 
						|
            if progress_done:
 | 
						|
                item_to_place = item
 | 
						|
                break
 | 
						|
            if item.advancement:
 | 
						|
                candidate_item_to_place = item
 | 
						|
                if world.unlocks_new_location(item):
 | 
						|
                    item_to_place = item
 | 
						|
                    break
 | 
						|
 | 
						|
        if item_to_place is None:
 | 
						|
            # check if we can reach all locations and that is why we find no new locations to place
 | 
						|
            if len(world.get_reachable_locations()) == len(world.get_locations()):
 | 
						|
                progress_done = True
 | 
						|
                continue
 | 
						|
            # 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 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
 | 
						|
        for location in fill_locations:
 | 
						|
            if world.state.can_reach(location) and location.item_rule(item_to_place):
 | 
						|
                spot_to_fill = location
 | 
						|
                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)
 | 
						|
 | 
						|
    logging.getLogger('').debug('Unplaced items: %s - Unfilled Locations: %s' % (itempool, fill_locations))
 | 
						|
 | 
						|
 | 
						|
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')
 | 
						|
 | 
						|
    world.push_item('Ganon', Triforce(), False)
 | 
						|
 | 
						|
    # set up item pool
 | 
						|
    world.itempool = [
 | 
						|
        ArrowUpgrade5(), ArrowUpgrade5(), ArrowUpgrade5(), ArrowUpgrade5(), ArrowUpgrade5(), ArrowUpgrade5(),
 | 
						|
        ArrowUpgrade10(),
 | 
						|
        SingleArrow(),
 | 
						|
        ProgressiveArmor(), ProgressiveArmor(),
 | 
						|
        BombUpgrade5(), BombUpgrade5(), BombUpgrade5(), BombUpgrade5(), BombUpgrade5(), BombUpgrade5(),
 | 
						|
        BombUpgrade10(),
 | 
						|
        Bombos(),
 | 
						|
        Book(),
 | 
						|
        BlueBoomerang(),
 | 
						|
        Bottle(), Bottle(), Bottle(), Bottle(),
 | 
						|
        Bow(),
 | 
						|
        Net(),
 | 
						|
        Byrna(),
 | 
						|
        Somaria(),
 | 
						|
        Ether(),
 | 
						|
        Rupees50(), Rupees50(), Rupees50(), Rupees50(), Rupees50(), Rupees50(), Rupees50(),
 | 
						|
        ProgressiveShield(), ProgressiveShield(), ProgressiveShield(),
 | 
						|
        ProgressiveSword(), ProgressiveSword(), ProgressiveSword(),
 | 
						|
        FireRod(),
 | 
						|
        Rupees5(), Rupees5(), Rupees5(), Rupees5(),
 | 
						|
        Flippers(),
 | 
						|
        Ocarina(),
 | 
						|
        Hammer(),
 | 
						|
        SancHeart(),
 | 
						|
        HeartContainer(), HeartContainer(), HeartContainer(), HeartContainer(), HeartContainer(), HeartContainer(), HeartContainer(), HeartContainer(), HeartContainer(), HeartContainer(),
 | 
						|
        Hookshot(),
 | 
						|
        IceRod(),
 | 
						|
        Lamp(),
 | 
						|
        Cape(),
 | 
						|
        Mirror(),
 | 
						|
        Powder(),
 | 
						|
        RedBoomerang(),
 | 
						|
        Pearl(),
 | 
						|
        Mushroom(),
 | 
						|
        Rupees100(),
 | 
						|
        Rupee(), Rupee(),
 | 
						|
        Boots(),
 | 
						|
        PieceOfHeart(), PieceOfHeart(), PieceOfHeart(), PieceOfHeart(), PieceOfHeart(), PieceOfHeart(), PieceOfHeart(), PieceOfHeart(), PieceOfHeart(), PieceOfHeart(), PieceOfHeart(), PieceOfHeart(),
 | 
						|
        PieceOfHeart(), PieceOfHeart(), PieceOfHeart(), PieceOfHeart(), PieceOfHeart(), PieceOfHeart(), PieceOfHeart(), PieceOfHeart(), PieceOfHeart(), PieceOfHeart(), PieceOfHeart(), PieceOfHeart(),
 | 
						|
        ProgressiveGlove(), ProgressiveGlove(),
 | 
						|
        Quake(),
 | 
						|
        Shovel(),
 | 
						|
        SilverArrows(),
 | 
						|
        Arrows10(), Arrows10(), Arrows10(), Arrows10(),
 | 
						|
        Bombs3(), Bombs3(), Bombs3(), Bombs3(), Bombs3(), Bombs3(), Bombs3(), Bombs3(), Bombs3(), Bombs3(),
 | 
						|
        Rupees300(), Rupees300(), Rupees300(), Rupees300(),
 | 
						|
        Rupees20(), Rupees20(), Rupees20(), Rupees20(), Rupees20(), Rupees20(), Rupees20(), Rupees20(), Rupees20(), Rupees20(), Rupees20(), Rupees20(), Rupees20(), Rupees20(), Rupees20(), Rupees20(),
 | 
						|
        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:
 | 
						|
        world.itempool.append(HalfMagic())
 | 
						|
 | 
						|
    # distribute crystals
 | 
						|
    crystals = [GreenPendant(), RedPendant(), BluePendant(), Crystal1(), Crystal2(), Crystal3(), Crystal4(), Crystal5(), Crystal6(), Crystal7()]
 | 
						|
    crystal_locations = [world.get_location('Armos - Pendant'), world.get_location('Lanmolas - Pendant'), world.get_location('Moldorm - Pendant'), world.get_location('Helmasaur - Crystal'),
 | 
						|
                         world.get_location('Blind - Crystal'), world.get_location('Mothula - Crystal'), world.get_location('Arrghus - Crystal'), world.get_location('Kholdstare - Crystal'),
 | 
						|
                         world.get_location('Vitreous - Crystal'), world.get_location('Trinexx - Crystal')]
 | 
						|
    random.shuffle(crystals)
 | 
						|
    for location, crystal in zip(crystal_locations, crystals):
 | 
						|
        world.push_item(location, crystal, False)
 | 
						|
 | 
						|
    # shuffle medallions
 | 
						|
    mm_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)]
 | 
						|
    tr_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)]
 | 
						|
    world.required_medallions = (mm_medallion, tr_medallion)
 | 
						|
 | 
						|
    # push dungeon items
 | 
						|
    fill_dungeons(world)
 | 
						|
 | 
						|
 | 
						|
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 = 10
 | 
						|
start = time.clock()
 | 
						|
for i in range(iterations):
 | 
						|
    print('Seed %s\n\n' % i)
 | 
						|
    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))
 | 
						|
            if location.item.advancement:
 | 
						|
                old_prog += 1
 | 
						|
            elif 'Small Key' in location.item.name:
 | 
						|
                old_sk += 1
 | 
						|
            elif 'Big Key' in location.item.name:
 | 
						|
                old_bk += 1
 | 
						|
            tally[location.name] = (old_sk, old_bk, old_prog)
 | 
						|
 | 
						|
diff = time.clock() - start
 | 
						|
print('Duration: %s, Average: %s' % (diff, diff/float(iterations)))
 | 
						|
 | 
						|
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()
 |