Update progression balancing algorithm (#300)
* New progression balancing algo: computes based on percentage of locations available rather than absolute number of locations
This commit is contained in:
parent
3ce5d14210
commit
15e0763ed5
|
@ -1093,6 +1093,8 @@ class Item():
|
||||||
trap: bool = False
|
trap: bool = False
|
||||||
# change manually to ensure that a specific non-progression item never goes on an excluded location
|
# change manually to ensure that a specific non-progression item never goes on an excluded location
|
||||||
never_exclude = False
|
never_exclude = False
|
||||||
|
# item is not considered by progression balancing despite being progression
|
||||||
|
skip_in_prog_balancing: bool = False
|
||||||
|
|
||||||
# need to find a decent place for these to live and to allow other games to register texts if they want.
|
# need to find a decent place for these to live and to allow other games to register texts if they want.
|
||||||
pedestal_credit_text: str = "and the Unknown Item"
|
pedestal_credit_text: str = "and the Unknown Item"
|
||||||
|
|
41
Fill.py
41
Fill.py
|
@ -301,6 +301,12 @@ def flood_items(world: MultiWorld):
|
||||||
|
|
||||||
|
|
||||||
def balance_multiworld_progression(world: MultiWorld):
|
def balance_multiworld_progression(world: MultiWorld):
|
||||||
|
# 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.
|
||||||
balanceable_players = {player for player in range(1, world.players + 1) if world.progression_balancing[player]}
|
balanceable_players = {player for player in range(1, world.players + 1) if world.progression_balancing[player]}
|
||||||
if not balanceable_players:
|
if not balanceable_players:
|
||||||
logging.info('Skipping multiworld progression balancing.')
|
logging.info('Skipping multiworld progression balancing.')
|
||||||
|
@ -311,21 +317,37 @@ def balance_multiworld_progression(world: MultiWorld):
|
||||||
unchecked_locations = set(world.get_locations())
|
unchecked_locations = set(world.get_locations())
|
||||||
|
|
||||||
reachable_locations_count = {player: 0 for player in world.get_all_ids()}
|
reachable_locations_count = {player: 0 for player in world.get_all_ids()}
|
||||||
|
total_locations_count = {player: sum(1 for loc in world.get_locations() if not loc.locked and loc.player == player) for player in world.player_ids}
|
||||||
|
sphere_num = 1
|
||||||
|
moved_item_count = 0
|
||||||
|
|
||||||
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)}
|
||||||
|
|
||||||
|
def item_percentage(player, num):
|
||||||
|
return num / total_locations_count[player]
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
# Gather non-locked locations. This ensures that only shuffled locations get counted for progression balancing,
|
||||||
|
# i.e. the items the players will be checking.
|
||||||
sphere_locations = get_sphere_locations(state, unchecked_locations)
|
sphere_locations = get_sphere_locations(state, unchecked_locations)
|
||||||
for location in sphere_locations:
|
for location in sphere_locations:
|
||||||
unchecked_locations.remove(location)
|
unchecked_locations.remove(location)
|
||||||
|
if not location.locked:
|
||||||
reachable_locations_count[location.player] += 1
|
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
|
||||||
|
|
||||||
if checked_locations:
|
if checked_locations:
|
||||||
threshold = max(reachable_locations_count.values()) - 20
|
# 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}")
|
||||||
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}
|
item_percentage(player, reachables) < threshold_percentage 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()
|
||||||
|
@ -333,25 +355,30 @@ def balance_multiworld_progression(world: MultiWorld):
|
||||||
balancing_sphere = sphere_locations.copy()
|
balancing_sphere = sphere_locations.copy()
|
||||||
candidate_items = collections.defaultdict(set)
|
candidate_items = collections.defaultdict(set)
|
||||||
while True:
|
while True:
|
||||||
|
# Check locations in the current sphere and gather progression items to swap earlier
|
||||||
for location in balancing_sphere:
|
for location in balancing_sphere:
|
||||||
if location.event:
|
if location.event:
|
||||||
balancing_state.collect(location.item, True, location)
|
balancing_state.collect(location.item, True, location)
|
||||||
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
|
if (not location.locked and not location.item.skip_in_prog_balancing and
|
||||||
player in balancing_players and
|
player in balancing_players and
|
||||||
location.player != player and
|
location.player != player and
|
||||||
location.progress_type != LocationProgressType.PRIORITY):
|
location.progress_type != LocationProgressType.PRIORITY):
|
||||||
candidate_items[player].add(location)
|
candidate_items[player].add(location)
|
||||||
|
logging.debug(f"Candidate item: {location.name}, {location.item.name}")
|
||||||
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)
|
||||||
|
if not location.locked:
|
||||||
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()):
|
item_percentage(player, reachables) >= threshold_percentage
|
||||||
|
for player, reachables in balancing_reachables.items()):
|
||||||
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.')
|
||||||
|
# Gather a set of locations which we can swap items into
|
||||||
unlocked_locations = collections.defaultdict(set)
|
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:
|
||||||
|
@ -374,7 +401,7 @@ def balance_multiworld_progression(world: MultiWorld):
|
||||||
items_to_replace.append(testing)
|
items_to_replace.append(testing)
|
||||||
else:
|
else:
|
||||||
reduced_sphere = get_sphere_locations(reducing_state, locations_to_test)
|
reduced_sphere = get_sphere_locations(reducing_state, locations_to_test)
|
||||||
if reachable_locations_count[player] + len(reduced_sphere) < threshold:
|
if item_percentage(player, reachable_locations_count[player] + len(reduced_sphere)) < threshold_percentage:
|
||||||
items_to_replace.append(testing)
|
items_to_replace.append(testing)
|
||||||
|
|
||||||
replaced_items = False
|
replaced_items = False
|
||||||
|
@ -386,6 +413,7 @@ def balance_multiworld_progression(world: MultiWorld):
|
||||||
items_to_replace.sort()
|
items_to_replace.sort()
|
||||||
world.random.shuffle(items_to_replace)
|
world.random.shuffle(items_to_replace)
|
||||||
|
|
||||||
|
# Start swapping items. Since we swap into earlier spheres, no need for accessibility checks.
|
||||||
while replacement_locations and items_to_replace:
|
while replacement_locations and items_to_replace:
|
||||||
old_location = items_to_replace.pop()
|
old_location = items_to_replace.pop()
|
||||||
for new_location in replacement_locations:
|
for new_location in replacement_locations:
|
||||||
|
@ -395,6 +423,7 @@ def balance_multiworld_progression(world: MultiWorld):
|
||||||
swap_location_item(old_location, new_location)
|
swap_location_item(old_location, new_location)
|
||||||
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} into {old_location}")
|
f"displacing {old_location.item} into {old_location}")
|
||||||
|
moved_item_count += 1
|
||||||
state.collect(new_location.item, True, new_location)
|
state.collect(new_location.item, True, new_location)
|
||||||
replaced_items = True
|
replaced_items = True
|
||||||
break
|
break
|
||||||
|
@ -402,9 +431,11 @@ def balance_multiworld_progression(world: MultiWorld):
|
||||||
logging.warning(f"Could not Progression Balance {old_location.item}")
|
logging.warning(f"Could not Progression Balance {old_location.item}")
|
||||||
|
|
||||||
if replaced_items:
|
if replaced_items:
|
||||||
|
logging.debug(f"Moved {moved_item_count} items so far\n")
|
||||||
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)
|
||||||
|
if not location.locked:
|
||||||
reachable_locations_count[location.player] += 1
|
reachable_locations_count[location.player] += 1
|
||||||
sphere_locations.add(location)
|
sphere_locations.add(location)
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,8 @@ class OOTItem(Item):
|
||||||
self.trap = name == 'Ice Trap'
|
self.trap = name == 'Ice Trap'
|
||||||
if force_not_advancement:
|
if force_not_advancement:
|
||||||
self.never_exclude = True
|
self.never_exclude = True
|
||||||
|
if name == 'Gold Skulltula Token':
|
||||||
|
self.skip_in_prog_balancing = True
|
||||||
|
|
||||||
# The playthrough calculation calls a function that uses "sweep_for_events(key_only=True)"
|
# The playthrough calculation calls a function that uses "sweep_for_events(key_only=True)"
|
||||||
# This checks if the item it's looking for is a small key, using the small key property.
|
# This checks if the item it's looking for is a small key, using the small key property.
|
||||||
|
|
|
@ -41,6 +41,8 @@ class SM64World(World):
|
||||||
def create_item(self, name: str) -> Item:
|
def create_item(self, name: str) -> Item:
|
||||||
item_id = item_table[name]
|
item_id = item_table[name]
|
||||||
item = SM64Item(name, name != "1Up Mushroom", item_id, self.player)
|
item = SM64Item(name, name != "1Up Mushroom", item_id, self.player)
|
||||||
|
if name == "Power Star":
|
||||||
|
item.skip_in_prog_balancing = True
|
||||||
return item
|
return item
|
||||||
|
|
||||||
def generate_basic(self):
|
def generate_basic(self):
|
||||||
|
|
Loading…
Reference in New Issue