2017-10-15 19:35:45 +00:00
import logging
2020-08-13 22:34:41 +00:00
import typing
2017-10-15 19:35:45 +00:00
2021-01-04 14:14:20 +00:00
from BaseClasses import CollectionState , PlandoItem
from Items import ItemFactory
2021-01-08 14:37:23 +00:00
from Regions import key_drop_data
2019-04-18 09:23:24 +00:00
2018-01-27 21:21:32 +00:00
class FillError ( RuntimeError ) :
pass
2017-11-04 18:23:57 +00:00
2021-01-04 14:14:20 +00:00
2020-04-14 16:59:00 +00:00
def fill_restrictive ( world , base_state : CollectionState , locations , itempool , single_player_placement = False ) :
2017-10-15 19:35:45 +00:00
def sweep_from_pool ( ) :
new_state = base_state . copy ( )
for item in itempool :
new_state . collect ( item , True )
new_state . sweep_for_events ( )
return new_state
2019-12-14 16:47:36 +00:00
unplaced_items = [ ]
2021-01-13 18:40:23 +00:00
placements = [ ]
2019-12-14 16:47:36 +00:00
2019-12-18 19:47:35 +00:00
no_access_checks = { }
reachable_items = { }
2019-12-14 16:47:36 +00:00
for item in itempool :
2019-12-18 19:47:35 +00:00
if world . accessibility [ item . player ] == ' none ' :
no_access_checks . setdefault ( item . player , [ ] ) . append ( item )
else :
reachable_items . setdefault ( item . player , [ ] ) . append ( item )
for player_items in [ no_access_checks , reachable_items ] :
while any ( player_items . values ( ) ) and locations :
items_to_place = [ [ itempool . remove ( items [ - 1 ] ) , items . pop ( ) ] [ - 1 ] for items in player_items . values ( ) if items ]
maximum_exploration_state = sweep_from_pool ( )
has_beaten_game = world . has_beaten_game ( maximum_exploration_state )
for item_to_place in items_to_place :
perform_access_check = True
if world . accessibility [ item_to_place . player ] == ' none ' :
perform_access_check = not world . has_beaten_game ( maximum_exploration_state , item_to_place . player ) if single_player_placement else not has_beaten_game
for location in locations :
if ( not single_player_placement or location . player == item_to_place . player ) \
and location . can_fill ( maximum_exploration_state , item_to_place , perform_access_check ) :
spot_to_fill = location
break
2020-04-10 04:41:32 +00:00
else :
2019-12-18 19:47:35 +00:00
# we filled all reachable spots. Maybe the game can be beaten anyway?
unplaced_items . insert ( 0 , item_to_place )
2020-04-14 16:59:00 +00:00
if world . accessibility [ item_to_place . player ] != ' none ' and world . can_beat_game ( ) :
2020-08-13 22:34:41 +00:00
logging . warning (
f ' Not all items placed. Game beatable anyway. (Could not place { item_to_place } ) ' )
2019-12-18 19:47:35 +00:00
continue
2021-01-02 11:49:43 +00:00
# fill in name of world for item
item_to_place . world = world
2020-08-20 18:13:00 +00:00
raise FillError ( f ' No more spots to place { item_to_place } , locations { locations } are invalid. '
2020-11-22 21:53:02 +00:00
f ' Already placed { len ( placements ) } : { " , " . join ( str ( place ) for place in placements ) } ' )
2019-12-18 19:47:35 +00:00
world . push_item ( spot_to_fill , item_to_place , False )
locations . remove ( spot_to_fill )
2021-01-13 18:40:23 +00:00
placements . append ( spot_to_fill )
2019-12-18 19:47:35 +00:00
spot_to_fill . event = True
2017-10-15 19:35:45 +00:00
2019-12-14 16:47:36 +00:00
itempool . extend ( unplaced_items )
2017-10-15 19:35:45 +00:00
2019-12-16 14:27:20 +00:00
def distribute_items_restrictive ( world , gftower_trash = False , fill_locations = None ) :
2017-10-15 20:34:46 +00:00
# If not passed in, then get a shuffled list of locations to fill in
if not fill_locations :
fill_locations = world . get_unfilled_locations ( )
2020-07-14 05:01:51 +00:00
world . random . shuffle ( fill_locations )
2017-10-15 19:35:45 +00:00
# get items to distribute
2020-07-14 05:01:51 +00:00
world . random . shuffle ( world . itempool )
2020-06-04 01:30:59 +00:00
progitempool = [ ]
localprioitempool = { player : [ ] for player in range ( 1 , world . players + 1 ) }
localrestitempool = { player : [ ] for player in range ( 1 , world . players + 1 ) }
prioitempool = [ ]
restitempool = [ ]
for item in world . itempool :
if item . advancement :
progitempool . append ( item )
elif item . name in world . local_items [ item . player ] :
if item . priority :
localprioitempool [ item . player ] . append ( item )
else :
localrestitempool [ item . player ] . append ( item )
elif item . priority :
prioitempool . append ( item )
else :
restitempool . append ( item )
2017-10-15 19:35:45 +00:00
# fill in gtower locations with trash first
2019-04-18 09:23:24 +00:00
for player in range ( 1 , world . players + 1 ) :
2020-08-17 01:51:55 +00:00
if not gftower_trash or not world . ganonstower_vanilla [ player ] or \
world . logic [ player ] in { ' owglitches ' , " nologic " } :
2020-08-17 01:55:46 +00:00
gtower_trash_count = 0
elif ' triforcehunt ' in world . goal [ player ] and ( ' local ' in world . goal [ player ] or world . players == 1 ) :
gtower_trash_count = world . random . randint ( world . crystals_needed_for_gt [ player ] * 2 ,
world . crystals_needed_for_gt [ player ] * 4 )
2020-08-17 01:51:55 +00:00
else :
2020-08-17 01:55:46 +00:00
gtower_trash_count = world . random . randint ( 0 , world . crystals_needed_for_gt [ player ] * 2 )
if gtower_trash_count :
gtower_locations = [ location for location in fill_locations if
' Ganons Tower ' in location . name and location . player == player ]
world . random . shuffle ( gtower_locations )
trashcnt = 0
localrest = localrestitempool [ player ]
if localrest :
gt_item_pool = restitempool + localrest
world . random . shuffle ( gt_item_pool )
2020-06-04 01:30:59 +00:00
else :
2020-08-17 01:55:46 +00:00
gt_item_pool = restitempool . copy ( )
while gtower_locations and gt_item_pool and trashcnt < gtower_trash_count :
spot_to_fill = gtower_locations . pop ( )
item_to_place = gt_item_pool . pop ( )
if item_to_place in localrest :
localrest . remove ( item_to_place )
else :
restitempool . remove ( item_to_place )
world . push_item ( spot_to_fill , item_to_place , False )
fill_locations . remove ( spot_to_fill )
trashcnt + = 1
2017-10-15 19:35:45 +00:00
2020-07-14 05:01:51 +00:00
world . random . shuffle ( fill_locations )
2017-10-15 19:35:45 +00:00
2019-12-13 21:37:52 +00:00
# Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots
2021-01-02 11:49:43 +00:00
standard_keyshuffle_players = { player for player , mode in world . mode . items ( ) if mode == ' standard ' and
world . keyshuffle [ player ] is True }
if standard_keyshuffle_players :
progitempool . sort (
key = lambda item : 1 if item . name == ' Small Key (Hyrule Castle) ' and
item . player in standard_keyshuffle_players else 0 )
2019-04-18 09:23:24 +00:00
2017-10-15 19:35:45 +00:00
fill_restrictive ( world , world . state , fill_locations , progitempool )
2020-08-25 16:05:16 +00:00
if any ( localprioitempool . values ( ) or
localrestitempool . values ( ) ) : # we need to make sure some fills are limited to certain worlds
2021-01-07 11:43:11 +00:00
local_locations = { player : [ ] for player in world . player_ids }
for location in fill_locations :
local_locations [ location . player ] . append ( location )
for locations in local_locations . values ( ) :
world . random . shuffle ( locations )
2020-07-09 14:16:31 +00:00
for player , items in localprioitempool . items ( ) : # items already shuffled
2021-01-07 11:43:11 +00:00
player_local_locations = local_locations [ player ]
2020-06-04 01:30:59 +00:00
for item_to_place in items :
2021-01-07 11:43:11 +00:00
if not player_local_locations :
logging . warning ( f " Ran out of local locations for player { player } , "
f " cannot place { item_to_place } . " )
break
spot_to_fill = player_local_locations . pop ( )
2020-06-04 01:30:59 +00:00
world . push_item ( spot_to_fill , item_to_place , False )
fill_locations . remove ( spot_to_fill )
2020-07-09 14:16:31 +00:00
for player , items in localrestitempool . items ( ) : # items already shuffled
2021-01-07 11:43:11 +00:00
player_local_locations = local_locations [ player ]
2020-06-04 01:30:59 +00:00
for item_to_place in items :
2021-01-07 11:43:11 +00:00
if not player_local_locations :
logging . warning ( f " Ran out of local locations for player { player } , "
f " cannot place { item_to_place } . " )
break
spot_to_fill = player_local_locations . pop ( )
2020-06-04 01:30:59 +00:00
world . push_item ( spot_to_fill , item_to_place , False )
fill_locations . remove ( spot_to_fill )
2020-07-14 05:01:51 +00:00
world . random . shuffle ( fill_locations )
2020-08-13 22:34:41 +00:00
prioitempool , fill_locations = fast_fill ( world , prioitempool , fill_locations )
2017-10-15 19:35:45 +00:00
2020-08-13 22:34:41 +00:00
restitempool , fill_locations = fast_fill ( world , restitempool , fill_locations )
2020-08-25 16:05:16 +00:00
unplaced = [ item for item in progitempool + prioitempool + restitempool ]
2020-08-01 04:22:59 +00:00
unfilled = [ location . name for location in fill_locations ]
2017-10-15 19:35:45 +00:00
2020-08-01 04:22:59 +00:00
if unplaced or unfilled :
logging . warning ( ' Unplaced items: %s - Unfilled Locations: %s ' , unplaced , unfilled )
2017-10-15 19:35:45 +00:00
2020-08-13 22:34:41 +00:00
def fast_fill ( world , item_pool : typing . List , fill_locations : typing . List ) - > typing . Tuple [ typing . List , typing . List ] :
placing = min ( len ( item_pool ) , len ( fill_locations ) )
for item , location in zip ( item_pool , fill_locations ) :
world . push_item ( location , item , False )
return item_pool [ placing : ] , fill_locations [ placing : ]
2017-10-15 20:34:46 +00:00
2017-11-04 18:23:57 +00:00
2017-10-15 19:35:45 +00:00
def flood_items ( world ) :
# get items to distribute
2020-07-14 05:01:51 +00:00
world . random . shuffle ( world . itempool )
2017-10-15 19:35:45 +00:00
itempool = world . itempool
progress_done = False
# sweep once to pick up preplaced items
world . state . sweep_for_events ( )
# fill world from top of itempool while we can
while not progress_done :
location_list = world . get_unfilled_locations ( )
2020-07-14 05:01:51 +00:00
world . random . shuffle ( location_list )
2017-10-15 19:35:45 +00:00
spot_to_fill = None
for location in location_list :
2018-01-02 05:39:53 +00:00
if location . can_fill ( world . state , itempool [ 0 ] ) :
2017-10-15 19:35:45 +00:00
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 :
2018-01-27 21:21:32 +00:00
raise FillError ( ' No more progress items left to place. ' )
2017-10-15 19:35:45 +00:00
# find item to replace with progress item
location_list = world . get_reachable_locations ( )
2020-07-14 05:01:51 +00:00
world . random . shuffle ( location_list )
2017-10-15 19:35:45 +00:00
for location in location_list :
2019-12-13 21:37:52 +00:00
if location . item is not None and not location . item . advancement and not location . item . priority and not location . item . smallkey and not location . item . bigkey :
2017-10-15 19:35:45 +00:00
# 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
2019-04-18 09:23:24 +00:00
def balance_multiworld_progression ( world ) :
2020-05-18 01:54:29 +00:00
balanceable_players = { player for player in range ( 1 , world . players + 1 ) if world . progression_balancing [ player ] }
if not balanceable_players :
logging . info ( ' Skipping multiworld progression balancing. ' )
else :
2020-07-30 18:17:04 +00:00
logging . info ( f ' Balancing multiworld progression for { len ( balanceable_players ) } Players. ' )
2020-05-18 01:54:29 +00:00
state = CollectionState ( world )
checked_locations = [ ]
unchecked_locations = world . get_locations ( ) . copy ( )
2020-07-14 05:01:51 +00:00
world . random . shuffle ( unchecked_locations )
2020-05-18 01:54:29 +00:00
reachable_locations_count = { player : 0 for player in range ( 1 , world . players + 1 ) }
def get_sphere_locations ( sphere_state , locations ) :
sphere_state . sweep_for_events ( key_only = True , locations = locations )
return [ loc for loc in locations if sphere_state . can_reach ( loc ) ]
while True :
sphere_locations = get_sphere_locations ( state , unchecked_locations )
for location in sphere_locations :
unchecked_locations . remove ( location )
reachable_locations_count [ location . player ] + = 1
if checked_locations :
threshold = max ( reachable_locations_count . values ( ) ) - 20
balancing_players = [ player for player , reachables in reachable_locations_count . items ( ) if
reachables < threshold and player in balanceable_players ]
if balancing_players :
balancing_state = state . copy ( )
balancing_unchecked_locations = unchecked_locations . copy ( )
balancing_reachables = reachable_locations_count . copy ( )
balancing_sphere = sphere_locations . copy ( )
candidate_items = [ ]
while True :
for location in balancing_sphere :
if location . event and (
world . keyshuffle [ location . item . player ] or not location . item . smallkey ) and (
world . bigkeyshuffle [ location . item . player ] or not location . item . bigkey ) :
balancing_state . collect ( location . item , True , location )
if location . item . player in balancing_players and not location . locked :
candidate_items . append ( location )
balancing_sphere = get_sphere_locations ( balancing_state , balancing_unchecked_locations )
for location in balancing_sphere :
balancing_unchecked_locations . remove ( location )
balancing_reachables [ location . player ] + = 1
if world . has_beaten_game ( balancing_state ) or all (
[ reachables > = threshold for reachables in balancing_reachables . values ( ) ] ) :
break
elif not balancing_sphere :
raise RuntimeError ( ' Not all required items reachable. Something went terribly wrong here. ' )
unlocked_locations = [ l for l in unchecked_locations if l not in balancing_unchecked_locations ]
items_to_replace = [ ]
for player in balancing_players :
locations_to_test = [ l for l in unlocked_locations if l . player == player ]
# only replace items that end up in another player's world
items_to_test = [ l for l in candidate_items if l . item . player == player and l . player != player ]
while items_to_test :
testing = items_to_test . pop ( )
reducing_state = state . copy ( )
for location in [ * [ l for l in items_to_replace if l . item . player == player ] , * items_to_test ] :
reducing_state . collect ( location . item , True , location )
reducing_state . sweep_for_events ( locations = locations_to_test )
if world . has_beaten_game ( balancing_state ) :
if not world . has_beaten_game ( reducing_state ) :
items_to_replace . append ( testing )
else :
reduced_sphere = get_sphere_locations ( reducing_state , locations_to_test )
if reachable_locations_count [ player ] + len ( reduced_sphere ) < threshold :
items_to_replace . append ( testing )
replaced_items = False
replacement_locations = [ l for l in checked_locations if not l . event and not l . locked ]
while replacement_locations and items_to_replace :
2019-12-10 18:23:12 +00:00
new_location = replacement_locations . pop ( )
2020-05-18 01:54:29 +00:00
old_location = items_to_replace . pop ( )
while not new_location . can_fill ( state , old_location . item , False ) or (
new_location . item and not old_location . can_fill ( state , new_location . item , False ) ) :
replacement_locations . insert ( 0 , new_location )
new_location = replacement_locations . pop ( )
new_location . item , old_location . item = old_location . item , new_location . item
new_location . event , old_location . event = True , False
logging . debug ( f " Progression balancing moved { new_location . item } to { new_location } , "
f " displacing { old_location . item } in { old_location } " )
state . collect ( new_location . item , True , new_location )
replaced_items = True
if replaced_items :
for location in get_sphere_locations ( state , [ l for l in unlocked_locations if
l . player in balancing_players ] ) :
unchecked_locations . remove ( location )
reachable_locations_count [ location . player ] + = 1
sphere_locations . append ( location )
for location in sphere_locations :
if location . event and ( world . keyshuffle [ location . item . player ] or not location . item . smallkey ) and (
world . bigkeyshuffle [ location . item . player ] or not location . item . bigkey ) :
state . collect ( location . item , True , location )
checked_locations . extend ( sphere_locations )
if world . has_beaten_game ( state ) :
break
elif not sphere_locations :
raise RuntimeError ( ' Not all required items reachable. Something went terribly wrong here. ' )
2021-01-04 14:14:20 +00:00
def distribute_planned ( world ) :
world_name_lookup = { world . player_names [ player_id ] [ 0 ] : player_id for player_id in world . player_ids }
for player in world . player_ids :
placement : PlandoItem
for placement in world . plando_items [ player ] :
2021-01-08 14:37:23 +00:00
if placement . location in key_drop_data :
placement . warn ( f " Can ' t place ' { placement . item } ' at ' { placement . location } ' , as key drop shuffle locations are not supported yet. " )
continue
2021-01-04 14:14:20 +00:00
item = ItemFactory ( placement . item , player )
target_world : int = placement . world
if target_world is False or world . players == 1 :
target_world = player # in own world
elif target_world is True : # in any other world
unfilled = list ( location for location in world . get_unfilled_locations_for_players (
placement . location ,
set ( world . player_ids ) - { player } ) if location . item_rule ( item )
)
if not unfilled :
2021-01-05 17:53:52 +00:00
placement . failed ( f " Could not find a world with an unfilled location { placement . location } " , FillError )
continue
2021-01-04 14:14:20 +00:00
target_world = world . random . choice ( unfilled ) . player
elif target_world is None : # any random world
unfilled = list ( location for location in world . get_unfilled_locations_for_players (
placement . location ,
set ( world . player_ids ) ) if location . item_rule ( item )
)
if not unfilled :
2021-01-05 17:53:52 +00:00
placement . failed ( f " Could not find a world with an unfilled location { placement . location } " , FillError )
continue
2021-01-04 14:14:20 +00:00
target_world = world . random . choice ( unfilled ) . player
elif type ( target_world ) == int : # target world by player id
2021-01-05 17:53:52 +00:00
if target_world not in range ( 1 , world . players + 1 ) :
placement . failed ( f " Cannot place item in world { target_world } as it is not in range of (1, { world . players } ) " , ValueError )
continue
2021-01-04 14:14:20 +00:00
else : # find world by name
2021-01-04 21:50:27 +00:00
if target_world not in world_name_lookup :
2021-01-05 17:53:52 +00:00
placement . failed ( f " Cannot place item to { target_world } ' s world as that world does not exist. " , ValueError )
continue
2021-01-04 14:14:20 +00:00
target_world = world_name_lookup [ target_world ]
location = world . get_location ( placement . location , target_world )
if location . item :
2021-01-05 17:53:52 +00:00
placement . failed ( f " Cannot place item into already filled location { location } . " )
continue
2021-01-04 14:14:20 +00:00
if location . can_fill ( world . state , item , False ) :
world . push_item ( location , item , collect = False )
location . event = True # flag location to be checked during fill
location . locked = True
logging . debug ( f " Plando placed { item } at { location } " )
else :
2021-01-05 17:53:52 +00:00
placement . failed ( f " Can ' t place { item } at { location } due to fill condition not met. " )
continue
2021-01-04 21:50:27 +00:00
if placement . from_pool : # Should happen AFTER the item is placed, in case it was allowed to skip failed placement.
try :
world . itempool . remove ( item )
except ValueError :
2021-01-05 17:53:52 +00:00
placement . warn ( f " Could not remove { item } from pool as it ' s already missing from it. " )