Massively speed up progression balancing for very large multiworlds
Several times faster was observed in testing. 10+ hours to less than 2 in the last sample.
This commit is contained in:
parent
280f3938ed
commit
f130829c0c
78
Fill.py
78
Fill.py
|
@ -42,9 +42,10 @@ def fill_restrictive(world, base_state: CollectionState, locations, itempool, si
|
||||||
for item_to_place in items_to_place:
|
for item_to_place in items_to_place:
|
||||||
perform_access_check = True
|
perform_access_check = True
|
||||||
if world.accessibility[item_to_place.player] == 'none':
|
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
|
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:
|
for location in locations:
|
||||||
if (not single_player_placement or location.player == item_to_place.player)\
|
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):
|
and location.can_fill(maximum_exploration_state, item_to_place, perform_access_check):
|
||||||
spot_to_fill = location
|
spot_to_fill = location
|
||||||
break
|
break
|
||||||
|
@ -70,6 +71,7 @@ def fill_restrictive(world, base_state: CollectionState, locations, itempool, si
|
||||||
|
|
||||||
itempool.extend(unplaced_items)
|
itempool.extend(unplaced_items)
|
||||||
|
|
||||||
|
|
||||||
def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None):
|
def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None):
|
||||||
# If not passed in, then get a shuffled list of locations to fill in
|
# If not passed in, then get a shuffled list of locations to fill in
|
||||||
if not fill_locations:
|
if not fill_locations:
|
||||||
|
@ -241,15 +243,14 @@ def balance_multiworld_progression(world):
|
||||||
else:
|
else:
|
||||||
logging.info(f'Balancing multiworld progression for {len(balanceable_players)} Players.')
|
logging.info(f'Balancing multiworld progression for {len(balanceable_players)} Players.')
|
||||||
state = CollectionState(world)
|
state = CollectionState(world)
|
||||||
checked_locations = []
|
checked_locations = set()
|
||||||
unchecked_locations = world.get_locations().copy()
|
unchecked_locations = set(world.get_locations())
|
||||||
world.random.shuffle(unchecked_locations)
|
|
||||||
|
|
||||||
reachable_locations_count = {player: 0 for player in world.player_ids}
|
reachable_locations_count = {player: 0 for player in world.player_ids}
|
||||||
|
|
||||||
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)
|
||||||
return [loc for loc in locations if sphere_state.can_reach(loc)]
|
return {loc for loc in locations if sphere_state.can_reach(loc)}
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
sphere_locations = get_sphere_locations(state, unchecked_locations)
|
sphere_locations = get_sphere_locations(state, unchecked_locations)
|
||||||
|
@ -259,14 +260,14 @@ def balance_multiworld_progression(world):
|
||||||
|
|
||||||
if checked_locations:
|
if checked_locations:
|
||||||
threshold = max(reachable_locations_count.values()) - 20
|
threshold = max(reachable_locations_count.values()) - 20
|
||||||
balancing_players = [player for player, reachables in reachable_locations_count.items() if
|
balancing_players = {player for player, reachables in reachable_locations_count.items() if
|
||||||
reachables < threshold and player in balanceable_players]
|
reachables < threshold and player in balanceable_players}
|
||||||
if balancing_players:
|
if balancing_players:
|
||||||
balancing_state = state.copy()
|
balancing_state = state.copy()
|
||||||
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 = collections.defaultdict(list)
|
candidate_items = collections.defaultdict(set)
|
||||||
while True:
|
while True:
|
||||||
for location in balancing_sphere:
|
for location in balancing_sphere:
|
||||||
if location.event:
|
if location.event:
|
||||||
|
@ -274,7 +275,7 @@ def balance_multiworld_progression(world):
|
||||||
player = location.item.player
|
player = location.item.player
|
||||||
# only replace items that end up in another player's world
|
# only replace items that end up in another player's world
|
||||||
if not location.locked and player in balancing_players and location.player != player:
|
if not location.locked and player in balancing_players and location.player != player:
|
||||||
candidate_items[player].append(location)
|
candidate_items[player].add(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)
|
||||||
|
@ -284,10 +285,10 @@ def balance_multiworld_progression(world):
|
||||||
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 = collections.defaultdict(set)
|
||||||
for l in unchecked_locations:
|
for l in unchecked_locations:
|
||||||
if l not in balancing_unchecked_locations:
|
if l not in balancing_unchecked_locations:
|
||||||
unlocked_locations[l.player].append(l)
|
unlocked_locations[l.player].add(l)
|
||||||
items_to_replace = []
|
items_to_replace = []
|
||||||
for player in balancing_players:
|
for player in balancing_players:
|
||||||
locations_to_test = unlocked_locations[player]
|
locations_to_test = unlocked_locations[player]
|
||||||
|
@ -297,7 +298,6 @@ def balance_multiworld_progression(world):
|
||||||
reducing_state = state.copy()
|
reducing_state = state.copy()
|
||||||
for location in itertools.chain((l for l in items_to_replace if l.item.player == player),
|
for location in itertools.chain((l for l in items_to_replace if l.item.player == player),
|
||||||
items_to_test):
|
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)
|
||||||
|
@ -311,33 +311,40 @@ def balance_multiworld_progression(world):
|
||||||
items_to_replace.append(testing)
|
items_to_replace.append(testing)
|
||||||
|
|
||||||
replaced_items = False
|
replaced_items = False
|
||||||
replacement_locations = [l for l in checked_locations if not l.event and not l.locked]
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
while replacement_locations and items_to_replace:
|
while replacement_locations and items_to_replace:
|
||||||
new_location = replacement_locations.pop()
|
|
||||||
old_location = items_to_replace.pop()
|
old_location = items_to_replace.pop()
|
||||||
|
for new_location in replacement_locations:
|
||||||
while not new_location.can_fill(state, old_location.item, False) or (
|
if new_location.can_fill(state, old_location.item, False) and \
|
||||||
new_location.item and not old_location.can_fill(state, new_location.item, False)):
|
old_location.can_fill(state, new_location.item, False):
|
||||||
replacement_locations.insert(0, new_location)
|
replacement_locations.remove(new_location)
|
||||||
new_location = replacement_locations.pop()
|
swap_location_item(old_location, new_location)
|
||||||
|
logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, "
|
||||||
swap_location_item(old_location, new_location)
|
f"displacing {old_location.item} into {old_location}")
|
||||||
logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, "
|
state.collect(new_location.item, True, new_location)
|
||||||
f"displacing {old_location.item} into {old_location}")
|
replaced_items = True
|
||||||
state.collect(new_location.item, True, new_location)
|
break
|
||||||
replaced_items = True
|
else:
|
||||||
|
logging.warning(f"Could not Progression Balance {old_location.item}")
|
||||||
|
|
||||||
if replaced_items:
|
if replaced_items:
|
||||||
unlocked = [fresh for player in balancing_players for fresh in unlocked_locations[player]]
|
unlocked = {fresh for player in balancing_players for fresh in unlocked_locations[player]}
|
||||||
for location in get_sphere_locations(state, unlocked):
|
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.add(location)
|
||||||
|
|
||||||
for location in sphere_locations:
|
for location in sphere_locations:
|
||||||
if location.event:
|
if location.event:
|
||||||
state.collect(location.item, True, location)
|
state.collect(location.item, True, location)
|
||||||
checked_locations.extend(sphere_locations)
|
checked_locations |= sphere_locations
|
||||||
|
|
||||||
if world.has_beaten_game(state):
|
if world.has_beaten_game(state):
|
||||||
break
|
break
|
||||||
|
@ -378,7 +385,8 @@ def distribute_planned(world):
|
||||||
set(world.player_ids) - {player}) if location.item_rule(item)
|
set(world.player_ids) - {player}) if location.item_rule(item)
|
||||||
)
|
)
|
||||||
if not unfilled:
|
if not unfilled:
|
||||||
placement.failed(f"Could not find a world with an unfilled location {placement.location}", FillError)
|
placement.failed(f"Could not find a world with an unfilled location {placement.location}",
|
||||||
|
FillError)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
target_world = world.random.choice(unfilled).player
|
target_world = world.random.choice(unfilled).player
|
||||||
|
@ -389,18 +397,22 @@ def distribute_planned(world):
|
||||||
set(world.player_ids)) if location.item_rule(item)
|
set(world.player_ids)) if location.item_rule(item)
|
||||||
)
|
)
|
||||||
if not unfilled:
|
if not unfilled:
|
||||||
placement.failed(f"Could not find a world with an unfilled location {placement.location}", FillError)
|
placement.failed(f"Could not find a world with an unfilled location {placement.location}",
|
||||||
|
FillError)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
target_world = world.random.choice(unfilled).player
|
target_world = world.random.choice(unfilled).player
|
||||||
|
|
||||||
elif type(target_world) == int: # target world by player id
|
elif type(target_world) == int: # target world by player id
|
||||||
if target_world not in range(1, world.players + 1):
|
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)
|
placement.failed(
|
||||||
|
f"Cannot place item in world {target_world} as it is not in range of (1, {world.players})",
|
||||||
|
ValueError)
|
||||||
continue
|
continue
|
||||||
else: # find world by name
|
else: # find world by name
|
||||||
if target_world not in world_name_lookup:
|
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)
|
placement.failed(f"Cannot place item to {target_world}'s world as that world does not exist.",
|
||||||
|
ValueError)
|
||||||
continue
|
continue
|
||||||
target_world = world_name_lookup[target_world]
|
target_world = world_name_lookup[target_world]
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue