diff --git a/test/general/test_implemented.py b/test/general/test_implemented.py index e76d5394..756cfa8b 100644 --- a/test/general/test_implemented.py +++ b/test/general/test_implemented.py @@ -52,3 +52,68 @@ class TestImplemented(unittest.TestCase): def test_no_failed_world_loads(self): if failed_world_loads: self.fail(f"The following worlds failed to load: {failed_world_loads}") + + def test_explicit_indirect_conditions_spheres(self): + """Tests that worlds using explicit indirect conditions produce identical spheres as when using implicit + indirect conditions""" + # Because the iteration order of blocked_connections in CollectionState.update_reachable_regions() is + # nondeterministic, this test may sometimes pass with the same seed even when there are missing indirect + # conditions. + for game_name, world_type in AutoWorldRegister.world_types.items(): + multiworld = setup_solo_multiworld(world_type) + world = multiworld.get_game_worlds(game_name)[0] + if not world.explicit_indirect_conditions: + # The world does not use explicit indirect conditions, so it can be skipped. + continue + # The world may override explicit_indirect_conditions as a property that cannot be set, so try modifying it. + try: + world.explicit_indirect_conditions = False + world.explicit_indirect_conditions = True + except Exception: + # Could not modify the attribute, so skip this world. + with self.subTest(game=game_name, skipped="world.explicit_indirect_conditions could not be set"): + continue + with self.subTest(game=game_name, seed=multiworld.seed): + distribute_items_restrictive(multiworld) + call_all(multiworld, "post_fill") + + # Note: `multiworld.get_spheres()` iterates a set of locations, so the order that locations are checked + # is nondeterministic and may vary between runs with the same seed. + explicit_spheres = list(multiworld.get_spheres()) + # Disable explicit indirect conditions and produce a second list of spheres. + world.explicit_indirect_conditions = False + implicit_spheres = list(multiworld.get_spheres()) + + # Both lists should be identical. + if explicit_spheres == implicit_spheres: + # Test passed. + continue + + # Find the first sphere that was different and provide a useful failure message. + zipped = zip(explicit_spheres, implicit_spheres) + for sphere_num, (sphere_explicit, sphere_implicit) in enumerate(zipped, start=1): + # Each sphere created with explicit indirect conditions should be identical to the sphere created + # with implicit indirect conditions. + if sphere_explicit != sphere_implicit: + reachable_only_with_implicit = sorted(sphere_implicit - sphere_explicit) + if reachable_only_with_implicit: + locations_and_parents = [(loc, loc.parent_region) for loc in reachable_only_with_implicit] + self.fail(f"Sphere {sphere_num} created with explicit indirect conditions did not contain" + f" the same locations as sphere {sphere_num} created with implicit indirect" + f" conditions. There may be missing indirect conditions for connections to the" + f" locations' parent regions or connections from other regions which connect to" + f" these regions." + f"\nLocations that should have been reachable in sphere {sphere_num} and their" + f" parent regions:" + f"\n{locations_and_parents}") + else: + # Some locations were only present in the sphere created with explicit indirect conditions. + # This should not happen because missing indirect conditions should only reduce + # accessibility, not increase accessibility. + reachable_only_with_explicit = sorted(sphere_explicit - sphere_implicit) + self.fail(f"Sphere {sphere_num} created with explicit indirect conditions contained more" + f" locations than sphere {sphere_num} created with implicit indirect conditions." + f" This should not happen." + f"\nUnexpectedly reachable locations in sphere {sphere_num}:" + f"\n{reachable_only_with_explicit}") + self.fail("Unreachable")