Beispiel #1
0
def test_round_trip(spoiler: bool, layout: dict, default_echoes_preset,
                    mocker):
    # Setup
    random_uuid = uuid.uuid4()
    mocker.patch("uuid.uuid4", return_value=random_uuid)

    preset = Preset(
        name="{} Custom".format(default_echoes_preset.game.long_name),
        description="A customized preset.",
        uuid=random_uuid,
        base_preset_uuid=default_echoes_preset.uuid,
        game=default_echoes_preset.game,
        configuration=dataclasses.replace(default_echoes_preset.configuration,
                                          **layout),
    )

    params = GeneratorParameters(
        seed_number=1000,
        spoiler=spoiler,
        presets=[preset],
    )

    # Run
    after = GeneratorParameters.from_bytes(params.as_bytes)

    # Assert
    assert params == after
Beispiel #2
0
def batch_distribute_helper(base_params: "GeneratorParameters",
                            seed_number: int,
                            timeout: int,
                            validate: bool,
                            output_dir: Path,
                            ) -> float:
    from randovania.generator import generator
    from randovania.layout.permalink import GeneratorParameters
    assert isinstance(base_params, GeneratorParameters)

    permalink = GeneratorParameters(
        seed_number=seed_number,
        spoiler=True,
        presets=base_params.presets,
    )

    start_time = time.perf_counter()
    description = asyncio.run(generator.generate_and_validate_description(
        generator_params=permalink, status_update=None,
        validate_after_generation=validate, timeout=timeout,
        attempts=0,
    ))
    delta_time = time.perf_counter() - start_time

    description.save_to_file(output_dir.joinpath("{}.{}".format(seed_number, description.file_extension())))
    return delta_time
Beispiel #3
0
    def from_json_dict(cls, json_dict: dict) -> "LayoutDescription":
        json_dict = description_migration.convert_to_current_version(json_dict)

        if "game_modifications" not in json_dict:
            raise ValueError("Unable to read details of a race game file")

        generator_parameters = GeneratorParameters(
            seed_number=json_dict["info"]["seed"],
            spoiler=json_dict["info"]["has_spoiler"],
            presets=[
                VersionedPreset(preset).get_preset()
                for preset in json_dict["info"]["presets"]
            ],
        )

        return LayoutDescription(
            randovania_version_text=json_dict["info"]["randovania_version"],
            randovania_version_git=bytes.fromhex(
                json_dict["info"]["randovania_version_git"]),
            generator_parameters=generator_parameters,
            all_patches=game_patches_serializer.decode(
                json_dict["game_modifications"], {
                    index: preset.configuration
                    for index, preset in enumerate(
                        generator_parameters.presets)
                }),
            item_order=json_dict["item_order"],
        )
Beispiel #4
0
def create_permalink(args):
    from randovania.layout.permalink import Permalink
    from randovania.layout.generator_parameters import GeneratorParameters
    from randovania.interface_common.preset_manager import PresetManager

    game: RandovaniaGame = RandovaniaGame(args.game)
    preset_manager = PresetManager(None)
    presets = []
    for preset_name in args.preset_name:
        versioned = preset_manager.included_preset_with(game, preset_name)
        if versioned is None:
            raise ValueError(
                "Unknown included preset '{}' for game {}. Valid options are: {}"
                .format(preset_name, game.long_name, [
                    preset.name
                    for preset in preset_manager.included_presets.values()
                    if preset.game == game
                ]))
        presets.append(versioned.get_preset())

    seed = args.seed_number
    if seed is None:
        seed = random.randint(0, 2**31)

    return Permalink.from_parameters(
        GeneratorParameters(
            seed,
            spoiler=not args.race,
            presets=presets,
        ), )
Beispiel #5
0
async def _create_description(generator_params: GeneratorParameters,
                              status_update: Callable[[str], None],
                              attempts: int,
                              ) -> LayoutDescription:
    """
    :param generator_params:
    :param status_update:
    :return:
    """
    rng = Random(generator_params.as_bytes)

    presets = [
        generator_params.get_preset(i)
        for i in range(generator_params.player_count)
    ]

    retrying = tenacity.AsyncRetrying(
        stop=tenacity.stop_after_attempt(attempts),
        retry=tenacity.retry_if_exception_type(UnableToGenerate),
        reraise=True
    )

    filler_results = await retrying(_create_pools_and_fill, rng, presets, status_update)

    all_patches = _distribute_remaining_items(rng, filler_results.player_results)
    return LayoutDescription.create_new(
        generator_parameters=generator_params,
        all_patches=all_patches,
        item_order=filler_results.action_log,
    )
def test_generate_logic(no_retry: bool, preset_name: Optional[str],
                        repeat: int, mocker, preset_manager):
    # Setup
    mock_generate: AsyncMock = mocker.patch(
        "randovania.generator.generator.generate_and_validate_description",
        new_callable=AsyncMock)
    mock_generate.return_value = MagicMock()
    mock_from_str: MagicMock = mocker.patch(
        "randovania.layout.permalink.Permalink.from_str", autospec=True)

    args = MagicMock()
    args.output_file = Path("asdfasdf/qwerqwerqwer/zxcvzxcv.json")
    args.no_retry = no_retry
    args.repeat = repeat

    if preset_name is None:
        # Permalink
        args.permalink = "<the permalink>"
    else:
        args.game = RandovaniaGame.METROID_PRIME_ECHOES.value
        args.preset_name = [preset_name]
        args.seed_number = 0
        args.race = False

    extra_args = {}
    if no_retry:
        extra_args["attempts"] = 0

    if preset_name is None:
        generator_params: GeneratorParameters = mock_from_str.return_value.parameters
    else:
        args.permalink = None
        preset = preset_manager.included_preset_with(
            RandovaniaGame.METROID_PRIME_ECHOES, preset_name).get_preset()
        generator_params = GeneratorParameters(0, True, [preset])

    # Run
    if preset_name is None:
        randovania.cli.commands.generate.generate_from_permalink_logic(args)
    else:
        randovania.cli.commands.generate.generate_from_preset_logic(args)

    # Assert
    if preset_name is None:
        mock_from_str.assert_called_once_with(args.permalink)
    else:
        mock_from_str.assert_not_called()

    mock_generate.assert_has_awaits([
        call(
            generator_params=generator_params,
            status_update=ANY,
            validate_after_generation=args.validate,
            timeout=None,
            **extra_args,
        )
    ] * repeat)

    save_file_mock: MagicMock = mock_generate.return_value.save_to_file
    save_file_mock.assert_called_once_with(args.output_file)
Beispiel #7
0
async def test_round_trip_generated_patches(default_preset):
    # Setup
    preset = dataclasses.replace(
        default_preset,
        uuid=uuid.UUID('b41fde84-1f57-4b79-8cd6-3e5a78077fa6'),
        base_preset_uuid=default_preset.uuid,
        configuration=dataclasses.replace(default_preset.configuration,
                                          trick_level=TrickLevelConfiguration(
                                              minimal_logic=True,
                                              specific_levels={},
                                              game=default_preset.game,
                                          )))

    description = await generator._create_description(
        generator_params=GeneratorParameters(
            seed_number=1000,
            spoiler=True,
            presets=[preset],
        ),
        status_update=lambda x: None,
        attempts=0,
    )
    all_patches = description.all_patches

    # Run
    encoded = game_patches_serializer.serialize(all_patches)
    decoded = game_patches_serializer.decode(encoded,
                                             {0: preset.configuration})
    decoded_with_original_game = {
        i: dataclasses.replace(d, game=orig.game)
        for (i, d), orig in zip(decoded.items(), all_patches.values())
    }

    # Assert
    assert all_patches == decoded_with_original_game
Beispiel #8
0
def test_generate_new_seed(tab, preset_manager, mocker):
    # Setup
    mock_randint: MagicMock = mocker.patch("random.randint", return_value=12341234)

    tab.window.create_preset_tree = MagicMock()
    tab.window.create_preset_tree.current_preset_data = preset_manager.default_preset
    tab.generate_seed_from_permalink = MagicMock()

    spoiler = MagicMock(spec=bool)
    retries = MagicMock(spec=int)

    # Run
    tab._generate_new_seed(spoiler, retries)

    # Assert
    tab.generate_seed_from_permalink.assert_called_once_with(
        Permalink.from_parameters(
            GeneratorParameters(
                seed_number=12341234,
                spoiler=spoiler,
                presets=[preset_manager.default_preset.get_preset()],
            )
        ), retries=retries
    )
    mock_randint.assert_called_once_with(0, 2 ** 31)
Beispiel #9
0
def test_decode(default_echoes_preset, mocker, extra_data):
    # We're mocking the database hash to avoid breaking tests every single time we change the database
    mocker.patch("randovania.layout.generator_parameters._game_db_hash",
                 autospec=True,
                 return_value=120)

    random_uuid = uuid.uuid4()
    mocker.patch("uuid.uuid4", return_value=random_uuid)

    # This test should break whenever we change how permalinks are created
    # When this happens, we must bump the permalink version and change the tests
    encoded = b'$\x00\x00\x1fD\x00\x000\xff\xbc\x00'
    if extra_data:
        encoded += b"="

    expected = GeneratorParameters(
        seed_number=1000,
        spoiler=True,
        presets=[
            dataclasses.replace(
                default_echoes_preset,
                name="{} Custom".format(default_echoes_preset.game.long_name),
                description="A customized preset.",
                uuid=random_uuid,
                base_preset_uuid=default_echoes_preset.uuid,
            )
        ],
    )

    # Uncomment this line to quickly get the new encoded permalink
    # assert expected.as_bytes == b""
    # print(expected.as_bytes)

    # Run
    if extra_data:
        expectation = pytest.raises(ValueError)
    else:
        expectation = contextlib.nullcontext()

    with expectation:
        link = GeneratorParameters.from_bytes(encoded)

    # Assert
    if not extra_data:
        assert link == expected
    def _generate_new_seed(self, spoiler: bool, retries: Optional[int] = None):
        preset = self._current_preset_data
        num_players = self.window.num_players_spin_box.value()

        self.generate_seed_from_permalink(Permalink.from_parameters(GeneratorParameters(
            seed_number=random.randint(0, 2 ** 31),
            spoiler=spoiler,
            presets=[preset.get_preset()] * num_players,
        )), retries=retries)
Beispiel #11
0
def test_decode_mock_other(encoded, num_players, mocker):
    # We're mocking the database hash to avoid breaking tests every single time we change the database
    mocker.patch("randovania.layout.generator_parameters._game_db_hash",
                 autospec=True,
                 return_value=120)

    preset = MagicMock()
    preset.game = RandovaniaGame.METROID_PRIME_ECHOES

    def read_values(decoder: BitPackDecoder, metadata):
        decoder.decode(100, 100)
        return preset

    mock_preset_unpack: MagicMock = mocker.patch(
        "randovania.layout.preset.Preset.bit_pack_unpack",
        side_effect=read_values)

    expected = GeneratorParameters(
        seed_number=1000,
        spoiler=True,
        presets=[preset] * num_players,
    )
    preset.bit_pack_encode.return_value = [(0, 100), (5, 100)]

    # Uncomment this line to quickly get the new encoded permalink
    # assert expected.as_bytes == b""
    # print(expected.as_bytes)

    # Run
    round_trip = expected.as_bytes
    link = GeneratorParameters.from_bytes(encoded)

    # Assert
    assert link == expected
    assert round_trip == encoded
    mock_preset_unpack.assert_has_calls([
        call(ANY, {
            "manager": ANY,
            "game": RandovaniaGame.METROID_PRIME_ECHOES
        }) for _ in range(num_players)
    ])
Beispiel #12
0
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
Beispiel #13
0
async def test_dock_weakness_distribute(default_blank_preset):
    _editor = PresetEditor(default_blank_preset.fork())
    with _editor as editor:
        editor.dock_rando_configuration = dataclasses.replace(
            editor.dock_rando_configuration,
            mode=DockRandoMode.TWO_WAY
        )
        preset = editor.create_custom_preset_with()
    
    gen_params = GeneratorParameters(5000, False, [preset])
    description = await generate_and_validate_description(gen_params, None, False)

    assert list(description.all_patches[0].all_dock_weaknesses())
Beispiel #14
0
def run_bootstrap(preset: Preset):
    game = filtered_database.game_description_for_layout(preset.configuration).get_mutable()
    generator = game.game.generator

    derived_nodes.create_derived_nodes(game)
    game.resource_database = generator.bootstrap.patch_resource_database(game.resource_database,
                                                                         preset.configuration)
    permalink = GeneratorParameters(
        seed_number=15000,
        spoiler=True,
        presets=[preset],
    )
    patches = generator.base_patches_factory.create_base_patches(preset.configuration, Random(15000),
                                                                 game, False, player_index=0)
    _, state = generator.bootstrap.logic_bootstrap(preset.configuration, game, patches)

    return game, state, permalink
Beispiel #15
0
    def from_str(cls, param: str) -> "Permalink":
        encoded_param = param.encode("utf-8")
        encoded_param += b"=" * ((4 - len(encoded_param)) % 4)

        try:
            b = base64.b64decode(encoded_param, altchars=b'-_', validate=True)
            if len(b) < 4:
                raise ValueError("String too short")

            cls.validate_version(b)
            data = PermalinkBinary.parse(b).value

        except construct.core.TerminatedError:
            raise ValueError("Extra text at the end")

        except construct.core.StreamError:
            raise ValueError("Missing text at the end")

        except construct.core.ChecksumError:
            raise ValueError("Incorrect checksum")

        except (binascii.Error, bitstruct.Error,
                construct.ConstructError) as e:
            raise ValueError(str(e))

        try:
            return Permalink(
                parameters=GeneratorParameters.from_bytes(
                    data.generator_params),
                seed_hash=data.seed_hash,
                randovania_version=data.randovania_version,
            )
        except Exception as e:
            games = generator_parameters.try_decode_game_list(
                data.generator_params)

            if data.randovania_version != cls.current_randovania_version():
                msg = "Detected version {}, current version is {}".format(
                    data.randovania_version.hex(),
                    cls.current_randovania_version().hex())
            else:
                msg = f"Error decoding parameters - {e}"
            raise UnsupportedPermalink(msg, data.seed_hash,
                                       data.randovania_version, games) from e
Beispiel #16
0
def test_as_bytes_caches(mock_bit_pack_encode: MagicMock,
                         default_echoes_preset):
    # Setup
    mock_bit_pack_encode.return_value = [(5, 256)]
    params = GeneratorParameters(
        seed_number=1000,
        spoiler=True,
        presets=[default_echoes_preset],
    )

    # Run
    str1 = params.as_bytes
    str2 = params.as_bytes

    # Assert
    assert str1 == b'\x05'
    assert str1 == str2
    assert object.__getattribute__(params, "__cached_as_bytes") is not None
    mock_bit_pack_encode.assert_called_once_with(params, {})
Beispiel #17
0
def test_batch_distribute_helper(mocker):
    # Setup
    description = MagicMock()
    mock_generate_description: AsyncMock = mocker.patch(
        "randovania.generator.generator.generate_and_validate_description",
        new_callable=AsyncMock,
        return_value=description)
    mock_perf_counter = mocker.patch("time.perf_counter",
                                     autospec=False)  # TODO: pytest-qt bug

    base_permalink = MagicMock(spec=GeneratorParameters)
    base_permalink.presets = [MagicMock()]
    seed_number = 5000
    validate = MagicMock()
    output_dir = MagicMock()
    timeout = 67
    description.file_extension.return_value = "rdvgame"

    expected_permalink = GeneratorParameters(
        seed_number=seed_number,
        spoiler=True,
        presets=base_permalink.presets,
    )

    mock_perf_counter.side_effect = [1000, 5000]

    # Run
    delta_time = batch_distribute.batch_distribute_helper(
        base_permalink, seed_number, timeout, validate, output_dir)

    # Assert
    mock_generate_description.assert_awaited_once_with(
        generator_params=expected_permalink,
        status_update=None,
        validate_after_generation=validate,
        timeout=timeout,
        attempts=0)

    assert delta_time == 4000
    output_dir.joinpath.assert_called_once_with(
        "{}.rdvgame".format(seed_number))
    description.save_to_file.assert_called_once_with(
        output_dir.joinpath.return_value)
Beispiel #18
0
async def test_generate_game(window, mocker, preset_manager):
    mock_generate_layout: MagicMock = mocker.patch(
        "randovania.interface_common.simplified_patcher.generate_layout")
    mock_randint: MagicMock = mocker.patch("random.randint", return_value=5000)
    mock_warning: AsyncMock = mocker.patch(
        "randovania.gui.lib.async_dialog.warning", new_callable=AsyncMock)

    spoiler = True
    game_session = MagicMock()
    game_session.presets = [
        preset_manager.default_preset, preset_manager.default_preset
    ]

    window._game_session = game_session
    window._upload_layout_description = AsyncMock()
    window._admin_global_action = AsyncMock()

    # Run
    await window.generate_game(spoiler, retries=3)

    # Assert
    mock_warning.assert_awaited_once_with(
        window,
        "Multiworld Limitation",
        ANY,
    )
    mock_randint.assert_called_once_with(0, 2**31)
    mock_generate_layout.assert_called_once_with(
        progress_update=ANY,
        parameters=GeneratorParameters(
            seed_number=mock_randint.return_value,
            spoiler=spoiler,
            presets=[
                preset_manager.default_preset.get_preset(),
                preset_manager.default_preset.get_preset(),
            ],
        ),
        options=window._options,
        retries=3,
    )
    window._upload_layout_description.assert_awaited_once_with(
        mock_generate_layout.return_value)