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