diff --git a/BaseClasses.py b/BaseClasses.py index 421fbf88..f2a57a6f 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -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)})' diff --git a/EntranceRandomizer.py b/EntranceRandomizer.py index 4a93bb85..1e405c8f 100755 --- a/EntranceRandomizer.py +++ b/EntranceRandomizer.py @@ -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: diff --git a/Fill.py b/Fill.py index b17f16da..7aff1325 100644 --- a/Fill.py +++ b/Fill.py @@ -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.') diff --git a/Main.py b/Main.py index e34524a1..b2be128e 100644 --- a/Main.py +++ b/Main.py @@ -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.') diff --git a/Mystery.py b/Mystery.py index 22eac6c6..667be8a9 100644 --- a/Mystery.py +++ b/Mystery.py @@ -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