turn progression balancing into a per-player option

This commit is contained in:
Fabian Dill 2020-05-18 03:54:29 +02:00
parent d24e5e1eeb
commit 11678fa20b
5 changed files with 109 additions and 98 deletions

View File

@ -112,6 +112,7 @@ class World(object):
set_player_attr('clock_mode', False)
set_player_attr('can_take_damage', True)
set_player_attr('glitch_boots', True)
set_player_attr('progression_balancing', True)
def get_name_string_for_object(self, obj) -> str:
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})'

View File

@ -323,7 +323,7 @@ def parse_arguments(argv, no_defaults=False):
'retro', 'accessibility', 'hints', 'beemizer',
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots',
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor',
'heartbeep',
'heartbeep', "skip_progression_balancing",
'remote_items', 'progressive', 'extendedmsu', 'dungeon_counters', 'glitch_boots']:
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
if player == 1:

177
Fill.py
View File

@ -324,98 +324,109 @@ def flood_items(world):
break
def balance_multiworld_progression(world):
state = CollectionState(world)
checked_locations = []
unchecked_locations = world.get_locations().copy()
random.shuffle(unchecked_locations)
balanceable_players = {player for player in range(1, world.players + 1) if world.progression_balancing[player]}
if not balanceable_players:
logging.info('Skipping multiworld progression balancing.')
else:
logging.info(f'Balancing multiworld progression for Players {balanceable_players}.')
state = CollectionState(world)
checked_locations = []
unchecked_locations = world.get_locations().copy()
random.shuffle(unchecked_locations)
reachable_locations_count = {}
for player in range(1, world.players + 1):
reachable_locations_count[player] = 0
reachable_locations_count = {player: 0 for player in range(1, world.players + 1)}
def get_sphere_locations(sphere_state, locations):
sphere_state.sweep_for_events(key_only=True, locations=locations)
return [loc for loc in locations if sphere_state.can_reach(loc)]
def get_sphere_locations(sphere_state, locations):
sphere_state.sweep_for_events(key_only=True, locations=locations)
return [loc for loc in locations if sphere_state.can_reach(loc)]
while True:
sphere_locations = get_sphere_locations(state, unchecked_locations)
for location in sphere_locations:
unchecked_locations.remove(location)
reachable_locations_count[location.player] += 1
while True:
sphere_locations = get_sphere_locations(state, unchecked_locations)
for location in sphere_locations:
unchecked_locations.remove(location)
reachable_locations_count[location.player] += 1
if checked_locations:
threshold = max(reachable_locations_count.values()) - 20
if checked_locations:
threshold = max(reachable_locations_count.values()) - 20
balancing_players = [player for player, reachables in reachable_locations_count.items() if
reachables < threshold and player in balanceable_players]
if balancing_players:
balancing_state = state.copy()
balancing_unchecked_locations = unchecked_locations.copy()
balancing_reachables = reachable_locations_count.copy()
balancing_sphere = sphere_locations.copy()
candidate_items = []
while True:
for location in balancing_sphere:
if location.event and (
world.keyshuffle[location.item.player] or not location.item.smallkey) and (
world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
balancing_state.collect(location.item, True, location)
if location.item.player in balancing_players and not location.locked:
candidate_items.append(location)
balancing_sphere = get_sphere_locations(balancing_state, balancing_unchecked_locations)
for location in balancing_sphere:
balancing_unchecked_locations.remove(location)
balancing_reachables[location.player] += 1
if world.has_beaten_game(balancing_state) or all(
[reachables >= threshold for reachables in balancing_reachables.values()]):
break
elif not balancing_sphere:
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
balancing_players = [player for player, reachables in reachable_locations_count.items() if reachables < threshold]
if balancing_players:
balancing_state = state.copy()
balancing_unchecked_locations = unchecked_locations.copy()
balancing_reachables = reachable_locations_count.copy()
balancing_sphere = sphere_locations.copy()
candidate_items = []
while True:
for location in balancing_sphere:
if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
balancing_state.collect(location.item, True, location)
if location.item.player in balancing_players and not location.locked:
candidate_items.append(location)
balancing_sphere = get_sphere_locations(balancing_state, balancing_unchecked_locations)
for location in balancing_sphere:
balancing_unchecked_locations.remove(location)
balancing_reachables[location.player] += 1
if world.has_beaten_game(balancing_state) or all([reachables >= threshold for reachables in balancing_reachables.values()]):
break
elif not balancing_sphere:
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
unlocked_locations = [l for l in unchecked_locations if l not in balancing_unchecked_locations]
items_to_replace = []
for player in balancing_players:
locations_to_test = [l for l in unlocked_locations if l.player == player]
# only replace items that end up in another player's world
items_to_test = [l for l in candidate_items if l.item.player == player and l.player != player]
while items_to_test:
testing = items_to_test.pop()
reducing_state = state.copy()
for location in [*[l for l in items_to_replace if l.item.player == player], *items_to_test]:
reducing_state.collect(location.item, True, location)
unlocked_locations = [l for l in unchecked_locations if l not in balancing_unchecked_locations]
items_to_replace = []
for player in balancing_players:
locations_to_test = [l for l in unlocked_locations if l.player == player]
# only replace items that end up in another player's world
items_to_test = [l for l in candidate_items if l.item.player == player and l.player != player]
while items_to_test:
testing = items_to_test.pop()
reducing_state = state.copy()
for location in [*[l for l in items_to_replace if l.item.player == player], *items_to_test]:
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)
if world.has_beaten_game(balancing_state):
if not world.has_beaten_game(reducing_state):
items_to_replace.append(testing)
else:
reduced_sphere = get_sphere_locations(reducing_state, locations_to_test)
if reachable_locations_count[player] + len(reduced_sphere) < threshold:
items_to_replace.append(testing)
if world.has_beaten_game(balancing_state):
if not world.has_beaten_game(reducing_state):
items_to_replace.append(testing)
else:
reduced_sphere = get_sphere_locations(reducing_state, locations_to_test)
if reachable_locations_count[player] + len(reduced_sphere) < threshold:
items_to_replace.append(testing)
replaced_items = False
replacement_locations = [l for l in checked_locations if not l.event and not l.locked]
while replacement_locations and items_to_replace:
new_location = replacement_locations.pop()
old_location = items_to_replace.pop()
while not new_location.can_fill(state, old_location.item, False) or (new_location.item and not old_location.can_fill(state, new_location.item, False)):
replacement_locations.insert(0, new_location)
replaced_items = False
replacement_locations = [l for l in checked_locations if not l.event and not l.locked]
while replacement_locations and items_to_replace:
new_location = replacement_locations.pop()
old_location = items_to_replace.pop()
new_location.item, old_location.item = old_location.item, new_location.item
new_location.event, old_location.event = True, False
state.collect(new_location.item, True, new_location)
replaced_items = True
if replaced_items:
for location in get_sphere_locations(state, [l for l in unlocked_locations if l.player in balancing_players]):
unchecked_locations.remove(location)
reachable_locations_count[location.player] += 1
sphere_locations.append(location)
while not new_location.can_fill(state, old_location.item, False) or (
new_location.item and not old_location.can_fill(state, new_location.item, False)):
replacement_locations.insert(0, new_location)
new_location = replacement_locations.pop()
for location in sphere_locations:
if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
state.collect(location.item, True, location)
checked_locations.extend(sphere_locations)
new_location.item, old_location.item = old_location.item, new_location.item
new_location.event, old_location.event = True, False
logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, "
f"displacing {old_location.item} in {old_location}")
state.collect(new_location.item, True, new_location)
replaced_items = True
if replaced_items:
for location in get_sphere_locations(state, [l for l in unlocked_locations if
l.player in balancing_players]):
unchecked_locations.remove(location)
reachable_locations_count[location.player] += 1
sphere_locations.append(location)
if world.has_beaten_game(state):
break
elif not sphere_locations:
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
for location in sphere_locations:
if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (
world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
state.collect(location.item, True, location)
checked_locations.extend(sphere_locations)
if world.has_beaten_game(state):
break
elif not sphere_locations:
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')

View File

@ -59,6 +59,7 @@ def main(args, seed=None):
world.dungeon_counters = args.dungeon_counters.copy()
world.extendedmsu = args.extendedmsu.copy()
world.glitch_boots = args.glitch_boots.copy()
world.progression_balancing = {player: not balance for player, balance in args.skip_progression_balancing.items()}
world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)}
@ -145,8 +146,7 @@ def main(args, seed=None):
elif args.algorithm == 'balanced':
distribute_items_restrictive(world, True)
if world.players > 1 and not args.skip_progression_balancing:
logger.info('Balancing multiworld progression.')
if world.players > 1:
balance_multiworld_progression(world)
logger.info('Patching ROM.')

View File

@ -62,7 +62,6 @@ def main():
except Exception as e:
raise ValueError(f"File {args.weights} is destroyed. Please fix your yaml.") from e
print(f"Weights: {args.weights} >> {weights_cache[args.weights]['description']}")
progression_balancing = True
if args.meta:
try:
weights_cache[args.meta] = get_weights(args.meta)
@ -72,8 +71,6 @@ def main():
print(f"Meta: {args.meta} >> {meta_weights['meta_description']}")
if args.samesettings:
raise Exception("Cannot mix --samesettings with --meta")
if "progression_balancing" in meta_weights:
progression_balancing = get_choice("progression_balancing", meta_weights)
for player in range(1, args.multi + 1):
path = getattr(args, f'p{player}')
@ -86,7 +83,6 @@ def main():
except Exception as e:
raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e
erargs = parse_arguments(['--multi', str(args.multi)])
erargs.skip_progression_balancing = not progression_balancing
erargs.seed = seed
erargs.name = {x: "" for x in range(1, args.multi + 1)} # only so it can be overwrittin in mystery
erargs.create_spoiler = args.create_spoiler
@ -94,6 +90,7 @@ def main():
erargs.outputname = seedname
erargs.outputpath = args.outputpath
erargs.teams = args.teams
erargs.progression_balancing = {}
# set up logger
if args.loglevel:
@ -126,7 +123,6 @@ def main():
logging.basicConfig(format='%(message)s', level=loglevel, filename=os.path.join(args.log_output_path, f"{seed}.log"))
else:
logging.basicConfig(format='%(message)s', level=loglevel)
logging.info(progression_balancing)
if args.rom:
erargs.rom = args.rom
@ -146,12 +142,11 @@ def main():
option = get_choice(key, meta_weights)
if option is not None:
for player, path in player_path_cache.items():
players_meta = weights_cache[path]["meta_ignore"]
if players_meta:
if key not in players_meta:
weights_cache[path][key] = option
elif type(players_meta) == dict and players_meta[key] and option not in players_meta[key]:
weights_cache[path][key] = option
players_meta = weights_cache[path].get("meta_ignore", [])
if key not in players_meta:
weights_cache[path][key] = option
elif type(players_meta) == dict and players_meta[key] and option not in players_meta[key]:
weights_cache[path][key] = option
for player in range(1, args.multi + 1):
path = player_path_cache[player]
@ -197,6 +192,9 @@ def main():
with open(os.path.join(args.outputpath if args.outputpath else ".", f"mystery_result_{seed}.yaml"), "wt") as f:
yaml.dump(important, f)
erargs.skip_progression_balancing = {player: not balanced for player, balanced in
erargs.progression_balancing.items()}
del (erargs.progression_balancing)
ERmain(erargs, seed)
@ -255,7 +253,8 @@ def roll_settings(weights):
glitches_required = 'none'
ret.logic = {None: 'noglitches', 'none': 'noglitches', 'no_logic': 'nologic', 'overworld_glitches': 'owglitches'}[
glitches_required]
ret.progression_balancing = get_choice('progression_balancing',
weights) if 'progression_balancing' in weights else True
# item_placement = get_choice('item_placement')
# not supported in ER