Speed up Progression Balancing, mostly by using generators and pre-sorts where the opportunity exists

In some cases multi-thousand element lists were created in-memory with near identical contents, per player, then discarded and reassembled.
Was testing against a case with 3 GB of additional memory use (50 players) which appeared to get stuck, but really was just very slow. This example case is fixed with these changes.
Additionally, progression balancing is now run after ShopSlotFill, so it is now "aware" of the changed progression shops can produce.
As well, special handling for keys was removed, as not all games will have the notion of keys.
This commit is contained in:
Fabian Dill 2021-02-05 08:07:12 +01:00
parent 239b365264
commit 96d544ac84
4 changed files with 34 additions and 32 deletions

View File

@ -606,9 +606,9 @@ class CollectionState(object):
new_locations = True new_locations = True
while new_locations: while new_locations:
reachable_events = {location for location in locations if location.event and reachable_events = {location for location in locations if location.event and
(not key_only or (not self.world.keyshuffle[ (not key_only or
location.item.player] and location.item.smallkey) or (not self.world.bigkeyshuffle[ (not self.world.keyshuffle[location.item.player] and location.item.smallkey)
location.item.player] and location.item.bigkey)) or (not self.world.bigkeyshuffle[location.item.player] and location.item.bigkey))
and location.can_reach(self)} and location.can_reach(self)}
new_locations = reachable_events - self.events new_locations = reachable_events - self.events
for event in new_locations: for event in new_locations:

50
Fill.py
View File

@ -1,5 +1,7 @@
import logging import logging
import typing import typing
import collections
import itertools
from BaseClasses import CollectionState, PlandoItem, Location from BaseClasses import CollectionState, PlandoItem, Location
from Items import ItemFactory from Items import ItemFactory
@ -243,12 +245,7 @@ def balance_multiworld_progression(world):
unchecked_locations = world.get_locations().copy() unchecked_locations = world.get_locations().copy()
world.random.shuffle(unchecked_locations) world.random.shuffle(unchecked_locations)
reachable_locations_count = {player: 0 for player in range(1, world.players + 1)} reachable_locations_count = {player: 0 for player in world.player_ids}
def event_key(location):
return location.event and (
world.keyshuffle[location.item.player] or not location.item.smallkey) and (
world.bigkeyshuffle[location.item.player] or not location.item.bigkey)
def get_sphere_locations(sphere_state, locations): def get_sphere_locations(sphere_state, locations):
sphere_state.sweep_for_events(key_only=True, locations=locations) sphere_state.sweep_for_events(key_only=True, locations=locations)
@ -269,33 +266,38 @@ def balance_multiworld_progression(world):
balancing_unchecked_locations = unchecked_locations.copy() balancing_unchecked_locations = unchecked_locations.copy()
balancing_reachables = reachable_locations_count.copy() balancing_reachables = reachable_locations_count.copy()
balancing_sphere = sphere_locations.copy() balancing_sphere = sphere_locations.copy()
candidate_items = [] candidate_items = collections.defaultdict(list)
while True: while True:
for location in balancing_sphere: for location in balancing_sphere:
if event_key(location): if location.event:
balancing_state.collect(location.item, True, location) balancing_state.collect(location.item, True, location)
if location.item.player in balancing_players and not location.locked: player = location.item.player
candidate_items.append(location) # only replace items that end up in another player's world
if not location.locked and player in balancing_players and location.player != player:
candidate_items[player].append(location)
balancing_sphere = get_sphere_locations(balancing_state, balancing_unchecked_locations) balancing_sphere = get_sphere_locations(balancing_state, balancing_unchecked_locations)
for location in balancing_sphere: for location in balancing_sphere:
balancing_unchecked_locations.remove(location) balancing_unchecked_locations.remove(location)
balancing_reachables[location.player] += 1 balancing_reachables[location.player] += 1
if world.has_beaten_game(balancing_state) or all( if world.has_beaten_game(balancing_state) or all(
[reachables >= threshold for reachables in balancing_reachables.values()]): reachables >= threshold for reachables in balancing_reachables.values()):
break break
elif not balancing_sphere: elif not balancing_sphere:
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.') raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
unlocked_locations = collections.defaultdict(list)
unlocked_locations = [l for l in unchecked_locations if l not in balancing_unchecked_locations] for l in unchecked_locations:
if l not in balancing_unchecked_locations:
unlocked_locations[l.player].append(l)
items_to_replace = [] items_to_replace = []
for player in balancing_players: for player in balancing_players:
locations_to_test = [l for l in unlocked_locations if l.player == player] locations_to_test = unlocked_locations[player]
# only replace items that end up in another player's world items_to_test = candidate_items[player]
items_to_test = [l for l in candidate_items if l.item.player == player and l.player != player]
while items_to_test: while items_to_test:
testing = items_to_test.pop() testing = items_to_test.pop()
reducing_state = state.copy() reducing_state = state.copy()
for location in [*[l for l in items_to_replace if l.item.player == player], *items_to_test]: for location in itertools.chain((l for l in items_to_replace if l.item.player == player),
items_to_test):
reducing_state.collect(location.item, True, location) reducing_state.collect(location.item, True, location)
reducing_state.sweep_for_events(locations=locations_to_test) reducing_state.sweep_for_events(locations=locations_to_test)
@ -320,21 +322,20 @@ def balance_multiworld_progression(world):
new_location = replacement_locations.pop() new_location = replacement_locations.pop()
swap_location_item(old_location, new_location) swap_location_item(old_location, new_location)
new_location.event, old_location.event = True, False
logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, " logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, "
f"displacing {old_location.item} in {old_location}") f"displacing {old_location.item} into {old_location}")
state.collect(new_location.item, True, new_location) state.collect(new_location.item, True, new_location)
replaced_items = True replaced_items = True
if replaced_items: if replaced_items:
for location in get_sphere_locations(state, [l for l in unlocked_locations if unlocked = [fresh for player in balancing_players for fresh in unlocked_locations[player]]
l.player in balancing_players]): for location in get_sphere_locations(state, unlocked):
unchecked_locations.remove(location) unchecked_locations.remove(location)
reachable_locations_count[location.player] += 1 reachable_locations_count[location.player] += 1
sphere_locations.append(location) sphere_locations.append(location)
for location in sphere_locations: for location in sphere_locations:
if event_key(location): if location.event:
state.collect(location.item, True, location) state.collect(location.item, True, location)
checked_locations.extend(sphere_locations) checked_locations.extend(sphere_locations)
@ -345,7 +346,7 @@ def balance_multiworld_progression(world):
def swap_location_item(location_1: Location, location_2: Location, check_locked=True): def swap_location_item(location_1: Location, location_2: Location, check_locked=True):
"""Swaps Items of locations. Does NOT swap flags like event, shop_slot or locked""" """Swaps Items of locations. Does NOT swap flags like shop_slot or locked, but does swap event"""
if check_locked: if check_locked:
if location_1.locked: if location_1.locked:
logging.warning(f"Swapping {location_1}, which is marked as locked.") logging.warning(f"Swapping {location_1}, which is marked as locked.")
@ -354,6 +355,7 @@ def swap_location_item(location_1: Location, location_2: Location, check_locked=
location_2.item, location_1.item = location_1.item, location_2.item location_2.item, location_1.item = location_1.item, location_2.item
location_1.item.location = location_1 location_1.item.location = location_1
location_2.item.location = location_2 location_2.item.location = location_2
location_1.event, location_2.event = location_2.event, location_1.event
def distribute_planned(world): def distribute_planned(world):

View File

@ -216,13 +216,13 @@ def main(args, seed=None):
elif args.algorithm == 'balanced': elif args.algorithm == 'balanced':
distribute_items_restrictive(world, True) distribute_items_restrictive(world, True)
if world.players > 1:
balance_multiworld_progression(world)
logger.info("Filling Shop Slots") logger.info("Filling Shop Slots")
ShopSlotFill(world) ShopSlotFill(world)
if world.players > 1:
balance_multiworld_progression(world)
logger.info('Patching ROM.') logger.info('Patching ROM.')

View File

@ -199,10 +199,10 @@ def main(args=None, callback=ERmain):
for option, player_settings in vars(erargs).items(): for option, player_settings in vars(erargs).items():
if type(player_settings) == dict: if type(player_settings) == dict:
if all(type(value) != list for value in player_settings.values()): if all(type(value) != list for value in player_settings.values()):
if len(frozenset(player_settings.values())) > 1: if len(player_settings.values()) > 1:
important[option] = {player: value for player, value in player_settings.items() if important[option] = {player: value for player, value in player_settings.items() if
player <= args.yaml_output} player <= args.yaml_output}
elif len(frozenset(player_settings.values())) > 0: elif len(player_settings.values()) > 0:
important[option] = player_settings[1] important[option] = player_settings[1]
else: else:
logging.debug(f"No player settings defined for option '{option}'") logging.debug(f"No player settings defined for option '{option}'")