variable-progression-balancing (#356)
This commit is contained in:
parent
a5ca118bbf
commit
c085ee47ed
50
Fill.py
50
Fill.py
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue