From 24b01cfcc7c48639c48ecd76a2a8290945a21fc6 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 3 Aug 2021 09:26:41 -0600 Subject: [PATCH] Performance optimizations --- BaseClasses.py | 4 +++ EntranceShuffle.py | 7 ++-- Fill.py | 79 +++++++++++++++++++++++++++------------------- Main.py | 17 +++++----- Rom.py | 3 +- 5 files changed, 64 insertions(+), 46 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index b9f33267..087bda1b 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -434,8 +434,12 @@ class World(object): def can_beat_game(self, starting_state=None): if starting_state: + if self.has_beaten_game(starting_state): + return True state = starting_state.copy() else: + if self.has_beaten_game(self.state): + return True state = CollectionState(self) if self.has_beaten_game(state): diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 1c1a6ab8..7e5dcdd3 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -2120,10 +2120,11 @@ def connect_doors(world, doors, targets, player): """This works inplace""" random.shuffle(doors) random.shuffle(targets) - while doors: - door = doors.pop() - target = targets.pop() + placing = min(len(doors), len(targets)) + for door, target in zip(doors, targets): connect_entrance(world, door, target, player) + doors[:] = doors[placing:] + targets[:] = targets[placing:] def skull_woods_shuffle(world, player): diff --git a/Fill.py b/Fill.py index 04278d43..d1905daf 100644 --- a/Fill.py +++ b/Fill.py @@ -1,4 +1,6 @@ import RaceRandom as random +import collections +import itertools import logging from BaseClasses import CollectionState @@ -491,9 +493,8 @@ def sell_keys(world, player): def balance_multiworld_progression(world): state = CollectionState(world) - checked_locations = [] - unchecked_locations = world.get_locations().copy() - random.shuffle(unchecked_locations) + checked_locations = set() + unchecked_locations = set(world.get_locations()) reachable_locations_count = {} for player in range(1, world.players + 1): @@ -501,7 +502,7 @@ def balance_multiworld_progression(world): 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) and sphere_state.not_flooding_a_key(sphere_state.world, loc)] + return {loc for loc in locations if sphere_state.can_reach(loc) and sphere_state.not_flooding_a_key(sphere_state.world, loc)} while True: sphere_locations = get_sphere_locations(state, unchecked_locations) @@ -512,38 +513,42 @@ def balance_multiworld_progression(world): if checked_locations: threshold = max(reachable_locations_count.values()) - 20 - balancing_players = [player for player, reachables in reachable_locations_count.items() if reachables < threshold] - if balancing_players is not None and len(balancing_players) > 0: + 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 = [] + candidate_items = collections.defaultdict(set) 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) + player = location.item.player + if player in balancing_players and not location.locked and location.player != player: + candidate_items[player].add(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()]): + 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] + unlocked_locations = collections.defaultdict(set) + for l in unchecked_locations: + if l not in balancing_unchecked_locations: + unlocked_locations[l.player].add(l) 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] + locations_to_test = unlocked_locations[player] + items_to_test = candidate_items[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]: + for location in itertools.chain((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) @@ -557,33 +562,43 @@ def balance_multiworld_progression(world): items_to_replace.append(testing) replaced_items = False - replacement_locations = [l for l in checked_locations if not l.event and not l.locked] + # sort then shuffle to maintain deterministic behaviour, + # while allowing use of set for better algorithm growth behaviour elsewhere + replacement_locations = sorted(l for l in checked_locations if not l.event and not l.locked) + world.random.shuffle(replacement_locations) + items_to_replace.sort() + world.random.shuffle(items_to_replace) while replacement_locations and items_to_replace: - new_location = replacement_locations.pop() old_location = items_to_replace.pop() + for new_location in replacement_locations: + if (new_location.can_fill(state, old_location.item, False) and + old_location.can_fill(state, new_location.item, False)): + replacement_locations.remove(new_location) + new_location.item, old_location.item = old_location.item, new_location.item + if world.shopsanity[new_location.player]: + check_shop_swap(new_location) + if world.shopsanity[old_location.player]: + check_shop_swap(old_location) + 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} into {old_location}") + state.collect(new_location.item, True, new_location) + replaced_items = True + break + else: + logging.warning(f"Could not Progression Balance {old_location.item}") - 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() - - new_location.item, old_location.item = old_location.item, new_location.item - if world.shopsanity[new_location.player]: - check_shop_swap(new_location) - if world.shopsanity[old_location.player]: - check_shop_swap(old_location) - 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]): + unlocked = {fresh for player in balancing_players for fresh in unlocked_locations[player]} + for location in get_sphere_locations(state, unlocked): unchecked_locations.remove(location) reachable_locations_count[location.player] += 1 - sphere_locations.append(location) + sphere_locations.add(location) 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) + checked_locations |= sphere_locations if world.has_beaten_game(state): break diff --git a/Main.py b/Main.py index b43d099a..719d0cc5 100644 --- a/Main.py +++ b/Main.py @@ -541,11 +541,11 @@ def create_playthrough(world): while sphere_candidates: state.sweep_for_events(key_only=True) - sphere = [] + sphere = set() # build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres for location in sphere_candidates: if state.can_reach(location) and state.not_flooding_a_key(world, location): - sphere.append(location) + sphere.add(location) for location in sphere: sphere_candidates.remove(location) @@ -566,7 +566,7 @@ def create_playthrough(world): # in the second phase, we cull each sphere such that the game is still beatable, reducing each range of influence to the bare minimum required inside it for num, sphere in reversed(list(enumerate(collection_spheres))): - to_delete = [] + to_delete = set() for location in sphere: # we remove the item at location and check if game is still beatable logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player) @@ -576,15 +576,14 @@ def create_playthrough(world): world.clear_exp_cache() if world.can_beat_game(state_cache[num]): # logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is not required') - to_delete.append(location) + to_delete.add(location) else: # still required, got to keep it around # logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is required') location.item = old_item # cull entries in spheres for spoiler walkthrough at end - for location in to_delete: - sphere.remove(location) + sphere -= to_delete # second phase, sphere 0 for item in [i for i in world.precollected_items if i.advancement]: @@ -600,7 +599,7 @@ def create_playthrough(world): # used to access it was deemed not required.) So we need to do one final sphere collection pass # to build up the correct spheres - required_locations = [item for sphere in collection_spheres for item in sphere] + required_locations = {item for sphere in collection_spheres for item in sphere} state = CollectionState(world) collection_spheres = [] while required_locations: @@ -637,7 +636,7 @@ def create_playthrough(world): old_world.spoiler.paths = dict() for player in range(1, world.players + 1): old_world.spoiler.paths.update({location.gen_name(): get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere if location.player == player}) - for _, path in dict(old_world.spoiler.paths).items(): + for path in dict(old_world.spoiler.paths).values(): if any(exit == 'Pyramid Fairy' for (_, exit) in path): if world.mode[player] != 'inverted': old_world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state, world.get_region('Big Bomb Shop', player)) @@ -645,6 +644,6 @@ def create_playthrough(world): old_world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = get_path(state, world.get_region('Inverted Big Bomb Shop', player)) # we can finally output our playthrough - old_world.spoiler.playthrough = OrderedDict([("0", [str(item) for item in world.precollected_items if item.advancement])]) + old_world.spoiler.playthrough = {"0": [str(item) for item in world.precollected_items if item.advancement]} for i, sphere in enumerate(collection_spheres): old_world.spoiler.playthrough[str(i + 1)] = {location.gen_name(): str(location.item) for location in sphere} diff --git a/Rom.py b/Rom.py index 8d86a16e..564126fb 100644 --- a/Rom.py +++ b/Rom.py @@ -102,8 +102,7 @@ class LocalRom(object): self.buffer[address] = value def write_bytes(self, startaddress, values): - for i, value in enumerate(values): - self.write_byte(startaddress + i, value) + self.buffer[startaddress:startaddress + len(values)] = values def write_to_file(self, file): with open(file, 'wb') as outfile: