2017-05-16 19:23:47 +00:00
from BaseClasses import World , CollectionState
2017-05-15 18:28:04 +00:00
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
2017-05-16 19:23:47 +00:00
import logging
2017-05-15 18:28:04 +00:00
def main ( seed = None , shuffle = ' Default ' , logic = ' no-glitches ' , mode = ' standard ' , difficulty = ' normal ' , goal = ' defeat ganon ' ) :
# initialize the world
2017-05-16 19:23:47 +00:00
world = World ( shuffle , logic , mode , difficulty , goal )
2017-05-15 18:28:04 +00:00
create_regions ( world )
random . seed ( seed )
2017-05-16 19:23:47 +00:00
link_entrances ( world )
set_rules ( world )
generate_itempool ( world )
2017-05-15 18:28:04 +00:00
distribute_items ( world )
2017-05-16 19:23:47 +00:00
# flood_items(world) # different algo, biased towards early game progress items
2017-05-15 18:28:04 +00:00
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 :
2017-05-16 19:23:47 +00:00
# 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
2017-05-15 18:28:04 +00:00
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 :
2017-05-16 19:23:47 +00:00
# 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
2017-05-15 18:28:04 +00:00
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 )
2017-05-16 19:23:47 +00:00
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
2017-05-15 18:28:04 +00:00
2017-05-16 19:23:47 +00:00
# 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. ' )
2017-05-15 18:28:04 +00:00
2017-05-16 19:23:47 +00:00
# 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 ' ] :
2017-05-15 18:28:04 +00:00
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 ( )
]
2017-05-16 19:23:47 +00:00
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?
2017-05-15 18:28:04 +00:00
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 )
2017-05-16 19:23:47 +00:00
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 ( )
2017-05-15 18:28:04 +00:00
tally = { }
2017-05-16 19:23:47 +00:00
iterations = 10
2017-05-15 18:28:04 +00:00
start = time . clock ( )
for i in range ( iterations ) :
print ( ' Seed %s \n \n ' % i )
2017-05-16 19:23:47 +00:00
w = main ( mode = ' open ' )
print ( create_playthrough ( w ) )
2017-05-15 18:28:04 +00:00
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 ] ) ) )
2017-05-16 19:23:47 +00:00
profiler . disable ( )
profiler . print_stats ( )