Module pyboy
Expand source code
#
# License: See LICENSE file
# GitHub: https://github.com/Baekalfen/PyBoy
#
__pdoc__ = {
'core': False,
'logger': False,
'pyboy': False,
'utils': False,
}
__all__ = ['PyBoy', 'WindowEvent']
from .pyboy import PyBoy
from .utils import WindowEvent
Sub-modules
pyboy.botsupport-
Tools to help interfacing with the Game Boy hardware
pyboy.plugins-
Plugins that extend PyBoy's functionality. The only publicly exposed, are the game wrappers.
Classes
class PyBoy (gamerom_file, *, bootrom_file=None, profiling=False, disable_renderer=False, **kwargs)-
PyBoy is loadable as an object in Python. This means, it can be initialized from another script, and be controlled and probed by the script. It is supported to spawn multiple emulators, just instantiate the class multiple times.
This object,
WindowEvent, and thepyboy.botsupportmodule, are the only official user-facing interfaces. All other parts of the emulator, are subject to change.A range of methods are exposed, which should allow for complete control of the emulator. Please open an issue on GitHub, if other methods are needed for your projects. Take a look at
interface_example.pyortetris_bot.pyfor a crude "bot", which interacts with the game.Only the
gamerom_fileargument is required.Args
gamerom_file:str- Filepath to a game-ROM for the original Game Boy.
Kwargs
bootrom_file:str- Filepath to a boot-ROM to use. If unsure, specify
None. profiling:bool- Profile the emulator and report opcode usage (internal use).
disable_renderer:bool- Can be used to optimize performance, by internally disable rendering of the screen.
color_palette:tuple- Specify the color palette to use for rendering.
Expand source code
class PyBoy: def __init__( self, gamerom_file, *, bootrom_file=None, profiling=False, disable_renderer=False, **kwargs ): """ PyBoy is loadable as an object in Python. This means, it can be initialized from another script, and be controlled and probed by the script. It is supported to spawn multiple emulators, just instantiate the class multiple times. This object, `pyboy.WindowEvent`, and the `pyboy.botsupport` module, are the only official user-facing interfaces. All other parts of the emulator, are subject to change. A range of methods are exposed, which should allow for complete control of the emulator. Please open an issue on GitHub, if other methods are needed for your projects. Take a look at `interface_example.py` or `tetris_bot.py` for a crude "bot", which interacts with the game. Only the `gamerom_file` argument is required. Args: gamerom_file (str): Filepath to a game-ROM for the original Game Boy. Kwargs: bootrom_file (str): Filepath to a boot-ROM to use. If unsure, specify `None`. profiling (bool): Profile the emulator and report opcode usage (internal use). disable_renderer (bool): Can be used to optimize performance, by internally disable rendering of the screen. color_palette (tuple): Specify the color palette to use for rendering. """ for k, v in defaults.items(): if k not in kwargs: kwargs[k] = kwargs.get(k, defaults[k]) if not os.path.isfile(gamerom_file): raise FileNotFoundError(f"ROM file {gamerom_file} was not found!") self.gamerom_file = gamerom_file self.mb = Motherboard( gamerom_file, bootrom_file, kwargs["color_palette"], disable_renderer, profiling=profiling ) # Performance measures self.avg_pre = 0 self.avg_tick = 0 self.avg_post = 0 # Absolute frame count of the emulation self.frame_count = 0 self.set_emulation_speed(1) self.paused = False self.events = [] self.done = False self.window_title = "PyBoy" ################### # Plugins self.plugin_manager = PluginManager(self, self.mb, kwargs) def tick(self): """ Progresses the emulator ahead by one frame. To run the emulator in real-time, this will need to be called 60 times a second (for example in a while-loop). This function will block for roughly 16,67ms at a time, to not run faster than real-time, unless you specify otherwise with the `PyBoy.set_emulation_speed` method. _Open an issue on GitHub if you need finer control, and we will take a look at it._ """ t_start = time.perf_counter() # Change to _ns when PyPy supports it self._handle_events(self.events) t_pre = time.perf_counter() self.frame_count += 1 if not self.paused: self.mb.tickframe() t_tick = time.perf_counter() self._post_tick() t_post = time.perf_counter() secs = t_pre-t_start self.avg_pre = 0.9 * self.avg_pre + 0.1 * secs secs = t_tick-t_pre self.avg_tick = 0.9 * self.avg_tick + 0.1 * secs secs = t_post-t_tick self.avg_post = 0.9 * self.avg_post + 0.1 * secs return self.done def _handle_events(self, events): # This feeds events into the tick-loop from the window. There might already be events in the list from the API. events = self.plugin_manager.handle_events(events) for event in events: if event == WindowEvent.QUIT: self.done = True elif event == WindowEvent.RELEASE_SPEED_UP: # Switch between unlimited and 1x real-time emulation speed self.target_emulationspeed = int(bool(self.target_emulationspeed) ^ True) logger.info("Speed limit: %s" % self.target_emulationspeed) elif event == WindowEvent.STATE_SAVE: with open(self.gamerom_file + ".state", "wb") as f: self.mb.save_state(IntIOWrapper(f)) elif event == WindowEvent.STATE_LOAD: with open(self.gamerom_file + ".state", "rb") as f: self.mb.load_state(IntIOWrapper(f)) elif event == WindowEvent.PASS: pass # Used in place of None in Cython, when key isn't mapped to anything elif event == WindowEvent.PAUSE_TOGGLE: if self.paused: self._unpause() else: self._pause() elif event == WindowEvent.PAUSE: self._pause() elif event == WindowEvent.UNPAUSE: self._unpause() elif event == WindowEvent._INTERNAL_RENDERER_FLUSH: self.plugin_manager._post_tick_windows() else: self.mb.buttonevent(event) def _pause(self): if self.paused: return self.paused = True self.save_target_emulationspeed = self.target_emulationspeed self.target_emulationspeed = 1 logger.info("Emulation paused!") self._update_window_title() def _unpause(self): if not self.paused: return self.paused = False self.target_emulationspeed = self.save_target_emulationspeed logger.info("Emulation unpaused!") self._update_window_title() def _post_tick(self): if self.frame_count % 60 == 0: self._update_window_title() self.plugin_manager.post_tick() self.plugin_manager.frame_limiter(self.target_emulationspeed) # Prepare an empty list, as the API might be used to send in events between ticks self.events = [] def _update_window_title(self): avg_emu = self.avg_pre + self.avg_tick + self.avg_post self.window_title = "CPU/frame: %0.2f%%" % ((self.avg_pre + self.avg_tick)/SPF*100) self.window_title += " Emulation: x%d" % (round(SPF/avg_emu) if avg_emu != 0 else 0) if self.paused: self.window_title += "[PAUSED]" self.window_title += self.plugin_manager.window_title() self.plugin_manager._set_title() def __del__(self): self.stop(save=False) def stop(self, save=True): """ Gently stops the emulator and all sub-modules. Args: save (bool): Specify whether to save the game upon stopping. It will always be saved in a file next to the provided game-ROM. """ logger.info("###########################") logger.info("# Emulator is turning off #") logger.info("###########################") self.plugin_manager.stop() self.mb.stop(save) def _cpu_hitrate(self): return self.mb.cpu.hitrate ################################################################### # Scripts and bot methods # def botsupport_manager(self): """ Returns ------- `pyboy.botsupport.BotSupportManager`: The manager, which gives easier access to the emulated game through the classes in `pyboy.botsupport`. """ return botsupport.BotSupportManager(self, self.mb) def game_wrapper(self): """ Provides an instance of a game-specific wrapper. The game is detected by the cartridge's hard-coded game title (see `pyboy.PyBoy.cartridge_title`). If the game isn't supported, None will be returned. To get more information, find the wrapper for your game in `pyboy.plugins`. Returns ------- `pyboy.plugins.base_plugin.PyBoyGameWrapper`: A game-specific wrapper object. """ return self.plugin_manager.gamewrapper() def get_memory_value(self, addr): """ Reads a given memory address of the Game Boy's current memory state. This will not directly give you access to all switchable memory banks. Open an issue on GitHub if that is needed, or use `PyBoy.set_memory_value` to send MBC commands to the virtual cartridge. Returns ------- int: An integer with the value of the memory address """ return self.mb.getitem(addr) def set_memory_value(self, addr, value): """ Write one byte to a given memory address of the Game Boy's current memory state. This will not directly give you access to all switchable memory banks. Open an issue on GitHub if that is needed, or use this function to send "Memory Bank Controller" (MBC) commands to the virtual cartridge. You can read about the MBC at [Pan Docs](http://bgb.bircd.org/pandocs.htm). Args: addr (int): Address to write the byte value (int): A byte of data """ self.mb.setitem(addr, value) def send_input(self, event): """ Send a single input to control the emulator. This is both Game Boy buttons and emulator controls. See `pyboy.WindowEvent` for which events to send. Args: event (pyboy.WindowEvent): The event to send """ self.events.append(WindowEvent(event)) def save_state(self, file_like_object): """ Saves the complete state of the emulator. It can be called at any time, and enable you to revert any progress in a game. You can either save it to a file, or in-memory. The following two examples will provide the file handle in each case. Remember to `seek` the in-memory buffer to the beginning before calling `PyBoy.load_state`: # Save to file file_like_object = open("state_file.state", "wb") # Save to memory import io file_like_object = io.BytesIO() file_like_object.seek(0) Args: file_like_object (io.BufferedIOBase): A file-like object for which to write the emulator state. """ if isinstance(file_like_object, str): raise Exception("String not allowed. Did you specify a filepath instead of a file-like object?") self.mb.save_state(IntIOWrapper(file_like_object)) def load_state(self, file_like_object): """ Restores the complete state of the emulator. It can be called at any time, and enable you to revert any progress in a game. You can either load it from a file, or from memory. See `PyBoy.save_state` for how to save the state, before you can load it here. To load a file, remember to load it as bytes: # Load file file_like_object = open("state_file.state", "rb") Args: file_like_object (io.BufferedIOBase): A file-like object for which to read the emulator state. """ if isinstance(file_like_object, str): raise Exception("String not allowed. Did you specify a filepath instead of a file-like object?") self.mb.load_state(IntIOWrapper(file_like_object)) def _serial(self): """ Provides all data that has been sent over the serial port since last call to this function. Returns ------- str : Buffer data """ return self.mb.getserial() def set_emulation_speed(self, target_speed): """ Set the target emulation speed. It might loose accuracy of keeping the exact speed, when using a high `target_speed`. The speed is defined as a multiple of real-time. I.e `target_speed=2` is double speed. A `target_speed` of `0` means unlimited. I.e. fastest possible execution. Args: target_speed (int): Target emulation speed as multiplier of real-time. """ if target_speed > 5: logger.warning("The emulation speed might not be accurate when speed-target is higher than 5") self.target_emulationspeed = target_speed def cartridge_title(self): """ Get the title stored on the currently loaded cartridge ROM. The title is all upper-case ASCII and may have been truncated to 11 characters. Returns ------- str : Game title """ return self.mb.cartridge.gamenameMethods
def tick(self)-
Progresses the emulator ahead by one frame.
To run the emulator in real-time, this will need to be called 60 times a second (for example in a while-loop). This function will block for roughly 16,67ms at a time, to not run faster than real-time, unless you specify otherwise with the
PyBoy.set_emulation_speed()method.Open an issue on GitHub if you need finer control, and we will take a look at it.
Expand source code
def tick(self): """ Progresses the emulator ahead by one frame. To run the emulator in real-time, this will need to be called 60 times a second (for example in a while-loop). This function will block for roughly 16,67ms at a time, to not run faster than real-time, unless you specify otherwise with the `PyBoy.set_emulation_speed` method. _Open an issue on GitHub if you need finer control, and we will take a look at it._ """ t_start = time.perf_counter() # Change to _ns when PyPy supports it self._handle_events(self.events) t_pre = time.perf_counter() self.frame_count += 1 if not self.paused: self.mb.tickframe() t_tick = time.perf_counter() self._post_tick() t_post = time.perf_counter() secs = t_pre-t_start self.avg_pre = 0.9 * self.avg_pre + 0.1 * secs secs = t_tick-t_pre self.avg_tick = 0.9 * self.avg_tick + 0.1 * secs secs = t_post-t_tick self.avg_post = 0.9 * self.avg_post + 0.1 * secs return self.done def stop(self, save=True)-
Gently stops the emulator and all sub-modules.
Args
save:bool- Specify whether to save the game upon stopping. It will always be saved in a file next to the provided game-ROM.
Expand source code
def stop(self, save=True): """ Gently stops the emulator and all sub-modules. Args: save (bool): Specify whether to save the game upon stopping. It will always be saved in a file next to the provided game-ROM. """ logger.info("###########################") logger.info("# Emulator is turning off #") logger.info("###########################") self.plugin_manager.stop() self.mb.stop(save) def botsupport_manager(self)-
Returns
BotSupportManager: The manager, which gives easier access to the emulated game through the classes inpyboy.botsupport.Expand source code
def botsupport_manager(self): """ Returns ------- `pyboy.botsupport.BotSupportManager`: The manager, which gives easier access to the emulated game through the classes in `pyboy.botsupport`. """ return botsupport.BotSupportManager(self, self.mb) def game_wrapper(self)-
Provides an instance of a game-specific wrapper. The game is detected by the cartridge's hard-coded game title (see
PyBoy.cartridge_title()).If the game isn't supported, None will be returned.
To get more information, find the wrapper for your game in
pyboy.plugins.Returns
PyBoyGameWrapper: A game-specific wrapper object.Expand source code
def game_wrapper(self): """ Provides an instance of a game-specific wrapper. The game is detected by the cartridge's hard-coded game title (see `pyboy.PyBoy.cartridge_title`). If the game isn't supported, None will be returned. To get more information, find the wrapper for your game in `pyboy.plugins`. Returns ------- `pyboy.plugins.base_plugin.PyBoyGameWrapper`: A game-specific wrapper object. """ return self.plugin_manager.gamewrapper() def get_memory_value(self, addr)-
Reads a given memory address of the Game Boy's current memory state. This will not directly give you access to all switchable memory banks. Open an issue on GitHub if that is needed, or use
PyBoy.set_memory_value()to send MBC commands to the virtual cartridge.Returns
int:- An integer with the value of the memory address
Expand source code
def get_memory_value(self, addr): """ Reads a given memory address of the Game Boy's current memory state. This will not directly give you access to all switchable memory banks. Open an issue on GitHub if that is needed, or use `PyBoy.set_memory_value` to send MBC commands to the virtual cartridge. Returns ------- int: An integer with the value of the memory address """ return self.mb.getitem(addr) def set_memory_value(self, addr, value)-
Write one byte to a given memory address of the Game Boy's current memory state.
This will not directly give you access to all switchable memory banks. Open an issue on GitHub if that is needed, or use this function to send "Memory Bank Controller" (MBC) commands to the virtual cartridge. You can read about the MBC at Pan Docs.
Args
addr:int- Address to write the byte
value:int- A byte of data
Expand source code
def set_memory_value(self, addr, value): """ Write one byte to a given memory address of the Game Boy's current memory state. This will not directly give you access to all switchable memory banks. Open an issue on GitHub if that is needed, or use this function to send "Memory Bank Controller" (MBC) commands to the virtual cartridge. You can read about the MBC at [Pan Docs](http://bgb.bircd.org/pandocs.htm). Args: addr (int): Address to write the byte value (int): A byte of data """ self.mb.setitem(addr, value) def send_input(self, event)-
Send a single input to control the emulator. This is both Game Boy buttons and emulator controls.
See
WindowEventfor which events to send.Args
event:WindowEvent- The event to send
Expand source code
def send_input(self, event): """ Send a single input to control the emulator. This is both Game Boy buttons and emulator controls. See `pyboy.WindowEvent` for which events to send. Args: event (pyboy.WindowEvent): The event to send """ self.events.append(WindowEvent(event)) def save_state(self, file_like_object)-
Saves the complete state of the emulator. It can be called at any time, and enable you to revert any progress in a game.
You can either save it to a file, or in-memory. The following two examples will provide the file handle in each case. Remember to
seekthe in-memory buffer to the beginning before callingPyBoy.load_state():# Save to file file_like_object = open("state_file.state", "wb") # Save to memory import io file_like_object = io.BytesIO() file_like_object.seek(0)Args
file_like_object:io.BufferedIOBase- A file-like object for which to write the emulator state.
Expand source code
def save_state(self, file_like_object): """ Saves the complete state of the emulator. It can be called at any time, and enable you to revert any progress in a game. You can either save it to a file, or in-memory. The following two examples will provide the file handle in each case. Remember to `seek` the in-memory buffer to the beginning before calling `PyBoy.load_state`: # Save to file file_like_object = open("state_file.state", "wb") # Save to memory import io file_like_object = io.BytesIO() file_like_object.seek(0) Args: file_like_object (io.BufferedIOBase): A file-like object for which to write the emulator state. """ if isinstance(file_like_object, str): raise Exception("String not allowed. Did you specify a filepath instead of a file-like object?") self.mb.save_state(IntIOWrapper(file_like_object)) def load_state(self, file_like_object)-
Restores the complete state of the emulator. It can be called at any time, and enable you to revert any progress in a game.
You can either load it from a file, or from memory. See
PyBoy.save_state()for how to save the state, before you can load it here.To load a file, remember to load it as bytes:
# Load file file_like_object = open("state_file.state", "rb")Args
file_like_object:io.BufferedIOBase- A file-like object for which to read the emulator state.
Expand source code
def load_state(self, file_like_object): """ Restores the complete state of the emulator. It can be called at any time, and enable you to revert any progress in a game. You can either load it from a file, or from memory. See `PyBoy.save_state` for how to save the state, before you can load it here. To load a file, remember to load it as bytes: # Load file file_like_object = open("state_file.state", "rb") Args: file_like_object (io.BufferedIOBase): A file-like object for which to read the emulator state. """ if isinstance(file_like_object, str): raise Exception("String not allowed. Did you specify a filepath instead of a file-like object?") self.mb.load_state(IntIOWrapper(file_like_object)) def set_emulation_speed(self, target_speed)-
Set the target emulation speed. It might loose accuracy of keeping the exact speed, when using a high
target_speed.The speed is defined as a multiple of real-time. I.e
target_speed=2is double speed.A
target_speedof0means unlimited. I.e. fastest possible execution.Args
target_speed:int- Target emulation speed as multiplier of real-time.
Expand source code
def set_emulation_speed(self, target_speed): """ Set the target emulation speed. It might loose accuracy of keeping the exact speed, when using a high `target_speed`. The speed is defined as a multiple of real-time. I.e `target_speed=2` is double speed. A `target_speed` of `0` means unlimited. I.e. fastest possible execution. Args: target_speed (int): Target emulation speed as multiplier of real-time. """ if target_speed > 5: logger.warning("The emulation speed might not be accurate when speed-target is higher than 5") self.target_emulationspeed = target_speed def cartridge_title(self)-
Get the title stored on the currently loaded cartridge ROM. The title is all upper-case ASCII and may have been truncated to 11 characters.
Returns
str:- Game title
Expand source code
def cartridge_title(self): """ Get the title stored on the currently loaded cartridge ROM. The title is all upper-case ASCII and may have been truncated to 11 characters. Returns ------- str : Game title """ return self.mb.cartridge.gamename
class WindowEvent (event)-
All supported events can be found in the class description below.
It can be used as follows:
>>> from pyboy import PyBoy, WindowEvent >>> pyboy = PyBoy('file.rom') >>> pyboy.send_input(WindowEvent.PRESS_ARROW_RIGHT)Expand source code
class WindowEvent: """ All supported events can be found in the class description below. It can be used as follows: >>> from pyboy import PyBoy, WindowEvent >>> pyboy = PyBoy('file.rom') >>> pyboy.send_input(WindowEvent.PRESS_ARROW_RIGHT) """ # ONLY ADD NEW EVENTS AT THE END OF THE LIST! # Otherwise, it will break replays, which depend on the id of the event (QUIT, PRESS_ARROW_UP, PRESS_ARROW_DOWN, PRESS_ARROW_RIGHT, PRESS_ARROW_LEFT, PRESS_BUTTON_A, PRESS_BUTTON_B, PRESS_BUTTON_SELECT, PRESS_BUTTON_START, RELEASE_ARROW_UP, RELEASE_ARROW_DOWN, RELEASE_ARROW_RIGHT, RELEASE_ARROW_LEFT, RELEASE_BUTTON_A, RELEASE_BUTTON_B, RELEASE_BUTTON_SELECT, RELEASE_BUTTON_START, _INTERNAL_TOGGLE_DEBUG, PRESS_SPEED_UP, RELEASE_SPEED_UP, STATE_SAVE, STATE_LOAD, PASS, SCREEN_RECORDING_TOGGLE, PAUSE, UNPAUSE, PAUSE_TOGGLE, PRESS_REWIND_BACK, PRESS_REWIND_FORWARD, RELEASE_REWIND_BACK, RELEASE_REWIND_FORWARD, WINDOW_FOCUS, WINDOW_UNFOCUS, _INTERNAL_RENDERER_FLUSH, _INTERNAL_MOUSE, _INTERNAL_MARK_TILE ) = range(36) def __init__(self, event): self.event = event def __eq__(self, x): if isinstance(x, int): return self.event == x else: return self.event == x.eventSubclasses
- pyboy.utils.WindowEventMouse
Class variables
var QUITvar PRESS_ARROW_UPvar PRESS_ARROW_DOWNvar PRESS_ARROW_RIGHTvar PRESS_ARROW_LEFTvar PRESS_BUTTON_Avar PRESS_BUTTON_Bvar PRESS_BUTTON_SELECTvar PRESS_BUTTON_STARTvar RELEASE_ARROW_UPvar RELEASE_ARROW_DOWNvar RELEASE_ARROW_RIGHTvar RELEASE_ARROW_LEFTvar RELEASE_BUTTON_Avar RELEASE_BUTTON_Bvar RELEASE_BUTTON_SELECTvar RELEASE_BUTTON_STARTvar PRESS_SPEED_UPvar RELEASE_SPEED_UPvar STATE_SAVEvar STATE_LOADvar PASSvar SCREEN_RECORDING_TOGGLEvar PAUSEvar UNPAUSEvar PAUSE_TOGGLEvar PRESS_REWIND_BACKvar PRESS_REWIND_FORWARDvar RELEASE_REWIND_BACKvar RELEASE_REWIND_FORWARDvar WINDOW_FOCUSvar WINDOW_UNFOCUS