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
2021-12-22 05:55:10 +00:00
from collections import Counter , deque
2021-12-21 01:14:50 +00:00
2022-01-20 03:19:07 +00:00
from BaseClasses import CollectionState , Location , LocationProgressType , MultiWorld , Item
2022-01-20 18:34:17 +00:00
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
2022-02-22 08:49:01 +00:00
def sweep_from_pool ( base_state : CollectionState , itempool : typing . Sequence [ Item ] = tuple ( ) ) :
2021-12-21 00:47:04 +00:00
new_state = base_state . copy ( )
for item in itempool :
new_state . collect ( item , True )
new_state . sweep_for_events ( )
return new_state
2017-10-15 19:35:45 +00:00
2021-12-21 00:47:04 +00:00
2022-02-22 08:49:01 +00:00
def fill_restrictive ( world : MultiWorld , base_state : CollectionState , locations : typing . List [ Location ] ,
itempool : typing . List [ Item ] , single_player_placement = False , lock = False ) :
2019-12-14 16:47:36 +00:00
unplaced_items = [ ]
2022-01-28 04:40:08 +00:00
placements : typing . List [ Location ] = [ ]
2019-12-14 16:47:36 +00:00
2021-12-21 01:20:01 +00:00
swapped_items = Counter ( )
2022-01-22 04:19:33 +00:00
reachable_items : typing . Dict [ int , deque ] = { }
2019-12-14 16:47:36 +00:00
for item in itempool :
2021-12-22 05:55:10 +00:00
reachable_items . setdefault ( item . player , deque ( ) ) . 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 :
2021-12-21 00:47:04 +00:00
# grab one item per player
items_to_place = [ items . pop ( )
for items in reachable_items . values ( ) if items ]
2021-03-18 16:27:31 +00:00
for item in items_to_place :
itempool . remove ( item )
2022-01-31 21:23:01 +00:00
maximum_exploration_state = sweep_from_pool (
base_state , itempool + unplaced_items )
2022-01-28 04:40:08 +00:00
2021-03-18 16:27:31 +00:00
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 :
2022-01-22 04:19:33 +00:00
spot_to_fill : typing . Optional [ Location ] = None
2021-09-16 22:17:54 +00:00
if world . accessibility [ item_to_place . player ] == ' minimal ' :
2021-03-18 16:27:31 +00:00
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 ) :
2021-12-21 00:47:04 +00:00
# poping by index is faster than removing by content,
spot_to_fill = locations . pop ( i )
2021-03-18 16:27:31 +00:00
# skipping a scan for the element
break
else :
2021-12-21 00:47:04 +00:00
# we filled all reachable spots.
2022-01-22 04:19:33 +00:00
# try swapping this item with previously placed items
2022-02-22 08:49:01 +00:00
for ( i , location ) in enumerate ( placements ) :
2021-12-21 00:47:04 +00:00
placed_item = location . item
2021-12-28 18:57:48 +00:00
# Unplaceable items can sometimes be swapped infinitely. Limit the
2021-12-21 01:23:19 +00:00
# number of times we will swap an individual item to prevent this
2022-01-28 04:40:08 +00:00
swap_count = swapped_items [ placed_item . player ,
placed_item . name ]
if swap_count > 1 :
2021-12-21 01:14:50 +00:00
continue
2022-01-28 04:40:08 +00:00
2021-12-21 00:47:04 +00:00
location . item = None
placed_item . location = None
2022-01-28 04:40:08 +00:00
swap_state = sweep_from_pool ( base_state )
2021-12-21 00:47:04 +00:00
if ( not single_player_placement or location . player == item_to_place . player ) \
and location . can_fill ( swap_state , item_to_place , perform_access_check ) :
2022-01-28 04:40:08 +00:00
# Verify that placing this item won't reduce available locations
prev_state = swap_state . copy ( )
prev_state . collect ( placed_item )
prev_loc_count = len (
world . get_reachable_locations ( prev_state ) )
swap_state . collect ( item_to_place , True )
new_loc_count = len (
world . get_reachable_locations ( swap_state ) )
if new_loc_count > = prev_loc_count :
# Add this item to the existing placement, and
# add the old item to the back of the queue
spot_to_fill = placements . pop ( i )
swap_count + = 1
swapped_items [ placed_item . player ,
placed_item . name ] = swap_count
reachable_items [ placed_item . player ] . appendleft (
placed_item )
itempool . append ( placed_item )
break
# Item can't be placed here, restore original item
location . item = placed_item
placed_item . location = location
2021-12-21 00:47:04 +00:00
2022-01-22 04:19:33 +00:00
if spot_to_fill is None :
2022-01-29 16:20:04 +00:00
# Can't place this item, move on to the next
2021-12-21 00:47:04 +00:00
unplaced_items . append ( item_to_place )
2022-01-29 16:20:04 +00:00
continue
2021-03-18 16:27:31 +00:00
world . push_item ( spot_to_fill , item_to_place , False )
spot_to_fill . locked = lock
placements . append ( spot_to_fill )
2022-01-31 21:23:01 +00:00
spot_to_fill . event = item_to_place . advancement
2017-10-15 19:35:45 +00:00
2022-01-29 16:20:04 +00:00
if len ( unplaced_items ) > 0 and len ( locations ) > 0 :
# There are leftover unplaceable items and locations that won't accept them
if world . can_beat_game ( ) :
logging . warning (
f ' Not all items placed. Game beatable anyway. (Could not place { unplaced_items } ) ' )
else :
raise FillError ( f ' No more spots to place { unplaced_items } , locations { locations } are invalid. '
f ' Already placed { len ( placements ) } : { " , " . join ( str ( place ) for place in placements ) } ' )
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
2022-01-22 04:19:33 +00:00
def distribute_items_restrictive ( world : MultiWorld ) :
2022-01-27 16:25:42 +00:00
fill_locations = sorted ( world . get_unfilled_locations ( ) )
2022-01-20 03:19:07 +00:00
world . random . shuffle ( fill_locations )
2017-10-15 19:35:45 +00:00
# get items to distribute
2022-01-27 16:25:42 +00:00
itempool = sorted ( world . itempool )
world . random . shuffle ( 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 ) }
2021-08-30 20:20:44 +00:00
nonlocalrestitempool = [ ]
2020-06-04 01:30:59 +00:00
restitempool = [ ]
2022-01-27 16:25:42 +00:00
for item in 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 )
2021-09-16 22:17:54 +00:00
elif item . name in world . local_items [ item . player ] . value :
2021-01-30 08:57:25 +00:00
localrestitempool [ item . player ] . append ( item )
2021-09-16 22:17:54 +00:00
elif item . name in world . non_local_items [ item . player ] . value :
2021-08-30 20:20:44 +00:00
nonlocalrestitempool . append ( item )
2020-06-04 01:30:59 +00:00
else :
restitempool . append ( item )
2017-10-15 19:35:45 +00:00
2022-01-20 03:19:07 +00:00
call_all ( world , " fill_hook " , progitempool , nonexcludeditempool ,
localrestitempool , nonlocalrestitempool , restitempool , fill_locations )
2022-01-22 04:19:33 +00:00
locations : typing . Dict [ LocationProgressType , typing . List [ Location ] ] = {
2022-02-02 15:29:29 +00:00
loc_type : [ ] for loc_type in LocationProgressType }
2022-01-22 03:34:59 +00:00
for loc in fill_locations :
locations [ loc . progress_type ] . append ( loc )
prioritylocations = locations [ LocationProgressType . PRIORITY ]
defaultlocations = locations [ LocationProgressType . DEFAULT ]
excludedlocations = locations [ LocationProgressType . EXCLUDED ]
2022-01-20 03:19:07 +00:00
fill_restrictive ( world , world . state , prioritylocations , progitempool )
if prioritylocations :
defaultlocations = prioritylocations + defaultlocations
2021-08-10 07:03:44 +00:00
2022-01-20 03:19:07 +00:00
if progitempool :
fill_restrictive ( world , world . state , defaultlocations , progitempool )
2022-01-28 08:29:29 +00:00
if progitempool :
2022-01-22 03:34:59 +00:00
raise FillError (
f ' Not enough locations for progress items. There are { len ( progitempool ) } more items than locations ' )
2017-10-15 19:35:45 +00:00
2021-07-15 21:52:30 +00:00
if nonexcludeditempool :
2022-01-20 03:19:07 +00:00
world . random . shuffle ( defaultlocations )
# needs logical fill to not conflict with local items
2022-01-31 21:23:01 +00:00
fill_restrictive (
world , world . state , defaultlocations , nonexcludeditempool )
2022-01-28 08:29:29 +00:00
if nonexcludeditempool :
2022-01-20 03:19:07 +00:00
raise FillError (
f ' Not enough locations for non-excluded items. There are { len ( nonexcludeditempool ) } more items than locations ' )
defaultlocations = defaultlocations + excludedlocations
world . random . shuffle ( defaultlocations )
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 }
2022-01-20 03:19:07 +00:00
for location in defaultlocations :
2021-01-07 11:43:11 +00:00
local_locations [ location . player ] . append ( location )
2022-01-20 03:19:07 +00:00
for player_locations in local_locations . values ( ) :
world . random . shuffle ( player_locations )
2021-01-07 11:43:11 +00:00
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 )
2022-01-20 03:19:07 +00:00
defaultlocations . remove ( spot_to_fill )
2020-06-04 01:30:59 +00:00
2021-08-30 20:20:44 +00:00
for item_to_place in nonlocalrestitempool :
2022-01-20 03:19:07 +00:00
for i , location in enumerate ( defaultlocations ) :
2021-08-30 20:20:44 +00:00
if location . player != item_to_place . player :
2022-01-20 03:19:07 +00:00
world . push_item ( defaultlocations . pop ( i ) , item_to_place , False )
2021-08-30 20:20:44 +00:00
break
else :
2022-01-20 03:19:07 +00:00
logging . warning (
f " Could not place non_local_item { item_to_place } among { defaultlocations } , tossing. " )
2021-08-30 20:20:44 +00:00
2022-01-20 03:19:07 +00:00
world . random . shuffle ( defaultlocations )
2017-10-15 19:35:45 +00:00
2022-01-20 03:19:07 +00:00
restitempool , defaultlocations = fast_fill (
world , restitempool , defaultlocations )
2021-08-24 07:52:12 +00:00
unplaced = progitempool + restitempool
2022-01-20 03:19:07 +00:00
unfilled = [ location . name for location in defaultlocations ]
2017-10-15 19:35:45 +00:00
2020-08-01 04:22:59 +00:00
if unplaced or unfilled :
2022-01-20 03:19:07 +00:00
logging . warning (
f ' Unplaced items( { len ( unplaced ) } ): { unplaced } - Unfilled Locations( { len ( unfilled ) } ): { unfilled } ' )
2022-01-23 23:18:00 +00:00
items_counter = Counter ( [ location . item . player for location in world . get_locations ( ) ] )
locations_counter = Counter ( [ location . player for location in world . get_locations ( ) ] )
items_counter . update ( [ item . player for item in unplaced ] )
locations_counter . update ( [ location . player for location in unfilled ] )
print_data = { " items " : items_counter , " locations " : locations_counter }
logging . info ( f ' Per-Player counts: { print_data } ) ' )
2017-10-15 19:35:45 +00:00
2022-02-22 08:49:01 +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 ) :
2022-03-12 21:05:03 +00:00
# A system to reduce situations where players have no checks remaining, popularly known as "BK mode."
# Overall progression balancing algorithm:
# Gather up all locations in a sphere.
# Define a threshold value based on the player with the most available locations.
# If other players are below the threshold value, swap progression in this sphere into earlier spheres,
# which gives more locations available by this sphere.
2022-03-14 19:10:49 +00:00
balanceable_players = { player for player in world . player_ids if world . progression_balancing [ player ] }
2020-05-18 01:54:29 +00:00
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
2022-03-14 19:10:49 +00:00
reachable_locations_count = { player : 0 for player in world . player_ids }
total_locations_count = Counter ( location . player for location in world . get_locations ( ) if not location . locked )
balanceable_players = { player for player in balanceable_players if total_locations_count [ player ] }
2022-03-12 21:05:03 +00:00
sphere_num = 1
moved_item_count = 0
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
2022-03-12 21:05:03 +00:00
def item_percentage ( player , num ) :
return num / total_locations_count [ player ]
2020-05-18 01:54:29 +00:00
while True :
2022-03-12 21:05:03 +00:00
# Gather non-locked locations. This ensures that only shuffled locations get counted for progression balancing,
# i.e. the items the players will be checking.
2020-05-18 01:54:29 +00:00
sphere_locations = get_sphere_locations ( state , unchecked_locations )
for location in sphere_locations :
unchecked_locations . remove ( location )
2022-03-12 21:05:03 +00:00
if not location . locked :
reachable_locations_count [ location . player ] + = 1
logging . debug ( f " Sphere { sphere_num } " )
logging . debug ( f " Reachable locations: { reachable_locations_count } " )
logging . debug ( f " Reachable percentages: { { player : round ( item_percentage ( player , num ) , 2 ) for player , num in reachable_locations_count . items ( ) } } \n " )
sphere_num + = 1
2020-05-18 01:54:29 +00:00
if checked_locations :
2022-03-12 21:05:03 +00:00
# The 10% threshold can be modified for "progression balancing strength" -- right now it approximates the old 20/216 bound.
threshold_percentage = max ( map ( lambda p : item_percentage ( p , reachable_locations_count [ p ] ) , reachable_locations_count ) ) - 0.10
logging . debug ( f " Threshold: { threshold_percentage } " )
2021-03-04 07:10:30 +00:00
balancing_players = { player for player , reachables in reachable_locations_count . items ( ) if
2022-03-12 21:05:03 +00:00
item_percentage ( player , reachables ) < threshold_percentage 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 :
2022-03-12 21:05:03 +00:00
# Check locations in the current sphere and gather progression items to swap earlier
2020-05-18 01:54:29 +00:00
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
2022-03-12 21:05:03 +00:00
if ( not location . locked and not location . item . skip_in_prog_balancing and
2022-01-20 03:19:07 +00:00
player in balancing_players and
location . player != player and
location . progress_type != LocationProgressType . PRIORITY ) :
2021-03-04 07:10:30 +00:00
candidate_items [ player ] . add ( location )
2022-03-12 21:05:03 +00:00
logging . debug ( f " Candidate item: { location . name } , { location . item . name } " )
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 )
2022-03-12 21:05:03 +00:00
if not location . locked :
balancing_reachables [ location . player ] + = 1
2020-05-18 01:54:29 +00:00
if world . has_beaten_game ( balancing_state ) or all (
2022-03-12 21:05:03 +00:00
item_percentage ( player , reachables ) > = threshold_percentage
for player , reachables in balancing_reachables . items ( ) ) :
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. ' )
2022-03-12 21:05:03 +00:00
# Gather a set of locations which we can swap items into
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 )
2022-03-12 21:05:03 +00:00
if item_percentage ( player , reachable_locations_count [ player ] + len ( reduced_sphere ) ) < threshold_percentage :
2020-05-18 01:54:29 +00:00
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
2022-03-12 21:05:03 +00:00
# Start swapping items. Since we swap into earlier spheres, no need for accessibility checks.
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 } " )
2022-03-12 21:05:03 +00:00
moved_item_count + = 1
2021-03-04 07:10:30 +00:00
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 :
2022-03-12 21:05:03 +00:00
logging . debug ( f " Moved { moved_item_count } items so far \n " )
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 )
2022-03-12 21:05:03 +00:00
if not location . locked :
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
2022-02-15 05:29:57 +00:00
2022-01-20 18:34:17 +00:00
def distribute_planned ( world : MultiWorld ) :
def warn ( warning : str , force ) :
2022-01-22 20:03:13 +00:00
if force in [ True , ' fail ' , ' failure ' , ' none ' , False , ' warn ' , ' warning ' ] :
2022-01-20 18:34:17 +00:00
logging . warning ( f ' { warning } ' )
else :
logging . debug ( f ' { warning } ' )
2021-01-11 12:35:48 +00:00
2022-01-20 18:34:17 +00:00
def failed ( warning : str , force ) :
2022-02-22 08:49:01 +00:00
if force in [ True , ' fail ' , ' failure ' ] :
2022-01-20 18:34:17 +00:00
raise Exception ( warning )
else :
warn ( warning , force )
2021-01-11 12:35:48 +00:00
2021-09-02 01:45:37 +00:00
# TODO: remove. Preferably by implementing key drop
from worlds . alttp . Regions import key_drop_data
2021-02-20 01:30:55 +00:00
world_name_lookup = world . world_name_lookup
2021-01-04 14:14:20 +00:00
2022-01-20 18:34:17 +00:00
plando_blocks = [ ]
player_ids = set ( world . player_ids )
for player in player_ids :
for block in world . plando_items [ player ] :
block [ ' player ' ] = player
if ' force ' not in block :
block [ ' force ' ] = ' silent '
if ' from_pool ' not in block :
block [ ' from_pool ' ] = True
if ' world ' not in block :
block [ ' world ' ] = False
items = [ ]
if " items " in block :
items = block [ " items " ]
if ' count ' not in block :
block [ ' count ' ] = False
elif " item " in block :
items = block [ " item " ]
if ' count ' not in block :
block [ ' count ' ] = 1
else :
2022-01-22 20:03:13 +00:00
failed ( " You must specify at least one item to place items with plando. " , block [ ' force ' ] )
continue
2022-01-20 18:34:17 +00:00
if isinstance ( items , dict ) :
item_list = [ ]
for key , value in items . items ( ) :
2022-01-22 20:03:13 +00:00
if value is True :
value = world . itempool . count ( world . worlds [ player ] . create_item ( key ) )
2022-01-20 18:34:17 +00:00
item_list + = [ key ] * value
items = item_list
if isinstance ( items , str ) :
items = [ items ]
block [ ' items ' ] = items
locations = [ ]
2022-01-22 20:03:13 +00:00
if ' location ' in block :
locations = block [ ' location ' ] # just allow 'location' to keep old yamls compatible
elif ' locations ' in block :
locations = block [ ' locations ' ]
if isinstance ( locations , str ) :
locations = [ locations ]
2022-01-20 18:34:17 +00:00
if isinstance ( locations , dict ) :
location_list = [ ]
for key , value in locations . items ( ) :
location_list + = [ key ] * value
locations = location_list
if isinstance ( locations , str ) :
locations = [ locations ]
block [ ' locations ' ] = locations
if not block [ ' count ' ] :
2022-02-22 08:49:01 +00:00
block [ ' count ' ] = ( min ( len ( block [ ' items ' ] ) , len ( block [ ' locations ' ] ) ) if
len ( block [ ' locations ' ] ) > 0 else len ( block [ ' items ' ] ) )
2022-01-20 18:34:17 +00:00
if isinstance ( block [ ' count ' ] , int ) :
block [ ' count ' ] = { ' min ' : block [ ' count ' ] , ' max ' : block [ ' count ' ] }
if ' min ' not in block [ ' count ' ] :
block [ ' count ' ] [ ' min ' ] = 0
if ' max ' not in block [ ' count ' ] :
2022-02-22 08:49:01 +00:00
block [ ' count ' ] [ ' max ' ] = ( min ( len ( block [ ' items ' ] ) , len ( block [ ' locations ' ] ) ) if
len ( block [ ' locations ' ] ) > 0 else len ( block [ ' items ' ] ) )
2022-01-20 18:34:17 +00:00
if block [ ' count ' ] [ ' max ' ] > len ( block [ ' items ' ] ) :
count = block [ ' count ' ]
failed ( f " Plando count { count } greater than items specified " , block [ ' force ' ] )
block [ ' count ' ] = len ( block [ ' items ' ] )
if block [ ' count ' ] [ ' max ' ] > len ( block [ ' locations ' ] ) > 0 :
count = block [ ' count ' ]
2022-01-22 20:03:13 +00:00
failed ( f " Plando count { count } greater than locations specified " , block [ ' force ' ] )
2022-01-20 18:34:17 +00:00
block [ ' count ' ] = len ( block [ ' locations ' ] )
block [ ' count ' ] [ ' target ' ] = world . random . randint ( block [ ' count ' ] [ ' min ' ] , block [ ' count ' ] [ ' max ' ] )
if block [ ' count ' ] [ ' target ' ] > 0 :
plando_blocks . append ( block )
2022-01-22 20:03:13 +00:00
# shuffle, but then sort blocks by number of locations minus number of items,
# so less-flexible blocks get priority
2022-01-20 18:34:17 +00:00
world . random . shuffle ( plando_blocks )
2022-01-22 20:03:13 +00:00
plando_blocks . sort ( key = lambda block : ( len ( block [ ' locations ' ] ) - block [ ' count ' ] [ ' target ' ]
if len ( block [ ' locations ' ] ) > 0
else len ( world . get_unfilled_locations ( player ) ) - block [ ' count ' ] [ ' target ' ] ) )
2022-01-20 18:34:17 +00:00
for placement in plando_blocks :
player = placement [ ' player ' ]
2021-06-14 21:42:13 +00:00
try :
2022-01-20 18:34:17 +00:00
target_world = placement [ ' world ' ]
locations = placement [ ' locations ' ]
items = placement [ ' items ' ]
maxcount = placement [ ' count ' ] [ ' target ' ]
from_pool = placement [ ' from_pool ' ]
2022-01-22 20:03:13 +00:00
if target_world is False or world . players == 1 : # target own world
2022-01-20 18:34:17 +00:00
worlds = { player }
2022-01-22 20:03:13 +00:00
elif target_world is True : # target any worlds besides own
2022-01-20 18:34:17 +00:00
worlds = set ( world . player_ids ) - { player }
2022-01-22 20:03:13 +00:00
elif target_world is None : # target all worlds
2022-01-20 18:34:17 +00:00
worlds = set ( world . player_ids )
2022-01-22 20:03:13 +00:00
elif type ( target_world ) == list : # list of target worlds
2022-01-20 18:34:17 +00:00
worlds = [ ]
for listed_world in target_world :
if listed_world not in world_name_lookup :
failed ( f " Cannot place item to { target_world } ' s world as that world does not exist. " ,
placement [ ' force ' ] )
2021-06-14 21:42:13 +00:00
continue
2022-01-20 18:34:17 +00:00
worlds . append ( world_name_lookup [ listed_world ] )
worlds = set ( worlds )
2022-01-22 20:03:13 +00:00
elif type ( target_world ) == int : # target world by slot number
2022-01-20 18:34:17 +00:00
if target_world not in range ( 1 , world . players + 1 ) :
failed (
f " Cannot place item in world { target_world } as it is not in range of (1, { world . players } ) " ,
2022-01-22 20:03:13 +00:00
placement [ ' force ' ] )
2021-01-05 17:53:52 +00:00
continue
2022-01-20 18:34:17 +00:00
worlds = { target_world }
2022-01-22 20:03:13 +00:00
else : # target world by slot name
2022-01-20 18:34:17 +00:00
if target_world not in world_name_lookup :
failed ( f " Cannot place item to { target_world } ' s world as that world does not exist. " ,
placement [ ' force ' ] )
2021-01-05 17:53:52 +00:00
continue
2022-01-20 18:34:17 +00:00
worlds = { world_name_lookup [ target_world ] }
candidates = list ( location for location in world . get_unfilled_locations_for_players ( locations ,
worlds ) )
world . random . shuffle ( candidates )
world . random . shuffle ( items )
count = 0
2022-01-22 20:03:13 +00:00
err = [ ]
2022-01-20 18:34:17 +00:00
successful_pairs = [ ]
for item_name in items :
item = world . worlds [ player ] . create_item ( item_name )
for location in reversed ( candidates ) :
if location in key_drop_data :
warn (
f " Can ' t place ' { item_name } ' at ' { placement . location } ' , as key drop shuffle locations are not supported yet. " )
2022-01-22 20:03:13 +00:00
continue
2022-01-20 18:34:17 +00:00
if not location . item :
if location . item_rule ( item ) :
if location . can_fill ( world . state , item , False ) :
successful_pairs . append ( [ item , location ] )
candidates . remove ( location )
count = count + 1
break
else :
2022-01-22 20:03:13 +00:00
err . append ( f " Can ' t place item at { location } due to fill condition not met. " )
2022-01-20 18:34:17 +00:00
else :
2022-01-22 20:03:13 +00:00
err . append ( f " { item_name } not allowed at { location } . " )
2022-01-20 18:34:17 +00:00
else :
2022-01-22 20:03:13 +00:00
err . append ( f " Cannot place { item_name } into already filled location { location } . " )
2022-01-20 18:34:17 +00:00
if count == maxcount :
break
if count < placement [ ' count ' ] [ ' min ' ] :
2022-01-22 20:03:13 +00:00
err = " " . join ( err )
m = placement [ ' count ' ] [ ' min ' ]
2022-01-20 18:34:17 +00:00
failed (
2022-01-22 20:03:13 +00:00
f " Plando block failed to place { m - count } of { m } item(s) for { world . player_name [ player ] } , error(s): { err } " ,
2022-01-20 18:34:17 +00:00
placement [ ' force ' ] )
for ( item , location ) in successful_pairs :
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 } " )
if from_pool :
2021-06-14 21:42:13 +00:00
try :
world . itempool . remove ( item )
except ValueError :
2022-01-20 18:34:17 +00:00
warn (
f " Could not remove { item } from pool for { world . player_name [ player ] } as it ' s already missing from it. " ,
2022-01-22 20:03:13 +00:00
placement [ ' force ' ] )
2022-01-20 18:34:17 +00:00
2021-06-14 21:42:13 +00:00
except Exception as e :
2022-01-20 18:34:17 +00:00
raise Exception (
f " Error running plando for player { player } ( { world . player_name [ player ] } ) " ) from e