variable-progression-balancing (#356)

This commit is contained in:
Doug Hoskisson 2022-05-11 00:13:21 -07:00 committed by GitHub
parent a5ca118bbf
commit c085ee47ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 116 additions and 44 deletions

50
Fill.py
View File

@ -305,16 +305,21 @@ def flood_items(world: MultiWorld) -> None:
def balance_multiworld_progression(world: MultiWorld) -> None: def balance_multiworld_progression(world: MultiWorld) -> None:
# A system to reduce situations where players have no checks remaining, popularly known as "BK mode." # A system to reduce situations where players have no checks remaining, popularly known as "BK mode."
# Overall progression balancing algorithm: # Overall progression balancing algorithm:
# Gather up all locations in a sphere. # Gather up all locations in a sphere.
# Define a threshold value based on the player with the most available locations. # 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, # If other players are below the threshold value, swap progression in this sphere into earlier spheres,
# which gives more locations available by this sphere. # which gives more locations available by this sphere.
balanceable_players: typing.Set[int] = {player for player in world.player_ids if world.progression_balancing[player]} balanceable_players: typing.Dict[int, float] = {
player: world.progression_balancing[player] / 100
for player in world.player_ids
if world.progression_balancing[player] > 0
}
if not balanceable_players: if not balanceable_players:
logging.info('Skipping multiworld progression balancing.') logging.info('Skipping multiworld progression balancing.')
else: else:
logging.info(f'Balancing multiworld progression for {len(balanceable_players)} Players.') logging.info(f'Balancing multiworld progression for {len(balanceable_players)} Players.')
logging.debug(balanceable_players)
state: CollectionState = CollectionState(world) state: CollectionState = CollectionState(world)
checked_locations: typing.Set[Location] = set() checked_locations: typing.Set[Location] = set()
unchecked_locations: typing.Set[Location] = set(world.get_locations()) unchecked_locations: typing.Set[Location] = set(world.get_locations())
@ -324,10 +329,16 @@ def balance_multiworld_progression(world: MultiWorld) -> None:
for player in world.player_ids for player in world.player_ids
if len(world.get_filled_locations(player)) != 0 if len(world.get_filled_locations(player)) != 0
} }
total_locations_count: Counter = Counter(location.player for location in world.get_locations() if total_locations_count: typing.Counter[int] = Counter(
not location.locked) location.player
balanceable_players = {player for player in balanceable_players if for location in world.get_locations()
total_locations_count[player]} if not location.locked
)
balanceable_players = {
player: balanceable_players[player]
for player in balanceable_players
if total_locations_count[player]
}
sphere_num: int = 1 sphere_num: int = 1
moved_item_count: int = 0 moved_item_count: int = 0
@ -359,13 +370,19 @@ def balance_multiworld_progression(world: MultiWorld) -> None:
sphere_num += 1 sphere_num += 1
if checked_locations: if checked_locations:
# The 10% threshold can be modified for "progression balancing strength" max_percentage = max(map(lambda p: item_percentage(p, reachable_locations_count[p]),
# right now it approximates the old 20/216 bound. reachable_locations_count))
threshold_percentage = max(map(lambda p: item_percentage(p, reachable_locations_count[p]), threshold_percentages = {
reachable_locations_count)) - 0.10 player: max_percentage * balanceable_players[player]
logging.debug(f"Threshold: {threshold_percentage}") for player in balanceable_players
balancing_players = {player for player, reachables in reachable_locations_count.items() if }
item_percentage(player, reachables) < threshold_percentage and player in balanceable_players} logging.debug(f"Thresholds: {threshold_percentages}")
balancing_players = {
player
for player, reachables in reachable_locations_count.items()
if (player in threshold_percentages
and item_percentage(player, reachables) < threshold_percentages[player])
}
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()
@ -391,8 +408,9 @@ def balance_multiworld_progression(world: MultiWorld) -> None:
if not location.locked: 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(
item_percentage(player, reachables) >= threshold_percentage item_percentage(player, reachables) >= threshold_percentages[player]
for player, reachables in balancing_reachables.items()): for player, reachables in balancing_reachables.items()
if player in threshold_percentages):
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.')
@ -424,7 +442,7 @@ def balance_multiworld_progression(world: MultiWorld) -> None:
else: else:
reduced_sphere = get_sphere_locations(reducing_state, locations_to_test) reduced_sphere = get_sphere_locations(reducing_state, locations_to_test)
p = item_percentage(player, reachable_locations_count[player] + len(reduced_sphere)) p = item_percentage(player, reachable_locations_count[player] + len(reduced_sphere))
if p < threshold_percentage: if p < threshold_percentages[player]:
items_to_replace.append(testing) items_to_replace.append(testing)
replaced_items = False replaced_items = False

View File

@ -572,8 +572,12 @@ class Accessibility(Choice):
default = 1 default = 1
class ProgressionBalancing(DefaultOnToggle): class ProgressionBalancing(Range):
"""A system that moves progression earlier, to try and prevent the player from getting stuck and bored early.""" """A system that can move progression earlier, to try and prevent the player from getting stuck and bored early.
[0-99, default 50] A lower setting means more getting stuck. A higher setting means less getting stuck."""
default = 50
range_start = 0
range_end = 99
display_name = "Progression Balancing" display_name = "Progression Balancing"

View File

@ -85,12 +85,15 @@ games you want settings for.
* `progression_balancing` is a system the Archipelago generator uses to try and reduce "BK mode" as much as possible. * `progression_balancing` is a system the Archipelago generator uses to try and reduce "BK mode" as much as possible.
This primarily involves moving necessary progression items into earlier logic spheres to make the games more This primarily involves moving necessary progression items into earlier logic spheres to make the games more
accessible so that players almost always have something to do. This can be turned `on` or `off` and is `on` by accessible so that players almost always have something to do. This can be in a range from 0 to 99, and is 50 by
default. default. This number represents a percentage of the furthest progressible player.
* For example: With the default of 50%, if the furthest player can access 40% of their items, the randomizer tries
to let you access at least 20% of your items. 50% of 40% is 20%.
* Note that it is not always guaranteed that it will be able to bring you up to this threshold.
* `triggers` is one of the more advanced options that allows you to create conditional adjustments. You can read * `triggers` is one of the more advanced options that allows you to create conditional adjustments. You can read
more triggers in the triggers guide. Triggers more triggers in the triggers guide. Triggers
guide: [Archipelago Triggers Guide](/tutorial/Archipelago/triggers/en) guide: [Archipelago Triggers Guide](/tutorial/Archipelago/triggers/en)
### Game Options ### Game Options
@ -198,8 +201,8 @@ triggers:
* `requires` is set to require release version 0.2.0 or higher. * `requires` is set to require release version 0.2.0 or higher.
* `accesibility` is set to `none` which will set this seed to beatable only meaning some locations and items may be * `accesibility` is set to `none` which will set this seed to beatable only meaning some locations and items may be
completely inaccessible but the seed will still be completable. completely inaccessible but the seed will still be completable.
* `progression_balancing` is set on meaning we will likely receive important items earlier increasing the chance of * `progression_balancing` is set on, giving it the default value, meaning we will likely receive important items
having things to do. earlier increasing the chance of having things to do.
* `A Link to the Past` defines a location for us to nest all the game options we would like to use for our * `A Link to the Past` defines a location for us to nest all the game options we would like to use for our
game `A Link to the Past`. game `A Link to the Past`.
* `smallkey_shuffle` is an option for A Link to the Past which determines how dungeon small keys are shuffled. In this * `smallkey_shuffle` is an option for A Link to the Past which determines how dungeon small keys are shuffled. In this

View File

@ -30,7 +30,7 @@ game: Minecraft
# Opciones compartidas por todos los juegos: # Opciones compartidas por todos los juegos:
accessibility: locations accessibility: locations
progression_balancing: on progression_balancing: 50
# Opciones Especficicas para Minecraft # Opciones Especficicas para Minecraft
Minecraft: Minecraft:

View File

@ -80,7 +80,7 @@ description: Template Name
name: YourName name: YourName
game: Minecraft game: Minecraft
accessibility: locations accessibility: locations
progression_balancing: off progression_balancing: 0
advancement_goal: advancement_goal:
few: 0 few: 0
normal: 1 normal: 1

View File

@ -62,9 +62,11 @@ accessibility:
items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations
locations: 50 # Guarantees you will be able to access all locations, and therefore all items locations: 50 # Guarantees you will be able to access all locations, and therefore all items
none: 0 # Guarantees only that the game is beatable. You may not be able to access all locations or acquire all items none: 0 # Guarantees only that the game is beatable. You may not be able to access all locations or acquire all items
progression_balancing: progression_balancing: # A system to reduce BK, as in times during which you can't do anything, by moving your items into an earlier access sphere
on: 50 # A system to reduce BK, as in times during which you can't do anything by moving your items into an earlier access sphere to make it likely you have stuff to do 0: 0 # Choose a lower number if you don't mind a longer multiworld, or can glitch/sequence break around missing items.
off: 0 # Turn this off if you don't mind a longer multiworld, or can glitch/sequence break around missing items. 25: 0
50: 50 # Make it likely you have stuff to do.
99: 0 # Get important items early, and stay at the front of the progression.
Ocarina of Time: Ocarina of Time:
logic_rules: # Set the logic used for the generator. logic_rules: # Set the logic used for the generator.
glitchless: 50 glitchless: 50

View File

@ -52,9 +52,11 @@ accessibility:
items: 0 # Garantiza que puedes obtener todos los objetos pero no todas las localizaciones items: 0 # Garantiza que puedes obtener todos los objetos pero no todas las localizaciones
locations: 50 # Garantiza que puedes obtener todas las localizaciones locations: 50 # Garantiza que puedes obtener todas las localizaciones
none: 0 # Solo garantiza que el juego pueda completarse. none: 0 # Solo garantiza que el juego pueda completarse.
progression_balancing: progression_balancing: # Un sistema para reducir tiempos de espera en una partida multiworld
on: 50 # Un sistema para reducir tiempos de espera en una partida multiworld 0: 0 # Con un número más bajo, es más probable esperar objetos de otros jugadores.
off: 0 25: 0
50: 50
99: 0 # Objetos importantes al principio del juego, para no esperar
Ocarina of Time: Ocarina of Time:
logic_rules: # Logica usada por el randomizer. logic_rules: # Logica usada por el randomizer.
glitchless: 50 glitchless: 50

View File

@ -32,9 +32,11 @@ accessibility:
items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations
locations: 50 # Guarantees you will be able to access all locations, and therefore all items locations: 50 # Guarantees you will be able to access all locations, and therefore all items
none: 0 # Guarantees only that the game is beatable. You may not be able to access all locations or acquire all items none: 0 # Guarantees only that the game is beatable. You may not be able to access all locations or acquire all items
progression_balancing: progression_balancing: # A system to reduce BK, as in times during which you can't do anything, by moving your items into an earlier access sphere
on: 50 # A system to reduce BK, as in times during which you can't do anything by moving your items into an earlier access sphere to make it likely you have stuff to do 0: 0 # Choose a lower number if you don't mind a longer multiworld, or can glitch/sequence break around missing items.
off: 0 # Turn this off if you don't mind a longer multiworld, or can glitch/sequence break around missing items. 25: 0
50: 50 # Make it likely you have stuff to do.
99: 0 # Get important items early, and stay at the front of the progression.
A Link to the Past: A Link to the Past:
### Logic Section ### ### Logic Section ###
glitches_required: # Determine the logic required to complete the seed glitches_required: # Determine the logic required to complete the seed

View File

@ -584,7 +584,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
class TestBalanceMultiworldProgression(unittest.TestCase): class TestBalanceMultiworldProgression(unittest.TestCase):
def assertRegionContains(self, region: Region, item: Item): def assertRegionContains(self, region: Region, item: Item) -> bool:
for location in region.locations: for location in region.locations:
if location.item and location.item == item: if location.item and location.item == item:
return True return True
@ -592,7 +592,7 @@ class TestBalanceMultiworldProgression(unittest.TestCase):
self.fail("Expected " + region.name + " to contain " + item.name + self.fail("Expected " + region.name + " to contain " + item.name +
"\n Contains" + str(list(map(lambda location: location.item, region.locations)))) "\n Contains" + str(list(map(lambda location: location.item, region.locations))))
def setUp(self): def setUp(self) -> None:
multi_world = generate_multi_world(2) multi_world = generate_multi_world(2)
self.multi_world = multi_world self.multi_world = multi_world
player1 = generate_player_data( player1 = generate_player_data(
@ -628,10 +628,10 @@ class TestBalanceMultiworldProgression(unittest.TestCase):
items = fillRegion(multi_world, region, [ items = fillRegion(multi_world, region, [
player2.prog_items[1]] + items) player2.prog_items[1]] + items)
multi_world.progression_balancing[player1.id] = True def test_balances_progression(self) -> None:
multi_world.progression_balancing[player2.id] = True self.multi_world.progression_balancing[self.player1.id].value = 50
self.multi_world.progression_balancing[self.player2.id].value = 50
def test_balances_progression(self):
self.assertRegionContains( self.assertRegionContains(
self.player1.regions[2], self.player2.prog_items[0]) self.player1.regions[2], self.player2.prog_items[0])
@ -640,7 +640,48 @@ class TestBalanceMultiworldProgression(unittest.TestCase):
self.assertRegionContains( self.assertRegionContains(
self.player1.regions[1], self.player2.prog_items[0]) self.player1.regions[1], self.player2.prog_items[0])
def test_ignores_priority_locations(self): def test_balances_progression_light(self) -> None:
self.multi_world.progression_balancing[self.player1.id].value = 1
self.multi_world.progression_balancing[self.player2.id].value = 1
self.assertRegionContains(
self.player1.regions[2], self.player2.prog_items[0])
balance_multiworld_progression(self.multi_world)
# TODO: arrange for a result that's different from the default
self.assertRegionContains(
self.player1.regions[1], self.player2.prog_items[0])
def test_balances_progression_heavy(self) -> None:
self.multi_world.progression_balancing[self.player1.id].value = 99
self.multi_world.progression_balancing[self.player2.id].value = 99
self.assertRegionContains(
self.player1.regions[2], self.player2.prog_items[0])
balance_multiworld_progression(self.multi_world)
# TODO: arrange for a result that's different from the default
self.assertRegionContains(
self.player1.regions[1], self.player2.prog_items[0])
def test_skips_balancing_progression(self) -> None:
self.multi_world.progression_balancing[self.player1.id].value = 0
self.multi_world.progression_balancing[self.player2.id].value = 0
self.assertRegionContains(
self.player1.regions[2], self.player2.prog_items[0])
balance_multiworld_progression(self.multi_world)
self.assertRegionContains(
self.player1.regions[2], self.player2.prog_items[0])
def test_ignores_priority_locations(self) -> None:
self.multi_world.progression_balancing[self.player1.id].value = 50
self.multi_world.progression_balancing[self.player2.id].value = 50
self.player2.prog_items[0].location.progress_type = LocationProgressType.PRIORITY self.player2.prog_items[0].location.progress_type = LocationProgressType.PRIORITY
balance_multiworld_progression(self.multi_world) balance_multiworld_progression(self.multi_world)

View File

@ -244,7 +244,7 @@ def generate_itempool(world):
world.push_item(world.get_location('Ganon', player), ItemFactory('Triforce', player), False) world.push_item(world.get_location('Ganon', player), ItemFactory('Triforce', player), False)
if world.goal[player] == 'icerodhunt': if world.goal[player] == 'icerodhunt':
world.progression_balancing[player].value = False world.progression_balancing[player].value = 0
loc = world.get_location('Turtle Rock - Boss', player) loc = world.get_location('Turtle Rock - Boss', player)
world.push_item(loc, ItemFactory('Triforce Piece', player), False) world.push_item(loc, ItemFactory('Triforce Piece', player), False)
world.treasure_hunt_count[player] = 1 world.treasure_hunt_count[player] = 1

View File

@ -30,7 +30,7 @@ def set_rules(world):
# Set access rules according to max glitches for multiworld progression. # Set access rules according to max glitches for multiworld progression.
# Set accessibility to none, and shuffle assuming the no logic players can always win # Set accessibility to none, and shuffle assuming the no logic players can always win
world.accessibility[player] = world.accessibility[player].from_text("minimal") world.accessibility[player] = world.accessibility[player].from_text("minimal")
world.progression_balancing[player].value = False world.progression_balancing[player].value = 0
else: else:
world.completion_condition[player] = lambda state: state.has('Triforce', player) world.completion_condition[player] = lambda state: state.has('Triforce', player)