2017-10-15 19:35:45 +00:00
import logging
2020-08-13 22:34:41 +00:00
import typing
2021-02-05 07:07:12 +00:00
import collections
import itertools
2017-10-15 19:35:45 +00:00
2021-07-21 16:08:15 +00:00
from BaseClasses import CollectionState , Location , MultiWorld
2021-01-17 05:54:38 +00:00
from worlds . alttp . Items import ItemFactory
from worlds . alttp . Regions import key_drop_data
2021-07-21 16:08:15 +00:00
from worlds . generic import PlandoItem
2021-08-10 07:03:44 +00:00
from worlds . AutoWorld import call_all
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
2021-06-13 05:57:34 +00:00
def fill_restrictive ( world : MultiWorld , base_state : CollectionState , locations , itempool , single_player_placement = False ,
2021-01-10 14:50:18 +00:00
lock = 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
reachable_items = { }
2019-12-14 16:47:36 +00:00
for item in itempool :
2021-03-18 16:27:31 +00:00
reachable_items . setdefault ( item . player , [ ] ) . append ( item )
2019-12-18 19:47:35 +00:00
2021-03-18 16:27:31 +00:00
while any ( reachable_items . values ( ) ) and locations :
items_to_place = [ items . pop ( ) for items in reachable_items . values ( ) if items ] # grab one item per player
for item in items_to_place :
itempool . remove ( item )
maximum_exploration_state = sweep_from_pool ( )
has_beaten_game = world . has_beaten_game ( maximum_exploration_state )
2019-12-18 19:47:35 +00:00
2021-03-18 16:27:31 +00:00
for item_to_place in items_to_place :
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
else :
2019-12-18 19:47:35 +00:00
perform_access_check = True
2021-03-18 16:27:31 +00:00
for i , location in enumerate ( 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 = locations . pop ( i ) # poping by index is faster than removing by content,
# skipping a scan for the element
break
else :
# we filled all reachable spots. Maybe the game can be beaten anyway?
unplaced_items . append ( item_to_place )
if world . accessibility [ item_to_place . player ] != ' none ' and world . can_beat_game ( ) :
logging . warning (
f ' Not all items placed. Game beatable anyway. (Could not place { item_to_place } ) ' )
continue
raise FillError ( f ' No more spots to place { item_to_place } , locations { locations } are invalid. '
f ' Already placed { len ( placements ) } : { " , " . join ( str ( place ) for place in placements ) } ' )
world . push_item ( spot_to_fill , item_to_place , False )
spot_to_fill . locked = lock
placements . append ( spot_to_fill )
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
2021-03-04 07:10:30 +00:00
2021-08-10 07:03:44 +00:00
def distribute_items_restrictive ( world : MultiWorld , 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 = [ ]
2021-07-15 21:52:30 +00:00
nonexcludeditempool = [ ]
2020-06-04 01:30:59 +00:00
localrestitempool = { player : [ ] for player in range ( 1 , world . players + 1 ) }
restitempool = [ ]
for item in world . itempool :
2021-07-15 21:52:30 +00:00
if item . advancement :
2020-06-04 01:30:59 +00:00
progitempool . append ( item )
2021-07-23 13:55:44 +00:00
elif item . never_exclude : # this only gets nonprogression items which should not appear in excluded locations
2021-07-15 21:52:30 +00:00
nonexcludeditempool . append ( item )
2020-06-04 01:30:59 +00:00
elif item . name in world . local_items [ item . player ] :
2021-01-30 08:57:25 +00:00
localrestitempool [ item . player ] . append ( item )
2020-06-04 01:30:59 +00:00
else :
restitempool . append ( item )
2017-10-15 19:35:45 +00:00
2021-02-27 15:27:35 +00:00
world . random . shuffle ( fill_locations )
2021-08-10 07:03:44 +00:00
call_all ( world , " fill_hook " , progitempool , nonexcludeditempool , localrestitempool , restitempool , fill_locations )
2017-10-15 19:35:45 +00:00
fill_restrictive ( world , world . state , fill_locations , progitempool )
2021-07-15 21:52:30 +00:00
if nonexcludeditempool :
world . random . shuffle ( fill_locations )
2021-07-23 13:55:44 +00:00
fill_restrictive ( world , world . state , fill_locations , nonexcludeditempool ) # needs logical fill to not conflict with local items
2021-07-15 21:52:30 +00:00
2021-01-30 08:57:25 +00:00
if any ( 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 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 )
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 )
2021-08-24 07:52:12 +00:00
unplaced = progitempool + 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 :
2021-08-24 07:52:12 +00:00
logging . warning ( f ' Unplaced items( { len ( unplaced ) } ): { unplaced } - Unfilled Locations( { len ( unfilled ) } ): { unfilled } ' )
2017-10-15 19:35:45 +00:00
2021-06-13 05:57:34 +00:00
def fast_fill ( world : MultiWorld , item_pool : typing . List , fill_locations : typing . List ) - > typing . Tuple [ typing . List , typing . List ] :
2020-08-13 22:34:41 +00:00
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
2021-06-13 05:57:34 +00:00
def flood_items ( world : MultiWorld ) :
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 )
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 :
2021-08-10 07:47:28 +00:00
if location . item is not None and not location . item . advancement :
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
2020-12-23 17:28:42 +00:00
2021-06-13 05:57:34 +00:00
def balance_multiworld_progression ( world : MultiWorld ) :
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 )
2021-03-04 07:10:30 +00:00
checked_locations = set ( )
unchecked_locations = set ( world . get_locations ( ) )
2020-05-18 01:54:29 +00:00
2021-02-05 07:07:12 +00:00
reachable_locations_count = { player : 0 for player in world . player_ids }
2021-01-17 21:08:28 +00:00
2020-05-18 01:54:29 +00:00
def get_sphere_locations ( sphere_state , locations ) :
sphere_state . sweep_for_events ( key_only = True , locations = locations )
2021-03-04 07:10:30 +00:00
return { loc for loc in locations if sphere_state . can_reach ( loc ) }
2020-05-18 01:54:29 +00:00
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
2021-03-04 07:10:30 +00:00
balancing_players = { player for player , reachables in reachable_locations_count . items ( ) if
reachables < threshold and player in balanceable_players }
2020-05-18 01:54:29 +00:00
if balancing_players :
balancing_state = state . copy ( )
balancing_unchecked_locations = unchecked_locations . copy ( )
balancing_reachables = reachable_locations_count . copy ( )
balancing_sphere = sphere_locations . copy ( )
2021-03-04 07:10:30 +00:00
candidate_items = collections . defaultdict ( set )
2020-05-18 01:54:29 +00:00
while True :
for location in balancing_sphere :
2021-02-05 07:07:12 +00:00
if location . event :
2020-05-18 01:54:29 +00:00
balancing_state . collect ( location . item , True , location )
2021-02-05 07:07:12 +00:00
player = location . item . player
# only replace items that end up in another player's world
if not location . locked and player in balancing_players and location . player != player :
2021-03-04 07:10:30 +00:00
candidate_items [ player ] . add ( location )
2020-05-18 01:54:29 +00:00
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 (
2021-02-05 07:07:12 +00:00
reachables > = threshold for reachables in balancing_reachables . values ( ) ) :
2020-05-18 01:54:29 +00:00
break
elif not balancing_sphere :
raise RuntimeError ( ' Not all required items reachable. Something went terribly wrong here. ' )
2021-03-04 07:10:30 +00:00
unlocked_locations = collections . defaultdict ( set )
2021-02-05 07:07:12 +00:00
for l in unchecked_locations :
if l not in balancing_unchecked_locations :
2021-03-04 07:10:30 +00:00
unlocked_locations [ l . player ] . add ( l )
2020-05-18 01:54:29 +00:00
items_to_replace = [ ]
for player in balancing_players :
2021-02-05 07:07:12 +00:00
locations_to_test = unlocked_locations [ player ]
items_to_test = candidate_items [ player ]
2020-05-18 01:54:29 +00:00
while items_to_test :
testing = items_to_test . pop ( )
reducing_state = state . copy ( )
2021-02-05 07:07:12 +00:00
for location in itertools . chain ( ( l for l in items_to_replace if l . item . player == player ) ,
items_to_test ) :
2020-05-18 01:54:29 +00:00
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
2021-03-04 07:10:30 +00:00
# sort then shuffle to maintain deterministic behaviour,
# while allowing use of set for better algorithm growth behaviour elsewhere
replacement_locations = sorted ( l for l in checked_locations if not l . event and not l . locked )
world . random . shuffle ( replacement_locations )
items_to_replace . sort ( )
world . random . shuffle ( items_to_replace )
2020-05-18 01:54:29 +00:00
2021-03-04 07:10:30 +00:00
while replacement_locations and items_to_replace :
old_location = items_to_replace . pop ( )
for new_location in replacement_locations :
if new_location . can_fill ( state , old_location . item , False ) and \
old_location . can_fill ( state , new_location . item , False ) :
replacement_locations . remove ( new_location )
swap_location_item ( old_location , new_location )
logging . debug ( f " Progression balancing moved { new_location . item } to { new_location } , "
f " displacing { old_location . item } into { old_location } " )
state . collect ( new_location . item , True , new_location )
replaced_items = True
break
else :
logging . warning ( f " Could not Progression Balance { old_location . item } " )
2021-02-05 07:07:12 +00:00
2020-05-18 01:54:29 +00:00
if replaced_items :
2021-03-04 07:10:30 +00:00
unlocked = { fresh for player in balancing_players for fresh in unlocked_locations [ player ] }
2021-02-05 07:07:12 +00:00
for location in get_sphere_locations ( state , unlocked ) :
2020-05-18 01:54:29 +00:00
unchecked_locations . remove ( location )
reachable_locations_count [ location . player ] + = 1
2021-03-04 07:10:30 +00:00
sphere_locations . add ( location )
2020-05-18 01:54:29 +00:00
for location in sphere_locations :
2021-02-05 07:07:12 +00:00
if location . event :
2020-05-18 01:54:29 +00:00
state . collect ( location . item , True , location )
2021-03-04 07:10:30 +00:00
checked_locations | = sphere_locations
2020-05-18 01:54:29 +00:00
if world . has_beaten_game ( state ) :
break
elif not sphere_locations :
2021-07-23 23:42:00 +00:00
logging . warning ( " Progression Balancing ran out of paths. " )
break
2021-01-04 14:14:20 +00:00
2021-01-11 12:35:48 +00:00
def swap_location_item ( location_1 : Location , location_2 : Location , check_locked = True ) :
2021-02-05 07:07:12 +00:00
""" Swaps Items of locations. Does NOT swap flags like shop_slot or locked, but does swap event """
2021-01-11 12:35:48 +00:00
if check_locked :
if location_1 . locked :
logging . warning ( f " Swapping { location_1 } , which is marked as locked. " )
if location_2 . locked :
logging . warning ( f " Swapping { location_2 } , which is marked as locked. " )
location_2 . item , location_1 . item = location_1 . item , location_2 . item
location_1 . item . location = location_1
location_2 . item . location = location_2
2021-02-05 07:07:12 +00:00
location_1 . event , location_2 . event = location_2 . event , location_1 . event
2021-01-11 12:35:48 +00:00
2021-06-13 05:57:34 +00:00
def distribute_planned ( world : MultiWorld ) :
2021-02-20 01:30:55 +00:00
world_name_lookup = world . world_name_lookup
2021-01-04 14:14:20 +00:00
for player in world . player_ids :
2021-06-14 21:42:13 +00:00
try :
placement : PlandoItem
for placement in world . plando_items [ player ] :
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. " )
2021-01-05 17:53:52 +00:00
continue
2021-06-14 21:42:13 +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 :
placement . failed ( f " Could not find a world with an unfilled location { placement . location } " ,
FillError )
continue
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 :
placement . failed ( f " Could not find a world with an unfilled location { placement . location } " ,
FillError )
continue
target_world = world . random . choice ( unfilled ) . player
elif type ( target_world ) == int : # target world by player id
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
else : # find world by name
if target_world not in world_name_lookup :
placement . failed ( f " Cannot place item to { target_world } ' s world as that world does not exist. " ,
ValueError )
continue
target_world = world_name_lookup [ target_world ]
location = world . get_location ( placement . location , target_world )
if location . item :
placement . failed ( f " Cannot place item into already filled location { location } . " )
2021-01-05 17:53:52 +00:00
continue
2021-01-04 14:14:20 +00:00
2021-06-14 21:42:13 +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 :
placement . failed ( f " Can ' t place { item } at { location } due to fill condition not met. " )
2021-01-05 17:53:52 +00:00
continue
2021-06-14 21:42:13 +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 :
placement . warn ( f " Could not remove { item } from pool as it ' s already missing from it. " )
except Exception as e :
2021-08-09 07:15:41 +00:00
raise Exception ( f " Error running plando for player { player } ( { world . player_name [ player ] } ) " ) from e