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
2019-05-29 23:10:16 +00:00
import os
2019-12-09 18:27:56 +00:00
import pickle
2017-12-17 05:25:46 +00:00
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
2019-07-27 13:13:13 +00:00
from InvertedRegions import create_inverted_regions , mark_dark_world_regions
from EntranceShuffle import link_entrances , link_inverted_entrances
2019-12-15 15:16:39 +00:00
from Rom import patch_rom , get_race_rom_patches , get_enemizer_patch , apply_rom_settings , 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
2019-04-18 09:23:24 +00:00
from Fill import distribute_items_cutoff , distribute_items_staleness , distribute_items_restrictive , flood_items , balance_multiworld_progression
2019-04-18 21:11:11 +00:00
from ItemList import generate_itempool , difficulties , fill_prizes
2019-12-09 18:27:56 +00:00
from Utils import output_path , parse_names_string
2017-05-15 18:28:04 +00:00
2019-08-24 19:38:22 +00:00
__version__ = ' 0.6.3-pre '
2019-04-29 21:11:23 +00:00
2017-05-25 13:58:35 +00:00
def main ( args , seed = None ) :
2019-10-16 06:20:28 +00:00
start = time . process_time ( )
2017-05-15 18:28:04 +00:00
# initialize the world
2019-12-13 21:37:52 +00:00
world = World ( args . multi , args . shuffle , args . logic , args . mode , args . swords , args . difficulty , args . item_functionality , args . timer , args . progressive , args . goal , args . algorithm , args . accessibility , args . shuffleganon , args . quickswap , args . fastmenu , args . disablemusic , 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 )
2019-12-13 21:37:52 +00:00
world . mapshuffle = args . mapshuffle
world . compassshuffle = args . compassshuffle
world . keyshuffle = args . keyshuffle
world . bigkeyshuffle = args . bigkeyshuffle
mcsb_name = ' '
if all ( [ world . mapshuffle , world . compassshuffle , world . keyshuffle , world . bigkeyshuffle ] ) :
mcsb_name = ' -keysanity '
elif [ world . mapshuffle , world . compassshuffle , world . keyshuffle , world . bigkeyshuffle ] . count ( True ) == 1 :
mcsb_name = ' -mapshuffle ' if world . mapshuffle else ' -compassshuffle ' if world . compassshuffle else ' -keyshuffle ' if world . keyshuffle else ' -bigkeyshuffle '
elif any ( [ world . mapshuffle , world . compassshuffle , world . keyshuffle , world . bigkeyshuffle ] ) :
mcsb_name = ' - %s %s %s %s shuffle ' % ( ' M ' if world . mapshuffle else ' ' , ' C ' if world . compassshuffle else ' ' , ' S ' if world . keyshuffle else ' ' , ' B ' if world . bigkeyshuffle else ' ' )
2019-08-11 12:55:38 +00:00
world . crystals_needed_for_ganon = random . randint ( 0 , 7 ) if args . crystals_ganon == ' random ' else int ( args . crystals_ganon )
world . crystals_needed_for_gt = random . randint ( 0 , 7 ) if args . crystals_gt == ' random ' else int ( args . crystals_gt )
2019-12-12 08:20:32 +00:00
world . open_pyramid = args . openpyramid
2019-08-11 12:55:38 +00:00
2019-04-18 09:23:24 +00:00
world . rom_seeds = { player : random . randint ( 0 , 999999999 ) for player in range ( 1 , world . players + 1 ) }
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
2019-12-16 15:54:46 +00:00
for player in range ( 1 , world . players + 1 ) :
2019-12-16 16:46:21 +00:00
world . difficulty_requirements [ player ] = difficulties [ world . difficulty [ player ] ]
2019-12-16 15:54:46 +00:00
if world . mode [ player ] == ' standard ' and ( args . shuffleenemies != ' none ' or args . enemy_health not in [ ' default ' , ' easy ' ] ) :
world . escape_assist [ player ] . append ( [ ' bombs ' ] ) # enemized escape assumes infinite bombs available and will likely be unbeatable without it
if world . mode [ player ] != ' inverted ' :
2019-07-27 13:13:13 +00:00
create_regions ( world , player )
2019-12-16 15:54:46 +00:00
else :
2019-07-27 13:13:13 +00:00
create_inverted_regions ( world , player )
2019-12-16 15:54:46 +00:00
create_dungeons ( world , player )
2017-05-15 18:28:04 +00:00
2017-05-20 12:07:40 +00:00
logger . info ( ' Shuffling the World about. ' )
2019-12-16 15:54:46 +00:00
for player in range ( 1 , world . players + 1 ) :
if world . mode [ player ] != ' inverted ' :
2019-07-27 13:13:13 +00:00
link_entrances ( world , player )
2019-12-16 15:54:46 +00:00
mark_light_world_regions ( world , player )
else :
2019-07-27 13:13:13 +00:00
link_inverted_entrances ( world , player )
2019-12-16 15:54:46 +00:00
mark_dark_world_regions ( world , player )
2017-05-20 12:07:40 +00:00
2019-04-18 21:11:11 +00:00
logger . info ( ' Generating Item Pool. ' )
2019-04-18 09:23:24 +00:00
for player in range ( 1 , world . players + 1 ) :
generate_itempool ( world , player )
2019-04-18 21:11:11 +00:00
2017-05-20 12:07:40 +00:00
logger . info ( ' Calculating Access Rules. ' )
2019-04-18 09:23:24 +00:00
for player in range ( 1 , world . players + 1 ) :
set_rules ( world , player )
2017-05-20 12:07:40 +00:00
2019-04-18 21:11:11 +00:00
logger . info ( ' Placing Dungeon Prizes. ' )
2017-08-05 15:52:18 +00:00
2019-04-18 21:11:11 +00:00
fill_prizes ( world )
2017-08-05 15:52:18 +00:00
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
2019-12-13 21:37:52 +00:00
if args . algorithm in [ ' balanced ' , ' vt26 ' ] or args . mapshuffle or args . compassshuffle or args . keyshuffle or args . bigkeyshuffle :
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 ' :
2019-12-16 14:27:20 +00:00
distribute_items_restrictive ( world , False )
2017-10-15 17:52:42 +00:00
elif args . algorithm == ' vt26 ' :
2018-03-02 02:36:30 +00:00
2019-12-16 14:27:20 +00:00
distribute_items_restrictive ( world , True , shuffled_locations )
2017-11-12 00:05:06 +00:00
elif args . algorithm == ' balanced ' :
2019-12-16 14:27:20 +00:00
distribute_items_restrictive ( world , True )
2017-06-03 19:28:02 +00:00
2019-04-18 09:23:24 +00:00
if world . players > 1 :
logger . info ( ' Balancing multiworld progression. ' )
balance_multiworld_progression ( 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-12-09 18:27:56 +00:00
player_names = parse_names_string ( args . names )
2019-12-16 12:09:43 +00:00
outfilebase = ' ER_ %s ' % ( args . outputname if args . outputname else world . seed )
2017-05-20 12:07:40 +00:00
2019-12-16 00:35:10 +00:00
use_enemizer = args . enemizercli and ( args . shufflebosses != ' none ' or args . shuffleenemies != ' none ' or args . enemy_health != ' default ' or args . enemy_health != ' default ' or args . enemy_damage or args . shufflepalette or args . shufflepots )
2019-05-29 23:10:16 +00:00
2019-04-18 09:23:24 +00:00
jsonout = { }
2017-06-04 11:09:47 +00:00
if not args . suppress_rom :
2019-12-09 18:27:56 +00:00
from MultiServer import MultiWorld
multidata = MultiWorld ( )
multidata . players = world . players
for player in range ( 1 , world . players + 1 ) :
2019-04-18 09:23:24 +00:00
2019-05-29 23:10:16 +00:00
local_rom = None
2019-04-18 09:23:24 +00:00
if args . jsonout :
rom = JsonRom ( )
else :
2019-05-29 23:10:16 +00:00
if use_enemizer :
local_rom = LocalRom ( args . rom )
rom = JsonRom ( )
else :
rom = LocalRom ( args . rom )
2019-12-15 09:54:49 +00:00
patch_rom ( world , player , rom , use_enemizer )
2019-05-29 23:10:16 +00:00
enemizer_patch = [ ]
if use_enemizer :
enemizer_patch = get_enemizer_patch ( world , player , rom , args . rom , args . enemizercli , args . shuffleenemies , args . enemy_health , args . enemy_damage , args . shufflepalette , args . shufflepots )
2019-04-18 09:23:24 +00:00
2019-12-09 18:27:56 +00:00
multidata . rom_names [ player ] = list ( rom . name )
for location in world . get_filled_locations ( player ) :
if type ( location . address ) is int :
multidata . locations [ ( location . address , player ) ] = ( location . item . code , location . item . player )
2019-04-18 09:23:24 +00:00
if args . jsonout :
2019-12-09 18:27:56 +00:00
jsonout [ f ' patch { player } ' ] = rom . patches
2019-05-29 23:10:16 +00:00
if use_enemizer :
2019-12-09 18:27:56 +00:00
jsonout [ f ' enemizer { player } ' ] = enemizer_patch
2019-12-15 15:16:39 +00:00
if args . race :
jsonout [ f ' race { player } ' ] = get_race_rom_patches ( rom )
2019-04-18 09:23:24 +00:00
else :
2019-05-29 23:10:16 +00:00
if use_enemizer :
local_rom . patch_enemizer ( rom . patches , os . path . join ( os . path . dirname ( args . enemizercli ) , " enemizerBasePatch.json " ) , enemizer_patch )
rom = local_rom
2019-12-15 15:16:39 +00:00
if args . race :
for addr , values in get_race_rom_patches ( rom ) . items ( ) :
rom . write_bytes ( int ( addr ) , values )
2019-12-09 18:27:56 +00:00
apply_rom_settings ( rom , args . heartbeep , args . heartcolor , world . quickswap , world . fastmenu , world . disable_music , sprite , player_names )
2019-12-16 12:09:43 +00:00
outfilesuffix = ( ' %s %s _ %s _ %s - %s - %s - %s %s _ %s - %s %s %s %s %s ' % ( f ' _P { player } ' if world . players > 1 else ' ' ,
f ' _ { player_names [ player ] } ' if player in player_names else ' ' ,
2019-12-16 16:46:21 +00:00
world . logic [ player ] , world . difficulty [ player ] , world . difficulty_adjustments [ player ] ,
2019-12-16 14:27:20 +00:00
world . mode [ player ] , world . goal [ player ] ,
2019-12-16 12:09:43 +00:00
" " if world . timer in [ ' none ' , ' display ' ] else " - " + world . timer ,
world . shuffle , world . algorithm , mcsb_name ,
" -retro " if world . retro else " " ,
" -prog_ " + world . progressive if world . progressive in [ ' off ' , ' random ' ] else " " ,
" -nohints " if not world . hints else " " ) ) if not args . outputname else ' '
rom . write_to_file ( output_path ( f ' { outfilebase } { outfilesuffix } .sfc ' ) )
2019-12-09 18:27:56 +00:00
with open ( output_path ( ' %s _multidata ' % outfilebase ) , ' wb ' ) as f :
pickle . dump ( multidata , f , pickle . HIGHEST_PROTOCOL )
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
2019-04-18 09:23:24 +00:00
if not args . skip_playthrough :
logger . info ( ' Calculating playthrough. ' )
create_playthrough ( world )
if args . jsonout :
print ( json . dumps ( { * * jsonout , ' spoiler ' : world . spoiler . to_json ( ) } ) )
elif args . create_spoiler and not args . skip_playthrough :
world . spoiler . to_file ( output_path ( ' %s _Spoiler.txt ' % outfilebase ) )
2017-05-20 12:07:40 +00:00
logger . info ( ' Done. Enjoy. ' )
2019-10-16 06:20:28 +00:00
logger . debug ( ' Total Time: %s ' , time . process_time ( ) - start )
2017-05-15 18:28:04 +00:00
return world
2017-05-16 19:23:47 +00:00
def copy_world ( world ) :
# ToDo: Not good yet
2019-12-13 21:37:52 +00:00
ret = World ( world . players , world . shuffle , world . logic , world . mode , world . swords , world . difficulty , world . difficulty_adjustments , world . timer , world . progressive , world . goal , world . algorithm , world . accessibility , world . shuffle_ganon , world . quickswap , world . fastmenu , world . disable_music , world . retro , world . custom , world . customitemarray , world . boss_shuffle , world . hints )
2019-04-18 09:23:24 +00:00
ret . required_medallions = world . required_medallions . copy ( )
ret . swamp_patch_required = world . swamp_patch_required . copy ( )
ret . ganon_at_pyramid = world . ganon_at_pyramid . copy ( )
ret . powder_patch_required = world . powder_patch_required . copy ( )
ret . ganonstower_vanilla = world . ganonstower_vanilla . copy ( )
2017-06-04 11:10:22 +00:00
ret . treasure_hunt_count = world . treasure_hunt_count
ret . treasure_hunt_icon = world . treasure_hunt_icon
2019-12-16 15:54:46 +00:00
ret . sewer_light_cone = world . sewer_light_cone . copy ( )
2017-06-04 11:10:22 +00:00
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
2019-04-18 21:11:11 +00:00
ret . can_access_trock_front = world . can_access_trock_front
ret . can_access_trock_big_chest = world . can_access_trock_big_chest
ret . can_access_trock_middle = world . can_access_trock_middle
2018-01-02 05:39:53 +00:00
ret . can_take_damage = world . can_take_damage
2019-12-16 16:46:21 +00:00
ret . difficulty_requirements = world . difficulty_requirements . copy ( )
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
2019-12-13 21:37:52 +00:00
ret . mapshuffle = world . mapshuffle
ret . compassshuffle = world . compassshuffle
ret . keyshuffle = world . keyshuffle
ret . bigkeyshuffle = world . bigkeyshuffle
2019-08-11 12:55:38 +00:00
ret . crystals_needed_for_ganon = world . crystals_needed_for_ganon
ret . crystals_needed_for_gt = world . crystals_needed_for_gt
2019-04-18 09:23:24 +00:00
2019-12-16 15:54:46 +00:00
for player in range ( 1 , world . players + 1 ) :
if world . mode [ player ] != ' inverted ' :
2019-07-27 13:13:13 +00:00
create_regions ( ret , player )
2019-12-16 15:54:46 +00:00
else :
2019-07-27 13:13:13 +00:00
create_inverted_regions ( ret , player )
2019-12-16 15:54:46 +00:00
create_dungeons ( ret , player )
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 ( ) :
2019-04-18 09:23:24 +00:00
ret . get_dungeon ( dungeon . name , dungeon . player ) . bosses [ level ] = boss
2018-09-26 17:12:20 +00:00
2018-02-17 23:38:54 +00:00
for shop in world . shops :
2019-04-18 09:23:24 +00:00
copied_shop = ret . get_region ( shop . region . name , shop . region . player ) . shop
2018-02-17 23:38:54 +00:00
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 :
2019-04-18 09:23:24 +00:00
copied_region = ret . get_region ( region . name , region . player )
2017-12-13 14:51:53 +00:00
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 :
2019-04-18 09:23:24 +00:00
ret . get_entrance ( entrance . name , entrance . player ) . 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 :
2019-04-18 09:23:24 +00:00
item = Item ( location . item . name , location . item . advancement , location . item . priority , location . item . type , player = location . item . player )
ret . get_location ( location . name , location . player ) . item = item
item . location = ret . get_location ( location . name , location . player )
2017-06-17 12:40:37 +00:00
if location . event :
2019-04-18 09:23:24 +00:00
ret . get_location ( location . name , location . player ) . event = True
2019-08-04 16:32:35 +00:00
if location . locked :
ret . get_location ( location . name , location . player ) . locked = 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 :
2019-04-18 09:23:24 +00:00
ret . itempool . append ( Item ( item . name , item . advancement , item . priority , item . type , player = item . player ) )
2017-05-16 19:23:47 +00:00
# copy progress items in state
2019-07-13 22:17:16 +00:00
ret . state . prog_items = world . state . prog_items . copy ( )
2019-08-10 19:30:14 +00:00
ret . precollected_items = world . precollected_items . copy ( )
2019-07-11 04:18:30 +00:00
ret . state . stale = { player : True for player in range ( 1 , world . players + 1 ) }
2017-05-16 19:23:47 +00:00
2019-04-18 09:23:24 +00:00
for player in range ( 1 , world . players + 1 ) :
set_rules ( ret , player )
2017-07-17 20:29:32 +00:00
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-04-18 09:23:24 +00:00
new_reg = Region ( region . name , region . type , region . hint_text , region . player )
2018-03-23 03:18:40 +00:00
ret . regions . append ( new_reg )
2019-12-14 18:19:08 +00:00
ret . initialize_regions ( [ new_reg ] )
2018-03-23 03:18:40 +00:00
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 :
2019-04-18 09:23:24 +00:00
new_reg = ret . get_region ( location . parent_region . name , location . parent_region . player )
2019-08-17 19:11:25 +00:00
new_loc = Location ( location . player , location . name , location . address , location . crystal , location . hint_text , new_reg )
2019-08-29 01:12:44 +00:00
# todo: this is potentially dangerous. later refactor so we
# can apply dynamic region rules on top of copied world like other rules
new_loc . access_rule = location . access_rule
new_loc . always_allow = location . always_allow
new_loc . item_rule = location . item_rule
2018-03-23 03:18:40 +00:00
new_reg . locations . append ( new_loc )
2019-08-17 19:11:25 +00:00
ret . clear_location_cache ( )
2018-03-23 03:18:40 +00:00
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-23 20:15:29 +00:00
# if we only check for beatable, we can do this sanity check first before writing down spheres
2019-08-04 21:40:13 +00:00
if world . accessibility == ' none ' and not world . can_beat_game ( ) :
2017-06-23 20:15:29 +00:00
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 :
2019-12-13 21:37:52 +00:00
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 :
2019-04-18 09:23:24 +00:00
logging . getLogger ( ' ' ) . debug ( ' The following items could not be reached: %s ' , [ ' %s (Player %d ) at %s (Player %d ) ' % ( location . item . name , location . item . player , location . name , location . player ) for location in sphere_candidates ] )
2019-08-04 21:40:13 +00:00
if not world . accessibility == ' none ' :
2017-06-23 20:15:29 +00:00
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
2019-04-18 09:23:24 +00:00
logging . getLogger ( ' ' ) . debug ( ' Checking if %s (Player %d ) is required to beat the game. ' , location . item . name , location . item . player )
2017-05-16 19:23:47 +00:00
old_item = location . item
location . item = None
state . remove ( old_item )
2019-07-11 04:12:09 +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 :
2019-12-13 21:37:52 +00:00
state . sweep_for_events ( key_only = True )
2018-01-06 19:25:49 +00:00
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
2019-04-18 09:23:24 +00:00
old_world . required_locations = [ ( location . name , location . player ) for sphere in collection_spheres for location in sphere ]
2017-06-04 14:15:59 +00:00
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 )
2019-04-18 09:23:24 +00:00
old_world . spoiler . paths = dict ( )
for player in range ( 1 , world . players + 1 ) :
old_world . spoiler . paths . update ( { str ( location ) : get_path ( state , location . parent_region ) for sphere in collection_spheres for location in sphere if location . player == player } )
for _ , path in dict ( old_world . spoiler . paths ) . items ( ) :
if any ( exit == ' Pyramid Fairy ' for ( _ , exit ) in path ) :
2019-12-16 15:54:46 +00:00
if world . mode [ player ] != ' inverted ' :
2019-07-27 13:13:13 +00:00
old_world . spoiler . paths [ str ( world . get_region ( ' Big Bomb Shop ' , player ) ) ] = get_path ( state , world . get_region ( ' Big Bomb Shop ' , player ) )
else :
old_world . spoiler . paths [ str ( world . get_region ( ' Inverted Big Bomb Shop ' , player ) ) ] = get_path ( state , world . get_region ( ' Inverted Big Bomb Shop ' , player ) )
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 ) ] )