diff --git a/Fill.py b/Fill.py index 7bbeb18b..dbe6b342 100644 --- a/Fill.py +++ b/Fill.py @@ -305,16 +305,21 @@ def flood_items(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." - # Overall progression balancing algorithm: + # 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: 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: logging.info('Skipping multiworld progression balancing.') else: logging.info(f'Balancing multiworld progression for {len(balanceable_players)} Players.') + logging.debug(balanceable_players) state: CollectionState = CollectionState(world) checked_locations: typing.Set[Location] = set() 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 if len(world.get_filled_locations(player)) != 0 } - total_locations_count: Counter = 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]} + total_locations_count: typing.Counter[int] = Counter( + location.player + for location in world.get_locations() + if not location.locked + ) + balanceable_players = { + player: balanceable_players[player] + for player in balanceable_players + if total_locations_count[player] + } sphere_num: int = 1 moved_item_count: int = 0 @@ -359,13 +370,19 @@ def balance_multiworld_progression(world: MultiWorld) -> None: sphere_num += 1 if checked_locations: - # 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 - item_percentage(player, reachables) < threshold_percentage and player in balanceable_players} + max_percentage = max(map(lambda p: item_percentage(p, reachable_locations_count[p]), + reachable_locations_count)) + threshold_percentages = { + player: max_percentage * balanceable_players[player] + for 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: balancing_state = state.copy() balancing_unchecked_locations = unchecked_locations.copy() @@ -391,8 +408,9 @@ def balance_multiworld_progression(world: MultiWorld) -> None: if not location.locked: balancing_reachables[location.player] += 1 if world.has_beaten_game(balancing_state) or all( - item_percentage(player, reachables) >= threshold_percentage - for player, reachables in balancing_reachables.items()): + item_percentage(player, reachables) >= threshold_percentages[player] + for player, reachables in balancing_reachables.items() + if player in threshold_percentages): break elif not balancing_sphere: raise RuntimeError('Not all required items reachable. Something went terribly wrong here.') @@ -424,7 +442,7 @@ def balance_multiworld_progression(world: MultiWorld) -> None: else: reduced_sphere = get_sphere_locations(reducing_state, locations_to_test) 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) replaced_items = False diff --git a/Options.py b/Options.py index b18d74a6..5a96c620 100644 --- a/Options.py +++ b/Options.py @@ -572,8 +572,12 @@ class Accessibility(Choice): default = 1 -class ProgressionBalancing(DefaultOnToggle): - """A system that moves progression earlier, to try and prevent the player from getting stuck and bored early.""" +class ProgressionBalancing(Range): + """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" diff --git a/WebHostLib/static/assets/tutorial/Archipelago/advanced_settings_en.md b/WebHostLib/static/assets/tutorial/Archipelago/advanced_settings_en.md index 072a7873..734962fe 100644 --- a/WebHostLib/static/assets/tutorial/Archipelago/advanced_settings_en.md +++ b/WebHostLib/static/assets/tutorial/Archipelago/advanced_settings_en.md @@ -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. 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 - default. + 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. 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 - more triggers in the triggers guide. Triggers - guide: [Archipelago Triggers Guide](/tutorial/Archipelago/triggers/en) +* `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 + guide: [Archipelago Triggers Guide](/tutorial/Archipelago/triggers/en) ### Game Options @@ -198,8 +201,8 @@ triggers: * `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 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 - having things to do. +* `progression_balancing` is set on, giving it the default value, meaning we will likely receive important items + 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 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 diff --git a/WebHostLib/static/assets/tutorial/Minecraft/minecraft_es.md b/WebHostLib/static/assets/tutorial/Minecraft/minecraft_es.md index e827ac79..3f2df6e7 100644 --- a/WebHostLib/static/assets/tutorial/Minecraft/minecraft_es.md +++ b/WebHostLib/static/assets/tutorial/Minecraft/minecraft_es.md @@ -30,7 +30,7 @@ game: Minecraft # Opciones compartidas por todos los juegos: accessibility: locations -progression_balancing: on +progression_balancing: 50 # Opciones Especficicas para Minecraft Minecraft: diff --git a/WebHostLib/static/assets/tutorial/Minecraft/minecraft_sv.md b/WebHostLib/static/assets/tutorial/Minecraft/minecraft_sv.md index eb799024..e86d2939 100644 --- a/WebHostLib/static/assets/tutorial/Minecraft/minecraft_sv.md +++ b/WebHostLib/static/assets/tutorial/Minecraft/minecraft_sv.md @@ -80,7 +80,7 @@ description: Template Name name: YourName game: Minecraft accessibility: locations -progression_balancing: off +progression_balancing: 0 advancement_goal: few: 0 normal: 1 diff --git a/WebHostLib/static/assets/tutorial/Ocarina of Time/setup_en.md b/WebHostLib/static/assets/tutorial/Ocarina of Time/setup_en.md index 0c0f25b9..d12dbe43 100644 --- a/WebHostLib/static/assets/tutorial/Ocarina of Time/setup_en.md +++ b/WebHostLib/static/assets/tutorial/Ocarina of Time/setup_en.md @@ -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 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 -progression_balancing: - 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 - off: 0 # Turn this off if you don't mind a longer multiworld, or can glitch/sequence break around missing items. +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 + 0: 0 # Choose a lower number 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: logic_rules: # Set the logic used for the generator. glitchless: 50 diff --git a/WebHostLib/static/assets/tutorial/Ocarina of Time/setup_es.md b/WebHostLib/static/assets/tutorial/Ocarina of Time/setup_es.md index 193866fb..4e40d03d 100644 --- a/WebHostLib/static/assets/tutorial/Ocarina of Time/setup_es.md +++ b/WebHostLib/static/assets/tutorial/Ocarina of Time/setup_es.md @@ -52,9 +52,11 @@ accessibility: items: 0 # Garantiza que puedes obtener todos los objetos pero no todas las localizaciones locations: 50 # Garantiza que puedes obtener todas las localizaciones none: 0 # Solo garantiza que el juego pueda completarse. -progression_balancing: - on: 50 # Un sistema para reducir tiempos de espera en una partida multiworld - off: 0 +progression_balancing: # 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. + 25: 0 + 50: 50 + 99: 0 # Objetos importantes al principio del juego, para no esperar Ocarina of Time: logic_rules: # Logica usada por el randomizer. glitchless: 50 diff --git a/playerSettings.yaml b/playerSettings.yaml index 716b9fa5..ce26888c 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -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 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 -progression_balancing: - 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 - off: 0 # Turn this off if you don't mind a longer multiworld, or can glitch/sequence break around missing items. +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 + 0: 0 # Choose a lower number 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: ### Logic Section ### glitches_required: # Determine the logic required to complete the seed diff --git a/test/general/TestFill.py b/test/general/TestFill.py index 80b549a1..c7a3276d 100644 --- a/test/general/TestFill.py +++ b/test/general/TestFill.py @@ -584,7 +584,7 @@ class TestDistributeItemsRestrictive(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: if location.item and location.item == item: return True @@ -592,7 +592,7 @@ class TestBalanceMultiworldProgression(unittest.TestCase): self.fail("Expected " + region.name + " to contain " + item.name + "\n Contains" + str(list(map(lambda location: location.item, region.locations)))) - def setUp(self): + def setUp(self) -> None: multi_world = generate_multi_world(2) self.multi_world = multi_world player1 = generate_player_data( @@ -628,10 +628,10 @@ class TestBalanceMultiworldProgression(unittest.TestCase): items = fillRegion(multi_world, region, [ player2.prog_items[1]] + items) - multi_world.progression_balancing[player1.id] = True - multi_world.progression_balancing[player2.id] = True + def test_balances_progression(self) -> None: + 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.player1.regions[2], self.player2.prog_items[0]) @@ -640,7 +640,48 @@ class TestBalanceMultiworldProgression(unittest.TestCase): self.assertRegionContains( 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 balance_multiworld_progression(self.multi_world) diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index e00c6667..ccb739e9 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -244,7 +244,7 @@ def generate_itempool(world): world.push_item(world.get_location('Ganon', player), ItemFactory('Triforce', player), False) 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) world.push_item(loc, ItemFactory('Triforce Piece', player), False) world.treasure_hunt_count[player] = 1 diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 589766d4..7e16d6e5 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -30,7 +30,7 @@ def set_rules(world): # Set access rules according to max glitches for multiworld progression. # Set accessibility to none, and shuffle assuming the no logic players can always win world.accessibility[player] = world.accessibility[player].from_text("minimal") - world.progression_balancing[player].value = False + world.progression_balancing[player].value = 0 else: world.completion_condition[player] = lambda state: state.has('Triforce', player)