def generate_description( permalink: Permalink, status_update: Optional[Callable[[str], None]], validate_after_generation: bool, timeout: Optional[int] = 600, attempts: int = 15, ) -> LayoutDescription: """ Creates a LayoutDescription for the given Permalink. :param permalink: :param status_update: :param validate_after_generation: :param timeout: Abort generation after this many seconds. :param attempts: Attempt this many generations. :return: """ if status_update is None: status_update = id try: result = _async_create_description( permalink=permalink, status_update=status_update, attempts=attempts, ) except UnableToGenerate as e: raise GenerationFailure( "Could not generate a game with the given settings", permalink=permalink, source=e) from e if validate_after_generation and permalink.player_count == 1: with multiprocessing.dummy.Pool(1) as dummy_pool: resolve_params = { "configuration": permalink.presets[0].configuration, "patches": result.all_patches[0], "status_update": status_update, } final_state_async = dummy_pool.apply_async(func=resolver.resolve, kwds=resolve_params) try: final_state_by_resolve = final_state_async.get(timeout) except multiprocessing.TimeoutError as e: raise GenerationFailure( "Timeout reached when validating possibility", permalink=permalink, source=e) from e if final_state_by_resolve is None: # Why is final_state_by_distribution not OK? raise GenerationFailure( "Generated game was considered impossible by the solver", permalink=permalink, source=ImpossibleForSolver()) return result
async def generate_and_validate_description( generator_params: GeneratorParameters, status_update: Optional[Callable[[str], None]], validate_after_generation: bool, timeout: Optional[int] = 600, attempts: int = 15, ) -> LayoutDescription: """ Creates a LayoutDescription for the given Permalink. :param generator_params: :param status_update: :param validate_after_generation: :param timeout: Abort generation after this many seconds. :param attempts: Attempt this many generations. :return: """ if status_update is None: status_update = id try: result = await _create_description( generator_params=generator_params, status_update=status_update, attempts=attempts, ) except UnableToGenerate as e: raise GenerationFailure( "Could not generate a game with the given settings", generator_params=generator_params, source=e) from e if validate_after_generation and generator_params.player_count == 1: final_state_async = resolver.resolve( configuration=generator_params.get_preset(0).configuration, patches=result.all_patches[0], status_update=status_update, ) try: final_state_by_resolve = await asyncio.wait_for( final_state_async, timeout) except asyncio.TimeoutError as e: raise GenerationFailure( "Timeout reached when validating possibility", generator_params=generator_params, source=e) from e if final_state_by_resolve is None: raise GenerationFailure( "Generated game was considered impossible by the solver", generator_params=generator_params, source=ImpossibleForSolver()) return result
def _create_patches( permalink: Permalink, game: GameDescription, status_update: Callable[[str], None], ) -> GamePatches: rng = Random(permalink.as_str) configuration = permalink.layout_configuration categories = { "translator", "major", "energy_tank", "sky_temple_key", "temple_key" } item_pool = tuple(sorted(calculate_item_pool(permalink, game))) available_pickups = list( shuffle(rng, calculate_available_pickups(item_pool, categories, None))) if configuration.starting_resources.configuration == StartingResourcesConfiguration.CUSTOM: raise GenerationFailure("Custom StartingResources is unsupported", permalink) patches = _create_base_patches(rng, game, permalink, available_pickups) logic, state = logic_bootstrap(configuration, game, patches) logic.game.simplify_connections(state.resources) filler_patches = retcon_playthrough_filler(logic, state, tuple(available_pickups), rng, status_update) return filler_patches.assign_new_pickups( _indices_for_unassigned_pickups(rng, game, filler_patches.pickup_assignment, item_pool))
def _sky_temple_key_distribution_logic( permalink: Permalink, previous_patches: GamePatches, available_pickups: List[PickupEntry], ) -> GamePatches: mode = permalink.layout_configuration.sky_temple_keys new_assignments = {} if mode == LayoutSkyTempleKeyMode.VANILLA: locations_to_place = _FLYING_ING_CACHES[:] elif mode == LayoutSkyTempleKeyMode.ALL_BOSSES or mode == LayoutSkyTempleKeyMode.ALL_GUARDIANS: locations_to_place = _GUARDIAN_INDICES[:] if mode == LayoutSkyTempleKeyMode.ALL_BOSSES: locations_to_place += _SUB_GUARDIAN_INDICES elif mode == LayoutSkyTempleKeyMode.FULLY_RANDOM: locations_to_place = [] else: raise GenerationFailure("Unknown Sky Temple Key mode: {}".format(mode), permalink) for pickup in available_pickups[:]: if not locations_to_place: break if pickup.item_category == "sky_temple_key": available_pickups.remove(pickup) index = locations_to_place.pop(0) if index in previous_patches.pickup_assignment: raise GenerationFailure( "Attempted to place '{}' in {}, but there's already '{}' there" .format(pickup, index, previous_patches.pickup_assignment[index]), permalink) new_assignments[index] = pickup if locations_to_place: raise GenerationFailure( "Missing Sky Temple Keys in available_pickups to place in all requested boss places", permalink) return previous_patches.assign_pickup_assignment(new_assignments)
def calculate_item_pool( permalink: Permalink, game: GameDescription, ) -> List[PickupEntry]: item_pool: List[PickupEntry] = [] pickup_quantities = permalink.layout_configuration.pickup_quantities try: pickup_quantities.validate_total_quantities() except ValueError as e: raise GenerationFailure( "Invalid configuration: {}".format(e), permalink=permalink, ) quantities_pickups = set(pickup_quantities.pickups()) database_pickups = set(game.pickup_database.all_useful_pickups) if quantities_pickups != database_pickups: raise GenerationFailure( "Diverging pickups in configuration.\nPickups in quantities: {}\nPickups in database: {}" .format( [pickup.name for pickup in quantities_pickups], [pickup.name for pickup in database_pickups], ), permalink=permalink, ) for pickup, quantity in pickup_quantities.items(): item_pool.extend([pickup] * quantity) quantity_delta = len(item_pool) - game.pickup_database.total_pickup_count if quantity_delta > 0: raise GenerationFailure( "Invalid configuration: requested {} more items than available slots ({})." .format(quantity_delta, game.pickup_database.total_pickup_count), permalink=permalink, ) elif quantity_delta < 0: item_pool.extend([game.pickup_database.useless_pickup] * -quantity_delta) return item_pool
def test_sky_temple_key_distribution_logic_vanilla_missing_pickup(dataclass_test_lib, empty_patches): # Setup permalink = dataclass_test_lib.mock_dataclass(Permalink) permalink.layout_configuration.sky_temple_keys = LayoutSkyTempleKeyMode.VANILLA available_pickups = [] # Run with pytest.raises(GenerationFailure) as exp: generator._sky_temple_key_distribution_logic(permalink, empty_patches, available_pickups) assert exp.value == GenerationFailure( "Missing Sky Temple Keys in available_pickups to place in all requested boss places", permalink)
def test_sky_temple_key_distribution_logic_vanilla_used_location(dataclass_test_lib, sky_temple_keys, empty_patches): # Setup permalink = dataclass_test_lib.mock_dataclass(Permalink) permalink.layout_configuration.sky_temple_keys = LayoutSkyTempleKeyMode.VANILLA initial_pickup_assignment = { generator._FLYING_ING_CACHES[0]: PickupEntry("Other Item", tuple(), "other", 0) } patches = empty_patches.assign_new_pickups(initial_pickup_assignment.items()) # Run with pytest.raises(GenerationFailure) as exp: generator._sky_temple_key_distribution_logic(permalink, patches, [sky_temple_keys[0]]) assert exp.value == GenerationFailure( "Attempted to place '{}' in PickupIndex 45, but there's already 'Pickup Other Item' there".format( sky_temple_keys[0] ), permalink)
def create_failure(message: str): return GenerationFailure(message, permalink=permalink)