From e4552e59abcae67dfdd6d713855726284f634023 Mon Sep 17 00:00:00 2001 From: Jet Date: Wed, 7 Feb 2024 19:28:28 +0000 Subject: [PATCH] hacky gymnasium register --- pyproject.toml | 24 +- src/python/__init__.py | 8 +- src/python/atari_env.py | 384 ++++++++++++++++++++++++++++++ src/python/register_gymnasium.py | 288 ++++++++++++++++++++++ src/python/roms/__init__.py | 25 +- src/python/roms/md5.txt | 107 ++++++++- src/python/roms/tetris.bin | Bin 2048 -> 0 bytes src/python/scripts/__init__.py | 0 src/python/scripts/import_roms.py | 102 -------- 9 files changed, 798 insertions(+), 140 deletions(-) create mode 100644 src/python/atari_env.py create mode 100644 src/python/register_gymnasium.py mode change 120000 => 100644 src/python/roms/md5.txt delete mode 100644 src/python/roms/tetris.bin delete mode 100644 src/python/scripts/__init__.py delete mode 100644 src/python/scripts/import_roms.py diff --git a/pyproject.toml b/pyproject.toml index aa2443541..ee11f6895 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ dynamic = ["version"] [project.optional-dependencies] test = [ "pytest>=7.0", - "gym~=0.23", + "gymnasium", ] [project.urls] @@ -50,21 +50,9 @@ homepage = "https://github.com/mgbellemare/Arcade-Learning-Environment" documentation = "https://github.com/mgbellemare/Arcade-Learning-Environment/tree/master/docs" changelog = "https://github.com/mgbellemare/Arcade-Learning-Environment/blob/master/CHANGELOG.md" -[project.scripts] -ale-import-roms = "ale_py.scripts.import_roms:main" - -[project.entry-points."gym.envs"] -ALE = "ale_py.gym:register_gym_envs" -__internal__ = "ale_py.gym:register_legacy_gym_envs" - [tool.setuptools] -packages = [ - "ale_py", - "ale_py.roms", - "ale_py.env", - "ale_py.scripts" -] -package-dir = {ale_py = "src/python", gym = "src/gym"} +packages = ["ale_py", "ale_py.roms"] +package-dir = {ale_py = "src/python"} package-data = {"ale_py" = ["py.typed", "*.pyi", "**/*.pyi"], "ale_py.roms" = ["*.bin", "md5.txt"]} [tool.pytest.ini_options] @@ -79,12 +67,6 @@ skip = ["*-win32", "*i686", "pp*", "*-musllinux*"] build-frontend = "build" -# Test configuration -# test-extras = ["test"] -# TODO(jfarebro): Temporarily use upstream Gym until v26 release. -test-requires = ["pytest", "git+https://github.com/openai/gym#egg=gym"] -test-command = "pytest {project}" - # vcpkg manylinux images manylinux-x86_64-image = "ghcr.io/jessefarebro/manylinux2014_x86_64-vcpkg" diff --git a/src/python/__init__.py b/src/python/__init__.py index 67f6ea483..c117f4937 100644 --- a/src/python/__init__.py +++ b/src/python/__init__.py @@ -16,8 +16,8 @@ ctypes.CDLL("msvcp140.dll") except OSError: raise OSError( - """Microsoft Visual C++ Redistribution Pack is not installed. -It can be downloaded from https://aka.ms/vs/16/release/vc_redist.x64.exe.""" + "Microsoft Visual C++ Redistribution Pack is not installed. \n" + "It can be downloaded from https://aka.ms/vs/16/release/vc_redist.x64.exe." ) # Loading DLLs on Windows is kind of a disaster @@ -50,5 +50,5 @@ __all__ = ["Action", "ALEInterface", "ALEState", "LoggerMode", "SDL_SUPPORT"] -from .register_gymnasium import register_gymnasium_envs -register_gymnasium_envs() +from .register_gymnasium import register_gymnasium +register_gymnasium() diff --git a/src/python/atari_env.py b/src/python/atari_env.py new file mode 100644 index 000000000..18c9bb827 --- /dev/null +++ b/src/python/atari_env.py @@ -0,0 +1,384 @@ +from __future__ import annotations + +import sys +from typing import Any, Literal, Optional, Sequence, Union + +import ale_py +from ale_py import roms +from ale_py.roms.utils import rom_id_to_name, rom_name_to_id +from gymnasium.utils import seeding +import numpy as np + +import gymnasium +import gymnasium.logger as logger +from gymnasium import error, spaces, utils + +if sys.version_info < (3, 11): + from typing_extensions import NotRequired, TypedDict +else: + from typing import NotRequired, TypedDict + + +class AtariEnvStepMetadata(TypedDict): + lives: int + episode_frame_number: int + frame_number: int + seeds: NotRequired[Sequence[int]] + + +class AtariEnv(gymnasium.Env, utils.EzPickle): + """ + (A)rcade (L)earning (Gym) (Env)ironment. + A Gym wrapper around the Arcade Learning Environment (ALE). + """ + + # No render modes + metadata = {"render_modes": ["human", "rgb_array"]} + + def __init__( + self, + game: str = "pong", + mode: Optional[int] = None, + difficulty: Optional[int] = None, + obs_type: Literal["rgb", "grayscale", "ram"] = "rgb", + frameskip: Union[tuple[int, int], int] = 4, + repeat_action_probability: float = 0.25, + full_action_space: bool = False, + max_num_frames_per_episode: Optional[int] = None, + render_mode: Optional[Literal["human", "rgb_array"]] = None, + ) -> None: + """ + Initialize the ALE for Gymnasium. + Default parameters are taken from Machado et al., 2018. + + Args: + game: str => Game to initialize env with. + mode: Optional[int] => Game mode, see Machado et al., 2018 + difficulty: Optional[int] => Game difficulty,see Machado et al., 2018 + obs_type: str => Observation type in { 'rgb', 'grayscale', 'ram' } + frameskip: Union[tuple[int, int], int] => + Stochastic frameskip as tuple or fixed. + repeat_action_probability: int => + Probability to repeat actions, see Machado et al., 2018 + full_action_space: bool => Use full action space? + max_num_frames_per_episode: int => Max number of frame per epsiode. + Once `max_num_frames_per_episode` is reached the episode is + truncated. + render_mode: str => One of { 'human', 'rgb_array' }. + If `human` we'll interactively display the screen and enable + game sounds. This will lock emulation to the ROMs specified FPS + If `rgb_array` we'll return the `rgb` key in step metadata with + the current environment RGB frame. + + Note: + - The game must be installed, see ale-import-roms, or ale-py-roms. + - Frameskip values of (low, high) will enable stochastic frame skip + which will sample a random frameskip uniformly each action. + - It is recommended to enable full action space. + See Machado et al., 2018 for more details. + + References: + `Revisiting the Arcade Learning Environment: Evaluation Protocols + and Open Problems for General Agents`, Machado et al., 2018, JAIR + URL: https://jair.org/index.php/jair/article/view/11182 + """ + if obs_type not in {"rgb", "grayscale", "ram"}: + raise error.Error( + f"Invalid observation type: {obs_type}. Expecting: rgb, grayscale, ram." + ) + + if type(frameskip) not in (int, tuple): + raise error.Error(f"Invalid frameskip type: {type(frameskip)}.") + if isinstance(frameskip, int) and frameskip <= 0: + raise error.Error( + f"Invalid frameskip of {frameskip}, frameskip must be positive." + ) + elif isinstance(frameskip, tuple) and len(frameskip) != 2: + raise error.Error( + f"Invalid stochastic frameskip length of {len(frameskip)}, expected length 2." + ) + elif isinstance(frameskip, tuple) and frameskip[0] > frameskip[1]: + raise error.Error( + f"Invalid stochastic frameskip, lower bound is greater than upper bound." + ) + elif isinstance(frameskip, tuple) and frameskip[0] <= 0: + raise error.Error( + f"Invalid stochastic frameskip lower bound is greater than upper bound." + ) + + if render_mode is not None and render_mode not in {"rgb_array", "human"}: + raise error.Error( + f"Render mode {render_mode} not supported (rgb_array, human)." + ) + + utils.EzPickle.__init__( + self, + game, + mode, + difficulty, + obs_type, + frameskip, + repeat_action_probability, + full_action_space, + max_num_frames_per_episode, + render_mode, + ) + + # Initialize ALE + self.ale = ale_py.ALEInterface() + + self._game = rom_id_to_name(game) + + self._game_mode = mode + self._game_difficulty = difficulty + + self._frameskip = frameskip + self._obs_type = obs_type + self.render_mode = render_mode + + # Set logger mode to error only + self.ale.setLoggerMode(ale_py.LoggerMode.Error) + # Config sticky action prob. + self.ale.setFloat("repeat_action_probability", repeat_action_probability) + + if max_num_frames_per_episode is not None: + self.ale.setInt("max_num_frames_per_episode", max_num_frames_per_episode) + + # If render mode is human we can display screen and sound + if render_mode == "human": + self.ale.setBool("display_screen", True) + self.ale.setBool("sound", True) + + # seed + load + self.seed_game() + self.load_game() + + # initialize action space + self._action_set = ( + self.ale.getLegalActionSet() + if full_action_space + else self.ale.getMinimalActionSet() + ) + self.action_space = spaces.Discrete(len(self._action_set)) + + # initialize observation space + if self._obs_type == "ram": + self.observation_space = spaces.Box( + low=0, high=255, dtype=np.uint8, shape=(self.ale.getRAMSize(),) + ) + elif self._obs_type == "rgb" or self._obs_type == "grayscale": + (screen_height, screen_width) = self.ale.getScreenDims() + image_shape = ( + screen_height, + screen_width, + ) + if self._obs_type == "rgb": + image_shape += (3,) + self.observation_space = spaces.Box( + low=0, high=255, dtype=np.uint8, shape=image_shape + ) + else: + raise error.Error(f"Unrecognized observation type: {self._obs_type}") + + def seed_game(self, seed: Optional[int] = None) -> tuple[int, int]: + """Seeds the internal and ALE RNG.""" + ss = np.random.SeedSequence(seed) + np_seed, ale_seed = ss.generate_state(n_words=2) + self._np_random, seed = seeding.np_random(int(np_seed)) + self.ale.setInt("random_seed", np.int32(ale_seed)) + return np_seed, ale_seed + + def load_game(self) -> None: + """This function initializes the ROM and sets the corresponding mode and difficulty.""" + if not hasattr(roms, self._game): + raise error.Error( + f'We\'re Unable to find the game "{self._game}". Note: Gym no longer distributes ROMs. ' + f"If you own a license to use the necessary ROMs for research purposes you can download them " + f'via `pip install gym[accept-rom-license]`. Otherwise, you should try importing "{self._game}" ' + f'via the command `ale-import-roms`. If you believe this is a mistake perhaps your copy of "{self._game}" ' + "is unsupported. To check if this is the case try providing the environment variable " + "`PYTHONWARNINGS=default::ImportWarning:ale_py.roms`. For more information see: " + "https://github.com/mgbellemare/Arcade-Learning-Environment#rom-management" + ) + self.ale.loadROM(getattr(roms, self._game)) + + if self._game_mode is not None: + self.ale.setMode(self._game_mode) + if self._game_difficulty is not None: + self.ale.setDifficulty(self._game_difficulty) + + def step( # pyright: ignore[reportIncompatibleMethodOverride] + self, + action: int, + ) -> tuple[np.ndarray, float, bool, bool, AtariEnvStepMetadata]: + """ + Perform one agent step, i.e., repeats `action` frameskip # of steps. + + Args: + action_ind: int => Action index to execute + + Returns: + tuple[np.ndarray, float, bool, bool, Dict[str, Any]] => + observation, reward, terminal, truncation, metadata + + Note: `metadata` contains the keys "lives" and "rgb" if + render_mode == 'rgb_array'. + """ + # If frameskip is a length 2 tuple then it's stochastic + # frameskip between [frameskip[0], frameskip[1]] uniformly. + if isinstance(self._frameskip, int): + frameskip = self._frameskip + elif isinstance(self._frameskip, tuple): + frameskip = self.np_random.integers(*self._frameskip) + else: + raise error.Error(f"Invalid frameskip type: {self._frameskip}") + + # Frameskip + reward = 0.0 + for _ in range(frameskip): + reward += self.ale.act(self._action_set[action]) + is_terminal = self.ale.game_over(with_truncation=False) + is_truncated = self.ale.game_truncated() + + return self._get_obs(), reward, is_terminal, is_truncated, self._get_info() + + def reset( # pyright: ignore[reportIncompatibleMethodOverride] + self, + *, + seed: Optional[int] = None, + options: Optional[dict[str, Any]] = None, + ) -> tuple[np.ndarray, AtariEnvStepMetadata]: + """Resets environment and returns initial observation.""" + # sets the seeds if it's specified for both ALE and frameskip np + # we only want to do this when commanded to so we don't reset all previous states, statistics, etc. + seeded_with = None + if seed is not None: + seeded_with = self.seed_game(seed) + self.load_game() + + self.ale.reset_game() + + obs = self._get_obs() + info = self._get_info() + if seeded_with is not None: + info["seeds"] = seeded_with + + return obs, info + + def render(self) -> Optional[np.ndarray]: + """ + Render is not supported by ALE. We use a paradigm similar to + Gym3 which allows you to specify `render_mode` during construction. + + For example, + gym.make("ale-py:Pong-v0", render_mode="human") + will display the ALE and maintain the proper interval to match the + FPS target set by the ROM. + """ + if self.render_mode == "rgb_array": + return self.ale.getScreenRGB() + elif self.render_mode == "human": + return + else: + raise error.Error( + f"Invalid render mode `{self.render_mode}`. " + "Supported modes: `human`, `rgb_array`." + ) + + def _get_obs(self) -> np.ndarray: + """ + Retreives the current observation. + This is dependent on `self._obs_type`. + """ + if self._obs_type == "ram": + return self.ale.getRAM() + elif self._obs_type == "rgb": + return self.ale.getScreenRGB() + elif self._obs_type == "grayscale": + return self.ale.getScreenGrayscale() + else: + raise error.Error(f"Unrecognized observation type: {self._obs_type}") + + def _get_info(self) -> AtariEnvStepMetadata: + return { + "lives": self.ale.lives(), + "episode_frame_number": self.ale.getEpisodeFrameNumber(), + "frame_number": self.ale.getFrameNumber(), + } + + def get_keys_to_action(self) -> dict[tuple[int], ale_py.Action]: + """ + Return keymapping -> actions for human play. + """ + UP = ord("w") + LEFT = ord("a") + RIGHT = ord("d") + DOWN = ord("s") + FIRE = ord(" ") + + mapping = { + ale_py.Action.NOOP: (None,), + ale_py.Action.UP: (UP,), + ale_py.Action.FIRE: (FIRE,), + ale_py.Action.DOWN: (DOWN,), + ale_py.Action.LEFT: (LEFT,), + ale_py.Action.RIGHT: (RIGHT,), + ale_py.Action.UPFIRE: (UP, FIRE), + ale_py.Action.DOWNFIRE: (DOWN, FIRE), + ale_py.Action.LEFTFIRE: (LEFT, FIRE), + ale_py.Action.RIGHTFIRE: (RIGHT, FIRE), + ale_py.Action.UPLEFT: (UP, LEFT), + ale_py.Action.UPRIGHT: (UP, RIGHT), + ale_py.Action.DOWNLEFT: (DOWN, LEFT), + ale_py.Action.DOWNRIGHT: (DOWN, RIGHT), + ale_py.Action.UPLEFTFIRE: (UP, LEFT, FIRE), + ale_py.Action.UPRIGHTFIRE: (UP, RIGHT, FIRE), + ale_py.Action.DOWNLEFTFIRE: (DOWN, LEFT, FIRE), + ale_py.Action.DOWNRIGHTFIRE: (DOWN, RIGHT, FIRE), + } + + # Map + # (key, key, ...) -> action_idx + # where action_idx is the integer value of the action enum + # + return dict( + zip( + map(lambda action: tuple(sorted(mapping[action])), self._action_set), + range(len(self._action_set)), + ) + ) + + def get_action_meanings(self) -> list[str]: + """ + Return the meaning of each integer action. + """ + keys = ale_py.Action.__members__.values() + values = ale_py.Action.__members__.keys() + mapping = dict(zip(keys, values)) + return [mapping[action] for action in self._action_set] + + def clone_state(self, include_rng=False) -> ale_py.ALEState: + """Clone emulator state w/o system state. Restoring this state will + *not* give an identical environment. For complete cloning and restoring + of the full state, see `{clone,restore}_full_state()`.""" + return self.ale.cloneState(include_rng=include_rng) + + def restore_state(self, state: ale_py.ALEState) -> None: + """Restore emulator state w/o system state.""" + self.ale.restoreState(state) + + def clone_full_state(self) -> ale_py.ALEState: + """Deprecated method which would clone the emulator and system state.""" + logger.warn( + "`clone_full_state()` is deprecated and will be removed in a future release of `ale-py`. " + "Please use `clone_state(include_rng=True)` which is equivalent to `clone_full_state`. " + ) + return self.ale.cloneSystemState() + + def restore_full_state(self, state: ale_py.ALEState) -> None: + """Restore emulator state w/ system state including pseudorandomness.""" + logger.warn( + "restore_full_state() is deprecated and will be removed in a future release of `ale-py`. " + "Please use `restore_state(state)` which will restore the state regardless of being a full or partial state. " + ) + self.ale.restoreSystemState(state) diff --git a/src/python/register_gymnasium.py b/src/python/register_gymnasium.py new file mode 100644 index 000000000..b39f16a4f --- /dev/null +++ b/src/python/register_gymnasium.py @@ -0,0 +1,288 @@ +from typing import Any, Callable, Mapping, NamedTuple, Sequence +from collections import defaultdict +from ale_py.roms.utils import rom_id_to_name +from gymnasium.envs.registration import register + +ALL_ATARI_GAMES = ( + "adventure", + "air_raid", + "alien", + "amidar", + "assault", + "asterix", + "asteroids", + "atlantis", + "atlantis2", + "backgammon", + "bank_heist", + "basic_math", + "battle_zone", + "beam_rider", + "berzerk", + "blackjack", + "bowling", + "boxing", + "breakout", + "carnival", + "casino", + "centipede", + "chopper_command", + "crazy_climber", + "crossbow", + "darkchambers", + "defender", + "demon_attack", + "donkey_kong", + "double_dunk", + "earthworld", + "elevator_action", + "enduro", + "entombed", + "et", + "fishing_derby", + "flag_capture", + "freeway", + "frogger", + "frostbite", + "galaxian", + "gopher", + "gravitar", + "hangman", + "haunted_house", + "hero", + "human_cannonball", + "ice_hockey", + "jamesbond", + "journey_escape", + "kaboom", + "kangaroo", + "keystone_kapers", + "king_kong", + "klax", + "koolaid", + "krull", + "kung_fu_master", + "laser_gates", + "lost_luggage", + "mario_bros", + "miniature_golf", + "montezuma_revenge", + "mr_do", + "ms_pacman", + "name_this_game", + "othello", + "pacman", + "phoenix", + "pitfall", + "pitfall2", + "pong", + "pooyan", + "private_eye", + "qbert", + "riverraid", + "road_runner", + "robotank", + "seaquest", + "sir_lancelot", + "skiing", + "solaris", + "space_invaders", + "space_war", + "star_gunner", + "superman", + "surround", + "tennis", + "tetris", + "tic_tac_toe_3d", + "time_pilot", + "trondead", + "turmoil", + "tutankham", + "up_n_down", + "venture", + "video_checkers", + "video_chess", + "video_cube", + "video_pinball", + "wizard_of_wor", + "word_zapper", + "yars_revenge", + "zaxxon", +) +LEGACY_ATARI_GAMES = ( + "adventure", + "air_raid", + "alien", + "amidar", + "assault", + "asterix", + "asteroids", + "atlantis", + "bank_heist", + "battle_zone", + "beam_rider", + "berzerk", + "bowling", + "boxing", + "breakout", + "carnival", + "centipede", + "chopper_command", + "crazy_climber", + "defender", + "demon_attack", + "double_dunk", + "elevator_action", + "enduro", + "fishing_derby", + "freeway", + "frostbite", + "gopher", + "gravitar", + "hero", + "ice_hockey", + "jamesbond", + "journey_escape", + "kangaroo", + "krull", + "kung_fu_master", + "montezuma_revenge", + "ms_pacman", + "name_this_game", + "phoenix", + "pitfall", + "pong", + "pooyan", + "private_eye", + "qbert", + "riverraid", + "road_runner", + "robotank", + "seaquest", + "skiing", + "solaris", + "space_invaders", + "star_gunner", + "tennis", + "time_pilot", + "tutankham", + "up_n_down", + "venture", + "video_pinball", + "wizard_of_wor", + "yars_revenge", + "zaxxon", +) + +class GymFlavour(NamedTuple): + """A Gymnasium Flavour.""" + + suffix: str + kwargs: Mapping[str, Any] | Callable[[str], Mapping[str, Any]] + + +class GymConfig(NamedTuple): + """A Gymnasium Configuration.""" + + version: str + kwargs: Mapping[str, Any] + flavours: Sequence[GymFlavour] + + +def _register_configs( + roms: Sequence[str], + obs_types: Sequence[str], + configs: Sequence[GymConfig], + prefix: str = "", +): + """Registers all possible configurations of the atari games given a list of roms.""" + for rom in roms: + for obs_type in obs_types: + for config in configs: + for flavour in config.flavours: + name = rom_id_to_name(rom) + if obs_type == "ram": + name = f"{name}-ram" + + # Parse config kwargs + if callable(config.kwargs): + config_kwargs = config.kwargs(rom) + else: + config_kwargs = config.kwargs + + # Parse flavour kwargs + if callable(flavour.kwargs): + flavour_kwargs = flavour.kwargs(rom) + else: + flavour_kwargs = flavour.kwargs + + # Register the environment + register( + id=f"{prefix}{name}{flavour.suffix}-{config.version}", + entry_point="ale_py.atari_env:AtariEnv", + kwargs={ + "game": rom, + "obs_type": obs_type, + **config_kwargs, + **flavour_kwargs, + }, + ) + + +def register_gymnasium(): + frameskip: dict[str, int] = defaultdict(lambda: 4, [("space_invaders", 3)]) + + configs = [ + GymConfig( + version="v0", + kwargs={ + "repeat_action_probability": 0.25, + "full_action_space": False, + "max_num_frames_per_episode": 108_000, + }, + flavours=[ + # Default for v0 has 10k steps, no idea why... + GymFlavour("", {"frameskip": (2, 5)}), + # Deterministic has 100k steps, close to the standard of 108k (30 mins gameplay) + GymFlavour("Deterministic", lambda rom: {"frameskip": frameskip[rom]}), + # NoFrameSkip imposes a max episode steps of frameskip * 100k, weird... + GymFlavour("NoFrameskip", {"frameskip": 1}), + ], + ), + GymConfig( + version="v4", + kwargs={ + "repeat_action_probability": 0.0, + "full_action_space": False, + "max_num_frames_per_episode": 108_000, + }, + flavours=[ + # Unlike v0, v4 has 100k max episode steps + GymFlavour("", {"frameskip": (2, 5)}), + GymFlavour("Deterministic", lambda rom: {"frameskip": frameskip[rom]}), + # Same weird frameskip * 100k max steps for v4? + GymFlavour("NoFrameskip", {"frameskip": 1}), + ], + ), + ] + _register_configs( + LEGACY_ATARI_GAMES, obs_types=("rgb", "ram"), configs=configs + ) + + # max_episode_steps is 108k frames which is 30 mins of gameplay. + # This corresponds to 108k / 4 = 27,000 steps + configs = [ + GymConfig( + version="v5", + kwargs={ + "repeat_action_probability": 0.25, + "full_action_space": False, + "frameskip": 4, + "max_num_frames_per_episode": 108_000, + }, + flavours=[GymFlavour("", {})], + ) + ] + _register_configs( + ALL_ATARI_GAMES, obs_types=("rgb", "ram"), configs=configs, prefix="ALE/" + ) diff --git a/src/python/roms/__init__.py b/src/python/roms/__init__.py index cf2e8b1ee..4fe89466f 100644 --- a/src/python/roms/__init__.py +++ b/src/python/roms/__init__.py @@ -7,7 +7,8 @@ from typing import List, Optional from ale_py import ALEInterface -from ale_py.roms import plugins, utils +from ale_py.roms.utils import rom_id_to_name, rom_name_to_id +from ale_py.roms.plugins import Package, EntryPoint, Directory, Plugin # pylint: disable=g-import-not-at-top if sys.version_info >= (3, 9): @@ -21,11 +22,11 @@ # 2. External ROMs # 3. ROMs from atari-py.roms # 4. ROMs from atari-py-roms.roms -_ROM_PLUGIN_REGISTRY: List[plugins.Plugin] = [ - plugins.Package("ale_py.roms"), - plugins.EntryPoint("ale_py.roms"), - plugins.Package("atari_py.atari_roms"), - plugins.Package("atari_py_roms.atari_roms"), +_ROM_PLUGIN_REGISTRY: List[Plugin] = [ + Package("ale_py.roms"), + EntryPoint("ale_py.roms"), + Package("atari_py.atari_roms"), + Package("atari_py_roms.atari_roms"), ] # Environment variable for ROM discovery. @@ -35,13 +36,13 @@ _ROM_DIRECTORY_ENV_VALUE = os.environ.get(_ROM_DIRECTORY_ENV_KEY, None) if _ROM_DIRECTORY_ENV_VALUE is not None: - _ROM_PLUGIN_REGISTRY.append(plugins.Directory(_ROM_DIRECTORY_ENV_VALUE)) + _ROM_PLUGIN_REGISTRY.append(Directory(_ROM_DIRECTORY_ENV_VALUE)) def _resolve_rom(name: str) -> Optional[pathlib.Path]: """Resolve a ROM path from the ROM registry.""" for package in _ROM_PLUGIN_REGISTRY: - rom_id = utils.rom_name_to_id(name) + rom_id = rom_name_to_id(name) # Resolve ROM from package try: @@ -73,7 +74,7 @@ def _resolve_rom(name: str) -> Optional[pathlib.Path]: continue # Deprecation warning for atari-py - if isinstance(package, plugins.Package) and package.package.startswith( + if isinstance(package, Package) and package.package.startswith( "atari_py" ): warnings.warn( @@ -105,7 +106,7 @@ def __dir__() -> List[str]: lambda line: line.strip() and not line.startswith("#"), fp.readlines() ) roms = [pathlib.Path(rom) for _, rom in map(str.split, lines)] - return [utils.rom_id_to_name(rom.stem) for rom in roms] + return [rom_id_to_name(rom.stem) for rom in roms] @functools.lru_cache(maxsize=None) @@ -130,9 +131,9 @@ def __getattr__(name: str) -> pathlib.Path: return path -def register_plugin(plugin: plugins.Plugin, *, index: int = 0) -> None: +def register_plugin(plugin: Plugin, *, index: int = 0) -> None: """Register a ROM plugin.""" - if not issubclass(type(plugin), plugins.Plugin): + if not issubclass(type(plugin), Plugin): raise ValueError(f"{repr(plugin)} is not a valid ROM plugin.") _ROM_PLUGIN_REGISTRY.insert(index, plugin) __getattr__.cache_clear() diff --git a/src/python/roms/md5.txt b/src/python/roms/md5.txt deleted file mode 120000 index b9bf6d10a..000000000 --- a/src/python/roms/md5.txt +++ /dev/null @@ -1 +0,0 @@ -../../../md5.txt \ No newline at end of file diff --git a/src/python/roms/md5.txt b/src/python/roms/md5.txt new file mode 100644 index 000000000..63b23408c --- /dev/null +++ b/src/python/roms/md5.txt @@ -0,0 +1,106 @@ +# The following is a list of the md5 checksums for the various roms supported by ALE. + +4b27f5397c442d25f0c418ccdacf1926 adventure.bin +35be55426c1fec32dfb503b4f0651572 air_raid.bin +f1a0a23e6464d954e3a9579c4ccd01c8 alien.bin +acb7750b4d0c4bd34969802a7deb2990 amidar.bin +de78b3a064d374390ac0710f95edde92 assault.bin +89a68746eff7f266bbf08de2483abe55 asterix.bin +ccbd36746ed4525821a8083b0d6d2c2c asteroids.bin +826481f6fc53ea47c9f272f7050eedf7 atlantis2.bin +9ad36e699ef6f45d9eb6c4cf90475c9f atlantis.bin +8556b42aa05f94bc29ff39c39b11bff4 backgammon.bin +00ce0bdd43aed84a983bef38fe7f5ee3 bank_heist.bin +819aeeb9a2e11deb54e6de334f843894 basic_math.bin +41f252a66c6301f1e8ab3612c19bc5d4 battle_zone.bin +79ab4123a83dc11d468fb2108ea09e2e beam_rider.bin +136f75c4dd02c29283752b7e5799f978 berzerk.bin +0a981c03204ac2b278ba392674682560 blackjack.bin +c9b7afad3bfd922e006a6bfc1d4f3fe7 bowling.bin +c3ef5c4653212088eda54dc91d787870 boxing.bin +f34f08e5eb96e500e851a80be3277a56 breakout.bin +028024fb8e5e5f18ea586652f9799c96 carnival.bin +b816296311019ab69a21cb9e9e235d12 casino.bin +91c2098e88a6b13f977af8c003e0bca5 centipede.bin +c1cb228470a87beb5f36e90ac745da26 chopper_command.bin +55ef7b65066428367844342ed59f956c crazy_climber.bin +8cd26dcf249456fe4aeb8db42d49df74 crossbow.bin +106855474c69d08c8ffa308d47337269 darkchambers.bin +0f643c34e40e3f1daafd9c524d3ffe64 defender.bin +f0e0addc07971561ab80d9abe1b8d333 demon_attack.bin +36b20c427975760cb9cf4a47e41369e4 donkey_kong.bin +368d88a6c071caba60b4f778615aae94 double_dunk.bin +5aea9974b975a6a844e6df10d2b861c4 earthworld.bin +71f8bacfbdca019113f3f0801849057e elevator_action.bin +94b92a882f6dbaa6993a46e2dcc58402 enduro.bin +6b683be69f92958abe0e2a9945157ad5 entombed.bin +615a3bf251a38eb6638cdc7ffbde5480 et.bin +b8865f05676e64f3bec72b9defdacfa7 fishing_derby.bin +30512e0e83903fc05541d2f6a6a62654 flag_capture.bin +8e0ab801b1705a740b476b7f588c6d16 freeway.bin +081e2c114c9c20b61acf25fc95c71bf4 frogger.bin +4ca73eb959299471788f0b685c3ba0b5 frostbite.bin +211774f4c5739042618be8ff67351177 galaxian.bin +c16c79aad6272baffb8aae9a7fff0864 gopher.bin +8ac18076d01a6b63acf6e2cab4968940 gravitar.bin +f16c709df0a6c52f47ff52b9d95b7d8d hangman.bin +f0a6e99f5875891246c3dbecbf2d2cea haunted_house.bin +fca4a5be1251927027f2c24774a02160 hero.bin +7972e5101fa548b952d852db24ad6060 human_cannonball.bin +a4c08c4994eb9d24fb78be1793e82e26 ice_hockey.bin +e51030251e440cffaab1ac63438b44ae jamesbond.bin +718ae62c70af4e5fd8e932fee216948a journey_escape.bin +5428cdfada281c569c74c7308c7f2c26 kaboom.bin +4326edb70ff20d0ee5ba58fa5cb09d60 kangaroo.bin +6c1f3f2e359dbf55df462ccbcdd2f6bf keystone_kapers.bin +0dd4c69b5f9a7ae96a7a08329496779a king_kong.bin +eed9eaf1a0b6a2b9bc4c8032cb43e3fb klax.bin +534e23210dd1993c828d944c6ac4d9fb koolaid.bin +4baada22435320d185c95b7dd2bcdb24 krull.bin +5b92a93b23523ff16e2789b820e2a4c5 kung_fu_master.bin +8e4cd60d93fcde8065c1a2b972a26377 laser_gates.bin +2d76c5d1aad506442b9e9fb67765e051 lost_luggage.bin +e908611d99890733be31733a979c62d8 mario_bros.bin +df62a658496ac98a3aa4a6ee5719c251 miniature_golf.bin +3347a6dd59049b15a38394aa2dafa585 montezuma_revenge.bin +aa7bb54d2c189a31bb1fa20099e42859 mr_do.bin +87e79cd41ce136fd4f72cc6e2c161bee ms_pacman.bin +36306070f0c90a72461551a7a4f3a209 name_this_game.bin +113cd09c9771ac278544b7e90efe7df2 othello.bin +fc2233fc116faef0d3c31541717ca2db pacman.bin +7e52a95074a66640fcfde124fffd491a phoenix.bin +6d842c96d5a01967be9680080dd5be54 pitfall2.bin +3e90cf23106f2e08b2781e41299de556 pitfall.bin +60e0ea3cbe0913d39803477945e9e5ec pong.bin +4799a40b6e889370b7ee55c17ba65141 pooyan.bin +ef3a4f64b6494ba770862768caf04b86 private_eye.bin +484b0076816a104875e00467d431c2d2 qbert.bin +393948436d1f4cc3192410bb918f9724 riverraid.bin +ce5cc62608be2cd3ed8abd844efb8919 road_runner.bin +4f618c2429138e0280969193ed6c107e robotank.bin +240bfbac5163af4df5ae713985386f92 seaquest.bin +dd0cbe5351551a538414fb9e37fc56e8 sir_lancelot.bin +b76fbadc8ffb1f83e2ca08b6fb4d6c9f skiing.bin +e72eb8d4410152bdcb69e7fba327b420 solaris.bin +72ffbef6504b75e69ee1045af9075f66 space_invaders.bin +b702641d698c60bcdc922dbd8c9dd49c space_war.bin +a3c1c70024d7aabb41381adbfb6d3b25 star_gunner.bin +a9531c763077464307086ec9a1fd057d superman.bin +4d7517ae69f95cfbc053be01312b7dba surround.bin +42cdd6a9e42a3639e190722b8ea3fc51 tennis.bin +b0e1ee07fbc73493eac5651a52f90f00 tetris.bin +0db4f4150fecf77e4ce72ca4d04c052f tic_tac_toe_3d.bin +fc2104dd2dadf9a6176c1c1c8f87ced9 time_pilot.bin +fb27afe896e7c928089307b32e5642ee trondead.bin +7a5463545dfb2dcfdafa6074b2f2c15e turmoil.bin +085322bae40d904f53bdcc56df0593fc tutankham.bin +a499d720e7ee35c62424de882a3351b6 up_n_down.bin +3e899eba0ca8cd2972da1ae5479b4f0d venture.bin +539d26b6e9df0da8e7465f0f5ad863b7 video_checkers.bin +f0b7db930ca0e548c41a97160b9f6275 video_chess.bin +3f540a30fdee0b20aed7288e4a5ea528 video_cube.bin +107cc025334211e6d29da0b6be46aec7 video_pinball.bin +7e8aa18bc9502eb57daaf5e7c1e94da7 wizard_of_wor.bin +ec3beb6d8b5689e867bafb5d5f507491 word_zapper.bin +c5930d0e8cdae3e037349bfa08e871be yars_revenge.bin +eea0da9b987d661264cce69a7c13c3bd zaxxon.bin diff --git a/src/python/roms/tetris.bin b/src/python/roms/tetris.bin deleted file mode 100644 index e3647abc165c2101c62674849d7e13546af3d175..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2048 zcmcIkT}UHW6rLF;YTPw5fmQ@f3}Lr4?W!rQ;DdC9=A~_aW*7Pg?H%Tv zd%ydgd%kn$-uZYfUYQ|`RLLmElcOAI1dTimIU+&707vp(Jnj0rT%CHIP6;?wz^VQCB2LLgue~6oi^`cekLhkps~PbK`Pqdr zPB{jad=FN#3XPbChYsijPTuvfg$94)Z@|U4h*H*&-2vib_JktK&@myhvl6>2e(tdWkpapxZT-ICjv4Iwz4E_zTnoce>dDADEvPE4>G3qvlu>U@ zX@AsUWP5VEA~eeUBxdQRMV>+b+fQO@td>$uMX|a%%p6x~CYD-LS#`VqB8ORx$*xvi z6-KeT9HtKB;4Ya*(L`|DjXF$$^y<(P4c1sI;88v%PS_wk9(Ep@NCWVM1q*O-OGYW< z(Y42TJnqB}9mEZtqz%^bQgC1e8wm17`-nKP!OmsX7(ZrB-u&|0I5TNw!8Rs5$dV>l zi}84MHlTTKJ$VgmNeP6x?59@tlo@y4Xg1jCHRZ?jx-v`eI7`mq&6bOzCB5rc9;J7c z_hr2QF1ak{U^|_2y;ELnc1@NS8~pbk$=hH6I60>;-8+H2I_|j$dhOR?Od(UC_dF|Y zN?RG_Vd2Ag%TixON-hmnq`Mihc%$gOgg3llg;RnHVb0?!Oc~kouq|t_b0N%$jaM7f zn#euS&JB3&a^6#byu95$!g~Zu9`c4F3+DOk49dyNJD{18ZW_Onh3g#scQ{IB@5s-T zaP>G7W0K*Oi;6WFT@jQ9RXLjsPluUgbUMo1QGaezej!X^xvkKGAcd6u@j~wkuW&>x zwFmcLPu?G4V1I2K)RvZ@Iv?j#XDia}l&d1GMl#|)e%K-=!Yz#Rv-#A!C)PrLFz0lc}l}&9jePN}T+G6^m%63HA zX|KV7uoOl(5S5|`56^o9Q%3Lr4n8+l2Nl;>gNJY!<8K|DFM)dN@cfZQm6%LK)Q2no zPjiXQJjXKFaDBru49oG&I1svQ&ic2j*XHbU_H@_0fP-FVkE7<1-h+?>odf%Z@9uK8 zIs^mnrQ=rQI&c}}Il9rkjJ~tx8SyFCIqysT1B1RdFAaad{M)gfNF=n}t3yL?d^4Jq zXn;_!kL{(U7h0KA-pGn9y6H7v46ab>9y@pRT+0 z2%5`ogmkwtaO-Fih+#vIJ)F=(fj}VSe$nj?g?u49n#(?fLOx^Q3!w=hh7CRTa3b`d KenF= (3, 9): - import importlib.resources as resources -else: - import importlib_resources as resources - - -def import_roms( - romdir: pathlib.Path, - datadir: pathlib.Path, - pkg: Optional[str] = None, - dry_run: bool = False, -) -> None: - """ - Recursively copies all compatible ROMs in romdir - to datadir using the proper filename for the ALE. - """ - - supported = {} - unsupported = [] - for path in romdir.glob("**/*.bin"): - rom = ale_py.ALEInterface.isSupportedROM(path) - if rom is not None: - supported[rom] = path - else: - unsupported.append(path) - - # Copy over supported files - for rom, path in supported.items(): - identifier = str(path) if pkg is None else f"{pkg}/{path.name}" - if not dry_run: - shutil.copyfile(path, datadir / f"{rom}.bin") - print(f"\033[92m{'[SUPPORTED]': <15}\033[0m {rom: >20} {identifier: >30}") - - print("\n") - # Print unsuported - for path in unsupported: - identifier = str(path) if pkg is None else f"{pkg}/{path.name}" - print(f"\033[91m{'[NOT SUPPORTED]': <15}\033[0m {'': >20} {identifier: >30}") - - # Print summary - if not dry_run: - print(f"\nImported {len(supported)} / {len(supported)} ROMs") - - -def main() -> None: - """ - CLI for ale-import-roms - """ - parser = argparse.ArgumentParser() - parser.add_argument("--version", action="version", version=ale_py.__version__) - parser.add_argument("--dry-run", action="store_true") - - group = parser.add_mutually_exclusive_group(required=True) - group.add_argument("--import-from-pkg") - group.add_argument("romdir", help="Directory containing ROMs", nargs="?") - - args = parser.parse_args() - - if args.romdir: - romdir = pathlib.Path(args.romdir) - - if not romdir.exists(): - print(f"Path {romdir} doesn't exist.") - sys.exit(1) - elif args.import_from_pkg: - if "." in args.import_from_pkg: - root, subpackage = args.import_from_pkg.split(".", maxsplit=1) - else: - root, subpackage = args.import_from_pkg, None - try: - with resources.path(root, subpackage) as path: - romdir = path.resolve() - if not romdir.exists(): - print(f"Unable to find path {subpackage} in module {root}.") - sys.exit(1) - except ModuleNotFoundError: - print(f"Unable to find module {root}.") - sys.exit(1) - except Exception as e: - print(f"Unknown error {str(e)}.") - sys.exit(1) - - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", category=DeprecationWarning, module="ale_py.roms" - ) - - datadir = resources.files("ale_py.roms") - import_roms(romdir, datadir, pkg=args.import_from_pkg, dry_run=args.dry_run) - - -if __name__ == "__main__": - main()