2017-12-17 05:25:46 +00:00
from collections import OrderedDict
2018-02-17 23:38:54 +00:00
import copy
2018-01-06 21:25:14 +00:00
from itertools import zip_longest
2017-12-17 05:25:46 +00:00
import json
import logging
import random
import time
2018-09-23 02:51:54 +00:00
from BaseClasses import World , CollectionState , Item , Region , Location , Shop
2017-12-13 14:51:53 +00:00
from Regions import create_regions , mark_light_world_regions
2017-05-25 10:09:50 +00:00
from EntranceShuffle import link_entrances
2017-11-13 05:29:42 +00:00
from Rom import patch_rom , Sprite , LocalRom , JsonRom
2017-05-15 18:28:04 +00:00
from Rules import set_rules
2017-10-15 17:52:42 +00:00
from Dungeons import create_dungeons , fill_dungeons , fill_dungeons_restrictive
2017-12-17 05:25:46 +00:00
from Fill import distribute_items_cutoff , distribute_items_staleness , distribute_items_restrictive , flood_items
2018-01-04 06:06:22 +00:00
from ItemList import generate_itempool , difficulties
2017-11-28 14:36:32 +00:00
from Utils import output_path
2017-05-15 18:28:04 +00:00
2018-03-25 22:21:36 +00:00
__version__ = ' 0.6.1 '
2018-01-18 07:10:39 +00:00
2018-03-25 22:21:36 +00:00
logic_hash = [ 215 , 244 , 99 , 97 , 253 , 98 , 31 , 150 , 207 , 70 , 50 , 78 , 59 , 73 , 221 , 191 ,
21 , 34 , 200 , 116 , 77 , 234 , 89 , 27 , 228 , 96 , 16 , 249 , 56 , 148 , 3 , 176 ,
17 , 227 , 24 , 20 , 238 , 67 , 37 , 219 , 62 , 223 , 60 , 123 , 246 , 92 , 164 , 177 ,
211 , 15 , 245 , 23 , 75 , 33 , 190 , 124 , 144 , 100 , 87 , 57 , 86 , 108 , 80 , 181 ,
6 , 28 , 2 , 71 , 182 , 155 , 222 , 229 , 90 , 91 , 32 , 126 , 25 , 226 , 133 , 41 ,
132 , 122 , 10 , 30 , 53 , 239 , 112 , 49 , 104 , 76 , 209 , 247 , 139 , 13 , 173 , 113 ,
159 , 69 , 145 , 161 , 11 , 102 , 149 , 143 , 129 , 178 , 45 , 217 , 196 , 232 , 208 , 119 ,
94 , 19 , 35 , 65 , 170 , 103 , 55 , 109 , 5 , 43 , 118 , 194 , 180 , 12 , 206 , 241 ,
8 , 105 , 210 , 231 , 179 , 83 , 137 , 18 , 212 , 236 , 225 , 66 , 63 , 142 , 138 , 131 ,
192 , 160 , 1 , 198 , 153 , 128 , 106 , 165 , 39 , 248 , 167 , 22 , 74 , 163 , 140 , 157 ,
214 , 84 , 154 , 127 , 195 , 172 , 136 , 168 , 68 , 134 , 152 , 95 , 111 , 235 , 26 , 42 ,
135 , 186 , 250 , 7 , 72 , 58 , 4 , 9 , 193 , 101 , 52 , 44 , 187 , 183 , 171 , 184 ,
197 , 130 , 47 , 189 , 81 , 203 , 51 , 110 , 146 , 175 , 213 , 88 , 79 , 93 , 64 , 107 ,
121 , 237 , 0 , 46 , 120 , 141 , 199 , 158 , 174 , 114 , 205 , 201 , 151 , 185 , 242 , 29 ,
162 , 117 , 85 , 54 , 14 , 202 , 216 , 169 , 230 , 252 , 188 , 251 , 36 , 233 , 147 , 82 ,
115 , 61 , 255 , 38 , 220 , 218 , 40 , 224 , 48 , 125 , 204 , 156 , 240 , 254 , 166 , 243 ]
2017-05-25 19:41:37 +00:00
2017-05-20 12:07:40 +00:00
2017-05-25 13:58:35 +00:00
def main ( args , seed = None ) :
2017-05-20 12:07:40 +00:00
start = time . clock ( )
2017-05-15 18:28:04 +00:00
# initialize the world
2019-01-23 09:04:42 +00:00
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 , args . shufflebosses , args . hints )
2017-05-20 12:07:40 +00:00
logger = logging . getLogger ( ' ' )
if seed is None :
random . seed ( None )
world . seed = random . randint ( 0 , 999999999 )
else :
2017-05-30 05:33:23 +00:00
world . seed = int ( seed )
2017-05-20 12:07:40 +00:00
random . seed ( world . seed )
2017-12-17 05:25:46 +00:00
logger . info ( ' ALttP Entrance Randomizer Version %s - Seed: %s \n \n ' , __version__ , world . seed )
2017-05-20 12:07:40 +00:00
2018-01-04 06:06:22 +00:00
world . difficulty_requirements = difficulties [ world . difficulty ]
2017-05-15 18:28:04 +00:00
create_regions ( world )
2017-11-04 18:23:57 +00:00
create_dungeons ( world )
2017-05-15 18:28:04 +00:00
2017-05-20 12:07:40 +00:00
logger . info ( ' Shuffling the World about. ' )
2017-07-18 10:44:13 +00:00
link_entrances ( world )
2017-12-13 14:51:53 +00:00
mark_light_world_regions ( world )
2017-05-20 12:07:40 +00:00
logger . info ( ' Calculating Access Rules. ' )
2017-07-18 10:44:13 +00:00
set_rules ( world )
2017-05-20 12:07:40 +00:00
2017-08-05 15:52:18 +00:00
logger . info ( ' Generating Item Pool. ' )
generate_itempool ( world )
2017-06-23 19:32:31 +00:00
logger . info ( ' Placing Dungeon Items. ' )
2017-05-20 12:07:40 +00:00
2017-10-15 20:34:46 +00:00
shuffled_locations = None
2018-01-03 01:01:52 +00:00
if args . algorithm in [ ' balanced ' , ' vt26 ' ] or args . keysanity :
2017-10-15 20:34:46 +00:00
shuffled_locations = world . get_unfilled_locations ( )
random . shuffle ( shuffled_locations )
fill_dungeons_restrictive ( world , shuffled_locations )
2017-10-15 17:52:42 +00:00
else :
fill_dungeons ( world )
2017-05-20 12:07:40 +00:00
logger . info ( ' Fill the world. ' )
2017-05-25 13:58:35 +00:00
if args . algorithm == ' flood ' :
2017-05-20 12:07:40 +00:00
flood_items ( world ) # different algo, biased towards early game progress items
2017-06-03 19:28:02 +00:00
elif args . algorithm == ' vt21 ' :
distribute_items_cutoff ( world , 1 )
elif args . algorithm == ' vt22 ' :
2017-06-04 14:15:59 +00:00
distribute_items_cutoff ( world , 0.66 )
2017-06-03 19:28:02 +00:00
elif args . algorithm == ' freshness ' :
distribute_items_staleness ( world )
2017-08-01 17:07:44 +00:00
elif args . algorithm == ' vt25 ' :
2017-07-20 09:22:35 +00:00
distribute_items_restrictive ( world , 0 )
2017-10-15 17:52:42 +00:00
elif args . algorithm == ' vt26 ' :
2018-03-02 02:36:30 +00:00
distribute_items_restrictive ( world , gt_filler ( world ) , shuffled_locations )
2017-11-12 00:05:06 +00:00
elif args . algorithm == ' balanced ' :
2018-03-02 02:36:30 +00:00
distribute_items_restrictive ( world , gt_filler ( world ) )
2017-06-03 19:28:02 +00:00
2017-05-20 12:07:40 +00:00
logger . info ( ' Calculating playthrough. ' )
2017-07-18 10:44:13 +00:00
create_playthrough ( world )
2017-05-20 12:07:40 +00:00
logger . info ( ' Patching ROM. ' )
2017-05-26 16:39:32 +00:00
if args . sprite is not None :
2017-12-17 05:25:46 +00:00
if isinstance ( args . sprite , Sprite ) :
2017-12-09 00:28:22 +00:00
sprite = args . sprite
else :
sprite = Sprite ( args . sprite )
2017-05-26 16:39:32 +00:00
else :
sprite = None
2019-01-23 09:04:42 +00:00
outfilebase = ' ER_ %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 " " , " -retro " if world . retro else " " , " -prog_ " + world . progressive if world . progressive in [ ' off ' , ' random ' ] else " " , " -nohints " if not world . hints else " " , world . seed )
2017-05-20 12:07:40 +00:00
2017-06-04 11:09:47 +00:00
if not args . suppress_rom :
2017-07-14 12:37:34 +00:00
if args . jsonout :
rom = JsonRom ( )
else :
rom = LocalRom ( args . rom )
2018-02-28 02:26:33 +00:00
patch_rom ( world , rom , bytearray ( logic_hash ) , args . heartbeep , args . heartcolor , sprite )
2017-07-16 21:20:54 +00:00
if args . jsonout :
2017-07-18 10:44:13 +00:00
print ( json . dumps ( { ' patch ' : rom . patches , ' spoiler ' : world . spoiler . to_json ( ) } ) )
2017-07-16 21:20:54 +00:00
else :
2017-11-28 14:36:32 +00:00
rom . write_to_file ( args . jsonout or output_path ( ' %s .sfc ' % outfilebase ) )
2017-06-04 11:09:47 +00:00
2017-07-16 21:20:54 +00:00
if args . create_spoiler and not args . jsonout :
2017-11-28 14:36:32 +00:00
world . spoiler . to_file ( output_path ( ' %s _Spoiler.txt ' % outfilebase ) )
2017-05-20 12:07:40 +00:00
logger . info ( ' Done. Enjoy. ' )
2017-12-17 05:25:46 +00:00
logger . debug ( ' Total Time: %s ' , time . clock ( ) - start )
2017-05-15 18:28:04 +00:00
return world
2018-03-02 02:36:30 +00:00
def gt_filler ( world ) :
if world . goal == ' triforcehunt ' :
return random . randint ( 15 , 50 )
return random . randint ( 0 , 15 )
2017-05-16 19:23:47 +00:00
def copy_world ( world ) :
# ToDo: Not good yet
2019-01-23 09:04:42 +00:00
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 , world . boss_shuffle , world . hints )
2017-05-16 19:23:47 +00:00
ret . required_medallions = list ( world . required_medallions )
2017-05-26 07:52:38 +00:00
ret . swamp_patch_required = world . swamp_patch_required
2017-08-01 17:08:46 +00:00
ret . ganon_at_pyramid = world . ganon_at_pyramid
2017-06-04 11:10:22 +00:00
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
2017-06-19 19:31:08 +00:00
ret . seed = world . seed
2017-07-17 21:13:39 +00:00
ret . can_access_trock_eyebridge = world . can_access_trock_eyebridge
2018-01-02 05:39:53 +00:00
ret . can_take_damage = world . can_take_damage
2018-01-04 06:06:22 +00:00
ret . difficulty_requirements = world . difficulty_requirements
2018-02-24 21:16:50 +00:00
ret . fix_fake_world = world . fix_fake_world
ret . lamps_needed_for_dark_rooms = world . lamps_needed_for_dark_rooms
2017-05-16 19:23:47 +00:00
create_regions ( ret )
2017-10-15 16:16:07 +00:00
create_dungeons ( ret )
2017-05-16 19:23:47 +00:00
2018-03-23 03:18:40 +00:00
copy_dynamic_regions_and_locations ( world , ret )
2018-02-17 23:38:54 +00:00
2018-09-26 17:12:20 +00:00
# copy bosses
for dungeon in world . dungeons :
for level , boss in dungeon . bosses . items ( ) :
ret . get_dungeon ( dungeon . name ) . bosses [ level ] = boss
2018-02-17 23:38:54 +00:00
for shop in world . shops :
copied_shop = ret . get_region ( shop . region . name ) . shop
copied_shop . active = shop . active
copied_shop . inventory = copy . copy ( shop . inventory )
2017-05-16 19:23:47 +00:00
# connect copied world
for region in world . regions :
2017-12-13 14:51:53 +00:00
copied_region = ret . get_region ( region . name )
copied_region . is_light_world = region . is_light_world
2018-01-27 22:17:03 +00:00
copied_region . is_dark_world = region . is_dark_world
2017-05-16 19:23:47 +00:00
for entrance in region . entrances :
2017-12-13 14:51:53 +00:00
ret . get_entrance ( entrance . name ) . connect ( copied_region )
2017-05-16 19:23:47 +00:00
# fill locations
for location in world . get_locations ( ) :
if location . item is not None :
2017-10-15 16:16:07 +00:00
item = Item ( location . item . name , location . item . advancement , location . item . priority , location . item . type )
2017-05-16 19:23:47 +00:00
ret . get_location ( location . name ) . item = item
item . location = ret . get_location ( location . name )
2017-06-17 12:40:37 +00:00
if location . event :
ret . get_location ( location . name ) . event = True
2017-05-16 19:23:47 +00:00
# copy remaining itempool. No item in itempool should have an assigned location
for item in world . itempool :
2017-10-15 16:16:07 +00:00
ret . itempool . append ( Item ( item . name , item . advancement , item . priority , item . type ) )
2017-05-16 19:23:47 +00:00
# copy progress items in state
ret . state . prog_items = list ( world . state . prog_items )
2017-07-17 20:29:32 +00:00
set_rules ( ret )
2017-05-16 19:23:47 +00:00
return ret
2018-03-23 03:18:40 +00:00
def copy_dynamic_regions_and_locations ( world , ret ) :
for region in world . dynamic_regions :
2019-02-22 11:59:46 +00:00
new_reg = Region ( region . name , region . type , region . hint_text )
2018-03-23 03:18:40 +00:00
ret . regions . append ( new_reg )
ret . dynamic_regions . append ( new_reg )
# Note: ideally exits should be copied here, but the current use case (Take anys) do not require this
if region . shop :
new_reg . shop = Shop ( new_reg , region . shop . room_id , region . shop . type , region . shop . shopkeeper_config , region . shop . replaceable )
ret . shops . append ( new_reg . shop )
for location in world . dynamic_locations :
new_loc = Location ( location . name , location . address , location . crystal , location . hint_text , location . parent_region )
new_reg = ret . get_region ( location . parent_region . name )
new_reg . locations . append ( new_loc )
2017-05-16 19:23:47 +00:00
def create_playthrough ( world ) :
# create a copy as we will modify it
2017-06-04 14:15:59 +00:00
old_world = world
2017-05-16 19:23:47 +00:00
world = copy_world ( world )
2017-05-26 07:53:34 +00:00
2017-06-04 12:44:23 +00:00
# in treasure hunt and pedestal goals, ganon is invincible
2017-08-01 17:07:44 +00:00
if world . goal in [ ' pedestal ' , ' triforcehunt ' ] :
2017-05-26 07:53:34 +00:00
world . get_location ( ' Ganon ' ) . item = None
2017-05-16 19:23:47 +00:00
2017-06-23 20:15:29 +00:00
# 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! ' )
2017-05-16 19:23:47 +00:00
# get locations containing progress items
2018-01-03 01:01:16 +00:00
prog_locations = [ location for location in world . get_filled_locations ( ) if location . item . advancement ]
2018-01-01 20:55:13 +00:00
state_cache = [ None ]
2017-05-16 19:23:47 +00:00
collection_spheres = [ ]
state = CollectionState ( world )
sphere_candidates = list ( prog_locations )
2017-05-26 07:55:24 +00:00
logging . getLogger ( ' ' ) . debug ( ' Building up collection spheres. ' )
2017-05-16 19:23:47 +00:00
while sphere_candidates :
2017-10-28 22:34:37 +00:00
if not world . keysanity :
state . sweep_for_events ( key_only = True )
2017-06-24 09:11:56 +00:00
2017-05-16 19:23:47 +00:00
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 )
2018-01-01 20:55:13 +00:00
state . collect ( location . item , True , location )
2017-06-17 12:40:37 +00:00
2017-05-16 19:23:47 +00:00
collection_spheres . append ( sphere )
2018-01-01 20:55:13 +00:00
state_cache . append ( state . copy ( ) )
2017-05-26 07:55:24 +00:00
2018-01-01 20:55:13 +00:00
logging . getLogger ( ' ' ) . debug ( ' Calculated sphere %i , containing %i of %i progress items. ' , len ( collection_spheres ) , len ( sphere ) , len ( prog_locations ) )
2017-05-26 07:55:24 +00:00
if not sphere :
2017-12-17 05:25:46 +00:00
logging . getLogger ( ' ' ) . debug ( ' The following items could not be reached: %s ' , [ ' %s at %s ' % ( location . item . name , location . name ) for location in sphere_candidates ] )
2017-06-23 20:15:29 +00:00
if not world . check_beatable_only :
raise RuntimeError ( ' Not all progression items reachable. Something went terribly wrong here. ' )
else :
break
2017-05-26 07:55:24 +00:00
2017-05-16 19:23:47 +00:00
# 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
2018-01-01 20:55:13 +00:00
for num , sphere in reversed ( list ( enumerate ( collection_spheres ) ) ) :
2017-05-16 19:23:47 +00:00
to_delete = [ ]
for location in sphere :
# we remove the item at location and check if game is still beatable
2017-12-17 05:25:46 +00:00
logging . getLogger ( ' ' ) . debug ( ' Checking if %s is required to beat the game. ' , location . item . name )
2017-05-16 19:23:47 +00:00
old_item = location . item
location . item = None
state . remove ( old_item )
2018-01-01 20:55:13 +00:00
if world . can_beat_game ( state_cache [ num ] ) :
2017-05-16 19:23:47 +00:00
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 )
2018-01-06 19:25:49 +00:00
# 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. ' )
2017-05-16 19:23:47 +00:00
2017-06-04 14:15:59 +00:00
# store the required locations for statistical analysis
old_world . required_locations = [ location . name for sphere in collection_spheres for location in sphere ]
2018-01-01 20:55:13 +00:00
def flist_to_iter ( node ) :
while node :
value , node = node
yield value
2018-01-06 21:25:14 +00:00
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 ' ) )
2018-01-01 20:55:13 +00:00
2017-05-16 19:23:47 +00:00
# we can finally output our playthrough
2017-11-19 01:43:37 +00:00
old_world . spoiler . playthrough = OrderedDict ( [ ( str ( i + 1 ) , { str ( location ) : str ( location . item ) for location in sphere } ) for i , sphere in enumerate ( collection_spheres ) ] )