+Getting Started
+===============
+The instructions are simple:
-Installation
-============
-The instructions are simple if you already have a functioning Python environment on your machine.
+```sh
+$ pip install pyboy
+```
- 1. Install PyBoy using __`pip install pyboy`__ (add __` --user`__ if your system asks)
- 2. If your system isn't supported by [pysdl2-dll](https://pypi.org/project/pysdl2-dll/), you'll need to install SDL2 from your package manager.
+For details, see [installation instructions](https://github.com/Baekalfen/PyBoy/wiki/Installation).
-If you need more details, or if you need to compile from source, check out the detailed [installation instructions](https://github.com/Baekalfen/PyBoy/wiki/Installation). We support: macOS, Raspberry Pi (Raspbian), Linux (Ubuntu), and Windows 10.
+Now you're ready! Either use PyBoy directly from the terminal
+```sh
+$ pyboy game_rom.gb
+```
-Now you're ready! Either use PyBoy directly from the terminal __`$ pyboy file.rom`__ or use it in your Python scripts:
+Or use it in your Python scripts:
```python
from pyboy import PyBoy
-pyboy = PyBoy('ROMs/gamerom.gb')
-while pyboy.tick(1, True):
+pyboy = PyBoy('game_rom.gb')
+while pyboy.tick():
pass
pyboy.stop()
```
@@ -68,60 +81,120 @@ pyboy.stop()
+
+The API
+=======
+
+If you are looking to make a bot or AI, then these resources are a good place to start:
+ * [PyBoy API Documentation](https://baekalfen.github.io/PyBoy/index.html)
+ * [Wiki Pages](https://github.com/Baekalfen/PyBoy/wiki/)
+ * [Using PyBoy with Gym](https://github.com/Baekalfen/PyBoy/wiki/Using-PyBoy-with-Gym)
+ * [Example: Kirby](https://github.com/Baekalfen/PyBoy/wiki/Example-Kirby)
+ * [Example: Tetris](https://github.com/Baekalfen/PyBoy/wiki/Example-Tetris)
+ * [Example: Super Mario Land](https://github.com/Baekalfen/PyBoy/wiki/Example-Super-Mario-Land)
+ * [Code Examples](https://github.com/Baekalfen/PyBoy/tree/master/examples)
+ * [Discord](https://discord.gg/Zrf2nyH)
+
+
When the emulator is running, you can easily access [PyBoy's API](https://baekalfen.github.io/PyBoy/index.html):
```python
+pyboy.set_emulation_speed(0) # No speed limit
pyboy.button('down')
pyboy.button('a')
-some_value = pyboy.memory[0xC345]
-
-pyboy.send_input(WindowEvent.PRESS_ARROW_DOWN)
-pyboy.tick() # Process one frame to let the game register the input
-pyboy.send_input(WindowEvent.RELEASE_ARROW_DOWN)
+pyboy.tick() # Process at least one frame to let the game register the input
+value_of_interest = pyboy.memory[0xC345]
-pil_image = pyboy.screen_image()
-pyboy.tick(1, True)
+pil_image = pyboy.screen.image
pil_image.save('screenshot.png')
```
-The Wiki shows how to interface with PyBoy from your own project: [Wiki](https://github.com/Baekalfen/PyBoy/wiki).
+The [Wiki](https://github.com/Baekalfen/PyBoy/wiki) shows how to interface with PyBoy from your own project.
+Performance
+===========
-Contributors
-============
+Performance is a priority for PyBoy, to make your AI training and scripts as fast as possible.
+
+The easiest way to improve your performance, is to skip rendering of unnecessary frames. If you know your
+character takes X frames to move, or the game doesn't take input every frame, you can skip those to potentially triple
+your performance. All game logic etc. will still process.
+
+Here is a simple comparison of rendering every frame, rendering every 15th frame, and not rendering any frames (higher is better). See [`pyboy.tick`](https://docs.pyboy.dk/#pyboy.PyBoy.tick) for how it works. Your performance will depend on the game.
+
+
+
+
+
+ Full rendering
+
+
+ Frame-skip 15
+
+
+ No rendering
+
+
+
+
+ x124 realtime
+
+
+ x344 realtime
+
+
+ x395 realtime
+
+
+
+
+
+```python
+for _ in range(target):
+ pyboy.tick()
-Thanks to all the people who have contributed to the project!
+```
+
- * Kristian Sims - [krs013](https://github.com/krs013)
+The Game Boy was originally running at 60 frames per second, so a speed-up of 100x realtime is 6,000 frames per
+second. And trivially from the table above, simulating 395 hours of gameplay can be done in 1 hour with no rendering.
-Student Projects
-----------------
+It's also recommended to be running multiple instances of PyBoy in parallel. On an 8-core machine, you could potentially
+do 3160 hours of gameplay in 1 hour.
- * __Rewind Time:__ Jacob Olsen - [JacobO1](https://github.com/JacobO1)
- * __Link Cable:__ Jonas Flach-Jensen - [thejomas](https://github.com/thejomas)
- * __Game Boy Color:__ Christian Marslev and Jonas Grønborg - [CKuke](https://github.com/CKuke) and [kaff3](https://github.com/kaff3)
+Contributing
+============
+Any contribution is appreciated. The currently known problems are tracked in [the Issues tab](https://github.com/Baekalfen/PyBoy/issues). Feel free to take a swing at any one of them. If you have something original in mind, come and [discuss it on on Discord](https://discord.gg/Zrf2nyH).
-Contribute
-==========
-Any contribution is appreciated. The currently known problems are tracked in the Issues tab. Feel free to take a swing at any one of them.
+[![Discord](https://img.shields.io/discord/584149554132418570?style=for-the-badge&logo=Discord&label=PyBoy)](https://discord.gg/Zrf2nyH)
For the more major features, there are the following that you can give a try. They are also described in more detail in the [project list in the Wiki](https://github.com/Baekalfen/PyBoy/wiki/Student-Projects):
+* Hacking games
* Link Cable
-* _(Experimental)_ AI - use the `botsupport` or game wrappers to train a neural network
-* _(Experimental)_ Game Wrappers - make wrappers for popular games
+* Debugger (VSCode, GDB, terminal or otherwise)
+* AI - [use the `api`](https://baekalfen.github.io/PyBoy/index.html) or game wrappers to train a neural network
+* Game Wrappers - make wrappers for popular games
If you want to implement something which is not on the list, feel free to do so anyway. If you want to merge it into our repo, then just send a pull request and we will have a look at it.
From 1ed7460bdce85228a87792b1e4c044c92f5293a2 Mon Sep 17 00:00:00 2001
From: Mads Ynddal
Date: Sun, 11 Feb 2024 09:46:52 +0100
Subject: [PATCH 64/65] Update docs
---
docs/api/constants.html | 192 +
docs/api/index.html | 117 +
docs/api/memory_scanner.html | 739 ++++
docs/api/screen.html | 783 ++++
docs/{botsupport => api}/sprite.html | 153 +-
docs/{botsupport => api}/tile.html | 425 ++-
docs/{botsupport => api}/tilemap.html | 344 +-
docs/botsupport/index.html | 480 ---
docs/botsupport/screen.html | 579 ---
docs/index.html | 3390 ++++++++++++-----
docs/openai_gym.html | 596 ---
docs/plugins/base_plugin.html | 302 +-
.../game_wrapper_kirby_dream_land.html | 188 +-
docs/plugins/game_wrapper_pokemon_gen1.html | 91 +-
.../game_wrapper_super_mario_land.html | 1084 ++++--
docs/plugins/game_wrapper_tetris.html | 269 +-
docs/plugins/index.html | 20 +-
docs/utils.html | 889 +++++
18 files changed, 6988 insertions(+), 3653 deletions(-)
create mode 100644 docs/api/constants.html
create mode 100644 docs/api/index.html
create mode 100644 docs/api/memory_scanner.html
create mode 100644 docs/api/screen.html
rename docs/{botsupport => api}/sprite.html (77%)
rename docs/{botsupport => api}/tile.html (51%)
rename docs/{botsupport => api}/tilemap.html (74%)
delete mode 100644 docs/botsupport/index.html
delete mode 100644 docs/botsupport/screen.html
delete mode 100644 docs/openai_gym.html
create mode 100644 docs/utils.html
diff --git a/docs/api/constants.html b/docs/api/constants.html
new file mode 100644
index 000000000..d4b592a0f
--- /dev/null
+++ b/docs/api/constants.html
@@ -0,0 +1,192 @@
+
+
+
+
+
+
+pyboy.api.constants API documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Module pyboy.api.constants
+
+
+
Memory constants used internally to calculate tile and tile map addresses.
+
+
+Expand source code
+
+
#
+# License: See LICENSE.md file
+# GitHub: https://github.com/Baekalfen/PyBoy
+#
+"""
+Memory constants used internally to calculate tile and tile map addresses.
+"""
+
+VRAM_OFFSET = 0x8000
+"""
+Start address of VRAM
+"""
+LCDC_OFFSET = 0xFF40
+"""
+LCDC Register
+"""
+OAM_OFFSET = 0xFE00
+"""
+Start address of Object-Attribute-Memory (OAM)
+"""
+LOW_TILEMAP = 0x1800 + VRAM_OFFSET
+"""
+Start address of lower tilemap
+"""
+HIGH_TILEMAP = 0x1C00 + VRAM_OFFSET
+"""
+Start address of high tilemap
+"""
+LOW_TILEDATA = VRAM_OFFSET
+"""
+Start address of lower tile data
+"""
+LOW_TILEDATA_NTILES = 0x100
+"""
+Number of tiles in lower tile data
+"""
+HIGH_TILEDATA = 0x800 + VRAM_OFFSET
+"""
+Start address of high tile data
+"""
+TILES = 384
+"""
+Number of tiles supported on Game Boy DMG (non-color)
+"""
+TILES_CGB = 768
+"""
+Number of tiles supported on Game Boy Color
+"""
+SPRITES = 40
+"""
+Number of sprites supported
+"""
+ROWS = 144
+"""
+Rows (horizontal lines) on the screen
+"""
+COLS = 160
+"""
+Columns (vertical lines) on the screen
+"""
+
+
+
+
+
+
Global variables
+
+
var VRAM_OFFSET
+
+
Start address of VRAM
+
+
var LCDC_OFFSET
+
+
LCDC Register
+
+
var OAM_OFFSET
+
+
Start address of Object-Attribute-Memory (OAM)
+
+
var LOW_TILEMAP
+
+
Start address of lower tilemap
+
+
var HIGH_TILEMAP
+
+
Start address of high tilemap
+
+
var LOW_TILEDATA
+
+
Start address of lower tile data
+
+
var LOW_TILEDATA_NTILES
+
+
Number of tiles in lower tile data
+
+
var HIGH_TILEDATA
+
+
Start address of high tile data
+
+
var TILES
+
+
Number of tiles supported on Game Boy DMG (non-color)
+
+
var TILES_CGB
+
+
Number of tiles supported on Game Boy Color
+
+
var SPRITES
+
+
Number of sprites supported
+
+
var ROWS
+
+
Rows (horizontal lines) on the screen
+
+
var COLS
+
+
Columns (vertical lines) on the screen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/api/index.html b/docs/api/index.html
new file mode 100644
index 000000000..69d4af7ea
--- /dev/null
+++ b/docs/api/index.html
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+pyboy.api API documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Module pyboy.api
+
+
+
Tools to help interfacing with the Game Boy hardware
+
+
+Expand source code
+
+
#
+# License: See LICENSE.md file
+# GitHub: https://github.com/Baekalfen/PyBoy
+#
+"""
+Tools to help interfacing with the Game Boy hardware
+"""
+
+from . import constants
+from .screen import Screen
+from .sprite import Sprite
+from .tile import Tile
+from .tilemap import TileMap
+
+# __pdoc__ = {
+# "constants": False,
+# "manager": False,
+# }
+# __all__ = ["API"]
The Game Boy has two tile maps, which defines what is rendered on the screen.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/api/memory_scanner.html b/docs/api/memory_scanner.html
new file mode 100644
index 000000000..4152de771
--- /dev/null
+++ b/docs/api/memory_scanner.html
@@ -0,0 +1,739 @@
+
+
+
+
+
+
+pyboy.api.memory_scanner API documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Module pyboy.api.memory_scanner
+
+
+
+
+Expand source code
+
+
from enum import Enum
+
+from pyboy.utils import bcd_to_dec
+
+
+class StandardComparisonType(Enum):
+ """Enumeration for defining types of comparisons that do not require a previous value."""
+ EXACT = 1
+ LESS_THAN = 2
+ GREATER_THAN = 3
+ LESS_THAN_OR_EQUAL = 4
+ GREATER_THAN_OR_EQUAL = 5
+
+
+class DynamicComparisonType(Enum):
+ """Enumeration for defining types of comparisons that require a previous value."""
+ UNCHANGED = 1
+ CHANGED = 2
+ INCREASED = 3
+ DECREASED = 4
+ MATCH = 5
+
+
+class ScanMode(Enum):
+ """Enumeration for defining scanning modes."""
+ INT = 1
+ BCD = 2
+
+
+class MemoryScanner():
+ """A class for scanning memory within a given range."""
+ def __init__(self, pyboy):
+ self.pyboy = pyboy
+ self._memory_cache = {}
+ self._memory_cache_byte_width = 1
+
+ def scan_memory(
+ self,
+ target_value=None,
+ start_addr=0x0000,
+ end_addr=0xFFFF,
+ standard_comparison_type=StandardComparisonType.EXACT,
+ value_type=ScanMode.INT,
+ byte_width=1,
+ byteorder="little"
+ ):
+ """
+ This function scans a specified range of memory for a target value from the `start_addr` to the `end_addr` (both included).
+
+ Example:
+ ```python
+ >>> current_score = 4 # You write current score in game
+ >>> pyboy.memory_scanner.scan_memory(current_score, start_addr=0xC000, end_addr=0xDFFF)
+ []
+
+ ```
+
+ Args:
+ start_addr (int): The starting address for the scan.
+ end_addr (int): The ending address for the scan.
+ target_value (int or None): The value to search for. If None, any value is considered a match.
+ standard_comparison_type (StandardComparisonType): The type of comparison to use.
+ value_type (ValueType): The type of value (INT or BCD) to consider.
+ byte_width (int): The number of bytes to consider for each value.
+ byteorder (str): The endian type to use. This is only used for 16-bit values and higher. See [int.from_bytes](https://docs.python.org/3/library/stdtypes.html#int.from_bytes) for more details.
+
+ Returns:
+ list of int: A list of addresses where the target value is found.
+ """
+ self._memory_cache = {}
+ self._memory_cache_byte_width = byte_width
+ for addr in range(start_addr, end_addr - (byte_width-1) + 1): # Adjust the loop to prevent reading past end_addr
+ # Read multiple bytes based on byte_width and byteorder
+ value_bytes = self.pyboy.memory[addr:addr + byte_width]
+ value = int.from_bytes(value_bytes, byteorder)
+
+ if value_type == ScanMode.BCD:
+ value = bcd_to_dec(value, byte_width, byteorder)
+
+ if target_value is None or self._check_value(value, target_value, standard_comparison_type.value):
+ self._memory_cache[addr] = value
+
+ return list(self._memory_cache.keys())
+
+ def rescan_memory(
+ self, new_value=None, dynamic_comparison_type=DynamicComparisonType.UNCHANGED, byteorder="little"
+ ):
+ """
+ Rescans the memory and updates the memory cache based on a dynamic comparison type.
+
+ Example:
+ ```python
+ >>> current_score = 4 # You write current score in game
+ >>> pyboy.memory_scanner.scan_memory(current_score, start_addr=0xC000, end_addr=0xDFFF)
+ []
+ >>> for _ in range(175):
+ ... pyboy.tick(1, True) # Progress the game to change score
+ True...
+ >>> current_score = 8 # You write the new score in game
+ >>> from pyboy.api.memory_scanner import DynamicComparisonType
+ >>> addresses = pyboy.memory_scanner.rescan_memory(current_score, DynamicComparisonType.MATCH)
+ >>> print(addresses) # If repeated enough, only one address will remain
+ []
+
+ ```
+
+ Args:
+ new_value (int, optional): The new value for comparison. If not provided, the current value in memory is used.
+ dynamic_comparison_type (DynamicComparisonType): The type of comparison to use. Defaults to UNCHANGED.
+
+ Returns:
+ list of int: A list of addresses remaining in the memory cache after the rescan.
+ """
+ for addr, value in self._memory_cache.copy().items():
+ current_value = int.from_bytes(
+ self.pyboy.memory[addr:addr + self._memory_cache_byte_width], byteorder=byteorder
+ )
+ if (dynamic_comparison_type == DynamicComparisonType.UNCHANGED):
+ if value != current_value:
+ self._memory_cache.pop(addr)
+ else:
+ self._memory_cache[addr] = current_value
+ elif (dynamic_comparison_type == DynamicComparisonType.CHANGED):
+ if value == current_value:
+ self._memory_cache.pop(addr)
+ else:
+ self._memory_cache[addr] = current_value
+ elif (dynamic_comparison_type == DynamicComparisonType.INCREASED):
+ if value >= current_value:
+ self._memory_cache.pop(addr)
+ else:
+ self._memory_cache[addr] = current_value
+ elif (dynamic_comparison_type == DynamicComparisonType.DECREASED):
+ if value <= current_value:
+ self._memory_cache.pop(addr)
+ else:
+ self._memory_cache[addr] = current_value
+ elif (dynamic_comparison_type == DynamicComparisonType.MATCH):
+ if new_value == None:
+ raise ValueError("new_value must be specified when using DynamicComparisonType.MATCH")
+ if current_value != new_value:
+ self._memory_cache.pop(addr)
+ else:
+ self._memory_cache[addr] = current_value
+ else:
+ raise ValueError("Invalid comparison type")
+ return list(self._memory_cache.keys())
+
+ def _check_value(self, value, target_value, standard_comparison_type):
+ """
+ Compares a value with the target value based on the specified compare type.
+
+ Args:
+ value (int): The value to compare.
+ target_value (int or None): The target value to compare against.
+ standard_comparison_type (StandardComparisonType): The type of comparison to use.
+
+ Returns:
+ bool: True if the comparison condition is met, False otherwise.
+ """
+ if standard_comparison_type == StandardComparisonType.EXACT.value:
+ return value == target_value
+ elif standard_comparison_type == StandardComparisonType.LESS_THAN.value:
+ return value < target_value
+ elif standard_comparison_type == StandardComparisonType.GREATER_THAN.value:
+ return value > target_value
+ elif standard_comparison_type == StandardComparisonType.LESS_THAN_OR_EQUAL.value:
+ return value <= target_value
+ elif standard_comparison_type == StandardComparisonType.GREATER_THAN_OR_EQUAL.value:
+ return value >= target_value
+ else:
+ raise ValueError("Invalid comparison type")
class ScanMode(Enum):
+ """Enumeration for defining scanning modes."""
+ INT = 1
+ BCD = 2
+
+
Ancestors
+
+
enum.Enum
+
+
Class variables
+
+
var INT
+
+
+
+
var BCD
+
+
+
+
+
+
+class MemoryScanner
+(pyboy)
+
+
+
A class for scanning memory within a given range.
+
+
+Expand source code
+
+
class MemoryScanner():
+ """A class for scanning memory within a given range."""
+ def __init__(self, pyboy):
+ self.pyboy = pyboy
+ self._memory_cache = {}
+ self._memory_cache_byte_width = 1
+
+ def scan_memory(
+ self,
+ target_value=None,
+ start_addr=0x0000,
+ end_addr=0xFFFF,
+ standard_comparison_type=StandardComparisonType.EXACT,
+ value_type=ScanMode.INT,
+ byte_width=1,
+ byteorder="little"
+ ):
+ """
+ This function scans a specified range of memory for a target value from the `start_addr` to the `end_addr` (both included).
+
+ Example:
+ ```python
+ >>> current_score = 4 # You write current score in game
+ >>> pyboy.memory_scanner.scan_memory(current_score, start_addr=0xC000, end_addr=0xDFFF)
+ []
+
+ ```
+
+ Args:
+ start_addr (int): The starting address for the scan.
+ end_addr (int): The ending address for the scan.
+ target_value (int or None): The value to search for. If None, any value is considered a match.
+ standard_comparison_type (StandardComparisonType): The type of comparison to use.
+ value_type (ValueType): The type of value (INT or BCD) to consider.
+ byte_width (int): The number of bytes to consider for each value.
+ byteorder (str): The endian type to use. This is only used for 16-bit values and higher. See [int.from_bytes](https://docs.python.org/3/library/stdtypes.html#int.from_bytes) for more details.
+
+ Returns:
+ list of int: A list of addresses where the target value is found.
+ """
+ self._memory_cache = {}
+ self._memory_cache_byte_width = byte_width
+ for addr in range(start_addr, end_addr - (byte_width-1) + 1): # Adjust the loop to prevent reading past end_addr
+ # Read multiple bytes based on byte_width and byteorder
+ value_bytes = self.pyboy.memory[addr:addr + byte_width]
+ value = int.from_bytes(value_bytes, byteorder)
+
+ if value_type == ScanMode.BCD:
+ value = bcd_to_dec(value, byte_width, byteorder)
+
+ if target_value is None or self._check_value(value, target_value, standard_comparison_type.value):
+ self._memory_cache[addr] = value
+
+ return list(self._memory_cache.keys())
+
+ def rescan_memory(
+ self, new_value=None, dynamic_comparison_type=DynamicComparisonType.UNCHANGED, byteorder="little"
+ ):
+ """
+ Rescans the memory and updates the memory cache based on a dynamic comparison type.
+
+ Example:
+ ```python
+ >>> current_score = 4 # You write current score in game
+ >>> pyboy.memory_scanner.scan_memory(current_score, start_addr=0xC000, end_addr=0xDFFF)
+ []
+ >>> for _ in range(175):
+ ... pyboy.tick(1, True) # Progress the game to change score
+ True...
+ >>> current_score = 8 # You write the new score in game
+ >>> from pyboy.api.memory_scanner import DynamicComparisonType
+ >>> addresses = pyboy.memory_scanner.rescan_memory(current_score, DynamicComparisonType.MATCH)
+ >>> print(addresses) # If repeated enough, only one address will remain
+ []
+
+ ```
+
+ Args:
+ new_value (int, optional): The new value for comparison. If not provided, the current value in memory is used.
+ dynamic_comparison_type (DynamicComparisonType): The type of comparison to use. Defaults to UNCHANGED.
+
+ Returns:
+ list of int: A list of addresses remaining in the memory cache after the rescan.
+ """
+ for addr, value in self._memory_cache.copy().items():
+ current_value = int.from_bytes(
+ self.pyboy.memory[addr:addr + self._memory_cache_byte_width], byteorder=byteorder
+ )
+ if (dynamic_comparison_type == DynamicComparisonType.UNCHANGED):
+ if value != current_value:
+ self._memory_cache.pop(addr)
+ else:
+ self._memory_cache[addr] = current_value
+ elif (dynamic_comparison_type == DynamicComparisonType.CHANGED):
+ if value == current_value:
+ self._memory_cache.pop(addr)
+ else:
+ self._memory_cache[addr] = current_value
+ elif (dynamic_comparison_type == DynamicComparisonType.INCREASED):
+ if value >= current_value:
+ self._memory_cache.pop(addr)
+ else:
+ self._memory_cache[addr] = current_value
+ elif (dynamic_comparison_type == DynamicComparisonType.DECREASED):
+ if value <= current_value:
+ self._memory_cache.pop(addr)
+ else:
+ self._memory_cache[addr] = current_value
+ elif (dynamic_comparison_type == DynamicComparisonType.MATCH):
+ if new_value == None:
+ raise ValueError("new_value must be specified when using DynamicComparisonType.MATCH")
+ if current_value != new_value:
+ self._memory_cache.pop(addr)
+ else:
+ self._memory_cache[addr] = current_value
+ else:
+ raise ValueError("Invalid comparison type")
+ return list(self._memory_cache.keys())
+
+ def _check_value(self, value, target_value, standard_comparison_type):
+ """
+ Compares a value with the target value based on the specified compare type.
+
+ Args:
+ value (int): The value to compare.
+ target_value (int or None): The target value to compare against.
+ standard_comparison_type (StandardComparisonType): The type of comparison to use.
+
+ Returns:
+ bool: True if the comparison condition is met, False otherwise.
+ """
+ if standard_comparison_type == StandardComparisonType.EXACT.value:
+ return value == target_value
+ elif standard_comparison_type == StandardComparisonType.LESS_THAN.value:
+ return value < target_value
+ elif standard_comparison_type == StandardComparisonType.GREATER_THAN.value:
+ return value > target_value
+ elif standard_comparison_type == StandardComparisonType.LESS_THAN_OR_EQUAL.value:
+ return value <= target_value
+ elif standard_comparison_type == StandardComparisonType.GREATER_THAN_OR_EQUAL.value:
+ return value >= target_value
+ else:
+ raise ValueError("Invalid comparison type")
This function scans a specified range of memory for a target value from the start_addr to the end_addr (both included).
+
Example:
+
>>> current_score = 4 # You write current score in game
+>>> pyboy.memory_scanner.scan_memory(current_score, start_addr=0xC000, end_addr=0xDFFF)
+[]
+
+
+
Args
+
+
start_addr : int
+
The starting address for the scan.
+
end_addr : int
+
The ending address for the scan.
+
target_value : int or None
+
The value to search for. If None, any value is considered a match.
The endian type to use. This is only used for 16-bit values and higher. See int.from_bytes for more details.
+
+
Returns
+
+
list of int
+
A list of addresses where the target value is found.
+
+
+
+Expand source code
+
+
def scan_memory(
+ self,
+ target_value=None,
+ start_addr=0x0000,
+ end_addr=0xFFFF,
+ standard_comparison_type=StandardComparisonType.EXACT,
+ value_type=ScanMode.INT,
+ byte_width=1,
+ byteorder="little"
+):
+ """
+ This function scans a specified range of memory for a target value from the `start_addr` to the `end_addr` (both included).
+
+ Example:
+ ```python
+ >>> current_score = 4 # You write current score in game
+ >>> pyboy.memory_scanner.scan_memory(current_score, start_addr=0xC000, end_addr=0xDFFF)
+ []
+
+ ```
+
+ Args:
+ start_addr (int): The starting address for the scan.
+ end_addr (int): The ending address for the scan.
+ target_value (int or None): The value to search for. If None, any value is considered a match.
+ standard_comparison_type (StandardComparisonType): The type of comparison to use.
+ value_type (ValueType): The type of value (INT or BCD) to consider.
+ byte_width (int): The number of bytes to consider for each value.
+ byteorder (str): The endian type to use. This is only used for 16-bit values and higher. See [int.from_bytes](https://docs.python.org/3/library/stdtypes.html#int.from_bytes) for more details.
+
+ Returns:
+ list of int: A list of addresses where the target value is found.
+ """
+ self._memory_cache = {}
+ self._memory_cache_byte_width = byte_width
+ for addr in range(start_addr, end_addr - (byte_width-1) + 1): # Adjust the loop to prevent reading past end_addr
+ # Read multiple bytes based on byte_width and byteorder
+ value_bytes = self.pyboy.memory[addr:addr + byte_width]
+ value = int.from_bytes(value_bytes, byteorder)
+
+ if value_type == ScanMode.BCD:
+ value = bcd_to_dec(value, byte_width, byteorder)
+
+ if target_value is None or self._check_value(value, target_value, standard_comparison_type.value):
+ self._memory_cache[addr] = value
+
+ return list(self._memory_cache.keys())
Rescans the memory and updates the memory cache based on a dynamic comparison type.
+
Example:
+
>>> current_score = 4 # You write current score in game
+>>> pyboy.memory_scanner.scan_memory(current_score, start_addr=0xC000, end_addr=0xDFFF)
+[]
+>>> for _ in range(175):
+... pyboy.tick(1, True) # Progress the game to change score
+True...
+>>> current_score = 8 # You write the new score in game
+>>> from pyboy.api.memory_scanner import DynamicComparisonType
+>>> addresses = pyboy.memory_scanner.rescan_memory(current_score, DynamicComparisonType.MATCH)
+>>> print(addresses) # If repeated enough, only one address will remain
+[]
+
+
+
Args
+
+
new_value : int, optional
+
The new value for comparison. If not provided, the current value in memory is used.
The type of comparison to use. Defaults to UNCHANGED.
+
+
Returns
+
+
list of int
+
A list of addresses remaining in the memory cache after the rescan.
+
+
+
+Expand source code
+
+
def rescan_memory(
+ self, new_value=None, dynamic_comparison_type=DynamicComparisonType.UNCHANGED, byteorder="little"
+):
+ """
+ Rescans the memory and updates the memory cache based on a dynamic comparison type.
+
+ Example:
+ ```python
+ >>> current_score = 4 # You write current score in game
+ >>> pyboy.memory_scanner.scan_memory(current_score, start_addr=0xC000, end_addr=0xDFFF)
+ []
+ >>> for _ in range(175):
+ ... pyboy.tick(1, True) # Progress the game to change score
+ True...
+ >>> current_score = 8 # You write the new score in game
+ >>> from pyboy.api.memory_scanner import DynamicComparisonType
+ >>> addresses = pyboy.memory_scanner.rescan_memory(current_score, DynamicComparisonType.MATCH)
+ >>> print(addresses) # If repeated enough, only one address will remain
+ []
+
+ ```
+
+ Args:
+ new_value (int, optional): The new value for comparison. If not provided, the current value in memory is used.
+ dynamic_comparison_type (DynamicComparisonType): The type of comparison to use. Defaults to UNCHANGED.
+
+ Returns:
+ list of int: A list of addresses remaining in the memory cache after the rescan.
+ """
+ for addr, value in self._memory_cache.copy().items():
+ current_value = int.from_bytes(
+ self.pyboy.memory[addr:addr + self._memory_cache_byte_width], byteorder=byteorder
+ )
+ if (dynamic_comparison_type == DynamicComparisonType.UNCHANGED):
+ if value != current_value:
+ self._memory_cache.pop(addr)
+ else:
+ self._memory_cache[addr] = current_value
+ elif (dynamic_comparison_type == DynamicComparisonType.CHANGED):
+ if value == current_value:
+ self._memory_cache.pop(addr)
+ else:
+ self._memory_cache[addr] = current_value
+ elif (dynamic_comparison_type == DynamicComparisonType.INCREASED):
+ if value >= current_value:
+ self._memory_cache.pop(addr)
+ else:
+ self._memory_cache[addr] = current_value
+ elif (dynamic_comparison_type == DynamicComparisonType.DECREASED):
+ if value <= current_value:
+ self._memory_cache.pop(addr)
+ else:
+ self._memory_cache[addr] = current_value
+ elif (dynamic_comparison_type == DynamicComparisonType.MATCH):
+ if new_value == None:
+ raise ValueError("new_value must be specified when using DynamicComparisonType.MATCH")
+ if current_value != new_value:
+ self._memory_cache.pop(addr)
+ else:
+ self._memory_cache[addr] = current_value
+ else:
+ raise ValueError("Invalid comparison type")
+ return list(self._memory_cache.keys())
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/api/screen.html b/docs/api/screen.html
new file mode 100644
index 000000000..bd8701056
--- /dev/null
+++ b/docs/api/screen.html
@@ -0,0 +1,783 @@
+
+
+
+
+
+
+pyboy.api.screen API documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Module pyboy.api.screen
+
+
+
This class gives access to the frame buffer and other screen parameters of PyBoy.
+
+
+Expand source code
+
+
#
+# License: See LICENSE.md file
+# GitHub: https://github.com/Baekalfen/PyBoy
+#
+"""
+This class gives access to the frame buffer and other screen parameters of PyBoy.
+"""
+
+import numpy as np
+
+from pyboy import utils
+from pyboy.logging import get_logger
+
+from .constants import COLS, ROWS
+
+logger = get_logger(__name__)
+
+try:
+ from PIL import Image
+except ImportError:
+ Image = None
+
+
+class Screen:
+ """
+ As part of the emulation, we generate a screen buffer in 32-bit RGBA format. This class has several helper methods
+ to make it possible to read this buffer out.
+
+ If you're making an AI or bot, it's highly recommended to _not_ use this class for detecting objects on the screen.
+ It's much more efficient to use `pyboy.PyBoy.tilemap_background`, `pyboy.PyBoy.tilemap_window`, and `pyboy.PyBoy.get_sprite` instead.
+ """
+ def __init__(self, mb):
+ self.mb = mb
+
+ self.raw_buffer = self.mb.lcd.renderer._screenbuffer
+ """
+ Provides a raw, unfiltered `bytes` object with the data from the screen. Check
+ `Screen.raw_buffer_format` to see which dataformat is used. **The returned type and dataformat are
+ subject to change.** The screen buffer is row-major.
+
+ Use this, only if you need to bypass the overhead of `Screen.image` or `Screen.ndarray`.
+
+ Example:
+ ```python
+ >>> import numpy as np
+ >>> rows, cols = pyboy.screen.raw_buffer_dims
+ >>> ndarray = np.frombuffer(
+ ... pyboy.screen.raw_buffer,
+ ... dtype=np.uint8,
+ ... ).reshape(rows, cols, 4) # Just an example, use pyboy.screen.ndarray instead
+
+ ```
+
+ Returns
+ -------
+ bytes:
+ 92160 bytes of screen data in a `bytes` object.
+ """
+ self.raw_buffer_dims = self.mb.lcd.renderer.buffer_dims
+ """
+ Returns the dimensions of the raw screen buffer. The screen buffer is row-major.
+
+ Example:
+ ```python
+ >>> pyboy.screen.raw_buffer_dims
+ (144, 160)
+
+ ```
+
+ Returns
+ -------
+ tuple:
+ A two-tuple of the buffer dimensions. E.g. (144, 160).
+ """
+ self.raw_buffer_format = self.mb.lcd.renderer.color_format
+ """
+ Returns the color format of the raw screen buffer. **This format is subject to change.**
+
+ Example:
+ ```python
+ >>> from PIL import Image
+ >>> pyboy.screen.raw_buffer_format
+ 'RGBA'
+ >>> image = Image.frombuffer(
+ ... pyboy.screen.raw_buffer_format,
+ ... pyboy.screen.raw_buffer_dims[::-1],
+ ... pyboy.screen.raw_buffer,
+ ... ) # Just an example, use pyboy.screen.image instead
+ >>> image.save('frame.png')
+
+ ```
+
+ Returns
+ -------
+ str:
+ Color format of the raw screen buffer. E.g. 'RGBA'.
+ """
+ self.image = None
+ """
+ Reference to a PIL Image from the screen buffer. **Remember to copy, resize or convert this object** if you
+ intend to store it. The backing buffer will update, but it will be the same `PIL.Image` object.
+
+ Convenient for screen captures, but might be a bottleneck, if you use it to train a neural network. In which
+ case, read up on the `pyboy.api` features, [Pan Docs](https://gbdev.io/pandocs/) on tiles/sprites,
+ and join our Discord channel for more help.
+
+ Example:
+ ```python
+ >>> image = pyboy.screen.image
+ >>> type(image)
+ <class 'PIL.Image.Image'>
+ >>> image.save('frame.png')
+
+ ```
+
+ Returns
+ -------
+ PIL.Image:
+ RGB image of (160, 144) pixels
+ """
+ if not Image:
+ logger.warning("Cannot generate screen image. Missing dependency \"Pillow\".")
+ else:
+ self._set_image()
+
+ self.ndarray = np.frombuffer(
+ self.mb.lcd.renderer._screenbuffer_raw,
+ dtype=np.uint8,
+ ).reshape(ROWS, COLS, 4)
+ """
+ References the screen data in NumPy format. **Remember to copy this object** if you intend to store it.
+ The backing buffer will update, but it will be the same `ndarray` object.
+
+ The format is given by `pyboy.api.screen.Screen.raw_buffer_format`. The screen buffer is row-major.
+
+ Example:
+ ```python
+ >>> pyboy.screen.ndarray.shape
+ (144, 160, 4)
+ >>> # Display "P" on screen from the PyBoy bootrom
+ >>> pyboy.screen.ndarray[66:80,64:72,0]
+ array([[255, 255, 255, 255, 255, 255, 255, 255],
+ [255, 0, 0, 0, 0, 0, 255, 255],
+ [255, 0, 0, 0, 0, 0, 0, 255],
+ [255, 0, 0, 255, 255, 0, 0, 255],
+ [255, 0, 0, 255, 255, 0, 0, 255],
+ [255, 0, 0, 255, 255, 0, 0, 255],
+ [255, 0, 0, 0, 0, 0, 0, 255],
+ [255, 0, 0, 0, 0, 0, 255, 255],
+ [255, 0, 0, 255, 255, 255, 255, 255],
+ [255, 0, 0, 255, 255, 255, 255, 255],
+ [255, 0, 0, 255, 255, 255, 255, 255],
+ [255, 0, 0, 255, 255, 255, 255, 255],
+ [255, 0, 0, 255, 255, 255, 255, 255],
+ [255, 255, 255, 255, 255, 255, 255, 255]], dtype=uint8)
+
+ ```
+
+ Returns
+ -------
+ numpy.ndarray:
+ Screendata in `ndarray` of bytes with shape (144, 160, 4)
+ """
+
+ def _set_image(self):
+ self.image = Image.frombuffer(
+ self.mb.lcd.renderer.color_format, self.mb.lcd.renderer.buffer_dims[::-1],
+ self.mb.lcd.renderer._screenbuffer_raw
+ )
+
+ @property
+ def tilemap_position_list(self):
+ """
+ This function provides the screen (SCX, SCY) and window (WX. WY) position for each horizontal line in the
+ screen buffer. These parameters are often used for visual effects, and some games will reset the registers at
+ the end of each call to `pyboy.PyBoy.tick()`.
+
+ See `Screen.get_tilemap_position` for more information.
+
+ Example:
+ ```python
+ >>> pyboy.tick(25)
+ True
+ >>> swoosh = pyboy.screen.tilemap_position_list[67:78]
+ >>> print(*swoosh, sep=newline) # Just to pretty-print it
+ [0, 0, -7, 0]
+ [1, 0, -7, 0]
+ [2, 0, -7, 0]
+ [2, 0, -7, 0]
+ [3, 0, -7, 0]
+ [3, 0, -7, 0]
+ [3, 0, -7, 0]
+ [3, 0, -7, 0]
+ [2, 0, -7, 0]
+ [1, 0, -7, 0]
+ [0, 0, -7, 0]
+
+ ```
+
+ Returns
+ -------
+ list:
+ Nested list of SCX, SCY, WX and WY for each scanline (144x4). Returns (0, 0, 0, 0) when LCD is off.
+ """
+ # self.tilemap_position_list = np.asarray(self.mb.lcd.renderer._scanlineparameters, dtype=np.uint8).reshape(144, 5)[:, :4]
+ # self.tilemap_position_list = self.mb.lcd.renderer._scanlineparameters
+
+ # # return self.mb.lcd.renderer._scanlineparameters
+ if self.mb.lcd._LCDC.lcd_enable:
+ return [[line[0], line[1], line[2], line[3]] for line in self.mb.lcd.renderer._scanlineparameters]
+ else:
+ return [[0, 0, 0, 0] for line in range(144)]
+
+ def get_tilemap_position(self):
+ """
+ These coordinates define the offset in the tile map from where the top-left corner of the screen is place. Note
+ that the tile map defines 256x256 pixels, but the screen can only show 160x144 pixels. When the offset is closer
+ to the right or bottom edge than 160x144 pixels, the screen will wrap around and render from the opposite site
+ of the tile map.
+
+ For more details, see "7.4 Viewport" in the [report](https://github.com/Baekalfen/PyBoy/raw/master/extras/PyBoy.pdf),
+ or the Pan Docs under [LCD Position and Scrolling](https://gbdev.io/pandocs/Scrolling.html).
+
+ Example:
+ ```python
+ >>> pyboy.screen.get_tilemap_position()
+ ((0, 0), (-7, 0))
+
+ ```
+
+ Returns
+ -------
+ tuple:
+ Returns the tuple of registers ((SCX, SCY), (WX - 7, WY))
+ """
+ return (self.mb.lcd.getviewport(), self.mb.lcd.getwindowpos())
+
+
+
+
+
+
+
+
+
+
Classes
+
+
+class Screen
+(mb)
+
+
+
As part of the emulation, we generate a screen buffer in 32-bit RGBA format. This class has several helper methods
+to make it possible to read this buffer out.
class Screen:
+ """
+ As part of the emulation, we generate a screen buffer in 32-bit RGBA format. This class has several helper methods
+ to make it possible to read this buffer out.
+
+ If you're making an AI or bot, it's highly recommended to _not_ use this class for detecting objects on the screen.
+ It's much more efficient to use `pyboy.PyBoy.tilemap_background`, `pyboy.PyBoy.tilemap_window`, and `pyboy.PyBoy.get_sprite` instead.
+ """
+ def __init__(self, mb):
+ self.mb = mb
+
+ self.raw_buffer = self.mb.lcd.renderer._screenbuffer
+ """
+ Provides a raw, unfiltered `bytes` object with the data from the screen. Check
+ `Screen.raw_buffer_format` to see which dataformat is used. **The returned type and dataformat are
+ subject to change.** The screen buffer is row-major.
+
+ Use this, only if you need to bypass the overhead of `Screen.image` or `Screen.ndarray`.
+
+ Example:
+ ```python
+ >>> import numpy as np
+ >>> rows, cols = pyboy.screen.raw_buffer_dims
+ >>> ndarray = np.frombuffer(
+ ... pyboy.screen.raw_buffer,
+ ... dtype=np.uint8,
+ ... ).reshape(rows, cols, 4) # Just an example, use pyboy.screen.ndarray instead
+
+ ```
+
+ Returns
+ -------
+ bytes:
+ 92160 bytes of screen data in a `bytes` object.
+ """
+ self.raw_buffer_dims = self.mb.lcd.renderer.buffer_dims
+ """
+ Returns the dimensions of the raw screen buffer. The screen buffer is row-major.
+
+ Example:
+ ```python
+ >>> pyboy.screen.raw_buffer_dims
+ (144, 160)
+
+ ```
+
+ Returns
+ -------
+ tuple:
+ A two-tuple of the buffer dimensions. E.g. (144, 160).
+ """
+ self.raw_buffer_format = self.mb.lcd.renderer.color_format
+ """
+ Returns the color format of the raw screen buffer. **This format is subject to change.**
+
+ Example:
+ ```python
+ >>> from PIL import Image
+ >>> pyboy.screen.raw_buffer_format
+ 'RGBA'
+ >>> image = Image.frombuffer(
+ ... pyboy.screen.raw_buffer_format,
+ ... pyboy.screen.raw_buffer_dims[::-1],
+ ... pyboy.screen.raw_buffer,
+ ... ) # Just an example, use pyboy.screen.image instead
+ >>> image.save('frame.png')
+
+ ```
+
+ Returns
+ -------
+ str:
+ Color format of the raw screen buffer. E.g. 'RGBA'.
+ """
+ self.image = None
+ """
+ Reference to a PIL Image from the screen buffer. **Remember to copy, resize or convert this object** if you
+ intend to store it. The backing buffer will update, but it will be the same `PIL.Image` object.
+
+ Convenient for screen captures, but might be a bottleneck, if you use it to train a neural network. In which
+ case, read up on the `pyboy.api` features, [Pan Docs](https://gbdev.io/pandocs/) on tiles/sprites,
+ and join our Discord channel for more help.
+
+ Example:
+ ```python
+ >>> image = pyboy.screen.image
+ >>> type(image)
+ <class 'PIL.Image.Image'>
+ >>> image.save('frame.png')
+
+ ```
+
+ Returns
+ -------
+ PIL.Image:
+ RGB image of (160, 144) pixels
+ """
+ if not Image:
+ logger.warning("Cannot generate screen image. Missing dependency \"Pillow\".")
+ else:
+ self._set_image()
+
+ self.ndarray = np.frombuffer(
+ self.mb.lcd.renderer._screenbuffer_raw,
+ dtype=np.uint8,
+ ).reshape(ROWS, COLS, 4)
+ """
+ References the screen data in NumPy format. **Remember to copy this object** if you intend to store it.
+ The backing buffer will update, but it will be the same `ndarray` object.
+
+ The format is given by `pyboy.api.screen.Screen.raw_buffer_format`. The screen buffer is row-major.
+
+ Example:
+ ```python
+ >>> pyboy.screen.ndarray.shape
+ (144, 160, 4)
+ >>> # Display "P" on screen from the PyBoy bootrom
+ >>> pyboy.screen.ndarray[66:80,64:72,0]
+ array([[255, 255, 255, 255, 255, 255, 255, 255],
+ [255, 0, 0, 0, 0, 0, 255, 255],
+ [255, 0, 0, 0, 0, 0, 0, 255],
+ [255, 0, 0, 255, 255, 0, 0, 255],
+ [255, 0, 0, 255, 255, 0, 0, 255],
+ [255, 0, 0, 255, 255, 0, 0, 255],
+ [255, 0, 0, 0, 0, 0, 0, 255],
+ [255, 0, 0, 0, 0, 0, 255, 255],
+ [255, 0, 0, 255, 255, 255, 255, 255],
+ [255, 0, 0, 255, 255, 255, 255, 255],
+ [255, 0, 0, 255, 255, 255, 255, 255],
+ [255, 0, 0, 255, 255, 255, 255, 255],
+ [255, 0, 0, 255, 255, 255, 255, 255],
+ [255, 255, 255, 255, 255, 255, 255, 255]], dtype=uint8)
+
+ ```
+
+ Returns
+ -------
+ numpy.ndarray:
+ Screendata in `ndarray` of bytes with shape (144, 160, 4)
+ """
+
+ def _set_image(self):
+ self.image = Image.frombuffer(
+ self.mb.lcd.renderer.color_format, self.mb.lcd.renderer.buffer_dims[::-1],
+ self.mb.lcd.renderer._screenbuffer_raw
+ )
+
+ @property
+ def tilemap_position_list(self):
+ """
+ This function provides the screen (SCX, SCY) and window (WX. WY) position for each horizontal line in the
+ screen buffer. These parameters are often used for visual effects, and some games will reset the registers at
+ the end of each call to `pyboy.PyBoy.tick()`.
+
+ See `Screen.get_tilemap_position` for more information.
+
+ Example:
+ ```python
+ >>> pyboy.tick(25)
+ True
+ >>> swoosh = pyboy.screen.tilemap_position_list[67:78]
+ >>> print(*swoosh, sep=newline) # Just to pretty-print it
+ [0, 0, -7, 0]
+ [1, 0, -7, 0]
+ [2, 0, -7, 0]
+ [2, 0, -7, 0]
+ [3, 0, -7, 0]
+ [3, 0, -7, 0]
+ [3, 0, -7, 0]
+ [3, 0, -7, 0]
+ [2, 0, -7, 0]
+ [1, 0, -7, 0]
+ [0, 0, -7, 0]
+
+ ```
+
+ Returns
+ -------
+ list:
+ Nested list of SCX, SCY, WX and WY for each scanline (144x4). Returns (0, 0, 0, 0) when LCD is off.
+ """
+ # self.tilemap_position_list = np.asarray(self.mb.lcd.renderer._scanlineparameters, dtype=np.uint8).reshape(144, 5)[:, :4]
+ # self.tilemap_position_list = self.mb.lcd.renderer._scanlineparameters
+
+ # # return self.mb.lcd.renderer._scanlineparameters
+ if self.mb.lcd._LCDC.lcd_enable:
+ return [[line[0], line[1], line[2], line[3]] for line in self.mb.lcd.renderer._scanlineparameters]
+ else:
+ return [[0, 0, 0, 0] for line in range(144)]
+
+ def get_tilemap_position(self):
+ """
+ These coordinates define the offset in the tile map from where the top-left corner of the screen is place. Note
+ that the tile map defines 256x256 pixels, but the screen can only show 160x144 pixels. When the offset is closer
+ to the right or bottom edge than 160x144 pixels, the screen will wrap around and render from the opposite site
+ of the tile map.
+
+ For more details, see "7.4 Viewport" in the [report](https://github.com/Baekalfen/PyBoy/raw/master/extras/PyBoy.pdf),
+ or the Pan Docs under [LCD Position and Scrolling](https://gbdev.io/pandocs/Scrolling.html).
+
+ Example:
+ ```python
+ >>> pyboy.screen.get_tilemap_position()
+ ((0, 0), (-7, 0))
+
+ ```
+
+ Returns
+ -------
+ tuple:
+ Returns the tuple of registers ((SCX, SCY), (WX - 7, WY))
+ """
+ return (self.mb.lcd.getviewport(), self.mb.lcd.getwindowpos())
+
+
Instance variables
+
+
var tilemap_position_list
+
+
This function provides the screen (SCX, SCY) and window (WX. WY) position for each horizontal line in the
+screen buffer. These parameters are often used for visual effects, and some games will reset the registers at
+the end of each call to PyBoy.tick().
Nested list of SCX, SCY, WX and WY for each scanline (144x4). Returns (0, 0, 0, 0) when LCD is off.
+
+
+
+Expand source code
+
+
@property
+def tilemap_position_list(self):
+ """
+ This function provides the screen (SCX, SCY) and window (WX. WY) position for each horizontal line in the
+ screen buffer. These parameters are often used for visual effects, and some games will reset the registers at
+ the end of each call to `pyboy.PyBoy.tick()`.
+
+ See `Screen.get_tilemap_position` for more information.
+
+ Example:
+ ```python
+ >>> pyboy.tick(25)
+ True
+ >>> swoosh = pyboy.screen.tilemap_position_list[67:78]
+ >>> print(*swoosh, sep=newline) # Just to pretty-print it
+ [0, 0, -7, 0]
+ [1, 0, -7, 0]
+ [2, 0, -7, 0]
+ [2, 0, -7, 0]
+ [3, 0, -7, 0]
+ [3, 0, -7, 0]
+ [3, 0, -7, 0]
+ [3, 0, -7, 0]
+ [2, 0, -7, 0]
+ [1, 0, -7, 0]
+ [0, 0, -7, 0]
+
+ ```
+
+ Returns
+ -------
+ list:
+ Nested list of SCX, SCY, WX and WY for each scanline (144x4). Returns (0, 0, 0, 0) when LCD is off.
+ """
+ # self.tilemap_position_list = np.asarray(self.mb.lcd.renderer._scanlineparameters, dtype=np.uint8).reshape(144, 5)[:, :4]
+ # self.tilemap_position_list = self.mb.lcd.renderer._scanlineparameters
+
+ # # return self.mb.lcd.renderer._scanlineparameters
+ if self.mb.lcd._LCDC.lcd_enable:
+ return [[line[0], line[1], line[2], line[3]] for line in self.mb.lcd.renderer._scanlineparameters]
+ else:
+ return [[0, 0, 0, 0] for line in range(144)]
+
+
+
var raw_buffer
+
+
Provides a raw, unfiltered bytes object with the data from the screen. Check
+Screen.raw_buffer_format to see which dataformat is used. The returned type and dataformat are
+subject to change. The screen buffer is row-major.
>>> import numpy as np
+>>> rows, cols = pyboy.screen.raw_buffer_dims
+>>> ndarray = np.frombuffer(
+... pyboy.screen.raw_buffer,
+... dtype=np.uint8,
+... ).reshape(rows, cols, 4) # Just an example, use pyboy.screen.ndarray instead
+
+
+
Returns
+
+
bytes:
+
92160 bytes of screen data in a bytes object.
+
+
+
var raw_buffer_dims
+
+
Returns the dimensions of the raw screen buffer. The screen buffer is row-major.
+
Example:
+
>>> pyboy.screen.raw_buffer_dims
+(144, 160)
+
+
+
Returns
+
+
tuple:
+
A two-tuple of the buffer dimensions. E.g. (144, 160).
+
+
+
var raw_buffer_format
+
+
Returns the color format of the raw screen buffer. This format is subject to change.
+
Example:
+
>>> from PIL import Image
+>>> pyboy.screen.raw_buffer_format
+'RGBA'
+>>> image = Image.frombuffer(
+... pyboy.screen.raw_buffer_format,
+... pyboy.screen.raw_buffer_dims[::-1],
+... pyboy.screen.raw_buffer,
+... ) # Just an example, use pyboy.screen.image instead
+>>> image.save('frame.png')
+
+
+
Returns
+
+
str:
+
Color format of the raw screen buffer. E.g. 'RGBA'.
+
+
+
var image
+
+
Reference to a PIL Image from the screen buffer. Remember to copy, resize or convert this object if you
+intend to store it. The backing buffer will update, but it will be the same PIL.Image object.
+
Convenient for screen captures, but might be a bottleneck, if you use it to train a neural network. In which
+case, read up on the pyboy.api features, Pan Docs on tiles/sprites,
+and join our Discord channel for more help.
References the screen data in NumPy format. Remember to copy this object if you intend to store it.
+The backing buffer will update, but it will be the same ndarray object.
Screendata in ndarray of bytes with shape (144, 160, 4)
+
+
+
+
Methods
+
+
+def get_tilemap_position(self)
+
+
+
These coordinates define the offset in the tile map from where the top-left corner of the screen is place. Note
+that the tile map defines 256x256 pixels, but the screen can only show 160x144 pixels. When the offset is closer
+to the right or bottom edge than 160x144 pixels, the screen will wrap around and render from the opposite site
+of the tile map.
Returns the tuple of registers ((SCX, SCY), (WX - 7, WY))
+
+
+
+Expand source code
+
+
def get_tilemap_position(self):
+ """
+ These coordinates define the offset in the tile map from where the top-left corner of the screen is place. Note
+ that the tile map defines 256x256 pixels, but the screen can only show 160x144 pixels. When the offset is closer
+ to the right or bottom edge than 160x144 pixels, the screen will wrap around and render from the opposite site
+ of the tile map.
+
+ For more details, see "7.4 Viewport" in the [report](https://github.com/Baekalfen/PyBoy/raw/master/extras/PyBoy.pdf),
+ or the Pan Docs under [LCD Position and Scrolling](https://gbdev.io/pandocs/Scrolling.html).
+
+ Example:
+ ```python
+ >>> pyboy.screen.get_tilemap_position()
+ ((0, 0), (-7, 0))
+
+ ```
+
+ Returns
+ -------
+ tuple:
+ Returns the tuple of registers ((SCX, SCY), (WX - 7, WY))
+ """
+ return (self.mb.lcd.getviewport(), self.mb.lcd.getwindowpos())
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/botsupport/sprite.html b/docs/api/sprite.html
similarity index 77%
rename from docs/botsupport/sprite.html
rename to docs/api/sprite.html
index 7c6c40099..7c119dff7 100644
--- a/docs/botsupport/sprite.html
+++ b/docs/api/sprite.html
@@ -4,7 +4,7 @@
-pyboy.botsupport.sprite API documentation
+pyboy.api.sprite API documentation
@@ -18,7 +18,7 @@
-
Module pyboy.botsupport.sprite
+
Module pyboy.api.sprite
This class presents an interface to the sprites held in the OAM data on the Game Boy.
@@ -56,7 +56,7 @@
Module pyboy.botsupport.sprite
call to `pyboy.PyBoy.tick`, so make sure to verify the `Sprite.tile_identifier` hasn't changed.
By knowing the tile identifiers of players, enemies, power-ups and so on, you'll be able to search for them
- using `pyboy.botsupport.BotSupportManager.sprite_by_tile_identifier` and feed it to your bot or AI.
+ using `pyboy.sprite_by_tile_identifier` and feed it to your bot or AI.
"""
assert 0 <= sprite_index < SPRITES, f"Sprite index of {sprite_index} is out of range (0-{SPRITES})"
self.mb = mb
@@ -99,7 +99,7 @@
Module pyboy.botsupport.sprite
self.tile_identifier = self.mb.getitem(OAM_OFFSET + self._offset + 2)
"""
The identifier of the tile the sprite uses. To get a better representation, see the method
- `pyboy.botsupport.sprite.Sprite.tiles`.
+ `pyboy.api.sprite.Sprite.tiles`.
For double-height sprites, this will only give the identifier of the first tile. The second tile will
always be the one immediately following the first (`tile_identifier + 1`).
@@ -114,7 +114,7 @@
Module pyboy.botsupport.sprite
self.attr_obj_bg_priority = _bit(attr, 7)
"""
To better understand this values, look in the [Pan Docs: VRAM Sprite Attribute Table
- (OAM)](http://bgb.bircd.org/pandocs.htm#vramspriteattributetableoam).
+ (OAM)](https://gbdev.io/pandocs/OAM.html).
Returns
-------
@@ -125,7 +125,7 @@
Module pyboy.botsupport.sprite
self.attr_y_flip = _bit(attr, 6)
"""
To better understand this values, look in the [Pan Docs: VRAM Sprite Attribute Table
- (OAM)](http://bgb.bircd.org/pandocs.htm#vramspriteattributetableoam).
+ (OAM)](https://gbdev.io/pandocs/OAM.html).
Returns
-------
@@ -136,7 +136,7 @@
Module pyboy.botsupport.sprite
self.attr_x_flip = _bit(attr, 5)
"""
To better understand this values, look in the [Pan Docs: VRAM Sprite Attribute Table
- (OAM)](http://bgb.bircd.org/pandocs.htm#vramspriteattributetableoam).
+ (OAM)](https://gbdev.io/pandocs/OAM.html).
Returns
-------
@@ -144,10 +144,21 @@
Module pyboy.botsupport.sprite
The state of the bit in the attributes lookup.
"""
- self.attr_palette_number = _bit(attr, 4)
+ self.attr_palette_number = 0
"""
To better understand this values, look in the [Pan Docs: VRAM Sprite Attribute Table
- (OAM)](http://bgb.bircd.org/pandocs.htm#vramspriteattributetableoam).
+ (OAM)](https://gbdev.io/pandocs/OAM.html).
+
+ Returns
+ -------
+ int:
+ The state of the bit(s) in the attributes lookup.
+ """
+
+ self.attr_cgb_bank_number = 0
+ """
+ To better understand this values, look in the [Pan Docs: VRAM Sprite Attribute Table
+ (OAM)](https://gbdev.io/pandocs/OAM.html).
Returns
-------
@@ -155,8 +166,14 @@
Module pyboy.botsupport.sprite
The state of the bit in the attributes lookup.
"""
+ if self.mb.cgb:
+ self.attr_palette_number = attr & 0b111
+ self.attr_cgb_bank_number = _bit(attr, 3)
+ else:
+ self.attr_palette_number = _bit(attr, 4)
+
LCDC = LCDCRegister(self.mb.getitem(LCDC_OFFSET))
- sprite_height = 16 if LCDC.sprite_height else 8
+ sprite_height = 16 if LCDC._get_sprite_height() else 8
self.shape = (8, sprite_height)
"""
Sprites can be set to be 8x8 or 8x16 pixels (16 pixels tall). This is defined globally for the rendering
@@ -176,12 +193,12 @@
Module pyboy.botsupport.sprite
immediately following the identifier given, and render it below the first.
More information can be found in the [Pan Docs: VRAM Sprite Attribute Table
- (OAM)](http://bgb.bircd.org/pandocs.htm#vramspriteattributetableoam)
+ (OAM)](https://gbdev.io/pandocs/OAM.html)
Returns
-------
list:
- A list of `pyboy.botsupport.tile.Tile` object(s) representing the graphics data for the sprite
+ A list of `pyboy.api.tile.Tile` object(s) representing the graphics data for the sprite
"""
if sprite_height == 16:
self.tiles += [Tile(self.mb, self.tile_identifier + 1)]
@@ -222,7 +239,7 @@
Module pyboy.botsupport.sprite
Classes
-
+
class Sprite(mb, sprite_index)
@@ -234,9 +251,9 @@
Classes
grid-size of 8x8 pixels precision, and can have no transparency.
Sprites on the Game Boy are tightly associated with tiles. The sprites can be seen as "upgraded" tiles, as the
image data still refers back to one (or two) tiles. The tile that a sprite will show, can change between each
-call to PyBoy.tick(), so make sure to verify the Sprite.tile_identifier hasn't changed.
By knowing the tile identifiers of players, enemies, power-ups and so on, you'll be able to search for them
-using BotSupportManager.sprite_by_tile_identifier() and feed it to your bot or AI.
+using pyboy.sprite_by_tile_identifier and feed it to your bot or AI.
Expand source code
@@ -257,7 +274,7 @@
Classes
call to `pyboy.PyBoy.tick`, so make sure to verify the `Sprite.tile_identifier` hasn't changed.
By knowing the tile identifiers of players, enemies, power-ups and so on, you'll be able to search for them
- using `pyboy.botsupport.BotSupportManager.sprite_by_tile_identifier` and feed it to your bot or AI.
+ using `pyboy.sprite_by_tile_identifier` and feed it to your bot or AI.
"""
assert 0 <= sprite_index < SPRITES, f"Sprite index of {sprite_index} is out of range (0-{SPRITES})"
self.mb = mb
@@ -300,7 +317,7 @@
Classes
self.tile_identifier = self.mb.getitem(OAM_OFFSET + self._offset + 2)
"""
The identifier of the tile the sprite uses. To get a better representation, see the method
- `pyboy.botsupport.sprite.Sprite.tiles`.
+ `pyboy.api.sprite.Sprite.tiles`.
For double-height sprites, this will only give the identifier of the first tile. The second tile will
always be the one immediately following the first (`tile_identifier + 1`).
@@ -315,7 +332,7 @@
Classes
self.attr_obj_bg_priority = _bit(attr, 7)
"""
To better understand this values, look in the [Pan Docs: VRAM Sprite Attribute Table
- (OAM)](http://bgb.bircd.org/pandocs.htm#vramspriteattributetableoam).
+ (OAM)](https://gbdev.io/pandocs/OAM.html).
Returns
-------
@@ -326,7 +343,7 @@
Classes
self.attr_y_flip = _bit(attr, 6)
"""
To better understand this values, look in the [Pan Docs: VRAM Sprite Attribute Table
- (OAM)](http://bgb.bircd.org/pandocs.htm#vramspriteattributetableoam).
+ (OAM)](https://gbdev.io/pandocs/OAM.html).
Returns
-------
@@ -337,7 +354,7 @@
Classes
self.attr_x_flip = _bit(attr, 5)
"""
To better understand this values, look in the [Pan Docs: VRAM Sprite Attribute Table
- (OAM)](http://bgb.bircd.org/pandocs.htm#vramspriteattributetableoam).
+ (OAM)](https://gbdev.io/pandocs/OAM.html).
Returns
-------
@@ -345,10 +362,21 @@
Classes
The state of the bit in the attributes lookup.
"""
- self.attr_palette_number = _bit(attr, 4)
+ self.attr_palette_number = 0
"""
To better understand this values, look in the [Pan Docs: VRAM Sprite Attribute Table
- (OAM)](http://bgb.bircd.org/pandocs.htm#vramspriteattributetableoam).
+ (OAM)](https://gbdev.io/pandocs/OAM.html).
+
+ Returns
+ -------
+ int:
+ The state of the bit(s) in the attributes lookup.
+ """
+
+ self.attr_cgb_bank_number = 0
+ """
+ To better understand this values, look in the [Pan Docs: VRAM Sprite Attribute Table
+ (OAM)](https://gbdev.io/pandocs/OAM.html).
Returns
-------
@@ -356,8 +384,14 @@
Classes
The state of the bit in the attributes lookup.
"""
+ if self.mb.cgb:
+ self.attr_palette_number = attr & 0b111
+ self.attr_cgb_bank_number = _bit(attr, 3)
+ else:
+ self.attr_palette_number = _bit(attr, 4)
+
LCDC = LCDCRegister(self.mb.getitem(LCDC_OFFSET))
- sprite_height = 16 if LCDC.sprite_height else 8
+ sprite_height = 16 if LCDC._get_sprite_height() else 8
self.shape = (8, sprite_height)
"""
Sprites can be set to be 8x8 or 8x16 pixels (16 pixels tall). This is defined globally for the rendering
@@ -377,12 +411,12 @@
Classes
immediately following the identifier given, and render it below the first.
More information can be found in the [Pan Docs: VRAM Sprite Attribute Table
- (OAM)](http://bgb.bircd.org/pandocs.htm#vramspriteattributetableoam)
+ (OAM)](https://gbdev.io/pandocs/OAM.html)
Returns
-------
list:
- A list of `pyboy.botsupport.tile.Tile` object(s) representing the graphics data for the sprite
+ A list of `pyboy.api.tile.Tile` object(s) representing the graphics data for the sprite
"""
if sprite_height == 16:
self.tiles += [Tile(self.mb, self.tile_identifier + 1)]
@@ -410,7 +444,7 @@
Classes
Instance variables
-
var y
+
var y
The Y-coordinate on the screen to show the Sprite. The (x,y) coordinate points to the top-left corner of the sprite.
Returns
@@ -419,7 +453,7 @@
Returns
Y-coordinate
-
var x
+
var x
The X-coordinate on the screen to show the Sprite. The (x,y) coordinate points to the top-left corner of the sprite.
Returns
@@ -428,10 +462,10 @@
Returns
X-coordinate
-
var tile_identifier
+
var tile_identifier
The identifier of the tile the sprite uses. To get a better representation, see the method
-Sprite.tiles.
For double-height sprites, this will only give the identifier of the first tile. The second tile will
always be the one immediately following the first (tile_identifier + 1).
Sprites can be set to be 8x8 or 8x16 pixels (16 pixels tall). This is defined globally for the rendering
hardware, so it's either all sprites using 8x16 pixels, or all sprites using 8x8 pixels.
@@ -488,20 +532,20 @@
Returns
(int, int):
The width and height of the sprite.
-
var tiles
+
var tiles
The Game Boy support sprites of single-height (8x8 pixels) and double-height (8x16 pixels).
In the single-height format, one tile is used. For double-height sprites, the Game Boy will also use the tile
immediately following the identifier given, and render it below the first.
A list of Tile object(s) representing the graphics data for the sprite
+
A list of Tile object(s) representing the graphics data for the sprite
-
var on_screen
+
var on_screen
To disable sprites from being rendered on screen, developers will place the sprite outside the area of the
screen. This is often a good way to determine if the sprite is inactive.
diff --git a/docs/botsupport/tile.html b/docs/api/tile.html
similarity index 51%
rename from docs/botsupport/tile.html
rename to docs/api/tile.html
index 54fdfe878..195df2998 100644
--- a/docs/botsupport/tile.html
+++ b/docs/api/tile.html
@@ -4,9 +4,9 @@
-pyboy.botsupport.tile API documentation
+pyboy.api.tile API documentation
+`pyboy.api.sprite.Sprite` and …" />
@@ -19,11 +19,11 @@
-
Module pyboy.botsupport.tile
+
Module pyboy.api.tile
The Game Boy uses tiles as the building block for all graphics on the screen. This base-class is used both for
-Sprite and TileMap, when refering to graphics.
+Sprite and TileMap, when refering to graphics.
Expand source code
@@ -34,50 +34,58 @@
Module pyboy.botsupport.tile
#
"""
The Game Boy uses tiles as the building block for all graphics on the screen. This base-class is used both for
-`pyboy.botsupport.sprite.Sprite` and `pyboy.botsupport.tilemap.TileMap`, when refering to graphics.
+`pyboy.api.sprite.Sprite` and `pyboy.api.tilemap.TileMap`, when refering to graphics.
"""
-import logging
-
import numpy as np
+
+import pyboy
from pyboy import utils
-from .constants import LOW_TILEDATA, VRAM_OFFSET
+from .constants import LOW_TILEDATA, TILES, TILES_CGB, VRAM_OFFSET
-logger = logging.getLogger(__name__)
+logger = pyboy.logging.get_logger(__name__)
try:
from PIL import Image
except ImportError:
Image = None
+try:
+ from cython import compiled
+ cythonmode = compiled
+except ImportError:
+ cythonmode = False
+
class Tile:
def __init__(self, mb, identifier):
"""
The Game Boy uses tiles as the building block for all graphics on the screen. This base-class is used for
- `pyboy.botsupport.BotSupportManager.tile`, `pyboy.botsupport.sprite.Sprite` and `pyboy.botsupport.tilemap.TileMap`, when
- refering to graphics.
+ `pyboy.PyBoy.get_tile`, `pyboy.api.sprite.Sprite` and `pyboy.api.tilemap.TileMap`, when refering to graphics.
This class is not meant to be instantiated by developers reading this documentation, but it will be created
- internally and returned by `pyboy.botsupport.sprite.Sprite.tiles` and
- `pyboy.botsupport.tilemap.TileMap.tile`.
+ internally and returned by `pyboy.api.sprite.Sprite.tiles` and
+ `pyboy.api.tilemap.TileMap.tile`.
The data of this class is static, apart from the image data, which is loaded from the Game Boy's memory when
needed. Beware that the graphics for the tile can change between each call to `pyboy.PyBoy.tick`.
"""
self.mb = mb
- assert 0 <= identifier < 384, "Identifier out of range"
+ if self.mb.cgb:
+ assert 0 <= identifier < TILES_CGB, "Identifier out of range"
+ else:
+ assert 0 <= identifier < TILES, "Identifier out of range"
- self.data_address = LOW_TILEDATA + (16*identifier)
+ self.data_address = LOW_TILEDATA + (16 * (identifier%TILES))
"""
The tile data is defined in a specific area of the Game Boy. This function returns the address of the tile data
- corresponding to the tile identifier. It is advised to use `pyboy.botsupport.tile.Tile.image` or one of the
+ corresponding to the tile identifier. It is advised to use `pyboy.api.tile.Tile.image` or one of the
other `image`-functions if you want to view the tile.
You can read how the data is read in the
- [Pan Docs: VRAM Tile Data](http://bgb.bircd.org/pandocs.htm#vramtiledata).
+ [Pan Docs: VRAM Tile Data](https://gbdev.io/pandocs/Tile_Data.html).
Returns
-------
@@ -85,10 +93,16 @@
Module pyboy.botsupport.tile
address in VRAM where tile data starts
"""
- self.tile_identifier = (self.data_address - LOW_TILEDATA) // 16
+ if identifier < TILES:
+ self.vram_bank = 0
+ else:
+ self.vram_bank = 1
+
+ self.tile_identifier = identifier
"""
The Game Boy has a slightly complicated indexing system for tiles. This identifier unifies the otherwise
- complicated indexing system on the Game Boy into a single range of 0-383 (both included).
+ complicated indexing system on the Game Boy into a single range of 0-383 (both included) or 0-767 for Game Boy
+ Color.
Returns
-------
@@ -106,12 +120,29 @@
Module pyboy.botsupport.tile
The width and height of the tile.
"""
+ self.raw_buffer_format = self.mb.lcd.renderer.color_format
+ """
+ Returns the color format of the raw screen buffer.
+
+ Returns
+ -------
+ str:
+ Color format of the raw screen buffer. E.g. 'RGBA'.
+ """
+
def image(self):
"""
- Use this function to get an easy-to-use `PIL.Image` object of the tile. The image is 8x8 pixels in RGBA colors.
+ Use this function to get an `PIL.Image` object of the tile. The image is 8x8 pixels. The format or "mode" might change at any time.
Be aware, that the graphics for this tile can change between each call to `pyboy.PyBoy.tick`.
+ Example:
+ ```python
+ >>> tile = pyboy.get_tile(1)
+ >>> tile.image().save('tile_1.png')
+
+ ```
+
Returns
-------
PIL.Image :
@@ -120,24 +151,55 @@
Module pyboy.botsupport.tile
if Image is None:
logger.error(f"{__name__}: Missing dependency \"Pillow\".")
return None
- return Image.frombytes("RGBA", (8, 8), bytes(self.image_data()))
- def image_ndarray(self):
+ if cythonmode:
+ return Image.fromarray(self._image_data().base, mode=self.raw_buffer_format)
+ else:
+ return Image.frombytes(self.raw_buffer_format, (8, 8), self._image_data())
+
+ def ndarray(self):
"""
- Use this function to get an easy-to-use `numpy.ndarray` object of the tile. The array has a shape of (8, 8, 4)
- and each value is of `numpy.uint8`. The values corresponds to and RGBA image of 8x8 pixels with each sub-color
- in a separate cell.
+ Use this function to get an `numpy.ndarray` object of the tile. The array has a shape of (8, 8, 4)
+ and each value is of `numpy.uint8`. The values corresponds to an image of 8x8 pixels with each sub-color
+ in a separate cell. The format is given by `pyboy.api.tile.Tile.raw_buffer_format`.
Be aware, that the graphics for this tile can change between each call to `pyboy.PyBoy.tick`.
+ Example:
+ ```python
+ >>> tile1 = pyboy.get_tile(1)
+ >>> tile1.ndarray()[:,:,0] # Upper part of "P"
+ array([[255, 255, 255, 255, 255, 255, 255, 255],
+ [255, 255, 255, 255, 255, 255, 255, 255],
+ [255, 255, 255, 255, 255, 255, 255, 255],
+ [255, 0, 0, 0, 0, 0, 255, 255],
+ [255, 0, 0, 0, 0, 0, 0, 255],
+ [255, 0, 0, 255, 255, 0, 0, 255],
+ [255, 0, 0, 255, 255, 0, 0, 255],
+ [255, 0, 0, 255, 255, 0, 0, 255]], dtype=uint8)
+ >>> tile2 = pyboy.get_tile(2)
+ >>> tile2.ndarray()[:,:,0] # Lower part of "P"
+ array([[255, 0, 0, 0, 0, 0, 0, 255],
+ [255, 0, 0, 0, 0, 0, 255, 255],
+ [255, 0, 0, 255, 255, 255, 255, 255],
+ [255, 0, 0, 255, 255, 255, 255, 255],
+ [255, 0, 0, 255, 255, 255, 255, 255],
+ [255, 0, 0, 255, 255, 255, 255, 255],
+ [255, 0, 0, 255, 255, 255, 255, 255],
+ [255, 255, 255, 255, 255, 255, 255, 255]], dtype=uint8)
+
+ ```
+
Returns
-------
numpy.ndarray :
Array of shape (8, 8, 4) with data type of `numpy.uint8`.
"""
- return np.asarray(self.image_data()).view(dtype=np.uint8).reshape(8, 8, 4)
+ # The data is laid out as (X, red, green, blue), where X is currently always zero, but this is not guarenteed
+ # across versions of PyBoy.
+ return np.asarray(self._image_data()).view(dtype=np.uint8).reshape(8, 8, 4)
- def image_data(self):
+ def _image_data(self):
"""
Use this function to get the raw tile data. The data is a `memoryview` corresponding to 8x8 pixels in RGBA
colors.
@@ -147,23 +209,24 @@
Module pyboy.botsupport.tile
Returns
-------
memoryview :
- Image data of tile in 8x8 pixels and RGBA colors.
+ Image data of tile in 8x8 pixels and RGB colors.
"""
self.data = np.zeros((8, 8), dtype=np.uint32)
for k in range(0, 16, 2): # 2 bytes for each line
- byte1 = self.mb.lcd.VRAM0[self.data_address + k - VRAM_OFFSET]
- byte2 = self.mb.lcd.VRAM0[self.data_address + k + 1 - VRAM_OFFSET]
+ if self.vram_bank == 0:
+ byte1 = self.mb.lcd.VRAM0[self.data_address + k - VRAM_OFFSET]
+ byte2 = self.mb.lcd.VRAM0[self.data_address + k + 1 - VRAM_OFFSET]
+ else:
+ byte1 = self.mb.lcd.VRAM1[self.data_address + k - VRAM_OFFSET]
+ byte2 = self.mb.lcd.VRAM1[self.data_address + k + 1 - VRAM_OFFSET]
for x in range(8):
colorcode = utils.color_code(byte1, byte2, 7 - x)
- # NOTE: ">> 8 | 0xFF000000" to keep compatibility with earlier code
- old_A_format = 0xFF000000
- self.data[k // 2][x] = self.mb.lcd.BGP.getcolor(colorcode) >> 8 | old_A_format
-
+ self.data[k // 2][x] = self.mb.lcd.BGP.getcolor(colorcode)
return self.data
def __eq__(self, other):
- return self.data_address == other.data_address
+ return self.data_address == other.data_address and self.vram_bank == other.vram_bank
def __repr__(self):
return f"Tile: {self.tile_identifier}"
@@ -178,17 +241,16 @@
Module pyboy.botsupport.tile
Classes
-
+
class Tile(mb, identifier)
The Game Boy uses tiles as the building block for all graphics on the screen. This base-class is used for
-BotSupportManager.tile(), Sprite and TileMap, when
-refering to graphics.
This class is not meant to be instantiated by developers reading this documentation, but it will be created
-internally and returned by Sprite.tiles and
-TileMap.tile().
The data of this class is static, apart from the image data, which is loaded from the Game Boy's memory when
needed. Beware that the graphics for the tile can change between each call to PyBoy.tick().
@@ -199,28 +261,30 @@
Classes
def __init__(self, mb, identifier):
"""
The Game Boy uses tiles as the building block for all graphics on the screen. This base-class is used for
- `pyboy.botsupport.BotSupportManager.tile`, `pyboy.botsupport.sprite.Sprite` and `pyboy.botsupport.tilemap.TileMap`, when
- refering to graphics.
+ `pyboy.PyBoy.get_tile`, `pyboy.api.sprite.Sprite` and `pyboy.api.tilemap.TileMap`, when refering to graphics.
This class is not meant to be instantiated by developers reading this documentation, but it will be created
- internally and returned by `pyboy.botsupport.sprite.Sprite.tiles` and
- `pyboy.botsupport.tilemap.TileMap.tile`.
+ internally and returned by `pyboy.api.sprite.Sprite.tiles` and
+ `pyboy.api.tilemap.TileMap.tile`.
The data of this class is static, apart from the image data, which is loaded from the Game Boy's memory when
needed. Beware that the graphics for the tile can change between each call to `pyboy.PyBoy.tick`.
"""
self.mb = mb
- assert 0 <= identifier < 384, "Identifier out of range"
+ if self.mb.cgb:
+ assert 0 <= identifier < TILES_CGB, "Identifier out of range"
+ else:
+ assert 0 <= identifier < TILES, "Identifier out of range"
- self.data_address = LOW_TILEDATA + (16*identifier)
+ self.data_address = LOW_TILEDATA + (16 * (identifier%TILES))
"""
The tile data is defined in a specific area of the Game Boy. This function returns the address of the tile data
- corresponding to the tile identifier. It is advised to use `pyboy.botsupport.tile.Tile.image` or one of the
+ corresponding to the tile identifier. It is advised to use `pyboy.api.tile.Tile.image` or one of the
other `image`-functions if you want to view the tile.
You can read how the data is read in the
- [Pan Docs: VRAM Tile Data](http://bgb.bircd.org/pandocs.htm#vramtiledata).
+ [Pan Docs: VRAM Tile Data](https://gbdev.io/pandocs/Tile_Data.html).
Returns
-------
@@ -228,10 +292,16 @@
Classes
address in VRAM where tile data starts
"""
- self.tile_identifier = (self.data_address - LOW_TILEDATA) // 16
+ if identifier < TILES:
+ self.vram_bank = 0
+ else:
+ self.vram_bank = 1
+
+ self.tile_identifier = identifier
"""
The Game Boy has a slightly complicated indexing system for tiles. This identifier unifies the otherwise
- complicated indexing system on the Game Boy into a single range of 0-383 (both included).
+ complicated indexing system on the Game Boy into a single range of 0-383 (both included) or 0-767 for Game Boy
+ Color.
Returns
-------
@@ -249,12 +319,29 @@
Classes
The width and height of the tile.
"""
+ self.raw_buffer_format = self.mb.lcd.renderer.color_format
+ """
+ Returns the color format of the raw screen buffer.
+
+ Returns
+ -------
+ str:
+ Color format of the raw screen buffer. E.g. 'RGBA'.
+ """
+
def image(self):
"""
- Use this function to get an easy-to-use `PIL.Image` object of the tile. The image is 8x8 pixels in RGBA colors.
+ Use this function to get an `PIL.Image` object of the tile. The image is 8x8 pixels. The format or "mode" might change at any time.
Be aware, that the graphics for this tile can change between each call to `pyboy.PyBoy.tick`.
+ Example:
+ ```python
+ >>> tile = pyboy.get_tile(1)
+ >>> tile.image().save('tile_1.png')
+
+ ```
+
Returns
-------
PIL.Image :
@@ -263,24 +350,55 @@
Classes
if Image is None:
logger.error(f"{__name__}: Missing dependency \"Pillow\".")
return None
- return Image.frombytes("RGBA", (8, 8), bytes(self.image_data()))
- def image_ndarray(self):
+ if cythonmode:
+ return Image.fromarray(self._image_data().base, mode=self.raw_buffer_format)
+ else:
+ return Image.frombytes(self.raw_buffer_format, (8, 8), self._image_data())
+
+ def ndarray(self):
"""
- Use this function to get an easy-to-use `numpy.ndarray` object of the tile. The array has a shape of (8, 8, 4)
- and each value is of `numpy.uint8`. The values corresponds to and RGBA image of 8x8 pixels with each sub-color
- in a separate cell.
+ Use this function to get an `numpy.ndarray` object of the tile. The array has a shape of (8, 8, 4)
+ and each value is of `numpy.uint8`. The values corresponds to an image of 8x8 pixels with each sub-color
+ in a separate cell. The format is given by `pyboy.api.tile.Tile.raw_buffer_format`.
Be aware, that the graphics for this tile can change between each call to `pyboy.PyBoy.tick`.
+ Example:
+ ```python
+ >>> tile1 = pyboy.get_tile(1)
+ >>> tile1.ndarray()[:,:,0] # Upper part of "P"
+ array([[255, 255, 255, 255, 255, 255, 255, 255],
+ [255, 255, 255, 255, 255, 255, 255, 255],
+ [255, 255, 255, 255, 255, 255, 255, 255],
+ [255, 0, 0, 0, 0, 0, 255, 255],
+ [255, 0, 0, 0, 0, 0, 0, 255],
+ [255, 0, 0, 255, 255, 0, 0, 255],
+ [255, 0, 0, 255, 255, 0, 0, 255],
+ [255, 0, 0, 255, 255, 0, 0, 255]], dtype=uint8)
+ >>> tile2 = pyboy.get_tile(2)
+ >>> tile2.ndarray()[:,:,0] # Lower part of "P"
+ array([[255, 0, 0, 0, 0, 0, 0, 255],
+ [255, 0, 0, 0, 0, 0, 255, 255],
+ [255, 0, 0, 255, 255, 255, 255, 255],
+ [255, 0, 0, 255, 255, 255, 255, 255],
+ [255, 0, 0, 255, 255, 255, 255, 255],
+ [255, 0, 0, 255, 255, 255, 255, 255],
+ [255, 0, 0, 255, 255, 255, 255, 255],
+ [255, 255, 255, 255, 255, 255, 255, 255]], dtype=uint8)
+
+ ```
+
Returns
-------
numpy.ndarray :
Array of shape (8, 8, 4) with data type of `numpy.uint8`.
"""
- return np.asarray(self.image_data()).view(dtype=np.uint8).reshape(8, 8, 4)
+ # The data is laid out as (X, red, green, blue), where X is currently always zero, but this is not guarenteed
+ # across versions of PyBoy.
+ return np.asarray(self._image_data()).view(dtype=np.uint8).reshape(8, 8, 4)
- def image_data(self):
+ def _image_data(self):
"""
Use this function to get the raw tile data. The data is a `memoryview` corresponding to 8x8 pixels in RGBA
colors.
@@ -290,68 +408,84 @@
Classes
Returns
-------
memoryview :
- Image data of tile in 8x8 pixels and RGBA colors.
+ Image data of tile in 8x8 pixels and RGB colors.
"""
self.data = np.zeros((8, 8), dtype=np.uint32)
for k in range(0, 16, 2): # 2 bytes for each line
- byte1 = self.mb.lcd.VRAM0[self.data_address + k - VRAM_OFFSET]
- byte2 = self.mb.lcd.VRAM0[self.data_address + k + 1 - VRAM_OFFSET]
+ if self.vram_bank == 0:
+ byte1 = self.mb.lcd.VRAM0[self.data_address + k - VRAM_OFFSET]
+ byte2 = self.mb.lcd.VRAM0[self.data_address + k + 1 - VRAM_OFFSET]
+ else:
+ byte1 = self.mb.lcd.VRAM1[self.data_address + k - VRAM_OFFSET]
+ byte2 = self.mb.lcd.VRAM1[self.data_address + k + 1 - VRAM_OFFSET]
for x in range(8):
colorcode = utils.color_code(byte1, byte2, 7 - x)
- # NOTE: ">> 8 | 0xFF000000" to keep compatibility with earlier code
- old_A_format = 0xFF000000
- self.data[k // 2][x] = self.mb.lcd.BGP.getcolor(colorcode) >> 8 | old_A_format
-
+ self.data[k // 2][x] = self.mb.lcd.BGP.getcolor(colorcode)
return self.data
def __eq__(self, other):
- return self.data_address == other.data_address
+ return self.data_address == other.data_address and self.vram_bank == other.vram_bank
def __repr__(self):
return f"Tile: {self.tile_identifier}"
Instance variables
-
var data_address
+
var data_address
The tile data is defined in a specific area of the Game Boy. This function returns the address of the tile data
-corresponding to the tile identifier. It is advised to use Tile.image() or one of the
+corresponding to the tile identifier. It is advised to use Tile.image() or one of the
other image-functions if you want to view the tile.
The Game Boy has a slightly complicated indexing system for tiles. This identifier unifies the otherwise
-complicated indexing system on the Game Boy into a single range of 0-383 (both included).
+complicated indexing system on the Game Boy into a single range of 0-383 (both included) or 0-767 for Game Boy
+Color.
Returns
int:
Unique identifier for the tile
-
var shape
+
var shape
Tiles are always 8x8 pixels.
Returns
(int, int):
The width and height of the tile.
+
var raw_buffer_format
+
+
Returns the color format of the raw screen buffer.
+
Returns
+
+
str:
+
Color format of the raw screen buffer. E.g. 'RGBA'.
+
+
Methods
-
+
def image(self)
-
Use this function to get an easy-to-use PIL.Image object of the tile. The image is 8x8 pixels in RGBA colors.
+
Use this function to get an PIL.Image object of the tile. The image is 8x8 pixels. The format or "mode" might change at any time.
Be aware, that the graphics for this tile can change between each call to PyBoy.tick().
def image(self):
"""
- Use this function to get an easy-to-use `PIL.Image` object of the tile. The image is 8x8 pixels in RGBA colors.
+ Use this function to get an `PIL.Image` object of the tile. The image is 8x8 pixels. The format or "mode" might change at any time.
Be aware, that the graphics for this tile can change between each call to `pyboy.PyBoy.tick`.
+ Example:
+ ```python
+ >>> tile = pyboy.get_tile(1)
+ >>> tile.image().save('tile_1.png')
+
+ ```
+
Returns
-------
PIL.Image :
@@ -375,17 +516,44 @@
Returns
if Image is None:
logger.error(f"{__name__}: Missing dependency \"Pillow\".")
return None
- return Image.frombytes("RGBA", (8, 8), bytes(self.image_data()))
Use this function to get an easy-to-use numpy.ndarray object of the tile. The array has a shape of (8, 8, 4)
-and each value is of numpy.uint8. The values corresponds to and RGBA image of 8x8 pixels with each sub-color
-in a separate cell.
+
Use this function to get an numpy.ndarray object of the tile. The array has a shape of (8, 8, 4)
+and each value is of numpy.uint8. The values corresponds to an image of 8x8 pixels with each sub-color
+in a separate cell. The format is given by Tile.raw_buffer_format.
Be aware, that the graphics for this tile can change between each call to PyBoy.tick().
def ndarray(self):
"""
- Use this function to get an easy-to-use `numpy.ndarray` object of the tile. The array has a shape of (8, 8, 4)
- and each value is of `numpy.uint8`. The values corresponds to and RGBA image of 8x8 pixels with each sub-color
- in a separate cell.
+ Use this function to get an `numpy.ndarray` object of the tile. The array has a shape of (8, 8, 4)
+ and each value is of `numpy.uint8`. The values corresponds to an image of 8x8 pixels with each sub-color
+ in a separate cell. The format is given by `pyboy.api.tile.Tile.raw_buffer_format`.
Be aware, that the graphics for this tile can change between each call to `pyboy.PyBoy.tick`.
+ Example:
+ ```python
+ >>> tile1 = pyboy.get_tile(1)
+ >>> tile1.ndarray()[:,:,0] # Upper part of "P"
+ array([[255, 255, 255, 255, 255, 255, 255, 255],
+ [255, 255, 255, 255, 255, 255, 255, 255],
+ [255, 255, 255, 255, 255, 255, 255, 255],
+ [255, 0, 0, 0, 0, 0, 255, 255],
+ [255, 0, 0, 0, 0, 0, 0, 255],
+ [255, 0, 0, 255, 255, 0, 0, 255],
+ [255, 0, 0, 255, 255, 0, 0, 255],
+ [255, 0, 0, 255, 255, 0, 0, 255]], dtype=uint8)
+ >>> tile2 = pyboy.get_tile(2)
+ >>> tile2.ndarray()[:,:,0] # Lower part of "P"
+ array([[255, 0, 0, 0, 0, 0, 0, 255],
+ [255, 0, 0, 0, 0, 0, 255, 255],
+ [255, 0, 0, 255, 255, 255, 255, 255],
+ [255, 0, 0, 255, 255, 255, 255, 255],
+ [255, 0, 0, 255, 255, 255, 255, 255],
+ [255, 0, 0, 255, 255, 255, 255, 255],
+ [255, 0, 0, 255, 255, 255, 255, 255],
+ [255, 255, 255, 255, 255, 255, 255, 255]], dtype=uint8)
+
+ ```
+
Returns
-------
numpy.ndarray :
Array of shape (8, 8, 4) with data type of `numpy.uint8`.
"""
- return np.asarray(self.image_data()).view(dtype=np.uint8).reshape(8, 8, 4)
-
-
-
-def image_data(self)
-
-
-
Use this function to get the raw tile data. The data is a memoryview corresponding to 8x8 pixels in RGBA
-colors.
-
Be aware, that the graphics for this tile can change between each call to PyBoy.tick().
-
Returns
-
-
memoryview :
-
Image data of tile in 8x8 pixels and RGBA colors.
-
-
-
-Expand source code
-
-
def image_data(self):
- """
- Use this function to get the raw tile data. The data is a `memoryview` corresponding to 8x8 pixels in RGBA
- colors.
-
- Be aware, that the graphics for this tile can change between each call to `pyboy.PyBoy.tick`.
-
- Returns
- -------
- memoryview :
- Image data of tile in 8x8 pixels and RGBA colors.
- """
- self.data = np.zeros((8, 8), dtype=np.uint32)
- for k in range(0, 16, 2): # 2 bytes for each line
- byte1 = self.mb.lcd.VRAM0[self.data_address + k - VRAM_OFFSET]
- byte2 = self.mb.lcd.VRAM0[self.data_address + k + 1 - VRAM_OFFSET]
-
- for x in range(8):
- colorcode = utils.color_code(byte1, byte2, 7 - x)
- # NOTE: ">> 8 | 0xFF000000" to keep compatibility with earlier code
- old_A_format = 0xFF000000
- self.data[k // 2][x] = self.mb.lcd.BGP.getcolor(colorcode) >> 8 | old_A_format
-
- return self.data
+ # The data is laid out as (X, red, green, blue), where X is currently always zero, but this is not guarenteed
+ # across versions of PyBoy.
+ return np.asarray(self._image_data()).view(dtype=np.uint8).reshape(8, 8, 4)
diff --git a/docs/botsupport/tilemap.html b/docs/api/tilemap.html
similarity index 74%
rename from docs/botsupport/tilemap.html
rename to docs/api/tilemap.html
index 1e20d84ff..e42b640b3 100644
--- a/docs/botsupport/tilemap.html
+++ b/docs/api/tilemap.html
@@ -4,7 +4,7 @@
-pyboy.botsupport.tilemap API documentation
+pyboy.api.tilemap API documentation
@@ -18,7 +18,7 @@
-
Module pyboy.botsupport.tilemap
+
Module pyboy.api.tilemap
The Game Boy has two tile maps, which defines what is rendered on the screen.
@@ -35,6 +35,7 @@
Module pyboy.botsupport.tilemap
"""
import numpy as np
+
from pyboy.core.lcd import LCDCRegister
from .constants import HIGH_TILEMAP, LCDC_OFFSET, LOW_TILEDATA_NTILES, LOW_TILEMAP
@@ -42,39 +43,37 @@
Module pyboy.botsupport.tilemap
class TileMap:
- def __init__(self, mb, select):
+ def __init__(self, pyboy, mb, select):
"""
The Game Boy has two tile maps, which defines what is rendered on the screen. These are also referred to as
"background" and "window".
- Use `pyboy.botsupport.BotSupportManager.tilemap_background` and
- `pyboy.botsupport.BotSupportManager.tilemap_window` to instantiate this object.
+ Use `pyboy.tilemap_background` and
+ `pyboy.tilemap_window` to instantiate this object.
This object defines `__getitem__`, which means it can be accessed with the square brackets to get a tile
identifier at a given coordinate.
Example:
```
- >>> tilemap = pyboy.tilemap_window
- >>> tile = tilemap[10,10]
- >>> print(tile)
- 34
- >>> print(tilemap[0:10,10])
- [43, 54, 23, 23, 23, 54, 12, 54, 54, 23]
- >>> print(tilemap[0:10,0:4])
- [[43, 54, 23, 23, 23, 54, 12, 54, 54, 23],
- [43, 54, 43, 23, 23, 43, 12, 39, 54, 23],
- [43, 54, 23, 12, 87, 54, 12, 54, 21, 23],
- [43, 54, 23, 43, 23, 87, 12, 50, 54, 72]]
+ >>> pyboy.tilemap_window[8,8]
+ 1
+ >>> pyboy.tilemap_window[7:12,8]
+ [0, 1, 0, 1, 0]
+ >>> pyboy.tilemap_window[7:12,8:11]
+ [[0, 1, 0, 1, 0], [0, 2, 3, 4, 5], [0, 0, 6, 0, 0]]
+
```
Each element in the matrix, is the tile identifier of the tile to be shown on screen for each position. If you
need the entire 32x32 tile map, you can use the shortcut: `tilemap[:,:]`.
"""
+ self.pyboy = pyboy
self.mb = mb
self._select = select
self._use_tile_objects = False
- self.refresh_lcdc()
+ self.frame_count_update = 0
+ self.__refresh_lcdc()
self.shape = (32, 32)
"""
@@ -86,7 +85,12 @@
Module pyboy.botsupport.tilemap
The width and height of the tile map.
"""
- def refresh_lcdc(self):
+ def _refresh_lcdc(self):
+ if self.frame_count_update == self.pyboy.frame_count:
+ return 0
+ self.__refresh_lcdc()
+
+ def __refresh_lcdc(self):
"""
The tile data and view that is showed on the background and window respectively can change dynamically. If you
believe it has changed, you can use this method to update the tilemap from the LCDC register.
@@ -108,9 +112,9 @@
Module pyboy.botsupport.tilemap
Example:
```
- >>> tilemap = pyboy.tilemap_window
- >>> print(tilemap.search_for_identifiers([43, 123]))
- [[[0,0], [2,4], [8,7]], []]
+ >>> pyboy.tilemap_window.search_for_identifiers([5,3])
+ [[[9, 11]], [[9, 9], [9, 12]]]
+
```
Meaning, that tile identifier `43` is found at the positions: (0,0), (2,4), and (8,7), while tile identifier
@@ -135,7 +139,7 @@
Module pyboy.botsupport.tilemap
"""
Returns the memory address in the tilemap for the tile at the given coordinate. The address contains the index
of tile which will be shown at this position. This should not be confused with the actual tile data of
- `pyboy.botsupport.tile.Tile.data_address`.
+ `pyboy.api.tile.Tile.data_address`.
This can be used as an global identifier for the specific location in a tile map.
@@ -144,9 +148,9 @@
Module pyboy.botsupport.tilemap
on the screen.
The index might also be a signed number. Depending on if it is signed or not, will change where the tile data
- is read from. Use `pyboy.botsupport.tilemap.TileMap.signed_tile_index` to test if the indexes are signed for
+ is read from. Use `pyboy.api.tilemap.TileMap.signed_tile_index` to test if the indexes are signed for
this tile view. You can read how the indexes work in the
- [Pan Docs: VRAM Tile Data](http://bgb.bircd.org/pandocs.htm#vramtiledata).
+ [Pan Docs: VRAM Tile Data](https://gbdev.io/pandocs/Tile_Data.html).
Args:
column (int): Column in this tile map.
@@ -157,7 +161,6 @@
Module pyboy.botsupport.tilemap
int:
Address in the tile map to read a tile index.
"""
-
if not 0 <= column < 32:
raise IndexError("column is out of bounds. Value of 0 to 31 is allowed")
if not 0 <= row < 32:
@@ -166,8 +169,8 @@
Module pyboy.botsupport.tilemap
def tile(self, column, row):
"""
- Provides a `pyboy.botsupport.tile.Tile`-object which allows for easy interpretation of the tile data. The
- object is agnostic to where it was found in the tilemap. I.e. equal `pyboy.botsupport.tile.Tile`-objects might
+ Provides a `pyboy.api.tile.Tile`-object which allows for easy interpretation of the tile data. The
+ object is agnostic to where it was found in the tilemap. I.e. equal `pyboy.api.tile.Tile`-objects might
be returned from two different coordinates in the tile map if they are shown different places on the screen.
Args:
@@ -176,7 +179,7 @@
Module pyboy.botsupport.tilemap
Returns
-------
- `pyboy.botsupport.tile.Tile`:
+ `pyboy.api.tile.Tile`:
Tile object corresponding to the tile index at the given coordinate in the
tile map.
"""
@@ -191,7 +194,7 @@
Module pyboy.botsupport.tilemap
0-383 (both included).
You can read how the indexes work in the
- [Pan Docs: VRAM Tile Data](http://bgb.bircd.org/pandocs.htm#vramtiledata).
+ [Pan Docs: VRAM Tile Data](https://gbdev.io/pandocs/Tile_Data.html).
Args:
column (int): Column in this tile map.
@@ -202,7 +205,7 @@
Used to change which object is returned when using the ``__getitem__`` method (i.e. `tilemap[0,0]`).
Args:
- switch (bool): If True, accesses will return `pyboy.botsupport.tile.Tile`-object. If False, accesses will
+ switch (bool): If True, accesses will return `pyboy.api.tile.Tile`-object. If False, accesses will
return an `int`.
"""
self._use_tile_objects = switch
- def __getitem__(self, xy):
- x, y = xy
+ def _fix_slice(self, addr):
+ if addr.step is None:
+ step = 1
+ else:
+ step = addr.step
+
+ if addr.start is None:
+ start = 0
+ else:
+ start = addr.start
+
+ if addr.stop is None:
+ stop = 32
+ else:
+ stop = addr.stop
- if x == slice(None):
- x = slice(0, 32, 1)
+ if step < 0:
+ raise ValueError("Reversed ranges are unsupported")
+ elif start > stop:
+ raise ValueError("Invalid range")
+ return start, stop, step
- if y == slice(None):
- y = slice(0, 32, 1)
+ def __getitem__(self, xy):
+ if isinstance(xy, (int, slice)):
+ x = xy
+ y = slice(None)
+ else:
+ x, y = xy
+
+ x_slice = isinstance(x, slice)
+ y_slice = isinstance(y, slice)
+ if x_slice:
+ x = self._fix_slice(x)
+ else:
+ assert isinstance(x, int)
- x_slice = isinstance(x, slice) # Assume slice, otherwise int
- y_slice = isinstance(y, slice) # Assume slice, otherwise int
- assert x_slice or isinstance(x, int)
- assert y_slice or isinstance(y, int)
+ if y_slice:
+ y = self._fix_slice(y)
+ else:
+ assert isinstance(y, int)
if self._use_tile_objects:
tile_fun = self.tile
@@ -260,11 +291,11 @@
Module pyboy.botsupport.tilemap
tile_fun = lambda x, y: self.tile_identifier(x, y)
if x_slice and y_slice:
- return [[tile_fun(_x, _y) for _x in range(x.stop)[x]] for _y in range(y.stop)[y]]
+ return [[tile_fun(_x, _y) for _x in range(*x)] for _y in range(*y)]
elif x_slice:
- return [tile_fun(_x, y) for _x in range(x.stop)[x]]
+ return [tile_fun(_x, y) for _x in range(*x)]
elif y_slice:
- return [tile_fun(x, _y) for _y in range(y.stop)[y]]
+ return [tile_fun(x, _y) for _y in range(*y)]
else:
return tile_fun(x, y)
@@ -278,29 +309,25 @@
Module pyboy.botsupport.tilemap
Classes
-
+
class TileMap
-(mb, select)
+(pyboy, mb, select)
The Game Boy has two tile maps, which defines what is rendered on the screen. These are also referred to as
"background" and "window".
Each element in the matrix, is the tile identifier of the tile to be shown on screen for each position. If you
need the entire 32x32 tile map, you can use the shortcut: tilemap[:,:].
@@ -309,39 +336,37 @@
Classes
Expand source code
class TileMap:
- def __init__(self, mb, select):
+ def __init__(self, pyboy, mb, select):
"""
The Game Boy has two tile maps, which defines what is rendered on the screen. These are also referred to as
"background" and "window".
- Use `pyboy.botsupport.BotSupportManager.tilemap_background` and
- `pyboy.botsupport.BotSupportManager.tilemap_window` to instantiate this object.
+ Use `pyboy.tilemap_background` and
+ `pyboy.tilemap_window` to instantiate this object.
This object defines `__getitem__`, which means it can be accessed with the square brackets to get a tile
identifier at a given coordinate.
Example:
```
- >>> tilemap = pyboy.tilemap_window
- >>> tile = tilemap[10,10]
- >>> print(tile)
- 34
- >>> print(tilemap[0:10,10])
- [43, 54, 23, 23, 23, 54, 12, 54, 54, 23]
- >>> print(tilemap[0:10,0:4])
- [[43, 54, 23, 23, 23, 54, 12, 54, 54, 23],
- [43, 54, 43, 23, 23, 43, 12, 39, 54, 23],
- [43, 54, 23, 12, 87, 54, 12, 54, 21, 23],
- [43, 54, 23, 43, 23, 87, 12, 50, 54, 72]]
+ >>> pyboy.tilemap_window[8,8]
+ 1
+ >>> pyboy.tilemap_window[7:12,8]
+ [0, 1, 0, 1, 0]
+ >>> pyboy.tilemap_window[7:12,8:11]
+ [[0, 1, 0, 1, 0], [0, 2, 3, 4, 5], [0, 0, 6, 0, 0]]
+
```
Each element in the matrix, is the tile identifier of the tile to be shown on screen for each position. If you
need the entire 32x32 tile map, you can use the shortcut: `tilemap[:,:]`.
"""
+ self.pyboy = pyboy
self.mb = mb
self._select = select
self._use_tile_objects = False
- self.refresh_lcdc()
+ self.frame_count_update = 0
+ self.__refresh_lcdc()
self.shape = (32, 32)
"""
@@ -353,7 +378,12 @@
Classes
The width and height of the tile map.
"""
- def refresh_lcdc(self):
+ def _refresh_lcdc(self):
+ if self.frame_count_update == self.pyboy.frame_count:
+ return 0
+ self.__refresh_lcdc()
+
+ def __refresh_lcdc(self):
"""
The tile data and view that is showed on the background and window respectively can change dynamically. If you
believe it has changed, you can use this method to update the tilemap from the LCDC register.
@@ -375,9 +405,9 @@
Classes
Example:
```
- >>> tilemap = pyboy.tilemap_window
- >>> print(tilemap.search_for_identifiers([43, 123]))
- [[[0,0], [2,4], [8,7]], []]
+ >>> pyboy.tilemap_window.search_for_identifiers([5,3])
+ [[[9, 11]], [[9, 9], [9, 12]]]
+
```
Meaning, that tile identifier `43` is found at the positions: (0,0), (2,4), and (8,7), while tile identifier
@@ -402,7 +432,7 @@
Classes
"""
Returns the memory address in the tilemap for the tile at the given coordinate. The address contains the index
of tile which will be shown at this position. This should not be confused with the actual tile data of
- `pyboy.botsupport.tile.Tile.data_address`.
+ `pyboy.api.tile.Tile.data_address`.
This can be used as an global identifier for the specific location in a tile map.
@@ -411,9 +441,9 @@
Classes
on the screen.
The index might also be a signed number. Depending on if it is signed or not, will change where the tile data
- is read from. Use `pyboy.botsupport.tilemap.TileMap.signed_tile_index` to test if the indexes are signed for
+ is read from. Use `pyboy.api.tilemap.TileMap.signed_tile_index` to test if the indexes are signed for
this tile view. You can read how the indexes work in the
- [Pan Docs: VRAM Tile Data](http://bgb.bircd.org/pandocs.htm#vramtiledata).
+ [Pan Docs: VRAM Tile Data](https://gbdev.io/pandocs/Tile_Data.html).
Args:
column (int): Column in this tile map.
@@ -424,7 +454,6 @@
Classes
int:
Address in the tile map to read a tile index.
"""
-
if not 0 <= column < 32:
raise IndexError("column is out of bounds. Value of 0 to 31 is allowed")
if not 0 <= row < 32:
@@ -433,8 +462,8 @@
Classes
def tile(self, column, row):
"""
- Provides a `pyboy.botsupport.tile.Tile`-object which allows for easy interpretation of the tile data. The
- object is agnostic to where it was found in the tilemap. I.e. equal `pyboy.botsupport.tile.Tile`-objects might
+ Provides a `pyboy.api.tile.Tile`-object which allows for easy interpretation of the tile data. The
+ object is agnostic to where it was found in the tilemap. I.e. equal `pyboy.api.tile.Tile`-objects might
be returned from two different coordinates in the tile map if they are shown different places on the screen.
Args:
@@ -443,7 +472,7 @@
Classes
Returns
-------
- `pyboy.botsupport.tile.Tile`:
+ `pyboy.api.tile.Tile`:
Tile object corresponding to the tile index at the given coordinate in the
tile map.
"""
@@ -458,7 +487,7 @@
Classes
0-383 (both included).
You can read how the indexes work in the
- [Pan Docs: VRAM Tile Data](http://bgb.bircd.org/pandocs.htm#vramtiledata).
+ [Pan Docs: VRAM Tile Data](https://gbdev.io/pandocs/Tile_Data.html).
Args:
column (int): Column in this tile map.
@@ -469,7 +498,7 @@
Used to change which object is returned when using the ``__getitem__`` method (i.e. `tilemap[0,0]`).
Args:
- switch (bool): If True, accesses will return `pyboy.botsupport.tile.Tile`-object. If False, accesses will
+ switch (bool): If True, accesses will return `pyboy.api.tile.Tile`-object. If False, accesses will
return an `int`.
"""
self._use_tile_objects = switch
- def __getitem__(self, xy):
- x, y = xy
+ def _fix_slice(self, addr):
+ if addr.step is None:
+ step = 1
+ else:
+ step = addr.step
+
+ if addr.start is None:
+ start = 0
+ else:
+ start = addr.start
+
+ if addr.stop is None:
+ stop = 32
+ else:
+ stop = addr.stop
+
+ if step < 0:
+ raise ValueError("Reversed ranges are unsupported")
+ elif start > stop:
+ raise ValueError("Invalid range")
+ return start, stop, step
- if x == slice(None):
- x = slice(0, 32, 1)
+ def __getitem__(self, xy):
+ if isinstance(xy, (int, slice)):
+ x = xy
+ y = slice(None)
+ else:
+ x, y = xy
- if y == slice(None):
- y = slice(0, 32, 1)
+ x_slice = isinstance(x, slice)
+ y_slice = isinstance(y, slice)
+ if x_slice:
+ x = self._fix_slice(x)
+ else:
+ assert isinstance(x, int)
- x_slice = isinstance(x, slice) # Assume slice, otherwise int
- y_slice = isinstance(y, slice) # Assume slice, otherwise int
- assert x_slice or isinstance(x, int)
- assert y_slice or isinstance(y, int)
+ if y_slice:
+ y = self._fix_slice(y)
+ else:
+ assert isinstance(y, int)
if self._use_tile_objects:
tile_fun = self.tile
@@ -527,17 +584,17 @@
Classes
tile_fun = lambda x, y: self.tile_identifier(x, y)
if x_slice and y_slice:
- return [[tile_fun(_x, _y) for _x in range(x.stop)[x]] for _y in range(y.stop)[y]]
+ return [[tile_fun(_x, _y) for _x in range(*x)] for _y in range(*y)]
elif x_slice:
- return [tile_fun(_x, y) for _x in range(x.stop)[x]]
+ return [tile_fun(_x, y) for _x in range(*x)]
elif y_slice:
- return [tile_fun(x, _y) for _y in range(y.stop)[y]]
+ return [tile_fun(x, _y) for _y in range(*y)]
else:
return tile_fun(x, y)
Instance variables
-
var shape
+
var shape
Tile maps are always 32x32 tiles.
Returns
@@ -547,42 +604,16 @@
Returns
Methods
-
-def refresh_lcdc(self)
-
-
-
The tile data and view that is showed on the background and window respectively can change dynamically. If you
-believe it has changed, you can use this method to update the tilemap from the LCDC register.
-
-
-Expand source code
-
-
def refresh_lcdc(self):
- """
- The tile data and view that is showed on the background and window respectively can change dynamically. If you
- believe it has changed, you can use this method to update the tilemap from the LCDC register.
- """
- LCDC = LCDCRegister(self.mb.getitem(LCDC_OFFSET))
- if self._select == "WINDOW":
- self.map_offset = HIGH_TILEMAP if LCDC.windowmap_select else LOW_TILEMAP
- self.signed_tile_data = not bool(LCDC.tiledata_select)
- elif self._select == "BACKGROUND":
- self.map_offset = HIGH_TILEMAP if LCDC.backgroundmap_select else LOW_TILEMAP
- self.signed_tile_data = not bool(LCDC.tiledata_select)
- else:
- raise KeyError(f"Invalid tilemap selected: {self._select}")
-
-
-
+
def search_for_identifiers(self, identifiers)
Provided a list of tile identifiers, this function will find all occurrences of these in the tilemap and return
the coordinates where each identifier is found.
Meaning, that tile identifier 43 is found at the positions: (0,0), (2,4), and (8,7), while tile identifier
123was not found anywhere.
@@ -607,9 +638,9 @@
Returns
Example:
```
- >>> tilemap = pyboy.tilemap_window
- >>> print(tilemap.search_for_identifiers([43, 123]))
- [[[0,0], [2,4], [8,7]], []]
+ >>> pyboy.tilemap_window.search_for_identifiers([5,3])
+ [[[9, 11]], [[9, 9], [9, 12]]]
+
```
Meaning, that tile identifier `43` is found at the positions: (0,0), (2,4), and (8,7), while tile identifier
@@ -631,12 +662,12 @@
Returns
return matches
-
+
def tile(self, column, row)
-
Provides a Tile-object which allows for easy interpretation of the tile data. The
-object is agnostic to where it was found in the tilemap. I.e. equal Tile-objects might
+
Provides a Tile-object which allows for easy interpretation of the tile data. The
+object is agnostic to where it was found in the tilemap. I.e. equal Tile-objects might
be returned from two different coordinates in the tile map if they are shown different places on the screen.
Tile:
Tile object corresponding to the tile index at the given coordinate in the
tile map.
@@ -655,8 +686,8 @@
Returns
def tile(self, column, row):
"""
- Provides a `pyboy.botsupport.tile.Tile`-object which allows for easy interpretation of the tile data. The
- object is agnostic to where it was found in the tilemap. I.e. equal `pyboy.botsupport.tile.Tile`-objects might
+ Provides a `pyboy.api.tile.Tile`-object which allows for easy interpretation of the tile data. The
+ object is agnostic to where it was found in the tilemap. I.e. equal `pyboy.api.tile.Tile`-objects might
be returned from two different coordinates in the tile map if they are shown different places on the screen.
Args:
@@ -665,14 +696,14 @@
Returns
Returns
-------
- `pyboy.botsupport.tile.Tile`:
+ `pyboy.api.tile.Tile`:
Tile object corresponding to the tile index at the given coordinate in the
tile map.
"""
return Tile(self.mb, self.tile_identifier(column, row))
-
+
def tile_identifier(self, column, row)
@@ -681,7 +712,7 @@
Returns
This identifier unifies the otherwise complicated indexing system on the Game Boy into a single range of
0-383 (both included).
0-383 (both included).
You can read how the indexes work in the
- [Pan Docs: VRAM Tile Data](http://bgb.bircd.org/pandocs.htm#vramtiledata).
+ [Pan Docs: VRAM Tile Data](https://gbdev.io/pandocs/Tile_Data.html).
Args:
column (int): Column in this tile map.
@@ -718,7 +749,7 @@
If True, accesses will return Tile-object. If False, accesses will
+
If True, accesses will return Tile-object. If False, accesses will
return an int.
@@ -746,7 +777,7 @@
Args
Used to change which object is returned when using the ``__getitem__`` method (i.e. `tilemap[0,0]`).
Args:
- switch (bool): If True, accesses will return `pyboy.botsupport.tile.Tile`-object. If False, accesses will
+ switch (bool): If True, accesses will return `pyboy.api.tile.Tile`-object. If False, accesses will
return an `int`.
"""
self._use_tile_objects = switch
@@ -765,20 +796,19 @@
The Game Boy has two tile maps, which defines what is rendered on the screen.
-
-
-
-
-
-
-
-
-
Classes
-
-
-class BotSupportManager
-(pyboy, mb)
-
-
-
-
-
-Expand source code
-
-
class BotSupportManager:
- def __init__(self, pyboy, mb):
- if not cythonmode:
- self.pyboy = pyboy
- self.mb = mb
-
- def __cinit__(self, pyboy, mb):
- self.pyboy = pyboy
- self.mb = mb
-
- def screen(self):
- """
- Use this method to get a `pyboy.botsupport.screen.Screen` object. This can be used to get the screen buffer in
- a variety of formats.
-
- It's also here you can find the screen position (SCX, SCY, WX, WY) for each scan line in the screen buffer. See
- `pyboy.botsupport.screen.Screen.tilemap_position` for more information.
-
- Returns
- -------
- `pyboy.botsupport.screen.Screen`:
- A Screen object with helper functions for reading the screen buffer.
- """
- return _screen.Screen(self.mb)
-
- def sprite(self, sprite_index):
- """
- Provides a `pyboy.botsupport.sprite.Sprite` object, which makes the OAM data more presentable. The given index
- corresponds to index of the sprite in the "Object Attribute Memory" (OAM).
-
- The Game Boy supports 40 sprites in total. Read more details about it, in the [Pan
- Docs](http://bgb.bircd.org/pandocs.htm).
-
- Args:
- index (int): Sprite index from 0 to 39.
- Returns
- -------
- `pyboy.botsupport.sprite.Sprite`:
- Sprite corresponding to the given index.
- """
- return _sprite.Sprite(self.mb, sprite_index)
-
- def sprite_by_tile_identifier(self, tile_identifiers, on_screen=True):
- """
- Provided a list of tile identifiers, this function will find all occurrences of sprites using the tile
- identifiers and return the sprite indexes where each identifier is found. Use the sprite indexes in the
- `pyboy.botsupport.BotSupportManager.sprite` function to get a `pyboy.botsupport.sprite.Sprite` object.
-
- Example:
- ```
- >>> print(pyboy.botsupport_manager().sprite_by_tile_identifier([43, 123]))
- [[0, 2, 4], []]
- ```
-
- Meaning, that tile identifier `43` is found at the sprite indexes: 0, 2, and 4, while tile identifier
- `123` was not found anywhere.
-
- Args:
- identifiers (list): List of tile identifiers (int)
- on_screen (bool): Require that the matched sprite is on screen
-
- Returns
- -------
- list:
- list of sprite matches for every tile identifier in the input
- """
-
- matches = []
- for i in tile_identifiers:
- match = []
- for s in range(_constants.SPRITES):
- sprite = _sprite.Sprite(self.mb, s)
- for t in sprite.tiles:
- if t.tile_identifier == i and (not on_screen or (on_screen and sprite.on_screen)):
- match.append(s)
- matches.append(match)
- return matches
-
- def tile(self, identifier):
- """
- The Game Boy can have 384 tiles loaded in memory at once. Use this method to get a
- `pyboy.botsupport.tile.Tile`-object for given identifier.
-
- The identifier is a PyBoy construct, which unifies two different scopes of indexes in the Game Boy hardware. See
- the `pyboy.botsupport.tile.Tile` object for more information.
-
- Returns
- -------
- `pyboy.botsupport.tile.Tile`:
- A Tile object for the given identifier.
- """
- return _tile.Tile(self.mb, identifier=identifier)
-
- def tilemap_background(self):
- """
- The Game Boy uses two tile maps at the same time to draw graphics on the screen. This method will provide one
- for the _background_ tiles. The game chooses whether it wants to use the low or the high tilemap.
-
- Read more details about it, in the [Pan Docs](http://bgb.bircd.org/pandocs.htm#vrambackgroundmaps).
-
- Returns
- -------
- `pyboy.botsupport.tilemap.TileMap`:
- A TileMap object for the tile map.
- """
- return _tilemap.TileMap(self.mb, "BACKGROUND")
-
- def tilemap_window(self):
- """
- The Game Boy uses two tile maps at the same time to draw graphics on the screen. This method will provide one
- for the _window_ tiles. The game chooses whether it wants to use the low or the high tilemap.
-
- Read more details about it, in the [Pan Docs](http://bgb.bircd.org/pandocs.htm#vrambackgroundmaps).
-
- Returns
- -------
- `pyboy.botsupport.tilemap.TileMap`:
- A TileMap object for the tile map.
- """
- return _tilemap.TileMap(self.mb, "WINDOW")
-
-
Methods
-
-
-def screen(self)
-
-
-
Use this method to get a Screen object. This can be used to get the screen buffer in
-a variety of formats.
-
It's also here you can find the screen position (SCX, SCY, WX, WY) for each scan line in the screen buffer. See
-Screen.tilemap_position() for more information.
-
Returns
-
Screen:
-A Screen object with helper functions for reading the screen buffer.
-
-
-Expand source code
-
-
def screen(self):
- """
- Use this method to get a `pyboy.botsupport.screen.Screen` object. This can be used to get the screen buffer in
- a variety of formats.
-
- It's also here you can find the screen position (SCX, SCY, WX, WY) for each scan line in the screen buffer. See
- `pyboy.botsupport.screen.Screen.tilemap_position` for more information.
-
- Returns
- -------
- `pyboy.botsupport.screen.Screen`:
- A Screen object with helper functions for reading the screen buffer.
- """
- return _screen.Screen(self.mb)
-
-
-
-def sprite(self, sprite_index)
-
-
-
Provides a Sprite object, which makes the OAM data more presentable. The given index
-corresponds to index of the sprite in the "Object Attribute Memory" (OAM).
-
The Game Boy supports 40 sprites in total. Read more details about it, in the Pan
-Docs.
def sprite(self, sprite_index):
- """
- Provides a `pyboy.botsupport.sprite.Sprite` object, which makes the OAM data more presentable. The given index
- corresponds to index of the sprite in the "Object Attribute Memory" (OAM).
-
- The Game Boy supports 40 sprites in total. Read more details about it, in the [Pan
- Docs](http://bgb.bircd.org/pandocs.htm).
-
- Args:
- index (int): Sprite index from 0 to 39.
- Returns
- -------
- `pyboy.botsupport.sprite.Sprite`:
- Sprite corresponding to the given index.
- """
- return _sprite.Sprite(self.mb, sprite_index)
Provided a list of tile identifiers, this function will find all occurrences of sprites using the tile
-identifiers and return the sprite indexes where each identifier is found. Use the sprite indexes in the
-BotSupportManager.sprite() function to get a Sprite object.
Meaning, that tile identifier 43 is found at the sprite indexes: 0, 2, and 4, while tile identifier
-123 was not found anywhere.
-
Args
-
-
identifiers : list
-
List of tile identifiers (int)
-
on_screen : bool
-
Require that the matched sprite is on screen
-
-
Returns
-
-
list:
-
list of sprite matches for every tile identifier in the input
-
-
-
-Expand source code
-
-
def sprite_by_tile_identifier(self, tile_identifiers, on_screen=True):
- """
- Provided a list of tile identifiers, this function will find all occurrences of sprites using the tile
- identifiers and return the sprite indexes where each identifier is found. Use the sprite indexes in the
- `pyboy.botsupport.BotSupportManager.sprite` function to get a `pyboy.botsupport.sprite.Sprite` object.
-
- Example:
- ```
- >>> print(pyboy.botsupport_manager().sprite_by_tile_identifier([43, 123]))
- [[0, 2, 4], []]
- ```
-
- Meaning, that tile identifier `43` is found at the sprite indexes: 0, 2, and 4, while tile identifier
- `123` was not found anywhere.
-
- Args:
- identifiers (list): List of tile identifiers (int)
- on_screen (bool): Require that the matched sprite is on screen
-
- Returns
- -------
- list:
- list of sprite matches for every tile identifier in the input
- """
-
- matches = []
- for i in tile_identifiers:
- match = []
- for s in range(_constants.SPRITES):
- sprite = _sprite.Sprite(self.mb, s)
- for t in sprite.tiles:
- if t.tile_identifier == i and (not on_screen or (on_screen and sprite.on_screen)):
- match.append(s)
- matches.append(match)
- return matches
-
-
-
-def tile(self, identifier)
-
-
-
The Game Boy can have 384 tiles loaded in memory at once. Use this method to get a
-Tile-object for given identifier.
-
The identifier is a PyBoy construct, which unifies two different scopes of indexes in the Game Boy hardware. See
-the Tile object for more information.
def tile(self, identifier):
- """
- The Game Boy can have 384 tiles loaded in memory at once. Use this method to get a
- `pyboy.botsupport.tile.Tile`-object for given identifier.
-
- The identifier is a PyBoy construct, which unifies two different scopes of indexes in the Game Boy hardware. See
- the `pyboy.botsupport.tile.Tile` object for more information.
-
- Returns
- -------
- `pyboy.botsupport.tile.Tile`:
- A Tile object for the given identifier.
- """
- return _tile.Tile(self.mb, identifier=identifier)
-
-
-
-def tilemap_background(self)
-
-
-
The Game Boy uses two tile maps at the same time to draw graphics on the screen. This method will provide one
-for the background tiles. The game chooses whether it wants to use the low or the high tilemap.
def tilemap_background(self):
- """
- The Game Boy uses two tile maps at the same time to draw graphics on the screen. This method will provide one
- for the _background_ tiles. The game chooses whether it wants to use the low or the high tilemap.
-
- Read more details about it, in the [Pan Docs](http://bgb.bircd.org/pandocs.htm#vrambackgroundmaps).
-
- Returns
- -------
- `pyboy.botsupport.tilemap.TileMap`:
- A TileMap object for the tile map.
- """
- return _tilemap.TileMap(self.mb, "BACKGROUND")
-
-
-
-def tilemap_window(self)
-
-
-
The Game Boy uses two tile maps at the same time to draw graphics on the screen. This method will provide one
-for the window tiles. The game chooses whether it wants to use the low or the high tilemap.
def tilemap_window(self):
- """
- The Game Boy uses two tile maps at the same time to draw graphics on the screen. This method will provide one
- for the _window_ tiles. The game chooses whether it wants to use the low or the high tilemap.
-
- Read more details about it, in the [Pan Docs](http://bgb.bircd.org/pandocs.htm#vrambackgroundmaps).
-
- Returns
- -------
- `pyboy.botsupport.tilemap.TileMap`:
- A TileMap object for the tile map.
- """
- return _tilemap.TileMap(self.mb, "WINDOW")
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/botsupport/screen.html b/docs/botsupport/screen.html
deleted file mode 100644
index 27f9864d0..000000000
--- a/docs/botsupport/screen.html
+++ /dev/null
@@ -1,579 +0,0 @@
-
-
-
-
-
-
-pyboy.botsupport.screen API documentation
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Module pyboy.botsupport.screen
-
-
-
This class gives access to the frame buffer and other screen parameters of PyBoy.
-
-
-Expand source code
-
-
#
-# License: See LICENSE.md file
-# GitHub: https://github.com/Baekalfen/PyBoy
-#
-"""
-This class gives access to the frame buffer and other screen parameters of PyBoy.
-"""
-
-import logging
-
-import numpy as np
-
-from .constants import COLS, ROWS
-
-logger = logging.getLogger(__name__)
-
-try:
- from PIL import Image
-except ImportError:
- Image = None
-
-
-class Screen:
- """
- As part of the emulation, we generate a screen buffer in 32-bit RGBA format. This class has several helper methods
- to make it possible to read this buffer out.
-
- If you're making an AI or bot, it's highly recommended to _not_ use this class for detecting objects on the screen.
- It's much more efficient to use `pyboy.botsupport.BotSupportManager.tilemap_background`, `pyboy.botsupport.BotSupportManager.tilemap_window`, and
- `pyboy.botsupport.BotSupportManager.sprite` instead.
- """
- def __init__(self, mb):
- self.mb = mb
-
- def tilemap_position(self):
- """
- These coordinates define the offset in the tile map from where the top-left corner of the screen is place. Note
- that the tile map defines 256x256 pixels, but the screen can only show 160x144 pixels. When the offset is closer
- to the right or bottom edge than 160x144 pixels, the screen will wrap around and render from the opposite site
- of the tile map.
-
- For more details, see "7.4 Viewport" in the [report](https://github.com/Baekalfen/PyBoy/raw/master/extras/PyBoy.pdf),
- or the Pan Docs under [LCD Position and Scrolling](http://bgb.bircd.org/pandocs.htm#lcdpositionandscrolling).
-
- Returns
- -------
- tuple:
- Returns the tuple of registers ((SCX, SCY), (WX - 7, WY))
- """
- return (self.mb.lcd.getviewport(), self.mb.lcd.getwindowpos())
-
- def tilemap_position_list(self):
- """
- This function provides the screen (SCX, SCY) and window (WX. WY) position for each horizontal line in the
- screen buffer. These parameters are often used for visual effects, and some games will reset the registers at
- the end of each call to `pyboy.PyBoy.tick()`. For such games, `Screen.tilemap_position` becomes useless.
-
- See `Screen.tilemap_position` for more information.
-
- Returns
- -------
- list:
- Nested list of SCX, SCY, WX and WY for each scanline (144x4). Returns (0, 0, 0, 0) when LCD is off.
- """
- if self.mb.lcd._LCDC.lcd_enable:
- return [[line[0], line[1], line[2], line[3]] for line in self.mb.lcd.renderer._scanlineparameters]
- else:
- return [[0, 0, 0, 0] for line in range(144)]
-
- def raw_screen_buffer(self):
- """
- Provides a raw, unfiltered `bytes` object with the data from the screen. Check
- `Screen.raw_screen_buffer_format` to see which dataformat is used. The returned type and dataformat are
- subject to change.
-
- Use this, only if you need to bypass the overhead of `Screen.screen_image` or `Screen.screen_ndarray`.
-
- Returns
- -------
- bytes:
- 92160 bytes of screen data in a `bytes` object.
- """
- return self.mb.lcd.renderer._screenbuffer_raw.tobytes()
-
- def raw_screen_buffer_dims(self):
- """
- Returns the dimensions of the raw screen buffer.
-
- Returns
- -------
- tuple:
- A two-tuple of the buffer dimensions. E.g. (160, 144).
- """
- return self.mb.lcd.renderer.buffer_dims
-
- def raw_screen_buffer_format(self):
- """
- Returns the color format of the raw screen buffer.
-
- Returns
- -------
- str:
- Color format of the raw screen buffer. E.g. 'RGB'.
- """
- return self.mb.lcd.renderer.color_format
-
- def screen_ndarray(self):
- """
- Provides the screen data in NumPy format. The dataformat is always RGB.
-
- Returns
- -------
- numpy.ndarray:
- Screendata in `ndarray` of bytes with shape (160, 144, 3)
- """
- return np.frombuffer(self.mb.lcd.renderer._screenbuffer_raw, dtype=np.uint8).reshape(ROWS, COLS, 4)[:, :, 1:]
-
- def screen_image(self):
- """
- Generates a PIL Image from the screen buffer.
-
- Convenient for screen captures, but might be a bottleneck, if you use it to train a neural network. In which
- case, read up on the `pyboy.botsupport` features, [Pan Docs](http://bgb.bircd.org/pandocs.htm) on tiles/sprites,
- and join our Discord channel for more help.
-
- Returns
- -------
- PIL.Image:
- RGB image of (160, 144) pixels
- """
- if not Image:
- logger.error("Cannot generate screen image. Missing dependency \"Pillow\".")
- return None
-
- # NOTE: Might have room for performance improvement
- # It's not possible to use the following, as the byte-order (endianess) isn't supported in Pillow
- # Image.frombytes('RGBA', self.buffer_dims, self.screen_buffer()).show()
- # FIXME: FORMAT IS BGR NOT RGB!!!
- return Image.fromarray(self.screen_ndarray()[:, :, [2, 1, 0]], "RGB")
-
-
-
-
-
-
-
-
-
-
Classes
-
-
-class Screen
-(mb)
-
-
-
As part of the emulation, we generate a screen buffer in 32-bit RGBA format. This class has several helper methods
-to make it possible to read this buffer out.
class Screen:
- """
- As part of the emulation, we generate a screen buffer in 32-bit RGBA format. This class has several helper methods
- to make it possible to read this buffer out.
-
- If you're making an AI or bot, it's highly recommended to _not_ use this class for detecting objects on the screen.
- It's much more efficient to use `pyboy.botsupport.BotSupportManager.tilemap_background`, `pyboy.botsupport.BotSupportManager.tilemap_window`, and
- `pyboy.botsupport.BotSupportManager.sprite` instead.
- """
- def __init__(self, mb):
- self.mb = mb
-
- def tilemap_position(self):
- """
- These coordinates define the offset in the tile map from where the top-left corner of the screen is place. Note
- that the tile map defines 256x256 pixels, but the screen can only show 160x144 pixels. When the offset is closer
- to the right or bottom edge than 160x144 pixels, the screen will wrap around and render from the opposite site
- of the tile map.
-
- For more details, see "7.4 Viewport" in the [report](https://github.com/Baekalfen/PyBoy/raw/master/extras/PyBoy.pdf),
- or the Pan Docs under [LCD Position and Scrolling](http://bgb.bircd.org/pandocs.htm#lcdpositionandscrolling).
-
- Returns
- -------
- tuple:
- Returns the tuple of registers ((SCX, SCY), (WX - 7, WY))
- """
- return (self.mb.lcd.getviewport(), self.mb.lcd.getwindowpos())
-
- def tilemap_position_list(self):
- """
- This function provides the screen (SCX, SCY) and window (WX. WY) position for each horizontal line in the
- screen buffer. These parameters are often used for visual effects, and some games will reset the registers at
- the end of each call to `pyboy.PyBoy.tick()`. For such games, `Screen.tilemap_position` becomes useless.
-
- See `Screen.tilemap_position` for more information.
-
- Returns
- -------
- list:
- Nested list of SCX, SCY, WX and WY for each scanline (144x4). Returns (0, 0, 0, 0) when LCD is off.
- """
- if self.mb.lcd._LCDC.lcd_enable:
- return [[line[0], line[1], line[2], line[3]] for line in self.mb.lcd.renderer._scanlineparameters]
- else:
- return [[0, 0, 0, 0] for line in range(144)]
-
- def raw_screen_buffer(self):
- """
- Provides a raw, unfiltered `bytes` object with the data from the screen. Check
- `Screen.raw_screen_buffer_format` to see which dataformat is used. The returned type and dataformat are
- subject to change.
-
- Use this, only if you need to bypass the overhead of `Screen.screen_image` or `Screen.screen_ndarray`.
-
- Returns
- -------
- bytes:
- 92160 bytes of screen data in a `bytes` object.
- """
- return self.mb.lcd.renderer._screenbuffer_raw.tobytes()
-
- def raw_screen_buffer_dims(self):
- """
- Returns the dimensions of the raw screen buffer.
-
- Returns
- -------
- tuple:
- A two-tuple of the buffer dimensions. E.g. (160, 144).
- """
- return self.mb.lcd.renderer.buffer_dims
-
- def raw_screen_buffer_format(self):
- """
- Returns the color format of the raw screen buffer.
-
- Returns
- -------
- str:
- Color format of the raw screen buffer. E.g. 'RGB'.
- """
- return self.mb.lcd.renderer.color_format
-
- def screen_ndarray(self):
- """
- Provides the screen data in NumPy format. The dataformat is always RGB.
-
- Returns
- -------
- numpy.ndarray:
- Screendata in `ndarray` of bytes with shape (160, 144, 3)
- """
- return np.frombuffer(self.mb.lcd.renderer._screenbuffer_raw, dtype=np.uint8).reshape(ROWS, COLS, 4)[:, :, 1:]
-
- def screen_image(self):
- """
- Generates a PIL Image from the screen buffer.
-
- Convenient for screen captures, but might be a bottleneck, if you use it to train a neural network. In which
- case, read up on the `pyboy.botsupport` features, [Pan Docs](http://bgb.bircd.org/pandocs.htm) on tiles/sprites,
- and join our Discord channel for more help.
-
- Returns
- -------
- PIL.Image:
- RGB image of (160, 144) pixels
- """
- if not Image:
- logger.error("Cannot generate screen image. Missing dependency \"Pillow\".")
- return None
-
- # NOTE: Might have room for performance improvement
- # It's not possible to use the following, as the byte-order (endianess) isn't supported in Pillow
- # Image.frombytes('RGBA', self.buffer_dims, self.screen_buffer()).show()
- # FIXME: FORMAT IS BGR NOT RGB!!!
- return Image.fromarray(self.screen_ndarray()[:, :, [2, 1, 0]], "RGB")
-
-
Methods
-
-
-def tilemap_position(self)
-
-
-
These coordinates define the offset in the tile map from where the top-left corner of the screen is place. Note
-that the tile map defines 256x256 pixels, but the screen can only show 160x144 pixels. When the offset is closer
-to the right or bottom edge than 160x144 pixels, the screen will wrap around and render from the opposite site
-of the tile map.
Returns the tuple of registers ((SCX, SCY), (WX - 7, WY))
-
-
-
-Expand source code
-
-
def tilemap_position(self):
- """
- These coordinates define the offset in the tile map from where the top-left corner of the screen is place. Note
- that the tile map defines 256x256 pixels, but the screen can only show 160x144 pixels. When the offset is closer
- to the right or bottom edge than 160x144 pixels, the screen will wrap around and render from the opposite site
- of the tile map.
-
- For more details, see "7.4 Viewport" in the [report](https://github.com/Baekalfen/PyBoy/raw/master/extras/PyBoy.pdf),
- or the Pan Docs under [LCD Position and Scrolling](http://bgb.bircd.org/pandocs.htm#lcdpositionandscrolling).
-
- Returns
- -------
- tuple:
- Returns the tuple of registers ((SCX, SCY), (WX - 7, WY))
- """
- return (self.mb.lcd.getviewport(), self.mb.lcd.getwindowpos())
-
-
-
-def tilemap_position_list(self)
-
-
-
This function provides the screen (SCX, SCY) and window (WX. WY) position for each horizontal line in the
-screen buffer. These parameters are often used for visual effects, and some games will reset the registers at
-the end of each call to PyBoy.tick(). For such games, Screen.tilemap_position() becomes useless.
Nested list of SCX, SCY, WX and WY for each scanline (144x4). Returns (0, 0, 0, 0) when LCD is off.
-
-
-
-Expand source code
-
-
def tilemap_position_list(self):
- """
- This function provides the screen (SCX, SCY) and window (WX. WY) position for each horizontal line in the
- screen buffer. These parameters are often used for visual effects, and some games will reset the registers at
- the end of each call to `pyboy.PyBoy.tick()`. For such games, `Screen.tilemap_position` becomes useless.
-
- See `Screen.tilemap_position` for more information.
-
- Returns
- -------
- list:
- Nested list of SCX, SCY, WX and WY for each scanline (144x4). Returns (0, 0, 0, 0) when LCD is off.
- """
- if self.mb.lcd._LCDC.lcd_enable:
- return [[line[0], line[1], line[2], line[3]] for line in self.mb.lcd.renderer._scanlineparameters]
- else:
- return [[0, 0, 0, 0] for line in range(144)]
-
-
-
-def raw_screen_buffer(self)
-
-
-
Provides a raw, unfiltered bytes object with the data from the screen. Check
-Screen.raw_screen_buffer_format() to see which dataformat is used. The returned type and dataformat are
-subject to change.
def raw_screen_buffer(self):
- """
- Provides a raw, unfiltered `bytes` object with the data from the screen. Check
- `Screen.raw_screen_buffer_format` to see which dataformat is used. The returned type and dataformat are
- subject to change.
-
- Use this, only if you need to bypass the overhead of `Screen.screen_image` or `Screen.screen_ndarray`.
-
- Returns
- -------
- bytes:
- 92160 bytes of screen data in a `bytes` object.
- """
- return self.mb.lcd.renderer._screenbuffer_raw.tobytes()
-
-
-
-def raw_screen_buffer_dims(self)
-
-
-
Returns the dimensions of the raw screen buffer.
-
Returns
-
-
tuple:
-
A two-tuple of the buffer dimensions. E.g. (160, 144).
-
-
-
-Expand source code
-
-
def raw_screen_buffer_dims(self):
- """
- Returns the dimensions of the raw screen buffer.
-
- Returns
- -------
- tuple:
- A two-tuple of the buffer dimensions. E.g. (160, 144).
- """
- return self.mb.lcd.renderer.buffer_dims
-
-
-
-def raw_screen_buffer_format(self)
-
-
-
Returns the color format of the raw screen buffer.
-
Returns
-
-
str:
-
Color format of the raw screen buffer. E.g. 'RGB'.
-
-
-
-Expand source code
-
-
def raw_screen_buffer_format(self):
- """
- Returns the color format of the raw screen buffer.
-
- Returns
- -------
- str:
- Color format of the raw screen buffer. E.g. 'RGB'.
- """
- return self.mb.lcd.renderer.color_format
-
-
-
-def screen_ndarray(self)
-
-
-
Provides the screen data in NumPy format. The dataformat is always RGB.
-
Returns
-
-
numpy.ndarray:
-
Screendata in ndarray of bytes with shape (160, 144, 3)
-
-
-
-Expand source code
-
-
def screen_ndarray(self):
- """
- Provides the screen data in NumPy format. The dataformat is always RGB.
-
- Returns
- -------
- numpy.ndarray:
- Screendata in `ndarray` of bytes with shape (160, 144, 3)
- """
- return np.frombuffer(self.mb.lcd.renderer._screenbuffer_raw, dtype=np.uint8).reshape(ROWS, COLS, 4)[:, :, 1:]
-
-
-
-def screen_image(self)
-
-
-
Generates a PIL Image from the screen buffer.
-
Convenient for screen captures, but might be a bottleneck, if you use it to train a neural network. In which
-case, read up on the pyboy.botsupport features, Pan Docs on tiles/sprites,
-and join our Discord channel for more help.
-
Returns
-
-
PIL.Image:
-
RGB image of (160, 144) pixels
-
-
-
-Expand source code
-
-
def screen_image(self):
- """
- Generates a PIL Image from the screen buffer.
-
- Convenient for screen captures, but might be a bottleneck, if you use it to train a neural network. In which
- case, read up on the `pyboy.botsupport` features, [Pan Docs](http://bgb.bircd.org/pandocs.htm) on tiles/sprites,
- and join our Discord channel for more help.
-
- Returns
- -------
- PIL.Image:
- RGB image of (160, 144) pixels
- """
- if not Image:
- logger.error("Cannot generate screen image. Missing dependency \"Pillow\".")
- return None
-
- # NOTE: Might have room for performance improvement
- # It's not possible to use the following, as the byte-order (endianess) isn't supported in Pillow
- # Image.frombytes('RGBA', self.buffer_dims, self.screen_buffer()).show()
- # FIXME: FORMAT IS BGR NOT RGB!!!
- return Image.fromarray(self.screen_ndarray()[:, :, [2, 1, 0]], "RGB")
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/index.html b/docs/index.html
index b7c45692f..dbcc86013 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -32,31 +32,31 @@
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 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 the files in examples/ for a crude
"bots", which interact with the game.
-
Only the gamerom_file argument is required.
+
Only the gamerom argument is required.
+
Example:
+
>>> pyboy = PyBoy('game_rom.gb')
+>>> for _ in range(60): # Use 'while True:' for infinite
+... pyboy.tick()
+True...
+>>> pyboy.stop()
+
+
Args
-
gamerom_file : str
+
gamerom : str
Filepath to a game-ROM for Game Boy or Game Boy Color.
Kwargs
-
bootrom_file (str): Filepath to a boot-ROM to use. If unsure, specify None.
-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.
-cgb_color_palette (list of tuple): Specify the color palette to use for rendering in CGB-mode for non-color games.
-
Other keyword arguments may exist for plugins that are not listed here. They can be viewed with the
-parser_arguments() method in the pyboy.plugins.manager module, or by running pyboy –help in the terminal.
+
+
window (str): "SDL2", "OpenGL", or "null"
+
scale (int): Window scale factor. Doesn't apply to API.
+
symbols (str): Filepath to a .sym file to use. If unsure, specify None.
+
bootrom (str): Filepath to a boot-ROM to use. If unsure, specify None.
+
sound (bool): Enable sound emulation and output.
+
sound_emulated (bool): Enable sound emulation without any output. Used for compatibility.
+
cgb (bool): Forcing Game Boy Color mode.
+
color_palette (tuple): Specify the color palette to use for rendering.
+
cgb_color_palette (list of tuple): Specify the color palette to use for rendering in CGB-mode for non-color games.
+
+
Other keyword arguments may exist for plugins that are not listed here. They can be viewed by running pyboy --help in the terminal.
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 the files in `examples/` for a crude
"bots", which interact with the game.
- Only the `gamerom_file` argument is required.
+ Only the `gamerom` argument is required.
+
+ Example:
+ ```python
+ >>> pyboy = PyBoy('game_rom.gb')
+ >>> for _ in range(60): # Use 'while True:' for infinite
+ ... pyboy.tick()
+ True...
+ >>> pyboy.stop()
+
+ ```
Args:
- gamerom_file (str): Filepath to a game-ROM for Game Boy or Game Boy Color.
+ gamerom (str): Filepath to a game-ROM for Game Boy or Game Boy Color.
Kwargs:
- bootrom_file (str): Filepath to a boot-ROM to use. If unsure, specify `None`.
- 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.
- cgb_color_palette (list of tuple): Specify the color palette to use for rendering in CGB-mode for non-color games.
-
- Other keyword arguments may exist for plugins that are not listed here. They can be viewed with the
- `parser_arguments()` method in the pyboy.plugins.manager module, or by running pyboy --help in the terminal.
+ * window (str): "SDL2", "OpenGL", or "null"
+ * scale (int): Window scale factor. Doesn't apply to API.
+ * symbols (str): Filepath to a .sym file to use. If unsure, specify `None`.
+ * bootrom (str): Filepath to a boot-ROM to use. If unsure, specify `None`.
+ * sound (bool): Enable sound emulation and output.
+ * sound_emulated (bool): Enable sound emulation without any output. Used for compatibility.
+ * cgb (bool): Forcing Game Boy Color mode.
+ * color_palette (tuple): Specify the color palette to use for rendering.
+ * cgb_color_palette (list of tuple): Specify the color palette to use for rendering in CGB-mode for non-color games.
+
+ Other keyword arguments may exist for plugins that are not listed here. They can be viewed by running `pyboy --help` in the terminal.
"""
self.initialized = False
+ if "bootrom_file" in kwargs:
+ logger.error(
+ "Deprecated use of 'bootrom_file'. Use 'bootrom' keyword argument instead. https://github.com/Baekalfen/PyBoy/wiki/Migrating-from-v1.x.x-to-v2.0.0"
+ )
+ bootrom = kwargs.pop("bootrom_file")
+
+ if "window_type" in kwargs:
+ logger.error(
+ "Deprecated use of 'window_type'. Use 'window' keyword argument instead. https://github.com/Baekalfen/PyBoy/wiki/Migrating-from-v1.x.x-to-v2.0.0"
+ )
+ window = kwargs.pop("window_type")
+
+ if window not in ["SDL2", "OpenGL", "null", "headless", "dummy"]:
+ raise KeyError(f'Unknown window type: {window}. Use "SDL2", "OpenGL", or "null"')
+
+ kwargs["window"] = window
+ kwargs["scale"] = scale
+ randomize = kwargs.pop("randomize", False) # Undocumented feature
+
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
+ log_level(kwargs.pop("log_level"))
+
+ if not os.path.isfile(gamerom):
+ raise FileNotFoundError(f"ROM file {gamerom} was not found!")
+ self.gamerom = gamerom
+
+ self.rom_symbols = {}
+ if symbols is not None:
+ if not os.path.isfile(symbols):
+ raise FileNotFoundError(f"Symbols file {symbols} was not found!")
+ self.symbols_file = symbols
+ self._load_symbols()
self.mb = Motherboard(
- gamerom_file,
- bootrom_file or kwargs.get("bootrom"), # Our current way to provide cli arguments is broken
+ gamerom,
+ bootrom,
kwargs["color_palette"],
kwargs["cgb_color_palette"],
- disable_renderer,
sound,
sound_emulated,
cgb,
randomize=randomize,
)
+ # Validate all kwargs
+ plugin_manager_keywords = []
+ for x in parser_arguments():
+ if not x:
+ continue
+ plugin_manager_keywords.extend(z.strip("-").replace("-", "_") for y in x for z in y[:-1])
+
+ for k, v in kwargs.items():
+ if k not in defaults and k not in plugin_manager_keywords:
+ logger.error("Unknown keyword argument: %s", k)
+ raise KeyError(f"Unknown keyword argument: {k}")
+
# Performance measures
self.avg_pre = 0
self.avg_tick = 0
@@ -169,39 +232,207 @@
Kwargs
self.set_emulation_speed(1)
self.paused = False
self.events = []
- self.old_events = []
+ self.queued_input = []
self.quitting = False
self.stopped = False
self.window_title = "PyBoy"
###################
- # Plugins
+ # API attributes
+ self.screen = Screen(self.mb)
+ """
+ Use this method to get a `pyboy.api.screen.Screen` object. This can be used to get the screen buffer in
+ a variety of formats.
- self.plugin_manager = PluginManager(self, self.mb, kwargs)
- self.initialized = True
+ It's also here you can find the screen position (SCX, SCY, WX, WY) for each scan line in the screen buffer. See
+ `pyboy.api.screen.Screen.tilemap_position_list` for more information.
+
+ Example:
+ ```python
+ >>> pyboy.screen.image.show()
+ >>> pyboy.screen.ndarray.shape
+ (144, 160, 4)
+ >>> pyboy.screen.raw_buffer_format
+ 'RGBA'
- def tick(self):
+ ```
+
+ Returns
+ -------
+ `pyboy.api.screen.Screen`:
+ A Screen object with helper functions for reading the screen buffer.
+ """
+ self.memory = PyBoyMemoryView(self.mb)
"""
- Progresses the emulator ahead by one frame.
+ Provides a `pyboy.PyBoyMemoryView` object for reading and writing the memory space of the Game Boy.
- 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.
+ For a more comprehensive description, see the `pyboy.PyBoyMemoryView` class.
+
+ Example:
+ ```python
+ >>> pyboy.memory[0x0000:0x0010] # Read 16 bytes from ROM bank 0
+ [49, 254, 255, 33, 0, 128, 175, 34, 124, 254, 160, 32, 249, 6, 48, 33]
+ >>> pyboy.memory[1, 0x2000] = 12 # Override address 0x2000 from ROM bank 1 with the value 12
+ >>> pyboy.memory[0xC000] = 1 # Write to address 0xC000 with value 1
+ ```
+
+ """
+
+ self.memory_scanner = MemoryScanner(self)
+ """
+ Provides a `pyboy.api.memory_scanner.MemoryScanner` object for locating addresses of interest in the memory space
+ of the Game Boy. This might require some trial and error. Values can be represented in memory in surprising ways.
_Open an issue on GitHub if you need finer control, and we will take a look at it._
+
+ Example:
+ ```python
+ >>> current_score = 4 # You write current score in game
+ >>> pyboy.memory_scanner.scan_memory(current_score, start_addr=0xC000, end_addr=0xDFFF)
+ []
+ >>> for _ in range(175):
+ ... pyboy.tick(1, True) # Progress the game to change score
+ True...
+ >>> current_score = 8 # You write the new score in game
+ >>> from pyboy.api.memory_scanner import DynamicComparisonType
+ >>> addresses = pyboy.memory_scanner.rescan_memory(current_score, DynamicComparisonType.MATCH)
+ >>> print(addresses) # If repeated enough, only one address will remain
+ []
+
+ ```
+ """
+
+ self.tilemap_background = TileMap(self, self.mb, "BACKGROUND")
+ """
+ The Game Boy uses two tile maps at the same time to draw graphics on the screen. This method will provide one
+ for the _background_ tiles. The game chooses whether it wants to use the low or the high tilemap.
+
+ Read more details about it, in the [Pan Docs](https://gbdev.io/pandocs/Tile_Maps.html).
+
+ Example:
+ ```
+ >>> pyboy.tilemap_background[8,8]
+ 1
+ >>> pyboy.tilemap_background[7:12,8]
+ [0, 1, 0, 1, 0]
+ >>> pyboy.tilemap_background[7:12,8:11]
+ [[0, 1, 0, 1, 0], [0, 2, 3, 4, 5], [0, 0, 6, 0, 0]]
+
+ ```
+
+ Returns
+ -------
+ `pyboy.api.tilemap.TileMap`:
+ A TileMap object for the tile map.
+ """
+
+ self.tilemap_window = TileMap(self, self.mb, "WINDOW")
+ """
+ The Game Boy uses two tile maps at the same time to draw graphics on the screen. This method will provide one
+ for the _window_ tiles. The game chooses whether it wants to use the low or the high tilemap.
+
+ Read more details about it, in the [Pan Docs](https://gbdev.io/pandocs/Tile_Maps.html).
+
+ Example:
+ ```
+ >>> pyboy.tilemap_window[8,8]
+ 1
+ >>> pyboy.tilemap_window[7:12,8]
+ [0, 1, 0, 1, 0]
+ >>> pyboy.tilemap_window[7:12,8:11]
+ [[0, 1, 0, 1, 0], [0, 2, 3, 4, 5], [0, 0, 6, 0, 0]]
+
+ ```
+
+ Returns
+ -------
+ `pyboy.api.tilemap.TileMap`:
+ A TileMap object for the tile map.
+ """
+
+ self.cartridge_title = self.mb.cartridge.gamename
+ """
+ The title stored on the currently loaded cartridge ROM. The title is all upper-case ASCII and may
+ have been truncated to 11 characters.
+
+ Example:
+ ```python
+ >>> pyboy.cartridge_title # Title of PyBoy's default ROM
+ 'DEFAULT-ROM'
+
+ ```
+
+ Returns
+ -------
+ str :
+ Game title
+ """
+
+ self._hooks = {}
+
+ self._plugin_manager = PluginManager(self, self.mb, kwargs)
+ """
+ Returns
+ -------
+ `pyboy.plugins.manager.PluginManager`:
+ Object for handling plugins in PyBoy
+ """
+
+ self.game_wrapper = self._plugin_manager.gamewrapper()
+ """
+ Provides an instance of a game-specific or generic wrapper. The game is detected by the cartridge's hard-coded
+ game title (see `pyboy.PyBoy.cartridge_title`).
+
+ If a game-specific wrapper is not found, a generic wrapper will be returned.
+
+ To get more information, find the wrapper for your game in `pyboy.plugins`.
+
+ Example:
+ ```python
+ >>> pyboy.game_wrapper.start_game()
+ >>> pyboy.game_wrapper.reset_game()
+
+ ```
+
+ Returns
+ -------
+ `pyboy.plugins.base_plugin.PyBoyGameWrapper`:
+ A game-specific wrapper object.
"""
+
+ self.initialized = True
+
+ def _tick(self, render):
if self.stopped:
- return True
+ return False
t_start = time.perf_counter_ns()
self._handle_events(self.events)
t_pre = time.perf_counter_ns()
if not self.paused:
- if self.mb.tick():
- # breakpoint reached
- self.plugin_manager.handle_breakpoint()
- else:
- self.frame_count += 1
+ self.__rendering(render)
+ # Reenter mb.tick until we eventually get a clean exit without breakpoints
+ while self.mb.tick():
+ # Breakpoint reached
+ # NOTE: Potentially reinject breakpoint that we have now stepped passed
+ self.mb.breakpoint_reinject()
+
+ # NOTE: PC has not been incremented when hitting breakpoint!
+ breakpoint_index = self.mb.breakpoint_reached()
+ if breakpoint_index != -1:
+ self.mb.breakpoint_remove(breakpoint_index)
+ self.mb.breakpoint_singlestep_latch = 0
+
+ if not self._handle_hooks():
+ self._plugin_manager.handle_breakpoint()
+ else:
+ if self.mb.breakpoint_singlestep_latch:
+ if not self._handle_hooks():
+ self._plugin_manager.handle_breakpoint()
+ # Keep singlestepping on, if that's what we're doing
+ self.mb.breakpoint_singlestep = self.mb.breakpoint_singlestep_latch
+
+ self.frame_count += 1
t_tick = time.perf_counter_ns()
self._post_tick()
t_post = time.perf_counter_ns()
@@ -215,25 +446,77 @@
Kwargs
nsecs = t_post - t_tick
self.avg_post = 0.9 * self.avg_post + (0.1*nsecs/1_000_000_000)
- return self.quitting
+ return not self.quitting
+
+ def tick(self, count=1, render=True):
+ """
+ Progresses the emulator ahead by `count` frame(s).
+
+ To run the emulator in real-time, it will need to process 60 frames a second (for example in a while-loop).
+ This function will block for roughly 16,67ms per frame, to not run faster than real-time, unless you specify
+ otherwise with the `PyBoy.set_emulation_speed` method.
+
+ If you need finer control than 1 frame, have a look at `PyBoy.hook_register` to inject code at a specific point
+ in the game.
+
+ Setting `render` to `True` will make PyBoy render the screen for *the last frame* of this tick. This can be seen
+ as a type of "frameskipping" optimization.
+
+ For AI training, it's adviced to use as high a count as practical, as it will otherwise reduce performance
+ substantially. While setting `render` to `False`, you can still access the `PyBoy.game_area` to get a simpler
+ representation of the game.
+
+ If `render` was enabled, use `pyboy.api.screen.Screen` to get a NumPy buffer or raw memory buffer.
+
+ Example:
+ ```python
+ >>> pyboy.tick() # Progress 1 frame with rendering
+ True
+ >>> pyboy.tick(1) # Progress 1 frame with rendering
+ True
+ >>> pyboy.tick(60, False) # Progress 60 frames *without* rendering
+ True
+ >>> pyboy.tick(60, True) # Progress 60 frames and render *only the last frame*
+ True
+ >>> for _ in range(60): # Progress 60 frames and render every frame
+ ... if not pyboy.tick(1, True):
+ ... break
+ >>>
+ ```
+
+ Args:
+ count (int): Number of ticks to process
+ render (bool): Whether to render an image for this tick
+ Returns
+ -------
+ (True or False):
+ False if emulation has ended otherwise True
+ """
+
+ running = False
+ while count != 0:
+ _render = render and count == 1 # Only render on last tick to improve performance
+ running = self._tick(_render)
+ count -= 1
+ return running
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)
+ events = self._plugin_manager.handle_events(events)
for event in events:
if event == WindowEvent.QUIT:
self.quitting = 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.debug("Speed limit: %s" % self.target_emulationspeed)
+ logger.debug("Speed limit: %d", self.target_emulationspeed)
elif event == WindowEvent.STATE_SAVE:
- with open(self.gamerom_file + ".state", "wb") as f:
+ with open(self.gamerom + ".state", "wb") as f:
self.mb.save_state(IntIOWrapper(f))
elif event == WindowEvent.STATE_LOAD:
- state_path = self.gamerom_file + ".state"
+ state_path = self.gamerom + ".state"
if not os.path.isfile(state_path):
- logger.error(f"State file not found: {state_path}")
+ logger.error("State file not found: %s", state_path)
continue
with open(state_path, "rb") as f:
self.mb.load_state(IntIOWrapper(f))
@@ -249,7 +532,7 @@
self._update_window_title()
def _post_tick(self):
+ # Fix buggy PIL. They will copy our image buffer and destroy the
+ # reference on some user operations like .save().
+ if not self.screen.image.readonly:
+ self.screen._set_image()
+
if self.frame_count % 60 == 0:
self._update_window_title()
- self.plugin_manager.post_tick()
- self.plugin_manager.frame_limiter(self.target_emulationspeed)
+ 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.old_events = self.events
self.events = []
+ while self.queued_input and self.frame_count == self.queued_input[0][0]:
+ _, _event = heapq.heappop(self.queued_input)
+ self.events.append(WindowEvent(_event))
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%s" % (round(SPF / avg_emu) if avg_emu > 0 else "INF")
+ self.window_title = f"CPU/frame: {(self.avg_pre + self.avg_tick) / SPF * 100:0.2f}%"
+ self.window_title += f' Emulation: x{(round(SPF / avg_emu) if avg_emu > 0 else "INF")}'
if self.paused:
self.window_title += "[PAUSED]"
- self.window_title += self.plugin_manager.window_title()
- self.plugin_manager._set_title()
+ self.window_title += self._plugin_manager.window_title()
+ self._plugin_manager._set_title()
def __del__(self):
self.stop(save=False)
@@ -302,6 +592,13 @@
Kwargs
"""
Gently stops the emulator and all sub-modules.
+ Example:
+ ```python
+ >>> pyboy.stop() # Stop emulator and save game progress (cartridge RAM)
+ >>> pyboy.stop(False) # Stop emulator and discard game progress (cartridge RAM)
+
+ ```
+
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.
@@ -310,7 +607,7 @@
Kwargs
logger.info("###########################")
logger.info("# Emulator is turning off #")
logger.info("###########################")
- self.plugin_manager.stop()
+ self._plugin_manager.stop()
self.mb.stop(save)
self.stopped = True
@@ -318,147 +615,172 @@
Kwargs
# 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 openai_gym(self, observation_type="tiles", action_type="press", simultaneous_actions=False, **kwargs):
+ def button(self, input, delay=1):
"""
- For Reinforcement learning, it is often easier to use the standard gym environment. This method will provide one.
- This function requires PyBoy to implement a Game Wrapper for the loaded ROM. You can find the supported games in pyboy.plugins.
- Additional kwargs are passed to the start_game method of the game_wrapper.
+ Send input to PyBoy in the form of "a", "b", "start", "select", "left", "right", "up" and "down".
+
+ The button will automatically be released at the following call to `PyBoy.tick`.
+
+ Example:
+ ```python
+ >>> pyboy.button('a') # Press button 'a' and release after `pyboy.tick()`
+ >>> pyboy.tick() # Button 'a' pressed
+ True
+ >>> pyboy.tick() # Button 'a' released
+ True
+ >>> pyboy.button('a', 3) # Press button 'a' and release after 3 `pyboy.tick()`
+ >>> pyboy.tick() # Button 'a' pressed
+ True
+ >>> pyboy.tick() # Button 'a' still pressed
+ True
+ >>> pyboy.tick() # Button 'a' still pressed
+ True
+ >>> pyboy.tick() # Button 'a' released
+ True
+ ```
Args:
- observation_type (str): Define what the agent will be able to see:
- * `"raw"`: Gives the raw pixels color
- * `"tiles"`: Gives the id of the sprites in 8x8 pixel zones of the game_area defined by the game_wrapper.
- * `"compressed"`: Gives a more detailled but heavier representation than `"minimal"`.
- * `"minimal"`: Gives a minimal representation defined by the game_wrapper (recommended).
-
- action_type (str): Define how the agent will interact with button inputs
- * `"press"`: The agent will only press inputs for 1 frame an then release it.
- * `"toggle"`: The agent will toggle inputs, first time it press and second time it release.
- * `"all"`: The agent have access to all inputs, press and release are separated.
-
- simultaneous_actions (bool): Allow to inject multiple input at once. This dramatically increases the action_space: \\(n \\rightarrow 2^n\\)
-
- Returns
- -------
- `pyboy.openai_gym.PyBoyGymEnv`:
- A Gym environment based on the `Pyboy` object.
+ input (str): button to press
+ delay (int, optional): Number of frames to delay the release. Defaults to 1
"""
- if gym_enabled:
- return PyBoyGymEnv(self, observation_type, action_type, simultaneous_actions, **kwargs)
+ input = input.lower()
+ if input == "left":
+ self.send_input(WindowEvent.PRESS_ARROW_LEFT)
+ heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_ARROW_LEFT))
+ elif input == "right":
+ self.send_input(WindowEvent.PRESS_ARROW_RIGHT)
+ heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_ARROW_RIGHT))
+ elif input == "up":
+ self.send_input(WindowEvent.PRESS_ARROW_UP)
+ heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_ARROW_UP))
+ elif input == "down":
+ self.send_input(WindowEvent.PRESS_ARROW_DOWN)
+ heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_ARROW_DOWN))
+ elif input == "a":
+ self.send_input(WindowEvent.PRESS_BUTTON_A)
+ heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_BUTTON_A))
+ elif input == "b":
+ self.send_input(WindowEvent.PRESS_BUTTON_B)
+ heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_BUTTON_B))
+ elif input == "start":
+ self.send_input(WindowEvent.PRESS_BUTTON_START)
+ heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_BUTTON_START))
+ elif input == "select":
+ self.send_input(WindowEvent.PRESS_BUTTON_SELECT)
+ heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_BUTTON_SELECT))
else:
- logger.error(f"{__name__}: Missing dependency \"gym\". ")
- return None
-
- 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)
+ raise Exception("Unrecognized input:", input)
- def set_memory_value(self, addr, value):
+ def button_press(self, input):
"""
- Write one byte to a given memory address of the Game Boy's current memory state.
+ Send input to PyBoy in the form of "a", "b", "start", "select", "left", "right", "up" and "down".
- This will not directly give you access to all switchable memory banks.
+ The button will remain press until explicitly released with `PyBoy.button_release` or `PyBoy.send_input`.
- __NOTE:__ This function will not let you change ROM addresses (0x0000 to 0x8000). If you write to these
- addresses, it will send commands to the "Memory Bank Controller" (MBC) of the virtual cartridge. You can read
- about the MBC at [Pan Docs](http://bgb.bircd.org/pandocs.htm).
+ Example:
+ ```python
+ >>> pyboy.button_press('a') # Press button 'a' and keep pressed after `PyBoy.tick()`
+ >>> pyboy.tick() # Button 'a' pressed
+ True
+ >>> pyboy.tick() # Button 'a' still pressed
+ True
+ >>> pyboy.button_release('a') # Release button 'a' on next call to `PyBoy.tick()`
+ >>> pyboy.tick() # Button 'a' released
+ True
- If you need to change ROM values, see `pyboy.PyBoy.override_memory_value`.
+ ```
Args:
- addr (int): Address to write the byte
- value (int): A byte of data
+ input (str): button to press
"""
- self.mb.setitem(addr, value)
+ input = input.lower()
+
+ if input == "left":
+ self.send_input(WindowEvent.PRESS_ARROW_LEFT)
+ elif input == "right":
+ self.send_input(WindowEvent.PRESS_ARROW_RIGHT)
+ elif input == "up":
+ self.send_input(WindowEvent.PRESS_ARROW_UP)
+ elif input == "down":
+ self.send_input(WindowEvent.PRESS_ARROW_DOWN)
+ elif input == "a":
+ self.send_input(WindowEvent.PRESS_BUTTON_A)
+ elif input == "b":
+ self.send_input(WindowEvent.PRESS_BUTTON_B)
+ elif input == "start":
+ self.send_input(WindowEvent.PRESS_BUTTON_START)
+ elif input == "select":
+ self.send_input(WindowEvent.PRESS_BUTTON_SELECT)
+ else:
+ raise Exception("Unrecognized input")
- def override_memory_value(self, rom_bank, addr, value):
+ def button_release(self, input):
"""
- Override one byte at a given memory address of the Game Boy's ROM.
+ Send input to PyBoy in the form of "a", "b", "start", "select", "left", "right", "up" and "down".
- This will let you override data in the ROM at any given bank. This is the memory allocated at 0x0000 to 0x8000, where 0x4000 to 0x8000 can be changed from the MBC.
+ This will release a button after a call to `PyBoy.button_press` or `PyBoy.send_input`.
- __NOTE__: Any changes here are not saved or loaded to game states! Use this function with caution and reapply
- any overrides when reloading the ROM.
+ Example:
+ ```python
+ >>> pyboy.button_press('a') # Press button 'a' and keep pressed after `PyBoy.tick()`
+ >>> pyboy.tick() # Button 'a' pressed
+ True
+ >>> pyboy.tick() # Button 'a' still pressed
+ True
+ >>> pyboy.button_release('a') # Release button 'a' on next call to `PyBoy.tick()`
+ >>> pyboy.tick() # Button 'a' released
+ True
- If you need to change a RAM address, see `pyboy.PyBoy.set_memory_value`.
+ ```
Args:
- rom_bank (int): ROM bank to do the overwrite in
- addr (int): Address to write the byte inside the ROM bank
- value (int): A byte of data
+ input (str): button to release
"""
- # TODO: If you change a RAM value outside of the ROM banks above, the memory value will stay the same no matter
- # what the game writes to the address. This can be used so freeze the value for health, cash etc.
- self.mb.cartridge.overrideitem(rom_bank, addr, value)
+ input = input.lower()
+ if input == "left":
+ self.send_input(WindowEvent.RELEASE_ARROW_LEFT)
+ elif input == "right":
+ self.send_input(WindowEvent.RELEASE_ARROW_RIGHT)
+ elif input == "up":
+ self.send_input(WindowEvent.RELEASE_ARROW_UP)
+ elif input == "down":
+ self.send_input(WindowEvent.RELEASE_ARROW_DOWN)
+ elif input == "a":
+ self.send_input(WindowEvent.RELEASE_BUTTON_A)
+ elif input == "b":
+ self.send_input(WindowEvent.RELEASE_BUTTON_B)
+ elif input == "start":
+ self.send_input(WindowEvent.RELEASE_BUTTON_START)
+ elif input == "select":
+ self.send_input(WindowEvent.RELEASE_BUTTON_SELECT)
+ else:
+ raise Exception("Unrecognized input")
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.
+ Send a single input to control the emulator. This is both Game Boy buttons and emulator controls. See
+ `pyboy.utils.WindowEvent` for which events to send.
- Args:
- event (pyboy.WindowEvent): The event to send
- """
- self.events.append(WindowEvent(event))
+ Consider using `PyBoy.button` instead for easier access.
- def get_input(
- self,
- ignore=(
- WindowEvent.PASS, WindowEvent._INTERNAL_TOGGLE_DEBUG, WindowEvent._INTERNAL_RENDERER_FLUSH,
- WindowEvent._INTERNAL_MOUSE, WindowEvent._INTERNAL_MARK_TILE
- )
- ):
- """
- Get current inputs except the events specified in "ignore" tuple.
- This is both Game Boy buttons and emulator controls.
+ Example:
+ ```python
+ >>> from pyboy.utils import WindowEvent
+ >>> pyboy.send_input(WindowEvent.PRESS_BUTTON_A) # Press button 'a' and keep pressed after `PyBoy.tick()`
+ >>> pyboy.tick() # Button 'a' pressed
+ True
+ >>> pyboy.tick() # Button 'a' still pressed
+ True
+ >>> pyboy.send_input(WindowEvent.RELEASE_BUTTON_A) # Release button 'a' on next call to `PyBoy.tick()`
+ >>> pyboy.tick() # Button 'a' released
+ True
- See `pyboy.WindowEvent` for which events to get.
+ ```
Args:
- ignore (tuple): Events this function should ignore
-
- Returns
- -------
- list:
- List of the `pyboy.utils.WindowEvent`s processed for the last call to `pyboy.PyBoy.tick`
+ event (pyboy.WindowEvent): The event to send
"""
- return [x for x in self.old_events if x not in ignore]
+ self.events.append(WindowEvent(event))
def save_state(self, file_like_object):
"""
@@ -468,13 +790,19 @@
Kwargs
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")
+ ```python
+ >>> # Save to file
+ >>> with open("state_file.state", "wb") as f:
+ ... pyboy.save_state(f)
+ >>>
+ >>> # Save to memory
+ >>> import io
+ >>> with io.BytesIO() as f:
+ ... f.seek(0)
+ ... pyboy.save_state(f)
+ 0
- # 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.
@@ -483,6 +811,9 @@
Kwargs
if isinstance(file_like_object, str):
raise Exception("String not allowed. Did you specify a filepath instead of a file-like object?")
+ if file_like_object.__class__.__name__ == "TextIOWrapper":
+ raise Exception("Text file not allowed. Did you specify open(..., 'wb')?")
+
self.mb.save_state(IntIOWrapper(file_like_object))
def load_state(self, file_like_object):
@@ -494,10 +825,12 @@
Kwargs
can load it here.
To load a file, remember to load it as bytes:
-
- # Load file
- file_like_object = open("state_file.state", "rb")
-
+ ```python
+ >>> # Load file
+ >>> with open("state_file.state", "rb") as f:
+ ... pyboy.load_state(f)
+ >>>
+ ```
Args:
file_like_object (io.BufferedIOBase): A file-like object for which to read the emulator state.
@@ -506,24 +839,142 @@
Kwargs
if isinstance(file_like_object, str):
raise Exception("String not allowed. Did you specify a filepath instead of a file-like object?")
+ if file_like_object.__class__.__name__ == "TextIOWrapper":
+ raise Exception("Text file not allowed. Did you specify open(..., 'rb')?")
+
self.mb.load_state(IntIOWrapper(file_like_object))
- def screen_image(self):
+ def game_area_dimensions(self, x, y, width, height, follow_scrolling=True):
+ """
+ If using the generic game wrapper (see `pyboy.PyBoy.game_wrapper`), you can use this to set the section of the
+ tilemaps to extract. This will default to the entire tilemap.
+
+ Example:
+ ```python
+ >>> pyboy.game_wrapper.shape
+ (32, 32)
+ >>> pyboy.game_area_dimensions(2, 2, 10, 18, False)
+ >>> pyboy.game_wrapper.shape
+ (10, 18)
+ ```
+
+ Args:
+ x (int): Offset from top-left corner of the screen
+ y (int): Offset from top-left corner of the screen
+ width (int): Width of game area
+ height (int): Height of game area
+ follow_scrolling (bool): Whether to follow the scrolling of [SCX and SCY](https://gbdev.io/pandocs/Scrolling.html)
+ """
+ self.game_wrapper._set_dimensions(x, y, width, height, follow_scrolling=True)
+
+ def game_area_collision(self):
+ """
+ Some game wrappers define a collision map. Check if your game wrapper has this feature implemented: `pyboy.plugins`.
+
+ The output will be unique for each game wrapper.
+
+ Example:
+ ```python
+ >>> # This example show nothing, but a supported game will
+ >>> pyboy.game_area_collision()
+ array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint32)
+
+ ```
+
+ Returns
+ -------
+ memoryview:
+ Simplified 2-dimensional memoryview of the collision map
+ """
+ return self.game_wrapper.game_area_collision()
+
+ def game_area_mapping(self, mapping, sprite_offset=0):
+ """
+ Define custom mappings for tile identifiers in the game area.
+
+ Example of custom mapping:
+ ```python
+ >>> mapping = [x for x in range(384)] # 1:1 mapping
+ >>> mapping[0] = 0 # Map tile identifier 0 -> 0
+ >>> mapping[1] = 0 # Map tile identifier 1 -> 0
+ >>> mapping[2] = 0 # Map tile identifier 2 -> 0
+ >>> mapping[3] = 0 # Map tile identifier 3 -> 0
+ >>> pyboy.game_area_mapping(mapping, 1000)
+
+ ```
+
+ Some game wrappers will supply mappings as well. See the specific documentation for your game wrapper:
+ `pyboy.plugins`.
+ ```python
+ >>> pyboy.game_area_mapping(pyboy.game_wrapper.mapping_one_to_one, 0)
+
+ ```
+
+ Args:
+ mapping (list or ndarray): list of 384 (DMG) or 768 (CGB) tile mappings. Use `None` to reset to a 1:1 mapping.
+ sprite_offest (int): Optional offset add to tile id for sprites
"""
- Shortcut for `pyboy.botsupport_manager.screen.screen_image`.
- Generates a PIL Image from the screen buffer.
+ if mapping is None:
+ mapping = [x for x in range(768)]
+
+ assert isinstance(sprite_offset, int)
+ assert isinstance(mapping, (np.ndarray, list))
+ assert len(mapping) == 384 or len(mapping) == 768
+
+ self.game_wrapper.game_area_mapping(mapping, sprite_offset)
- Convenient for screen captures, but might be a bottleneck, if you use it to train a neural network. In which
- case, read up on the `pyboy.botsupport` features, [Pan Docs](http://bgb.bircd.org/pandocs.htm) on tiles/sprites,
- and join our Discord channel for more help.
+ def game_area(self):
+ """
+ Use this method to get a matrix of the "game area" of the screen. This view is simplified to be perfect for
+ machine learning applications.
+
+ The layout will vary from game to game. Below is an example from Tetris:
+
+ Example:
+ ```python
+ >>> pyboy.game_area()
+ array([[ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 130, 130, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 130, 130, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47]], dtype=uint32)
+
+ ```
+
+ If you want a "compressed", "minimal" or raw mapping of tiles, you can change the mapping using
+ `pyboy.PyBoy.game_area_mapping`. Either you'll have to supply your own mapping, or you can find one
+ that is built-in with the game wrapper plugin for your game. See `pyboy.PyBoy.game_area_mapping`.
Returns
-------
- PIL.Image:
- RGB image of (160, 144) pixels
+ memoryview:
+ Simplified 2-dimensional memoryview of the screen
"""
- return self.botsupport_manager().screen().screen_image()
+
+ return self.game_wrapper.game_area()
def _serial(self):
"""
@@ -547,366 +998,803 @@
Kwargs
Some window types do not implement a frame-limiter, and will always run at full speed.
+ Example:
+ ```python
+ >>> pyboy.tick() # Delays 16.67ms
+ True
+ >>> pyboy.set_emulation_speed(0) # Disable limit
+ >>> pyboy.tick() # As fast as possible
+ True
+ ```
+
Args:
target_speed (int): Target emulation speed as multiplier of real-time.
"""
+ if self.initialized and self._plugin_manager.window_null_enabled:
+ logger.warning(
+ 'This window type does not support frame-limiting. `pyboy.set_emulation_speed(...)` will have no effect, as it\'s always running at full speed.'
+ )
+
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):
+ def __rendering(self, value):
"""
- 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.
+ Disable or enable rendering
+ """
+ self.mb.lcd.disable_renderer = not value
+ def _is_cpu_stuck(self):
+ return self.mb.cpu.is_stuck
+
+ def _load_symbols(self):
+ gamerom_file_no_ext, rom_ext = os.path.splitext(self.gamerom)
+ for sym_path in [self.symbols_file, gamerom_file_no_ext + ".sym", gamerom_file_no_ext + rom_ext + ".sym"]:
+ if sym_path and os.path.isfile(sym_path):
+ logger.info("Loading symbol file: %s", sym_path)
+ with open(sym_path) as f:
+ for _line in f.readlines():
+ line = _line.strip()
+ if line == "":
+ continue
+ elif line.startswith(";"):
+ continue
+ elif line.startswith("["):
+ # Start of key group
+ # [labels]
+ # [definitions]
+ continue
+
+ try:
+ bank, addr, sym_label = re.split(":| ", line.strip())
+ bank = int(bank, 16)
+ addr = int(addr, 16)
+ if not bank in self.rom_symbols:
+ self.rom_symbols[bank] = {}
+
+ if not addr in self.rom_symbols[bank]:
+ self.rom_symbols[bank][addr] = []
+
+ self.rom_symbols[bank][addr].append(sym_label)
+ except ValueError as ex:
+ logger.warning("Skipping .sym line: %s", line.strip())
+ return self.rom_symbols
+
+ def _lookup_symbol(self, symbol):
+ for bank, addresses in self.rom_symbols.items():
+ for addr, labels in addresses.items():
+ if symbol in labels:
+ return bank, addr
+ raise ValueError("Symbol not found: %s" % symbol)
+
+ def symbol_lookup(self, symbol):
+ """
+ Look up a specific symbol from provided symbols file.
+
+ This can be useful in combination with `PyBoy.memory` or even `PyBoy.hook_register`.
+
+ See `PyBoy.hook_register` for how to load symbol into PyBoy.
+
+ Example:
+ ```python
+ >>> # Directly
+ >>> pyboy.memory[pyboy.symbol_lookup("Tileset")]
+ 0
+ >>> # By bank and address
+ >>> bank, addr = pyboy.symbol_lookup("Tileset")
+ >>> pyboy.memory[bank, addr]
+ 0
+ >>> pyboy.memory[bank, addr:addr+10]
+ [0, 0, 0, 0, 0, 0, 102, 102, 102, 102]
+
+ ```
Returns
-------
- str :
- Game title
+ (int, int):
+ ROM/RAM bank, address
"""
- return self.mb.cartridge.gamename
+ return self._lookup_symbol(symbol)
- def _rendering(self, value):
+ def hook_register(self, bank, addr, callback, context):
"""
- Disable or enable rendering
- """
- self.mb.lcd.disable_renderer = not value
+ Adds a hook into a specific bank and memory address.
+ When the Game Boy executes this address, the provided callback function will be called.
- def _is_cpu_stuck(self):
- return self.mb.cpu.is_stuck
-
-
Methods
-
-
-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.
+ By providing an object as `context`, you can later get access to information inside and outside of the callback.
- 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.
+ Example:
+ ```python
+ >>> context = "Hello from hook"
+ >>> def my_callback(context):
+ ... print(context)
+ >>> pyboy.hook_register(0, 0x100, my_callback, context)
+ >>> pyboy.tick(70)
+ Hello from hook
+ True
- _Open an issue on GitHub if you need finer control, and we will take a look at it._
- """
- if self.stopped:
- return True
-
- t_start = time.perf_counter_ns()
- self._handle_events(self.events)
- t_pre = time.perf_counter_ns()
- if not self.paused:
- if self.mb.tick():
- # breakpoint reached
- self.plugin_manager.handle_breakpoint()
- else:
- self.frame_count += 1
- t_tick = time.perf_counter_ns()
- self._post_tick()
- t_post = time.perf_counter_ns()
+ ```
- nsecs = t_pre - t_start
- self.avg_pre = 0.9 * self.avg_pre + (0.1*nsecs/1_000_000_000)
+ If a symbol file is loaded, this function can also automatically resolve a bank and address from a symbol. To
+ enable this, you'll need to place a `.sym` file next to your ROM, or provide it using:
+ `PyBoy(..., symbols_file="game_rom.gb.sym")`.
- nsecs = t_tick - t_pre
- self.avg_tick = 0.9 * self.avg_tick + (0.1*nsecs/1_000_000_000)
+ Then provide `None` for `bank` and the symbol for `addr` to trigger the automatic lookup.
- nsecs = t_post - t_tick
- self.avg_post = 0.9 * self.avg_post + (0.1*nsecs/1_000_000_000)
+ Example:
+ ```python
+ >>> # Continued example above
+ >>> pyboy.hook_register(None, "Main.move", lambda x: print(x), "Hello from hook2")
+ >>> pyboy.tick(80)
+ Hello from hook2
+ True
- return self.quitting
-
-
-
-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.
- """
- if self.initialized and not self.stopped:
- logger.info("###########################")
- logger.info("# Emulator is turning off #")
- logger.info("###########################")
- self.plugin_manager.stop()
- self.mb.stop(save)
- self.stopped = True
def botsupport_manager(self):
- """
+ Args:
+ bank (int or None): ROM or RAM bank (None for symbol lookup)
+ addr (int or str): Address in the Game Boy's address space (str for symbol lookup)
+ callback (func): A function which takes `context` as argument
+ context (object): Argument to pass to callback when hook is called
+ """
+ if bank is None and isinstance(addr, str):
+ bank, addr = self._lookup_symbol(addr)
- 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)
+ opcode = self.memory[bank, addr]
+ if opcode == 0xDB:
+ raise ValueError("Hook already registered for this bank and address.")
+ self.mb.breakpoint_add(bank, addr)
+ bank_addr_opcode = (bank & 0xFF) << 24 | (addr & 0xFFFF) << 8 | (opcode & 0xFF)
+ self._hooks[bank_addr_opcode] = (callback, context)
+
+ def hook_deregister(self, bank, addr):
+ """
+ Remove a previously registered hook from a specific bank and memory address.
+
+ Example:
+ ```python
+ >>> context = "Hello from hook"
+ >>> def my_callback(context):
+ ... print(context)
+ >>> pyboy.hook_register(0, 0x2000, my_callback, context)
+ >>> pyboy.hook_deregister(0, 0x2000)
+
+ ```
+
+ This function can also deregister a hook based on a symbol. See `PyBoy.hook_register` for details.
+
+ Example:
+ ```python
+ >>> pyboy.hook_register(None, "Main", lambda x: print(x), "Hello from hook")
+ >>> pyboy.hook_deregister(None, "Main")
+
+ ```
+
+ Args:
+ bank (int or None): ROM or RAM bank (None for symbol lookup)
+ addr (int or str): Address in the Game Boy's address space (str for symbol lookup)
+ """
+ if bank is None and isinstance(addr, str):
+ bank, addr = self._lookup_symbol(addr)
+
+ index = self.mb.breakpoint_find(bank, addr)
+ if index == -1:
+ raise ValueError("Breakpoint not found for bank and addr")
+
+ _, _, opcode = self.mb.breakpoints_list[index]
+ self.mb.breakpoint_remove(index)
+ bank_addr_opcode = (bank & 0xFF) << 24 | (addr & 0xFFFF) << 8 | (opcode & 0xFF)
+ self._hooks.pop(bank_addr_opcode)
+
+ def _handle_hooks(self):
+ if _handler := self._hooks.get(self.mb.breakpoint_waiting):
+ (callback, context) = _handler
+ callback(context)
+ return True
+ return False
+
+ def get_sprite(self, sprite_index):
+ """
+ Provides a `pyboy.api.sprite.Sprite` object, which makes the OAM data more presentable. The given index
+ corresponds to index of the sprite in the "Object Attribute Memory" (OAM).
+
+ The Game Boy supports 40 sprites in total. Read more details about it, in the [Pan
+ Docs](http://bgb.bircd.org/pandocs.htm).
+
+ ```python
+ >>> s = pyboy.get_sprite(12)
+ >>> s
+ Sprite [12]: Position: (-8, -16), Shape: (8, 8), Tiles: (Tile: 0), On screen: False
+ >>> s.on_screen
+ False
+ >>> s.tiles
+ [Tile: 0]
+
+ ```
+
+ Args:
+ index (int): Sprite index from 0 to 39.
+ Returns
+ -------
+ `pyboy.api.sprite.Sprite`:
+ Sprite corresponding to the given index.
+ """
+ return Sprite(self.mb, sprite_index)
+
+ def get_sprite_by_tile_identifier(self, tile_identifiers, on_screen=True):
+ """
+ Provided a list of tile identifiers, this function will find all occurrences of sprites using the tile
+ identifiers and return the sprite indexes where each identifier is found. Use the sprite indexes in the
+ `pyboy.PyBoy.get_sprite` function to get a `pyboy.api.sprite.Sprite` object.
+
+ Example:
+ ```python
+ >>> print(pyboy.get_sprite_by_tile_identifier([43, 123]))
+ [[0, 2, 4], []]
+
+ ```
+
+ Meaning, that tile identifier `43` is found at the sprite indexes: 0, 2, and 4, while tile identifier
+ `123` was not found anywhere.
+
+ Args:
+ identifiers (list): List of tile identifiers (int)
+ on_screen (bool): Require that the matched sprite is on screen
+
+ Returns
+ -------
+ list:
+ list of sprite matches for every tile identifier in the input
+ """
+
+ matches = []
+ for i in tile_identifiers:
+ match = []
+ for s in range(constants.SPRITES):
+ sprite = Sprite(self.mb, s)
+ for t in sprite.tiles:
+ if t.tile_identifier == i and (not on_screen or (on_screen and sprite.on_screen)):
+ match.append(s)
+ matches.append(match)
+ return matches
+
+ def get_tile(self, identifier):
+ """
+ The Game Boy can have 384 tiles loaded in memory at once (768 for Game Boy Color). Use this method to get a
+ `pyboy.api.tile.Tile`-object for given identifier.
+
+ The identifier is a PyBoy construct, which unifies two different scopes of indexes in the Game Boy hardware. See
+ the `pyboy.api.tile.Tile` object for more information.
+
+ Example:
+ ```python
+ >>> t = pyboy.get_tile(2)
+ >>> t
+ Tile: 2
+ >>> t.shape
+ (8, 8)
+
+ ```
+
+ Returns
+ -------
+ `pyboy.api.tile.Tile`:
+ A Tile object for the given identifier.
+ """
+ return Tile(self.mb, identifier=identifier)
+
Instance variables
+
+
var screen
+
+
Use this method to get a Screen object. This can be used to get the screen buffer in
+a variety of formats.
+
It's also here you can find the screen position (SCX, SCY, WX, WY) for each scan line in the screen buffer. See
+Screen.tilemap_position_list for more information.
For Reinforcement learning, it is often easier to use the standard gym environment. This method will provide one.
-This function requires PyBoy to implement a Game Wrapper for the loaded ROM. You can find the supported games in pyboy.plugins.
-Additional kwargs are passed to the start_game method of the game_wrapper.
-
Args
+
Provides a PyBoyMemoryView object for reading and writing the memory space of the Game Boy.
+
For a more comprehensive description, see the PyBoyMemoryView class.
+
Example:
+
>>> pyboy.memory[0x0000:0x0010] # Read 16 bytes from ROM bank 0
+[49, 254, 255, 33, 0, 128, 175, 34, 124, 254, 160, 32, 249, 6, 48, 33]
+>>> pyboy.memory[1, 0x2000] = 12 # Override address 0x2000 from ROM bank 1 with the value 12
+>>> pyboy.memory[0xC000] = 1 # Write to address 0xC000 with value 1
+
+
+
var memory_scanner
+
+
Provides a MemoryScanner object for locating addresses of interest in the memory space
+of the Game Boy. This might require some trial and error. Values can be represented in memory in surprising ways.
+
Open an issue on GitHub if you need finer control, and we will take a look at it.
+
Example:
+
>>> current_score = 4 # You write current score in game
+>>> pyboy.memory_scanner.scan_memory(current_score, start_addr=0xC000, end_addr=0xDFFF)
+[]
+>>> for _ in range(175):
+... pyboy.tick(1, True) # Progress the game to change score
+True...
+>>> current_score = 8 # You write the new score in game
+>>> from pyboy.api.memory_scanner import DynamicComparisonType
+>>> addresses = pyboy.memory_scanner.rescan_memory(current_score, DynamicComparisonType.MATCH)
+>>> print(addresses) # If repeated enough, only one address will remain
+[]
+
+
+
+
var tilemap_background
+
+
The Game Boy uses two tile maps at the same time to draw graphics on the screen. This method will provide one
+for the background tiles. The game chooses whether it wants to use the low or the high tilemap.
The Game Boy uses two tile maps at the same time to draw graphics on the screen. This method will provide one
+for the window tiles. The game chooses whether it wants to use the low or the high tilemap.
The title stored on the currently loaded cartridge ROM. The title is all upper-case ASCII and may
+have been truncated to 11 characters.
+
Example:
+
>>> pyboy.cartridge_title # Title of PyBoy's default ROM
+'DEFAULT-ROM'
+
+
+
Returns
-
observation_type : str
-
Define what the agent will be able to see:
+
str :
+
Game title
+
+
+
var game_wrapper
+
+
Provides an instance of a game-specific or generic wrapper. The game is detected by the cartridge's hard-coded
+game title (see PyBoy.cartridge_title).
+
If a game-specific wrapper is not found, a generic wrapper will be returned.
+
To get more information, find the wrapper for your game in pyboy.plugins.
"tiles":
-Gives the id of the sprites in 8x8 pixel zones of the game_area defined by the game_wrapper.
-
"compressed": Gives a more detailled but heavier representation than "minimal".
-
"minimal": Gives a minimal representation defined by the game_wrapper (recommended).
-
+
Methods
-
action_type : str
-
Define how the agent will interact with button inputs
-
-
-
"press": The agent will only press inputs for 1 frame an then release it.
-
"toggle": The agent will toggle inputs, first time it press and second time it release.
-
"all": The agent have access to all inputs, press and release are separated.
-
+
+def tick(self, count=1, render=True)
+
+
+
Progresses the emulator ahead by count frame(s).
+
To run the emulator in real-time, it will need to process 60 frames a second (for example in a while-loop).
+This function will block for roughly 16,67ms per frame, to not run faster than real-time, unless you specify
+otherwise with the PyBoy.set_emulation_speed() method.
+
If you need finer control than 1 frame, have a look at PyBoy.hook_register() to inject code at a specific point
+in the game.
+
Setting render to True will make PyBoy render the screen for the last frame of this tick. This can be seen
+as a type of "frameskipping" optimization.
+
For AI training, it's adviced to use as high a count as practical, as it will otherwise reduce performance
+substantially. While setting render to False, you can still access the PyBoy.game_area() to get a simpler
+representation of the game.
+
If render was enabled, use Screen to get a NumPy buffer or raw memory buffer.
+
Example:
+
>>> pyboy.tick() # Progress 1 frame with rendering
+True
+>>> pyboy.tick(1) # Progress 1 frame with rendering
+True
+>>> pyboy.tick(60, False) # Progress 60 frames *without* rendering
+True
+>>> pyboy.tick(60, True) # Progress 60 frames and render *only the last frame*
+True
+>>> for _ in range(60): # Progress 60 frames and render every frame
+... if not pyboy.tick(1, True):
+... break
+>>>
+
+
Args
-
simultaneous_actions : bool
-
Allow to inject multiple input at once. This dramatically increases the action_space: n \rightarrow 2^n
+
count : int
+
Number of ticks to process
+
render : bool
+
Whether to render an image for this tick
Returns
-
PyBoyGymEnv:
-A Gym environment based on the Pyboy object.
+
(True or False):
+False if emulation has ended otherwise True
def tick(self, count=1, render=True):
"""
- For Reinforcement learning, it is often easier to use the standard gym environment. This method will provide one.
- This function requires PyBoy to implement a Game Wrapper for the loaded ROM. You can find the supported games in pyboy.plugins.
- Additional kwargs are passed to the start_game method of the game_wrapper.
-
- Args:
- observation_type (str): Define what the agent will be able to see:
- * `"raw"`: Gives the raw pixels color
- * `"tiles"`: Gives the id of the sprites in 8x8 pixel zones of the game_area defined by the game_wrapper.
- * `"compressed"`: Gives a more detailled but heavier representation than `"minimal"`.
- * `"minimal"`: Gives a minimal representation defined by the game_wrapper (recommended).
+ Progresses the emulator ahead by `count` frame(s).
- action_type (str): Define how the agent will interact with button inputs
- * `"press"`: The agent will only press inputs for 1 frame an then release it.
- * `"toggle"`: The agent will toggle inputs, first time it press and second time it release.
- * `"all"`: The agent have access to all inputs, press and release are separated.
+ To run the emulator in real-time, it will need to process 60 frames a second (for example in a while-loop).
+ This function will block for roughly 16,67ms per frame, to not run faster than real-time, unless you specify
+ otherwise with the `PyBoy.set_emulation_speed` method.
- simultaneous_actions (bool): Allow to inject multiple input at once. This dramatically increases the action_space: \\(n \\rightarrow 2^n\\)
+ If you need finer control than 1 frame, have a look at `PyBoy.hook_register` to inject code at a specific point
+ in the game.
+
+ Setting `render` to `True` will make PyBoy render the screen for *the last frame* of this tick. This can be seen
+ as a type of "frameskipping" optimization.
+
+ For AI training, it's adviced to use as high a count as practical, as it will otherwise reduce performance
+ substantially. While setting `render` to `False`, you can still access the `PyBoy.game_area` to get a simpler
+ representation of the game.
+
+ If `render` was enabled, use `pyboy.api.screen.Screen` to get a NumPy buffer or raw memory buffer.
+
+ Example:
+ ```python
+ >>> pyboy.tick() # Progress 1 frame with rendering
+ True
+ >>> pyboy.tick(1) # Progress 1 frame with rendering
+ True
+ >>> pyboy.tick(60, False) # Progress 60 frames *without* rendering
+ True
+ >>> pyboy.tick(60, True) # Progress 60 frames and render *only the last frame*
+ True
+ >>> for _ in range(60): # Progress 60 frames and render every frame
+ ... if not pyboy.tick(1, True):
+ ... break
+ >>>
+ ```
+ Args:
+ count (int): Number of ticks to process
+ render (bool): Whether to render an image for this tick
Returns
-------
- `pyboy.openai_gym.PyBoyGymEnv`:
- A Gym environment based on the `Pyboy` object.
+ (True or False):
+ False if emulation has ended otherwise True
"""
- if gym_enabled:
- return PyBoyGymEnv(self, observation_type, action_type, simultaneous_actions, **kwargs)
- else:
- logger.error(f"{__name__}: Missing dependency \"gym\". ")
- return None
+
+ running = False
+ while count != 0:
+ _render = render and count == 1 # Only render on last tick to improve performance
+ running = self._tick(_render)
+ count -= 1
+ return running
-
-def game_wrapper(self)
+
+def stop(self, save=True)
-
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.
>>> pyboy.stop() # Stop emulator and save game progress (cartridge RAM)
+>>> pyboy.stop(False) # Stop emulator and discard game progress (cartridge RAM)
+
+
+
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 game_wrapper(self):
+
def stop(self, save=True):
"""
- 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`).
+ Gently stops the emulator and all sub-modules.
- If the game isn't supported, None will be returned.
+ Example:
+ ```python
+ >>> pyboy.stop() # Stop emulator and save game progress (cartridge RAM)
+ >>> pyboy.stop(False) # Stop emulator and discard game progress (cartridge RAM)
- To get more information, find the wrapper for your game in `pyboy.plugins`.
+ ```
- Returns
- -------
- `pyboy.plugins.base_plugin.PyBoyGameWrapper`:
- A game-specific wrapper object.
+ 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.
"""
- return self.plugin_manager.gamewrapper()
+ if self.initialized and not self.stopped:
+ logger.info("###########################")
+ logger.info("# Emulator is turning off #")
+ logger.info("###########################")
+ self._plugin_manager.stop()
+ self.mb.stop(save)
+ self.stopped = True
-
-def get_memory_value(self, addr)
+
+def button(self, input, delay=1)
-
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
+
Send input to PyBoy in the form of "a", "b", "start", "select", "left", "right", "up" and "down".
+
The button will automatically be released at the following call to PyBoy.tick().
+
Example:
+
>>> pyboy.button('a') # Press button 'a' and release after `pyboy.tick()`
+>>> pyboy.tick() # Button 'a' pressed
+True
+>>> pyboy.tick() # Button 'a' released
+True
+>>> pyboy.button('a', 3) # Press button 'a' and release after 3 `pyboy.tick()`
+>>> pyboy.tick() # Button 'a' pressed
+True
+>>> pyboy.tick() # Button 'a' still pressed
+True
+>>> pyboy.tick() # Button 'a' still pressed
+True
+>>> pyboy.tick() # Button 'a' released
+True
+
+
Args
-
int:
-
An integer with the value of the memory address
+
input : str
+
button to press
+
delay : int, optional
+
Number of frames to delay the release. Defaults to 1
Expand source code
-
def get_memory_value(self, addr):
+
def button(self, input, delay=1):
"""
- 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.
+ Send input to PyBoy in the form of "a", "b", "start", "select", "left", "right", "up" and "down".
+
+ The button will automatically be released at the following call to `PyBoy.tick`.
+
+ Example:
+ ```python
+ >>> pyboy.button('a') # Press button 'a' and release after `pyboy.tick()`
+ >>> pyboy.tick() # Button 'a' pressed
+ True
+ >>> pyboy.tick() # Button 'a' released
+ True
+ >>> pyboy.button('a', 3) # Press button 'a' and release after 3 `pyboy.tick()`
+ >>> pyboy.tick() # Button 'a' pressed
+ True
+ >>> pyboy.tick() # Button 'a' still pressed
+ True
+ >>> pyboy.tick() # Button 'a' still pressed
+ True
+ >>> pyboy.tick() # Button 'a' released
+ True
+ ```
- Returns
- -------
- int:
- An integer with the value of the memory address
+ Args:
+ input (str): button to press
+ delay (int, optional): Number of frames to delay the release. Defaults to 1
"""
- return self.mb.getitem(addr)
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.
-
NOTE: This function will not let you change ROM addresses (0x0000 to 0x8000). If you write to these
-addresses, it will send commands to the "Memory Bank Controller" (MBC) of the virtual cartridge. You can read
-about the MBC at Pan Docs.
>>> pyboy.button_press('a') # Press button 'a' and keep pressed after `PyBoy.tick()`
+>>> pyboy.tick() # Button 'a' pressed
+True
+>>> pyboy.tick() # Button 'a' still pressed
+True
+>>> pyboy.button_release('a') # Release button 'a' on next call to `PyBoy.tick()`
+>>> pyboy.tick() # Button 'a' released
+True
+
+
Args
-
addr : int
-
Address to write the byte
-
value : int
-
A byte of data
+
input : str
+
button to press
Expand source code
-
def set_memory_value(self, addr, value):
+
def button_press(self, input):
"""
- Write one byte to a given memory address of the Game Boy's current memory state.
+ Send input to PyBoy in the form of "a", "b", "start", "select", "left", "right", "up" and "down".
- This will not directly give you access to all switchable memory banks.
+ The button will remain press until explicitly released with `PyBoy.button_release` or `PyBoy.send_input`.
- __NOTE:__ This function will not let you change ROM addresses (0x0000 to 0x8000). If you write to these
- addresses, it will send commands to the "Memory Bank Controller" (MBC) of the virtual cartridge. You can read
- about the MBC at [Pan Docs](http://bgb.bircd.org/pandocs.htm).
+ Example:
+ ```python
+ >>> pyboy.button_press('a') # Press button 'a' and keep pressed after `PyBoy.tick()`
+ >>> pyboy.tick() # Button 'a' pressed
+ True
+ >>> pyboy.tick() # Button 'a' still pressed
+ True
+ >>> pyboy.button_release('a') # Release button 'a' on next call to `PyBoy.tick()`
+ >>> pyboy.tick() # Button 'a' released
+ True
- If you need to change ROM values, see `pyboy.PyBoy.override_memory_value`.
+ ```
Args:
- addr (int): Address to write the byte
- value (int): A byte of data
+ input (str): button to press
"""
- self.mb.setitem(addr, value)
Override one byte at a given memory address of the Game Boy's ROM.
-
This will let you override data in the ROM at any given bank. This is the memory allocated at 0x0000 to 0x8000, where 0x4000 to 0x8000 can be changed from the MBC.
-
NOTE: Any changes here are not saved or loaded to game states! Use this function with caution and reapply
-any overrides when reloading the ROM.
def button_release(self, input):
"""
- Override one byte at a given memory address of the Game Boy's ROM.
+ Send input to PyBoy in the form of "a", "b", "start", "select", "left", "right", "up" and "down".
- This will let you override data in the ROM at any given bank. This is the memory allocated at 0x0000 to 0x8000, where 0x4000 to 0x8000 can be changed from the MBC.
+ This will release a button after a call to `PyBoy.button_press` or `PyBoy.send_input`.
- __NOTE__: Any changes here are not saved or loaded to game states! Use this function with caution and reapply
- any overrides when reloading the ROM.
+ Example:
+ ```python
+ >>> pyboy.button_press('a') # Press button 'a' and keep pressed after `PyBoy.tick()`
+ >>> pyboy.tick() # Button 'a' pressed
+ True
+ >>> pyboy.tick() # Button 'a' still pressed
+ True
+ >>> pyboy.button_release('a') # Release button 'a' on next call to `PyBoy.tick()`
+ >>> pyboy.tick() # Button 'a' released
+ True
- If you need to change a RAM address, see `pyboy.PyBoy.set_memory_value`.
+ ```
Args:
- rom_bank (int): ROM bank to do the overwrite in
- addr (int): Address to write the byte inside the ROM bank
- value (int): A byte of data
+ input (str): button to release
"""
- # TODO: If you change a RAM value outside of the ROM banks above, the memory value will stay the same no matter
- # what the game writes to the address. This can be used so freeze the value for health, cash etc.
- self.mb.cartridge.overrideitem(rom_bank, addr, value)
def send_input(self, event):
"""
- Send a single input to control the emulator. This is both Game Boy buttons and emulator controls.
+ Send a single input to control the emulator. This is both Game Boy buttons and emulator controls. See
+ `pyboy.utils.WindowEvent` for which events to send.
- See `pyboy.WindowEvent` for which events to send.
+ Consider using `PyBoy.button` instead for easier access.
- Args:
- event (pyboy.WindowEvent): The event to send
- """
- self.events.append(WindowEvent(event))
List of the pyboy.utils.WindowEvents processed for the last call to PyBoy.tick()
-
-
-
-Expand source code
-
-
def get_input(
- self,
- ignore=(
- WindowEvent.PASS, WindowEvent._INTERNAL_TOGGLE_DEBUG, WindowEvent._INTERNAL_RENDERER_FLUSH,
- WindowEvent._INTERNAL_MOUSE, WindowEvent._INTERNAL_MARK_TILE
- )
-):
- """
- Get current inputs except the events specified in "ignore" tuple.
- This is both Game Boy buttons and emulator controls.
+ Example:
+ ```python
+ >>> from pyboy.utils import WindowEvent
+ >>> pyboy.send_input(WindowEvent.PRESS_BUTTON_A) # Press button 'a' and keep pressed after `PyBoy.tick()`
+ >>> pyboy.tick() # Button 'a' pressed
+ True
+ >>> pyboy.tick() # Button 'a' still pressed
+ True
+ >>> pyboy.send_input(WindowEvent.RELEASE_BUTTON_A) # Release button 'a' on next call to `PyBoy.tick()`
+ >>> pyboy.tick() # Button 'a' released
+ True
- See `pyboy.WindowEvent` for which events to get.
+ ```
Args:
- ignore (tuple): Events this function should ignore
-
- Returns
- -------
- list:
- List of the `pyboy.utils.WindowEvent`s processed for the last call to `pyboy.PyBoy.tick`
+ event (pyboy.WindowEvent): The event to send
"""
- return [x for x in self.old_events if x not in ignore]
+ self.events.append(WindowEvent(event))
@@ -978,13 +1836,17 @@
Returns
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 file
+>>> with open("state_file.state", "wb") as f:
+... pyboy.save_state(f)
+>>>
+>>> # Save to memory
+>>> import io
+>>> with io.BytesIO() as f:
+... f.seek(0)
+... pyboy.save_state(f)
+0
-# Save to memory
-import io
-file_like_object = io.BytesIO()
-file_like_object.seek(0)
Args
@@ -1003,13 +1865,19 @@
Args
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")
+ ```python
+ >>> # Save to file
+ >>> with open("state_file.state", "wb") as f:
+ ... pyboy.save_state(f)
+ >>>
+ >>> # Save to memory
+ >>> import io
+ >>> with io.BytesIO() as f:
+ ... f.seek(0)
+ ... pyboy.save_state(f)
+ 0
- # 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.
@@ -1018,6 +1886,9 @@
Args
if isinstance(file_like_object, str):
raise Exception("String not allowed. Did you specify a filepath instead of a file-like object?")
+ if file_like_object.__class__.__name__ == "TextIOWrapper":
+ raise Exception("Text file not allowed. Did you specify open(..., 'wb')?")
+
self.mb.save_state(IntIOWrapper(file_like_object))
@@ -1030,8 +1901,10 @@
Args
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.
>>> # Load file
+>>> with open("state_file.state", "rb") as f:
+... pyboy.load_state(f)
+>>>
Args
@@ -1051,10 +1924,12 @@
Args
can load it here.
To load a file, remember to load it as bytes:
-
- # Load file
- file_like_object = open("state_file.state", "rb")
-
+ ```python
+ >>> # Load file
+ >>> with open("state_file.state", "rb") as f:
+ ... pyboy.load_state(f)
+ >>>
+ ```
Args:
file_like_object (io.BufferedIOBase): A file-like object for which to read the emulator state.
@@ -1063,43 +1938,277 @@
Args
if isinstance(file_like_object, str):
raise Exception("String not allowed. Did you specify a filepath instead of a file-like object?")
+ if file_like_object.__class__.__name__ == "TextIOWrapper":
+ raise Exception("Text file not allowed. Did you specify open(..., 'rb')?")
+
self.mb.load_state(IntIOWrapper(file_like_object))
-
-def screen_image(self)
+
+def game_area_dimensions(self, x, y, width, height, follow_scrolling=True)
+
+
+
If using the generic game wrapper (see PyBoy.game_wrapper), you can use this to set the section of the
+tilemaps to extract. This will default to the entire tilemap.
def game_area_dimensions(self, x, y, width, height, follow_scrolling=True):
+ """
+ If using the generic game wrapper (see `pyboy.PyBoy.game_wrapper`), you can use this to set the section of the
+ tilemaps to extract. This will default to the entire tilemap.
+
+ Example:
+ ```python
+ >>> pyboy.game_wrapper.shape
+ (32, 32)
+ >>> pyboy.game_area_dimensions(2, 2, 10, 18, False)
+ >>> pyboy.game_wrapper.shape
+ (10, 18)
+ ```
+
+ Args:
+ x (int): Offset from top-left corner of the screen
+ y (int): Offset from top-left corner of the screen
+ width (int): Width of game area
+ height (int): Height of game area
+ follow_scrolling (bool): Whether to follow the scrolling of [SCX and SCY](https://gbdev.io/pandocs/Scrolling.html)
+ """
+ self.game_wrapper._set_dimensions(x, y, width, height, follow_scrolling=True)
+
+
+
+def game_area_collision(self)
-
Shortcut for pyboy.botsupport_manager.screen.screen_image.
-
Generates a PIL Image from the screen buffer.
-
Convenient for screen captures, but might be a bottleneck, if you use it to train a neural network. In which
-case, read up on the pyboy.botsupport features, Pan Docs on tiles/sprites,
-and join our Discord channel for more help.
+
Some game wrappers define a collision map. Check if your game wrapper has this feature implemented: pyboy.plugins.
If you want a "compressed", "minimal" or raw mapping of tiles, you can change the mapping using
+PyBoy.game_area_mapping(). Either you'll have to supply your own mapping, or you can find one
+that is built-in with the game wrapper plugin for your game. See PyBoy.game_area_mapping().
+
Returns
+
+
memoryview:
+
Simplified 2-dimensional memoryview of the screen
+
+
+
+Expand source code
+
+
def game_area(self):
+ """
+ Use this method to get a matrix of the "game area" of the screen. This view is simplified to be perfect for
+ machine learning applications.
+
+ The layout will vary from game to game. Below is an example from Tetris:
+
+ Example:
+ ```python
+ >>> pyboy.game_area()
+ array([[ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 130, 130, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 130, 130, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
+ [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47]], dtype=uint32)
+
+ ```
+
+ If you want a "compressed", "minimal" or raw mapping of tiles, you can change the mapping using
+ `pyboy.PyBoy.game_area_mapping`. Either you'll have to supply your own mapping, or you can find one
+ that is built-in with the game wrapper plugin for your game. See `pyboy.PyBoy.game_area_mapping`.
Returns
-------
- PIL.Image:
- RGB image of (160, 144) pixels
+ memoryview:
+ Simplified 2-dimensional memoryview of the screen
"""
- return self.botsupport_manager().screen().screen_image()
+
+ return self.game_wrapper.game_area()
@@ -1111,6 +2220,13 @@
Returns
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.
Some window types do not implement a frame-limiter, and will always run at full speed.
+
Example:
+
>>> pyboy.tick() # Delays 16.67ms
+True
+>>> pyboy.set_emulation_speed(0) # Disable limit
+>>> pyboy.tick() # As fast as possible
+True
+
Args
target_speed : int
@@ -1131,334 +2247,895 @@
Args
Some window types do not implement a frame-limiter, and will always run at full speed.
+ Example:
+ ```python
+ >>> pyboy.tick() # Delays 16.67ms
+ True
+ >>> pyboy.set_emulation_speed(0) # Disable limit
+ >>> pyboy.tick() # As fast as possible
+ True
+ ```
+
Args:
target_speed (int): Target emulation speed as multiplier of real-time.
"""
+ if self.initialized and self._plugin_manager.window_null_enabled:
+ logger.warning(
+ 'This window type does not support frame-limiting. `pyboy.set_emulation_speed(...)` will have no effect, as it\'s always running at full speed.'
+ )
+
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)
+
+def symbol_lookup(self, symbol)
-
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.
+
Look up a specific symbol from provided symbols file.
>>> # Directly
+>>> pyboy.memory[pyboy.symbol_lookup("Tileset")]
+0
+>>> # By bank and address
+>>> bank, addr = pyboy.symbol_lookup("Tileset")
+>>> pyboy.memory[bank, addr]
+0
+>>> pyboy.memory[bank, addr:addr+10]
+[0, 0, 0, 0, 0, 0, 102, 102, 102, 102]
+
+
Returns
-
-
str :
-
Game title
-
+
(int, int):
+ROM/RAM bank, address
Expand source code
-
def cartridge_title(self):
+
def symbol_lookup(self, symbol):
"""
- 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.
+ Look up a specific symbol from provided symbols file.
+
+ This can be useful in combination with `PyBoy.memory` or even `PyBoy.hook_register`.
+ See `PyBoy.hook_register` for how to load symbol into PyBoy.
+
+ Example:
+ ```python
+ >>> # Directly
+ >>> pyboy.memory[pyboy.symbol_lookup("Tileset")]
+ 0
+ >>> # By bank and address
+ >>> bank, addr = pyboy.symbol_lookup("Tileset")
+ >>> pyboy.memory[bank, addr]
+ 0
+ >>> pyboy.memory[bank, addr:addr+10]
+ [0, 0, 0, 0, 0, 0, 102, 102, 102, 102]
+
+ ```
Returns
-------
- str :
- Game title
+ (int, int):
+ ROM/RAM bank, address
"""
- return self.mb.cartridge.gamename
+ return self._lookup_symbol(symbol)
-
-
-
-class WindowEvent
-(event)
+
+def hook_register(self, bank, addr, callback, context)
-
All supported events can be found in the class description below.
If a symbol file is loaded, this function can also automatically resolve a bank and address from a symbol. To
+enable this, you'll need to place a .sym file next to your ROM, or provide it using:
+PyBoy(..., symbols_file="game_rom.gb.sym").
+
Then provide None for bank and the symbol for addr to trigger the automatic lookup.
+
Example:
+
>>> # Continued example above
+>>> pyboy.hook_register(None, "Main.move", lambda x: print(x), "Hello from hook2")
+>>> pyboy.tick(80)
+Hello from hook2
+True
+
+
+
Args
+
+
bank : int or None
+
ROM or RAM bank (None for symbol lookup)
+
addr : int or str
+
Address in the Game Boy's address space (str for symbol lookup)
+
callback : func
+
A function which takes context as argument
+
context : object
+
Argument to pass to callback when hook is called
+
+
+
+Expand source code
-
class WindowEvent:
+
def hook_register(self, bank, addr, callback, context):
"""
- All supported events can be found in the class description below.
+ Adds a hook into a specific bank and memory address.
+ When the Game Boy executes this address, the provided callback function will be called.
- It can be used as follows:
+ By providing an object as `context`, you can later get access to information inside and outside of the callback.
- >>> from pyboy import PyBoy, WindowEvent
- >>> pyboy = PyBoy('file.rom')
- >>> pyboy.send_input(WindowEvent.PRESS_ARROW_RIGHT)
- """
+ Example:
+ ```python
+ >>> context = "Hello from hook"
+ >>> def my_callback(context):
+ ... print(context)
+ >>> pyboy.hook_register(0, 0x100, my_callback, context)
+ >>> pyboy.tick(70)
+ Hello from hook
+ True
- # 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,
- SCREENSHOT_RECORD,
- DEBUG_MEMORY_SCROLL_DOWN,
- DEBUG_MEMORY_SCROLL_UP,
- MOD_SHIFT_ON,
- MOD_SHIFT_OFF,
- FULL_SCREEN_TOGGLE,
- ) = range(42)
-
- def __init__(self, event):
- self.event = event
-
- def __eq__(self, x):
- if isinstance(x, int):
- return self.event == x
- else:
- return self.event == x.event
-
- def __int__(self):
- return self.event
-
- def __str__(self):
- return (
- "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",
- "SCREENSHOT_RECORD",
- "DEBUG_MEMORY_SCROLL_DOWN",
- "DEBUG_MEMORY_SCROLL_UP",
- "MOD_SHIFT_ON",
- "MOD_SHIFT_OFF",
- "FULL_SCREEN_TOGGLE",
- )[self.event]
+ ```
+
+ If a symbol file is loaded, this function can also automatically resolve a bank and address from a symbol. To
+ enable this, you'll need to place a `.sym` file next to your ROM, or provide it using:
+ `PyBoy(..., symbols_file="game_rom.gb.sym")`.
+
+ Then provide `None` for `bank` and the symbol for `addr` to trigger the automatic lookup.
+
+ Example:
+ ```python
+ >>> # Continued example above
+ >>> pyboy.hook_register(None, "Main.move", lambda x: print(x), "Hello from hook2")
+ >>> pyboy.tick(80)
+ Hello from hook2
+ True
+
+ ```
+
+ Args:
+ bank (int or None): ROM or RAM bank (None for symbol lookup)
+ addr (int or str): Address in the Game Boy's address space (str for symbol lookup)
+ callback (func): A function which takes `context` as argument
+ context (object): Argument to pass to callback when hook is called
+ """
+ if bank is None and isinstance(addr, str):
+ bank, addr = self._lookup_symbol(addr)
+
+ opcode = self.memory[bank, addr]
+ if opcode == 0xDB:
+ raise ValueError("Hook already registered for this bank and address.")
+ self.mb.breakpoint_add(bank, addr)
+ bank_addr_opcode = (bank & 0xFF) << 24 | (addr & 0xFFFF) << 8 | (opcode & 0xFF)
+ self._hooks[bank_addr_opcode] = (callback, context)
-
Subclasses
-
-
pyboy.utils.WindowEventMouse
-
-
Class variables
-
-
var QUIT
-
-
-
-
var PRESS_ARROW_UP
-
-
-
-
var PRESS_ARROW_DOWN
-
-
-
-
var PRESS_ARROW_RIGHT
-
-
-
-
var PRESS_ARROW_LEFT
-
-
-
-
var PRESS_BUTTON_A
-
-
-
-
var PRESS_BUTTON_B
-
-
-
-
var PRESS_BUTTON_SELECT
-
-
-
-
var PRESS_BUTTON_START
-
-
-
-
var RELEASE_ARROW_UP
-
-
-
-
var RELEASE_ARROW_DOWN
-
-
-
-
var RELEASE_ARROW_RIGHT
-
-
-
-
var RELEASE_ARROW_LEFT
-
-
-
-
var RELEASE_BUTTON_A
-
-
-
-
var RELEASE_BUTTON_B
-
-
-
-
var RELEASE_BUTTON_SELECT
-
-
-
-
var RELEASE_BUTTON_START
-
-
-
-
var PRESS_SPEED_UP
-
-
-
-
var RELEASE_SPEED_UP
-
-
-
-
var STATE_SAVE
-
-
-
-
var STATE_LOAD
-
-
-
-
var PASS
-
-
-
-
var SCREEN_RECORDING_TOGGLE
-
-
-
-
var PAUSE
-
-
-
-
var UNPAUSE
-
-
-
-
var PAUSE_TOGGLE
-
-
-
-
var PRESS_REWIND_BACK
-
-
-
-
var PRESS_REWIND_FORWARD
-
-
-
var RELEASE_REWIND_BACK
-
-
-
-
var RELEASE_REWIND_FORWARD
-
-
-
-
var WINDOW_FOCUS
-
-
-
-
var WINDOW_UNFOCUS
-
-
-
-
var SCREENSHOT_RECORD
+
+def hook_deregister(self, bank, addr)
+
-
+
Remove a previously registered hook from a specific bank and memory address.
Address in the Game Boy's address space (str for symbol lookup)
+
+
+
+Expand source code
+
+
def hook_deregister(self, bank, addr):
+ """
+ Remove a previously registered hook from a specific bank and memory address.
+
+ Example:
+ ```python
+ >>> context = "Hello from hook"
+ >>> def my_callback(context):
+ ... print(context)
+ >>> pyboy.hook_register(0, 0x2000, my_callback, context)
+ >>> pyboy.hook_deregister(0, 0x2000)
+
+ ```
+
+ This function can also deregister a hook based on a symbol. See `PyBoy.hook_register` for details.
+
+ Example:
+ ```python
+ >>> pyboy.hook_register(None, "Main", lambda x: print(x), "Hello from hook")
+ >>> pyboy.hook_deregister(None, "Main")
+
+ ```
+
+ Args:
+ bank (int or None): ROM or RAM bank (None for symbol lookup)
+ addr (int or str): Address in the Game Boy's address space (str for symbol lookup)
+ """
+ if bank is None and isinstance(addr, str):
+ bank, addr = self._lookup_symbol(addr)
+
+ index = self.mb.breakpoint_find(bank, addr)
+ if index == -1:
+ raise ValueError("Breakpoint not found for bank and addr")
+
+ _, _, opcode = self.mb.breakpoints_list[index]
+ self.mb.breakpoint_remove(index)
+ bank_addr_opcode = (bank & 0xFF) << 24 | (addr & 0xFFFF) << 8 | (opcode & 0xFF)
+ self._hooks.pop(bank_addr_opcode)
+
-
var DEBUG_MEMORY_SCROLL_DOWN
+
+def get_sprite(self, sprite_index)
+
-
+
Provides a Sprite object, which makes the OAM data more presentable. The given index
+corresponds to index of the sprite in the "Object Attribute Memory" (OAM).
+
The Game Boy supports 40 sprites in total. Read more details about it, in the Pan
+Docs.
+
>>> s = pyboy.get_sprite(12)
+>>> s
+Sprite [12]: Position: (-8, -16), Shape: (8, 8), Tiles: (Tile: 0), On screen: False
+>>> s.on_screen
+False
+>>> s.tiles
+[Tile: 0]
+
+
def get_sprite(self, sprite_index):
+ """
+ Provides a `pyboy.api.sprite.Sprite` object, which makes the OAM data more presentable. The given index
+ corresponds to index of the sprite in the "Object Attribute Memory" (OAM).
+
+ The Game Boy supports 40 sprites in total. Read more details about it, in the [Pan
+ Docs](http://bgb.bircd.org/pandocs.htm).
+
+ ```python
+ >>> s = pyboy.get_sprite(12)
+ >>> s
+ Sprite [12]: Position: (-8, -16), Shape: (8, 8), Tiles: (Tile: 0), On screen: False
+ >>> s.on_screen
+ False
+ >>> s.tiles
+ [Tile: 0]
+
+ ```
+
+ Args:
+ index (int): Sprite index from 0 to 39.
+ Returns
+ -------
+ `pyboy.api.sprite.Sprite`:
+ Sprite corresponding to the given index.
+ """
+ return Sprite(self.mb, sprite_index)
Provided a list of tile identifiers, this function will find all occurrences of sprites using the tile
+identifiers and return the sprite indexes where each identifier is found. Use the sprite indexes in the
+PyBoy.get_sprite() function to get a Sprite object.
Meaning, that tile identifier 43 is found at the sprite indexes: 0, 2, and 4, while tile identifier
+123 was not found anywhere.
+
Args
+
+
identifiers : list
+
List of tile identifiers (int)
+
on_screen : bool
+
Require that the matched sprite is on screen
+
+
Returns
+
+
list:
+
list of sprite matches for every tile identifier in the input
+
+
+
+Expand source code
+
+
def get_sprite_by_tile_identifier(self, tile_identifiers, on_screen=True):
+ """
+ Provided a list of tile identifiers, this function will find all occurrences of sprites using the tile
+ identifiers and return the sprite indexes where each identifier is found. Use the sprite indexes in the
+ `pyboy.PyBoy.get_sprite` function to get a `pyboy.api.sprite.Sprite` object.
+
+ Example:
+ ```python
+ >>> print(pyboy.get_sprite_by_tile_identifier([43, 123]))
+ [[0, 2, 4], []]
+
+ ```
+
+ Meaning, that tile identifier `43` is found at the sprite indexes: 0, 2, and 4, while tile identifier
+ `123` was not found anywhere.
+
+ Args:
+ identifiers (list): List of tile identifiers (int)
+ on_screen (bool): Require that the matched sprite is on screen
+
+ Returns
+ -------
+ list:
+ list of sprite matches for every tile identifier in the input
+ """
+
+ matches = []
+ for i in tile_identifiers:
+ match = []
+ for s in range(constants.SPRITES):
+ sprite = Sprite(self.mb, s)
+ for t in sprite.tiles:
+ if t.tile_identifier == i and (not on_screen or (on_screen and sprite.on_screen)):
+ match.append(s)
+ matches.append(match)
+ return matches
+
-
var MOD_SHIFT_ON
+
+def get_tile(self, identifier)
+
-
+
The Game Boy can have 384 tiles loaded in memory at once (768 for Game Boy Color). Use this method to get a
+Tile-object for given identifier.
+
The identifier is a PyBoy construct, which unifies two different scopes of indexes in the Game Boy hardware. See
+the Tile object for more information.
+
Example:
+
>>> t = pyboy.get_tile(2)
+>>> t
+Tile: 2
+>>> t.shape
+(8, 8)
+
+
def get_tile(self, identifier):
+ """
+ The Game Boy can have 384 tiles loaded in memory at once (768 for Game Boy Color). Use this method to get a
+ `pyboy.api.tile.Tile`-object for given identifier.
+
+ The identifier is a PyBoy construct, which unifies two different scopes of indexes in the Game Boy hardware. See
+ the `pyboy.api.tile.Tile` object for more information.
+
+ Example:
+ ```python
+ >>> t = pyboy.get_tile(2)
+ >>> t
+ Tile: 2
+ >>> t.shape
+ (8, 8)
+
+ ```
+
+ Returns
+ -------
+ `pyboy.api.tile.Tile`:
+ A Tile object for the given identifier.
+ """
+ return Tile(self.mb, identifier=identifier)
+
-
var MOD_SHIFT_OFF
-
-
+
-
var FULL_SCREEN_TOGGLE
+
+class PyBoyMemoryView
+(mb)
+
-
-
-
+
This class cannot be used directly, but is accessed through PyBoy.memory.
+
This class serves four purposes: Reading memory (ROM/RAM), writing memory (ROM/RAM), overriding memory (ROM/RAM) and special registers.
Memory can be accessed as individual bytes (pyboy.memory[0x00]) or as slices (pyboy.memory[0x00:0x10]). And if
+applicable, a specific ROM/RAM bank can be defined before the address (pyboy.memory[0, 0x00] or pyboy.memory[0, 0x00:0x10]).
+
The boot ROM is accessed using the special "-1" ROM bank.
+
The find addresses of interest, either search online for something like: "[game title] RAM map", or find them yourself
+using PyBoy.memory_scanner.
+
Read:
+
If you're developing a bot or AI with this API, you're most likely going to be using read the most. This is how you
+would efficiently read the score, time, coins, positions etc. in a game's memory.
+
>>> pyboy.memory[0x0000] # Read one byte at address 0x0000
+49
+>>> pyboy.memory[0x0000:0x0010] # Read 16 bytes from 0x0000 to 0x0010 (excluding 0x0010)
+[49, 254, 255, 33, 0, 128, 175, 34, 124, 254, 160, 32, 249, 6, 48, 33]
+>>> pyboy.memory[-1, 0x0000:0x0010] # Read 16 bytes from 0x0000 to 0x0010 (excluding 0x0010) from the boot ROM
+[49, 254, 255, 33, 0, 128, 175, 34, 124, 254, 160, 32, 249, 6, 48, 33]
+>>> pyboy.memory[0, 0x0000:0x0010] # Read 16 bytes from 0x0000 to 0x0010 (excluding 0x0010) from ROM bank 0
+[64, 65, 66, 67, 68, 69, 70, 65, 65, 65, 71, 65, 65, 65, 72, 73]
+>>> pyboy.memory[2, 0xA000] # Read from external RAM on cartridge (if any) from bank 2 at address 0xA000
+0
+
+
Write:
+
Writing to Game Boy memory can be complicated because of the limited address space. There's a lot of memory that
+isn't directly accessible, and can be hidden through "memory banking". This means that the same address range
+(for example 0x4000 to 0x8000) can change depending on what state the game is in.
+
If you want to change an address in the ROM, then look at override below. Issuing writes to the ROM area actually
+sends commands to the Memory Bank Controller (MBC) on the cartridge.
+
A write is done by assigning to the PyBoy.memory object. It's recommended to define the bank to avoid mistakes
+(pyboy.memory[2, 0xA000]=1). Without defining the bank, PyBoy will pick the current bank for the given address if
+needed (pyboy.memory[0xA000]=1).
+
At this point, all reads will return a new list of the values in the given range. The slices will not reference back to the PyBoy memory. This feature might come in the future.
+
>>> pyboy.memory[0xC000] = 123 # Write to WRAM at address 0xC000
+>>> pyboy.memory[0xC000:0xC00A] = [0,1,2,3,4,5,6,7,8,9] # Write to WRAM from address 0xC000 to 0xC00A
+>>> pyboy.memory[0xC010:0xC01A] = 0 # Write to WRAM from address 0xC010 to 0xC01A
+>>> pyboy.memory[0x1000] = 123 # Not writing 123 at address 0x1000! This sends a command to the cartridge's MBC.
+>>> pyboy.memory[2, 0xA000] = 123 # Write to external RAM on cartridge (if any) for bank 2 at address 0xA000
+>>> # Game Boy Color (CGB) only:
+>>> pyboy_cgb.memory[1, 0x8000] = 25 # Write to VRAM bank 1 at address 0xD000 when in CGB mode
+>>> pyboy_cgb.memory[6, 0xD000] = 25 # Write to WRAM bank 6 at address 0xD000 when in CGB mode
+
+
Override:
+
Override data at a given memory address of the Game Boy's ROM.
+
This can be used to reprogram a game ROM to change its behavior.
+
This will not let your override RAM or a special register. This will let you override data in the ROM at any given bank.
+This is the memory allocated at 0x0000 to 0x8000, where 0x4000 to 0x8000 can be changed from the MBC.
+
NOTE: Any changes here are not saved or loaded to game states! Use this function with caution and reapply
+any overrides when reloading the ROM.
+
To override, it's required to provide the ROM-bank you're changing. Otherwise, it'll be considered a regular 'write' as described above.
+
>>> pyboy.memory[0, 0x0010] = 10 # Override ROM-bank 0 at address 0x0010
+>>> pyboy.memory[0, 0x0010:0x001A] = [0,1,2,3,4,5,6,7,8,9] # Override ROM-bank 0 at address 0x0010 to 0x001A
+>>> pyboy.memory[-1, 0x0010] = 10 # Override boot ROM at address 0x0010
+>>> pyboy.memory[1, 0x6000] = 12 # Override ROM-bank 1 at address 0x6000
+>>> pyboy.memory[0x1000] = 12 # This will not override, as there is no ROM bank assigned!
+
+
Special Registers:
+
The Game Boy has a range of memory addresses known as hardware registers. These control parts of the hardware like LCD,
+Timer, DMA, serial and so on. Even though they might appear as regular RAM addresses, reading/writing these addresses
+often results in special side-effects.
+
The DIV (0xFF04) register for example provides a number that increments 16 thousand times each second. This can be
+used as a source of randomness in games. If you read the value, you'll get a pseudo-random number. But if you write
+any value to the register, it'll reset to zero.
+
>>> pyboy.memory[0xFF04] # DIV register
+163
+>>> pyboy.memory[0xFF04] = 123 # Trying to write to it will always reset it to zero
+>>> pyboy.memory[0xFF04]
+0
+
+
+
+Expand source code
+
+
class PyBoyMemoryView:
+ """
+ This class cannot be used directly, but is accessed through `PyBoy.memory`.
+
+ This class serves four purposes: Reading memory (ROM/RAM), writing memory (ROM/RAM), overriding memory (ROM/RAM) and special registers.
+
+ See the [Pan Docs: Memory Map](https://gbdev.io/pandocs/Memory_Map.html) for a great overview of the memory space.
+
+ Memory can be accessed as individual bytes (`pyboy.memory[0x00]`) or as slices (`pyboy.memory[0x00:0x10]`). And if
+ applicable, a specific ROM/RAM bank can be defined before the address (`pyboy.memory[0, 0x00]` or `pyboy.memory[0, 0x00:0x10]`).
+
+ The boot ROM is accessed using the special "-1" ROM bank.
+
+ The find addresses of interest, either search online for something like: "[game title] RAM map", or find them yourself
+ using `PyBoy.memory_scanner`.
+
+ **Read:**
+
+ If you're developing a bot or AI with this API, you're most likely going to be using read the most. This is how you
+ would efficiently read the score, time, coins, positions etc. in a game's memory.
+
+ ```python
+ >>> pyboy.memory[0x0000] # Read one byte at address 0x0000
+ 49
+ >>> pyboy.memory[0x0000:0x0010] # Read 16 bytes from 0x0000 to 0x0010 (excluding 0x0010)
+ [49, 254, 255, 33, 0, 128, 175, 34, 124, 254, 160, 32, 249, 6, 48, 33]
+ >>> pyboy.memory[-1, 0x0000:0x0010] # Read 16 bytes from 0x0000 to 0x0010 (excluding 0x0010) from the boot ROM
+ [49, 254, 255, 33, 0, 128, 175, 34, 124, 254, 160, 32, 249, 6, 48, 33]
+ >>> pyboy.memory[0, 0x0000:0x0010] # Read 16 bytes from 0x0000 to 0x0010 (excluding 0x0010) from ROM bank 0
+ [64, 65, 66, 67, 68, 69, 70, 65, 65, 65, 71, 65, 65, 65, 72, 73]
+ >>> pyboy.memory[2, 0xA000] # Read from external RAM on cartridge (if any) from bank 2 at address 0xA000
+ 0
+ ```
+
+ **Write:**
+
+ Writing to Game Boy memory can be complicated because of the limited address space. There's a lot of memory that
+ isn't directly accessible, and can be hidden through "memory banking". This means that the same address range
+ (for example 0x4000 to 0x8000) can change depending on what state the game is in.
+
+ If you want to change an address in the ROM, then look at override below. Issuing writes to the ROM area actually
+ sends commands to the [Memory Bank Controller (MBC)](https://gbdev.io/pandocs/MBCs.html#mbcs) on the cartridge.
+
+ A write is done by assigning to the `PyBoy.memory` object. It's recommended to define the bank to avoid mistakes
+ (`pyboy.memory[2, 0xA000]=1`). Without defining the bank, PyBoy will pick the current bank for the given address if
+ needed (`pyboy.memory[0xA000]=1`).
+
+ At this point, all reads will return a new list of the values in the given range. The slices will not reference back to the PyBoy memory. This feature might come in the future.
+
+ ```python
+ >>> pyboy.memory[0xC000] = 123 # Write to WRAM at address 0xC000
+ >>> pyboy.memory[0xC000:0xC00A] = [0,1,2,3,4,5,6,7,8,9] # Write to WRAM from address 0xC000 to 0xC00A
+ >>> pyboy.memory[0xC010:0xC01A] = 0 # Write to WRAM from address 0xC010 to 0xC01A
+ >>> pyboy.memory[0x1000] = 123 # Not writing 123 at address 0x1000! This sends a command to the cartridge's MBC.
+ >>> pyboy.memory[2, 0xA000] = 123 # Write to external RAM on cartridge (if any) for bank 2 at address 0xA000
+ >>> # Game Boy Color (CGB) only:
+ >>> pyboy_cgb.memory[1, 0x8000] = 25 # Write to VRAM bank 1 at address 0xD000 when in CGB mode
+ >>> pyboy_cgb.memory[6, 0xD000] = 25 # Write to WRAM bank 6 at address 0xD000 when in CGB mode
+ ```
+
+ **Override:**
+
+ Override data at a given memory address of the Game Boy's ROM.
+
+ This can be used to reprogram a game ROM to change its behavior.
+
+ This will not let your override RAM or a special register. This will let you override data in the ROM at any given bank.
+ This is the memory allocated at 0x0000 to 0x8000, where 0x4000 to 0x8000 can be changed from the MBC.
+
+ _NOTE_: Any changes here are not saved or loaded to game states! Use this function with caution and reapply
+ any overrides when reloading the ROM.
+
+ To override, it's required to provide the ROM-bank you're changing. Otherwise, it'll be considered a regular 'write' as described above.
+
+ ```python
+ >>> pyboy.memory[0, 0x0010] = 10 # Override ROM-bank 0 at address 0x0010
+ >>> pyboy.memory[0, 0x0010:0x001A] = [0,1,2,3,4,5,6,7,8,9] # Override ROM-bank 0 at address 0x0010 to 0x001A
+ >>> pyboy.memory[-1, 0x0010] = 10 # Override boot ROM at address 0x0010
+ >>> pyboy.memory[1, 0x6000] = 12 # Override ROM-bank 1 at address 0x6000
+ >>> pyboy.memory[0x1000] = 12 # This will not override, as there is no ROM bank assigned!
+ ```
+
+ **Special Registers:**
+
+ The Game Boy has a range of memory addresses known as [hardware registers](https://gbdev.io/pandocs/Hardware_Reg_List.html). These control parts of the hardware like LCD,
+ Timer, DMA, serial and so on. Even though they might appear as regular RAM addresses, reading/writing these addresses
+ often results in special side-effects.
+
+ The [DIV (0xFF04) register](https://gbdev.io/pandocs/Timer_and_Divider_Registers.html#ff04--div-divider-register) for example provides a number that increments 16 thousand times each second. This can be
+ used as a source of randomness in games. If you read the value, you'll get a pseudo-random number. But if you write
+ *any* value to the register, it'll reset to zero.
+
+ ```python
+ >>> pyboy.memory[0xFF04] # DIV register
+ 163
+ >>> pyboy.memory[0xFF04] = 123 # Trying to write to it will always reset it to zero
+ >>> pyboy.memory[0xFF04]
+ 0
+ ```
+
+ """
+ def __init__(self, mb):
+ self.mb = mb
+
+ def _fix_slice(self, addr):
+ if addr.start is None:
+ return (-1, 0, 0)
+ if addr.stop is None:
+ return (0, -1, 0)
+ start = addr.start
+ stop = addr.stop
+ if start > stop:
+ return (-1, -1, 0)
+ if addr.step is None:
+ step = 1
+ else:
+ step = addr.step
+ return start, stop, step
+
+ def __getitem__(self, addr):
+ is_bank = isinstance(addr, tuple)
+ bank = 0
+ if is_bank:
+ bank, addr = addr
+ assert isinstance(bank, int), "Bank has to be integer. Slicing is not supported."
+ is_single = isinstance(addr, int)
+ if not is_single:
+ start, stop, step = self._fix_slice(addr)
+ assert start >= 0 or stop >= 0, "Start address has to come before end address"
+ assert start >= 0, "Start address required"
+ assert stop >= 0, "End address required"
+ return self.__getitem(start, stop, step, bank, is_single, is_bank)
+ else:
+ return self.__getitem(addr, 0, 1, bank, is_single, is_bank)
+
+ def __getitem(self, start, stop, step, bank, is_single, is_bank):
+ slice_length = (stop-start) // step
+ if is_bank:
+ # Reading a specific bank
+ if start < 0x8000:
+ if start >= 0x4000:
+ start -= 0x4000
+ stop -= 0x4000
+ # Cartridge ROM Banks
+ assert stop < 0x4000, "Out of bounds for reading ROM bank"
+ if bank == -1:
+ assert start <= 0xFF, "Start address out of range for bootrom"
+ assert stop <= 0xFF, "Start address out of range for bootrom"
+ if not is_single:
+ mem_slice = [0] * slice_length
+ for x in range(start, stop, step):
+ mem_slice[(x-start) // step] = self.mb.bootrom.bootrom[x]
+ return mem_slice
+ else:
+ return self.mb.bootrom.bootrom[start]
+ else:
+ assert bank <= self.mb.cartridge.external_rom_count, "ROM Bank out of range"
+ if not is_single:
+ mem_slice = [0] * slice_length
+ for x in range(start, stop, step):
+ mem_slice[(x-start) // step] = self.mb.cartridge.rombanks[bank, x]
+ return mem_slice
+ else:
+ return self.mb.cartridge.rombanks[bank, start]
+ elif start < 0xA000:
+ start -= 0x8000
+ stop -= 0x8000
+ # CGB VRAM Banks
+ assert self.mb.cgb or (bank == 0), "Selecting bank of VRAM is only supported for CGB mode"
+ assert stop < 0x2000, "Out of bounds for reading VRAM bank"
+ assert bank <= 1, "VRAM Bank out of range"
+
+ if bank == 0:
+ if not is_single:
+ mem_slice = [0] * slice_length
+ for x in range(start, stop, step):
+ mem_slice[(x-start) // step] = self.mb.lcd.VRAM0[x]
+ return mem_slice
+ else:
+ return self.mb.lcd.VRAM0[start]
+ else:
+ if not is_single:
+ mem_slice = [0] * slice_length
+ for x in range(start, stop, step):
+ mem_slice[(x-start) // step] = self.mb.lcd.VRAM1[x]
+ return mem_slice
+ else:
+ return self.mb.lcd.VRAM1[start]
+ elif start < 0xC000:
+ start -= 0xA000
+ stop -= 0xA000
+ # Cartridge RAM banks
+ assert stop < 0x2000, "Out of bounds for reading cartridge RAM bank"
+ assert bank <= self.mb.cartridge.external_ram_count, "ROM Bank out of range"
+ if not is_single:
+ mem_slice = [0] * slice_length
+ for x in range(start, stop, step):
+ mem_slice[(x-start) // step] = self.mb.cartridge.rambanks[bank, x]
+ return mem_slice
+ else:
+ return self.mb.cartridge.rambanks[bank, start]
+ elif start < 0xE000:
+ start -= 0xC000
+ stop -= 0xC000
+ if start >= 0x1000:
+ start -= 0x1000
+ stop -= 0x1000
+ # CGB VRAM banks
+ assert self.mb.cgb or (bank == 0), "Selecting bank of WRAM is only supported for CGB mode"
+ assert stop < 0x1000, "Out of bounds for reading VRAM bank"
+ assert bank <= 7, "WRAM Bank out of range"
+ if not is_single:
+ mem_slice = [0] * slice_length
+ for x in range(start, stop, step):
+ mem_slice[(x-start) // step] = self.mb.ram.internal_ram0[x + bank*0x1000]
+ return mem_slice
+ else:
+ return self.mb.ram.internal_ram0[start + bank*0x1000]
+ else:
+ assert None, "Invalid memory address for bank"
+ elif not is_single:
+ # Reading slice of memory space
+ mem_slice = [0] * slice_length
+ for x in range(start, stop, step):
+ mem_slice[(x-start) // step] = self.mb.getitem(x)
+ return mem_slice
+ else:
+ # Reading specific address of memory space
+ return self.mb.getitem(start)
+
+ def __setitem__(self, addr, v):
+ is_bank = isinstance(addr, tuple)
+ bank = 0
+ if is_bank:
+ bank, addr = addr
+ assert isinstance(bank, int), "Bank has to be integer. Slicing is not supported."
+ is_single = isinstance(addr, int)
+ if not is_single:
+ start, stop, step = self._fix_slice(addr)
+ assert start >= 0, "Start address required"
+ assert stop >= 0, "End address required"
+ self.__setitem(start, stop, step, v, bank, is_single, is_bank)
+ else:
+ self.__setitem(addr, 0, 0, v, bank, is_single, is_bank)
+
+ def __setitem(self, start, stop, step, v, bank, is_single, is_bank):
+ if is_bank:
+ # Writing a specific bank
+ if start < 0x8000:
+ """
+ Override one byte at a given memory address of the Game Boy's ROM.
+
+ This will let you override data in the ROM at any given bank. This is the memory allocated at 0x0000 to 0x8000, where 0x4000 to 0x8000 can be changed from the MBC.
+
+ __NOTE__: Any changes here are not saved or loaded to game states! Use this function with caution and reapply
+ any overrides when reloading the ROM.
+
+ If you need to change a RAM address, see `pyboy.PyBoy.memory`.
+
+ Args:
+ rom_bank (int): ROM bank to do the overwrite in
+ addr (int): Address to write the byte inside the ROM bank
+ value (int): A byte of data
+ """
+ if start >= 0x4000:
+ start -= 0x4000
+ stop -= 0x4000
+ # Cartridge ROM Banks
+ assert stop < 0x4000, "Out of bounds for reading ROM bank"
+ assert bank <= self.mb.cartridge.external_rom_count, "ROM Bank out of range"
+
+ # TODO: If you change a RAM value outside of the ROM banks above, the memory value will stay the same no matter
+ # what the game writes to the address. This can be used so freeze the value for health, cash etc.
+ if bank == -1:
+ assert start <= 0xFF, "Start address out of range for bootrom"
+ assert stop <= 0xFF, "Start address out of range for bootrom"
+ if not is_single:
+ # Writing slice of memory space
+ if hasattr(v, "__iter__"):
+ assert (stop-start) // step == len(v), "slice does not match length of data"
+ _v = iter(v)
+ for x in range(start, stop, step):
+ self.mb.bootrom.bootrom[x] = next(_v)
+ else:
+ for x in range(start, stop, step):
+ self.mb.bootrom.bootrom[x] = v
+ else:
+ self.mb.bootrom.bootrom[start] = v
+ else:
+ if not is_single:
+ # Writing slice of memory space
+ if hasattr(v, "__iter__"):
+ assert (stop-start) // step == len(v), "slice does not match length of data"
+ _v = iter(v)
+ for x in range(start, stop, step):
+ self.mb.cartridge.overrideitem(bank, x, next(_v))
+ else:
+ for x in range(start, stop, step):
+ self.mb.cartridge.overrideitem(bank, x, v)
+ else:
+ self.mb.cartridge.overrideitem(bank, start, v)
+
+ elif start < 0xA000:
+ start -= 0x8000
+ stop -= 0x8000
+ # CGB VRAM Banks
+ assert self.mb.cgb or (bank == 0), "Selecting bank of VRAM is only supported for CGB mode"
+ assert stop < 0x2000, "Out of bounds for reading VRAM bank"
+ assert bank <= 1, "VRAM Bank out of range"
+
+ if bank == 0:
+ if not is_single:
+ # Writing slice of memory space
+ if hasattr(v, "__iter__"):
+ assert (stop-start) // step == len(v), "slice does not match length of data"
+ _v = iter(v)
+ for x in range(start, stop, step):
+ self.mb.lcd.VRAM0[x] = next(_v)
+ else:
+ for x in range(start, stop, step):
+ self.mb.lcd.VRAM0[x] = v
+ else:
+ self.mb.lcd.VRAM0[start] = v
+ else:
+ if not is_single:
+ # Writing slice of memory space
+ if hasattr(v, "__iter__"):
+ assert (stop-start) // step == len(v), "slice does not match length of data"
+ _v = iter(v)
+ for x in range(start, stop, step):
+ self.mb.lcd.VRAM1[x] = next(_v)
+ else:
+ for x in range(start, stop, step):
+ self.mb.lcd.VRAM1[x] = v
+ else:
+ self.mb.lcd.VRAM1[start] = v
+ elif start < 0xC000:
+ start -= 0xA000
+ stop -= 0xA000
+ # Cartridge RAM banks
+ assert stop < 0x2000, "Out of bounds for reading cartridge RAM bank"
+ assert bank <= self.mb.cartridge.external_ram_count, "ROM Bank out of range"
+ if not is_single:
+ # Writing slice of memory space
+ if hasattr(v, "__iter__"):
+ assert (stop-start) // step == len(v), "slice does not match length of data"
+ _v = iter(v)
+ for x in range(start, stop, step):
+ self.mb.cartridge.rambanks[bank, x] = next(_v)
+ else:
+ for x in range(start, stop, step):
+ self.mb.cartridge.rambanks[bank, x] = v
+ else:
+ self.mb.cartridge.rambanks[bank, start] = v
+ elif start < 0xE000:
+ start -= 0xC000
+ stop -= 0xC000
+ if start >= 0x1000:
+ start -= 0x1000
+ stop -= 0x1000
+ # CGB VRAM banks
+ assert self.mb.cgb or (bank == 0), "Selecting bank of WRAM is only supported for CGB mode"
+ assert stop < 0x1000, "Out of bounds for reading VRAM bank"
+ assert bank <= 7, "WRAM Bank out of range"
+ if not is_single:
+ # Writing slice of memory space
+ if hasattr(v, "__iter__"):
+ assert (stop-start) // step == len(v), "slice does not match length of data"
+ _v = iter(v)
+ for x in range(start, stop, step):
+ self.mb.ram.internal_ram0[x + bank*0x1000] = next(_v)
+ else:
+ for x in range(start, stop, step):
+ self.mb.ram.internal_ram0[x + bank*0x1000] = v
+ else:
+ self.mb.ram.internal_ram0[start + bank*0x1000] = v
+ else:
+ assert None, "Invalid memory address for bank"
+ elif not is_single:
+ # Writing slice of memory space
+ if hasattr(v, "__iter__"):
+ assert (stop-start) // step == len(v), "slice does not match length of data"
+ _v = iter(v)
+ for x in range(start, stop, step):
+ self.mb.setitem(x, next(_v))
+ else:
+ for x in range(start, stop, step):
+ self.mb.setitem(x, v)
+ else:
+ # Writing specific address of memory space
+ self.mb.setitem(start, v)
#
-# License: See LICENSE.md file
-# GitHub: https://github.com/Baekalfen/PyBoy
-#
-
-import numpy as np
-
-from .botsupport.constants import TILES
-from .utils import WindowEvent
-
-try:
- from gym import Env
- from gym.spaces import Discrete, MultiDiscrete, Box
- enabled = True
-except ImportError:
-
- class Env:
- pass
-
- enabled = False
-
-
-class PyBoyGymEnv(Env):
- """ A gym environement built from a `pyboy.PyBoy`
-
- This function requires PyBoy to implement a Game Wrapper for the loaded ROM. You can find the supported games in pyboy.plugins.
- Additional kwargs are passed to the start_game method of the game_wrapper.
-
- Args:
- observation_type (str): Define what the agent will be able to see:
- * `"raw"`: Gives the raw pixels color
- * `"tiles"`: Gives the id of the sprites and tiles in 8x8 pixel zones of the game_area.
- * `"compressed"`: Like `"tiles"` but with slightly simplified id's (i.e. each type of enemy has a unique id).
- * `"minimal"`: Like `"compressed"` but gives a minimal representation (recommended; i.e. all enemies have the same id).
-
- action_type (str): Define how the agent will interact with button inputs
- * `"press"`: The agent will only press inputs for 1 frame an then release it.
- * `"toggle"`: The agent will toggle inputs, first time it press and second time it release.
- * `"all"`: The agent have access to all inputs, press and release are separated.
-
- simultaneous_actions (bool): Allow to inject multiple input at once. This dramatically increases the action_space: \\(n \\rightarrow 2^n\\)
-
- Attributes:
- game_wrapper (`pyboy.plugins.base_plugin.PyBoyGameWrapper`): The game_wrapper of the PyBoy game instance over which the environment is built.
- action_space (Gym space): The action space of the environment.
- observation_space (Gym space): The observation space of the environment (depends of observation_type).
- actions (list): The list of input IDs of allowed input for the agent (depends of action_type).
-
- """
- def __init__(self, pyboy, observation_type="tiles", action_type="toggle", simultaneous_actions=False, **kwargs):
- # Build pyboy game
- self.pyboy = pyboy
- if str(type(pyboy)) != "<class 'pyboy.pyboy.PyBoy'>":
- raise TypeError("pyboy must be a Pyboy object")
-
- # Build game_wrapper
- self.game_wrapper = pyboy.game_wrapper()
- if self.game_wrapper is None:
- raise ValueError(
- "You need to build a game_wrapper to use this function. Otherwise there is no way to build a reward function automaticaly."
- )
- self.last_fitness = self.game_wrapper.fitness
-
- # Building the action_space
- self._DO_NOTHING = WindowEvent.PASS
- self._buttons = [
- WindowEvent.PRESS_ARROW_UP, WindowEvent.PRESS_ARROW_DOWN, WindowEvent.PRESS_ARROW_RIGHT,
- WindowEvent.PRESS_ARROW_LEFT, WindowEvent.PRESS_BUTTON_A, WindowEvent.PRESS_BUTTON_B,
- WindowEvent.PRESS_BUTTON_SELECT, WindowEvent.PRESS_BUTTON_START
- ]
- self._button_is_pressed = {button: False for button in self._buttons}
-
- self._buttons_release = [
- WindowEvent.RELEASE_ARROW_UP, WindowEvent.RELEASE_ARROW_DOWN, WindowEvent.RELEASE_ARROW_RIGHT,
- WindowEvent.RELEASE_ARROW_LEFT, WindowEvent.RELEASE_BUTTON_A, WindowEvent.RELEASE_BUTTON_B,
- WindowEvent.RELEASE_BUTTON_SELECT, WindowEvent.RELEASE_BUTTON_START
- ]
- self._release_button = {button: r_button for button, r_button in zip(self._buttons, self._buttons_release)}
-
- self.actions = [self._DO_NOTHING] + self._buttons
- if action_type == "all":
- self.actions += self._buttons_release
- elif action_type not in ["press", "toggle"]:
- raise ValueError(f"action_type {action_type} is invalid")
- self.action_type = action_type
-
- if simultaneous_actions:
- raise NotImplementedError("Not implemented yet, raise an issue on GitHub if needed")
- else:
- self.action_space = Discrete(len(self.actions))
-
- # Building the observation_space
- if observation_type == "raw":
- screen = np.asarray(self.pyboy.botsupport_manager().screen().screen_ndarray())
- self.observation_space = Box(low=0, high=255, shape=screen.shape, dtype=np.uint8)
- elif observation_type in ["tiles", "compressed", "minimal"]:
- size_ids = TILES
- if observation_type == "compressed":
- try:
- size_ids = np.max(self.game_wrapper.tiles_compressed) + 1
- except AttributeError:
- raise AttributeError(
- "You need to add the tiles_compressed attibute to the game_wrapper to use the compressed observation_type"
- )
- elif observation_type == "minimal":
- try:
- size_ids = np.max(self.game_wrapper.tiles_minimal) + 1
- except AttributeError:
- raise AttributeError(
- "You need to add the tiles_minimal attibute to the game_wrapper to use the minimal observation_type"
- )
- nvec = size_ids * np.ones(self.game_wrapper.shape)
- self.observation_space = MultiDiscrete(nvec)
- else:
- raise NotImplementedError(f"observation_type {observation_type} is invalid")
- self.observation_type = observation_type
-
- self._started = False
- self._kwargs = kwargs
-
- def _get_observation(self):
- if self.observation_type == "raw":
- observation = np.asarray(self.pyboy.botsupport_manager().screen().screen_ndarray(), dtype=np.uint8)
- elif self.observation_type in ["tiles", "compressed", "minimal"]:
- observation = self.game_wrapper._game_area_np(self.observation_type)
- else:
- raise NotImplementedError(f"observation_type {self.observation_type} is invalid")
- return observation
-
- def step(self, action_id):
- info = {}
-
- action = self.actions[action_id]
- if action == self._DO_NOTHING:
- pyboy_done = self.pyboy.tick()
- else:
- if self.action_type == "toggle":
- if self._button_is_pressed[action]:
- self._button_is_pressed[action] = False
- action = self._release_button[action]
- else:
- self._button_is_pressed[action] = True
-
- self.pyboy.send_input(action)
- pyboy_done = self.pyboy.tick()
-
- if self.action_type == "press":
- self.pyboy.send_input(self._release_button[action])
-
- new_fitness = self.game_wrapper.fitness
- reward = new_fitness - self.last_fitness
- self.last_fitness = new_fitness
-
- observation = self._get_observation()
- done = pyboy_done or self.game_wrapper.game_over()
-
- return observation, reward, done, info
-
- def reset(self):
- """ Reset (or start) the gym environment throught the game_wrapper """
- if not self._started:
- self.game_wrapper.start_game(**self._kwargs)
- self._started = True
- else:
- self.game_wrapper.reset_game()
- self.last_fitness = self.game_wrapper.fitness
- self.button_is_pressed = {button: False for button in self._buttons}
- return self._get_observation()
-
- def render(self):
- pass
-
- def close(self):
- self.pyboy.stop(save=False)
This function requires PyBoy to implement a Game Wrapper for the loaded ROM. You can find the supported games in pyboy.plugins.
-Additional kwargs are passed to the start_game method of the game_wrapper.
-
Args
-
-
observation_type : str
-
Define what the agent will be able to see:
-
-
-
"raw": Gives the raw pixels color
-
"tiles":
-Gives the id of the sprites and tiles in 8x8 pixel zones of the game_area.
-
"compressed": Like "tiles" but with slightly simplified id's (i.e. each type of enemy has a unique id).
-
"minimal": Like "compressed" but gives a minimal representation (recommended; i.e. all enemies have the same id).
-
-
-
action_type : str
-
Define how the agent will interact with button inputs
-
-
-
"press": The agent will only press inputs for 1 frame an then release it.
-
"toggle": The agent will toggle inputs, first time it press and second time it release.
-
"all": The agent have access to all inputs, press and release are separated.
-
-
-
simultaneous_actions : bool
-
Allow to inject multiple input at once. This dramatically increases the action_space: n \rightarrow 2^n
-
-
Attributes
-
-
game_wrapper (PyBoyGameWrapper): The game_wrapper of the PyBoy game instance over which the environment is built.
-
action_space : Gym space
-
The action space of the environment.
-
observation_space : Gym space
-
The observation space of the environment (depends of observation_type).
-
actions : list
-
The list of input IDs of allowed input for the agent (depends of action_type).
-
-
-
-Expand source code
-
-
class PyBoyGymEnv(Env):
- """ A gym environement built from a `pyboy.PyBoy`
-
- This function requires PyBoy to implement a Game Wrapper for the loaded ROM. You can find the supported games in pyboy.plugins.
- Additional kwargs are passed to the start_game method of the game_wrapper.
-
- Args:
- observation_type (str): Define what the agent will be able to see:
- * `"raw"`: Gives the raw pixels color
- * `"tiles"`: Gives the id of the sprites and tiles in 8x8 pixel zones of the game_area.
- * `"compressed"`: Like `"tiles"` but with slightly simplified id's (i.e. each type of enemy has a unique id).
- * `"minimal"`: Like `"compressed"` but gives a minimal representation (recommended; i.e. all enemies have the same id).
-
- action_type (str): Define how the agent will interact with button inputs
- * `"press"`: The agent will only press inputs for 1 frame an then release it.
- * `"toggle"`: The agent will toggle inputs, first time it press and second time it release.
- * `"all"`: The agent have access to all inputs, press and release are separated.
-
- simultaneous_actions (bool): Allow to inject multiple input at once. This dramatically increases the action_space: \\(n \\rightarrow 2^n\\)
-
- Attributes:
- game_wrapper (`pyboy.plugins.base_plugin.PyBoyGameWrapper`): The game_wrapper of the PyBoy game instance over which the environment is built.
- action_space (Gym space): The action space of the environment.
- observation_space (Gym space): The observation space of the environment (depends of observation_type).
- actions (list): The list of input IDs of allowed input for the agent (depends of action_type).
-
- """
- def __init__(self, pyboy, observation_type="tiles", action_type="toggle", simultaneous_actions=False, **kwargs):
- # Build pyboy game
- self.pyboy = pyboy
- if str(type(pyboy)) != "<class 'pyboy.pyboy.PyBoy'>":
- raise TypeError("pyboy must be a Pyboy object")
-
- # Build game_wrapper
- self.game_wrapper = pyboy.game_wrapper()
- if self.game_wrapper is None:
- raise ValueError(
- "You need to build a game_wrapper to use this function. Otherwise there is no way to build a reward function automaticaly."
- )
- self.last_fitness = self.game_wrapper.fitness
-
- # Building the action_space
- self._DO_NOTHING = WindowEvent.PASS
- self._buttons = [
- WindowEvent.PRESS_ARROW_UP, WindowEvent.PRESS_ARROW_DOWN, WindowEvent.PRESS_ARROW_RIGHT,
- WindowEvent.PRESS_ARROW_LEFT, WindowEvent.PRESS_BUTTON_A, WindowEvent.PRESS_BUTTON_B,
- WindowEvent.PRESS_BUTTON_SELECT, WindowEvent.PRESS_BUTTON_START
- ]
- self._button_is_pressed = {button: False for button in self._buttons}
-
- self._buttons_release = [
- WindowEvent.RELEASE_ARROW_UP, WindowEvent.RELEASE_ARROW_DOWN, WindowEvent.RELEASE_ARROW_RIGHT,
- WindowEvent.RELEASE_ARROW_LEFT, WindowEvent.RELEASE_BUTTON_A, WindowEvent.RELEASE_BUTTON_B,
- WindowEvent.RELEASE_BUTTON_SELECT, WindowEvent.RELEASE_BUTTON_START
- ]
- self._release_button = {button: r_button for button, r_button in zip(self._buttons, self._buttons_release)}
-
- self.actions = [self._DO_NOTHING] + self._buttons
- if action_type == "all":
- self.actions += self._buttons_release
- elif action_type not in ["press", "toggle"]:
- raise ValueError(f"action_type {action_type} is invalid")
- self.action_type = action_type
-
- if simultaneous_actions:
- raise NotImplementedError("Not implemented yet, raise an issue on GitHub if needed")
- else:
- self.action_space = Discrete(len(self.actions))
-
- # Building the observation_space
- if observation_type == "raw":
- screen = np.asarray(self.pyboy.botsupport_manager().screen().screen_ndarray())
- self.observation_space = Box(low=0, high=255, shape=screen.shape, dtype=np.uint8)
- elif observation_type in ["tiles", "compressed", "minimal"]:
- size_ids = TILES
- if observation_type == "compressed":
- try:
- size_ids = np.max(self.game_wrapper.tiles_compressed) + 1
- except AttributeError:
- raise AttributeError(
- "You need to add the tiles_compressed attibute to the game_wrapper to use the compressed observation_type"
- )
- elif observation_type == "minimal":
- try:
- size_ids = np.max(self.game_wrapper.tiles_minimal) + 1
- except AttributeError:
- raise AttributeError(
- "You need to add the tiles_minimal attibute to the game_wrapper to use the minimal observation_type"
- )
- nvec = size_ids * np.ones(self.game_wrapper.shape)
- self.observation_space = MultiDiscrete(nvec)
- else:
- raise NotImplementedError(f"observation_type {observation_type} is invalid")
- self.observation_type = observation_type
-
- self._started = False
- self._kwargs = kwargs
-
- def _get_observation(self):
- if self.observation_type == "raw":
- observation = np.asarray(self.pyboy.botsupport_manager().screen().screen_ndarray(), dtype=np.uint8)
- elif self.observation_type in ["tiles", "compressed", "minimal"]:
- observation = self.game_wrapper._game_area_np(self.observation_type)
- else:
- raise NotImplementedError(f"observation_type {self.observation_type} is invalid")
- return observation
-
- def step(self, action_id):
- info = {}
-
- action = self.actions[action_id]
- if action == self._DO_NOTHING:
- pyboy_done = self.pyboy.tick()
- else:
- if self.action_type == "toggle":
- if self._button_is_pressed[action]:
- self._button_is_pressed[action] = False
- action = self._release_button[action]
- else:
- self._button_is_pressed[action] = True
-
- self.pyboy.send_input(action)
- pyboy_done = self.pyboy.tick()
-
- if self.action_type == "press":
- self.pyboy.send_input(self._release_button[action])
-
- new_fitness = self.game_wrapper.fitness
- reward = new_fitness - self.last_fitness
- self.last_fitness = new_fitness
-
- observation = self._get_observation()
- done = pyboy_done or self.game_wrapper.game_over()
-
- return observation, reward, done, info
-
- def reset(self):
- """ Reset (or start) the gym environment throught the game_wrapper """
- if not self._started:
- self.game_wrapper.start_game(**self._kwargs)
- self._started = True
- else:
- self.game_wrapper.reset_game()
- self.last_fitness = self.game_wrapper.fitness
- self.button_is_pressed = {button: False for button in self._buttons}
- return self._get_observation()
-
- def render(self):
- pass
-
- def close(self):
- self.pyboy.stop(save=False)
-
-
Ancestors
-
-
gym.core.Env
-
typing.Generic
-
-
Methods
-
-
-def step(self, action_id)
-
-
-
Run one timestep of the environment's dynamics.
-
When end of episode is reached, you are responsible for calling :meth:reset to reset this environment's state.
-Accepts an action and returns either a tuple (observation, reward, terminated, truncated, info).
-
Args
-
-
action : ActType
-
an action provided by the agent
-
-
Returns
-
-
observation (object): this will be an element of the environment's :attr:observation_space.
-
This may, for instance, be a numpy array containing the positions and velocities of certain objects.
-
reward (float): The amount of reward returned as a result of taking the action.
-
terminated (bool): whether a terminal state (as defined under the MDP of the task) is reached.
-
In this case further step() calls could return undefined results.
-
truncated (bool): whether a truncation condition outside the scope of the MDP is satisfied.
-
Typically a timelimit, but could also be used to indicate agent physically going out of bounds.
-
Can be used to end the episode prematurely before a terminal state is reached.
-
info (dictionary): info contains auxiliary diagnostic information (helpful for debugging, learning, and logging).
-
-This might, for instance, contain
-
metrics that describe the agent's performance state, variables that are
-hidden from observations, or individual reward terms that are combined to produce the total reward.
-It also can contain information that distinguishes truncation and termination, however this is deprecated in favour
-of returning two booleans, and will be removed in a future version.
-
(deprecated)
-
done (bool): A boolean value for if the episode has ended, in which case further :meth:step calls will return undefined results.
-
-A done signal may be emitted for different reasons
-
Maybe the task underlying the environment was solved successfully,
-a certain timelimit was exceeded, or the physics simulation has entered an invalid state.
Reset (or start) the gym environment throught the game_wrapper
-
-
-Expand source code
-
-
def reset(self):
- """ Reset (or start) the gym environment throught the game_wrapper """
- if not self._started:
- self.game_wrapper.start_game(**self._kwargs)
- self._started = True
- else:
- self.game_wrapper.reset_game()
- self.last_fitness = self.game_wrapper.fitness
- self.button_is_pressed = {button: False for button in self._buttons}
- return self._get_observation()
-
-
-
-def render(self)
-
-
-
Compute the render frames as specified by render_mode attribute during initialization of the environment.
-
The set of supported modes varies per environment. (And some
-third-party environments may not support rendering at all.)
-By convention, if render_mode is:
-
-
None (default): no render is computed.
-
human: render return None.
-The environment is continuously rendered in the current display or terminal. Usually for human consumption.
-
rgb_array: return a single frame representing the current state of the environment.
-A frame is a numpy.ndarray with shape (x, y, 3) representing RGB values for an x-by-y pixel image.
-
rgb_array_list: return a list of frames representing the states of the environment since the last reset.
-Each frame is a numpy.ndarray with shape (x, y, 3), as with rgb_array.
-
ansi: Return a strings (str) or StringIO.StringIO containing a
-terminal-style text representation for each time step.
-The text can include newlines and ANSI escape sequences (e.g. for colors).
-
-
Note
-
Make sure that your class's metadata 'render_modes' key includes
-the list of supported modes. It's recommended to call super()
-in implementations to use the functionality of this method.
-
-
-Expand source code
-
-
def render(self):
- pass
-
-
-
-def close(self)
-
-
-
Override close in your subclass to perform any necessary cleanup.
-
Environments will automatically :meth:close() themselves when
-garbage collected or when the program exits.
-
-
-Expand source code
-
-
def close(self):
- self.pyboy.stop(save=False)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/plugins/base_plugin.html b/docs/plugins/base_plugin.html
index 260852b8c..43575932b 100644
--- a/docs/plugins/base_plugin.html
+++ b/docs/plugins/base_plugin.html
@@ -39,14 +39,15 @@
"""
if not self.pyboy.frame_count == 0:
- logger.warning("Calling start_game from an already running game. This might not work.")
+ logger.warning("Calling start_game from an already running game. This might not work.")
+ self.game_has_started = True
+ self.saved_state.seek(0)
+ self.pyboy.save_state(self.saved_state)
@@ -621,7 +717,7 @@
Returns
memoryview:
Simplified 2-dimensional memoryview of the screen
"""
- tiles_matrix = self._game_area_tiles()
+ tiles_matrix = self.mapping[self._game_area_tiles()]
sprites = self._sprites_on_screen()
xx = self.game_area_section[0]
yy = self.game_area_section[1]
@@ -631,11 +727,25 @@
Returns
_x = (s.x // 8) - xx
_y = (s.y // 8) - yy
if 0 <= _y < height and 0 <= _x < width:
- tiles_matrix[_y][
- _x] = s.tile_identifier + self.sprite_offset # Adding offset to try to seperate sprites from tiles
+ tiles_matrix[_y][_x] = self.mapping[
+ s.tile_identifier] + self.sprite_offset # Adding offset to try to seperate sprites from tiles
return tiles_matrix
+
This class wraps Kirby Dream Land, and provides easy access to score and a "fitness" score for AIs.
+
This class wraps Kirby Dream Land, and provides easy access for AIs.
If you call print on an instance of this object, it will show an overview of everything this object provides.
@@ -240,15 +200,13 @@
Classes
class GameWrapperKirbyDreamLand(PyBoyGameWrapper):
"""
- This class wraps Kirby Dream Land, and provides easy access to score and a "fitness" score for AIs.
+ This class wraps Kirby Dream Land, and provides easy access for AIs.
If you call `print` on an instance of this object, it will show an overview of everything this object provides.
"""
cartridge_title = "KIRBY DREAM LA"
def __init__(self, *args, **kwargs):
- self.shape = (20, 16)
- """The shape of the game area"""
self.score = 0
"""The score provided by the game"""
self.health = 0
@@ -257,35 +215,26 @@
Classes
"""The lives remaining provided by the game"""
self._game_over = False
"""The game over state"""
- self.fitness = 0
- """
- A built-in fitness scoring. Taking score, health, and lives left into account.
- .. math::
- fitness = score \\cdot health \\cdot lives\\_left
- """
- super().__init__(*args, game_area_section=(0, 0) + self.shape, game_area_wrap_around=True, **kwargs)
+ super().__init__(*args, game_area_section=(0, 0, 20, 16), game_area_follow_scxy=True, **kwargs)
def post_tick(self):
self._tile_cache_invalid = True
self._sprite_cache_invalid = True
self.score = 0
- score_digits = 5
+ score_digits = 4
for n in range(score_digits):
- self.score += self.pyboy.get_memory_value(0xD06F + n) * 10**(score_digits-n)
+ self.score += self.pyboy.memory[0xD070 + n] * 10**(score_digits - n)
# Check if game is over
prev_health = self.health
- self.health = self.pyboy.get_memory_value(0xD086)
+ self.health = self.pyboy.memory[0xD086]
if self.lives_left == 0:
if prev_health > 0 and self.health == 0:
self._game_over = True
- self.lives_left = self.pyboy.get_memory_value(0xD089) - 1
-
- if self.game_has_started:
- self.fitness = self.score * self.health * self.lives_left
+ self.lives_left = self.pyboy.memory[0xD089] - 1
def start_game(self, timer_div=None):
"""
@@ -296,39 +245,30 @@
Classes
instantly.
Kwargs:
- timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
+ * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
"""
PyBoyGameWrapper.start_game(self, timer_div=timer_div)
# Boot screen
while True:
- self.pyboy.tick()
- self.tilemap_background.refresh_lcdc()
+ self.pyboy.tick(1, False)
if self.tilemap_background[0:3, 16] == [231, 224, 235]: # 'HAL' on the first screen
break
# Wait for transition to finish (start screen)
- for _ in range(25):
- self.pyboy.tick()
-
- self.pyboy.send_input(WindowEvent.PRESS_BUTTON_START)
+ self.pyboy.tick(25, False)
+ self.pyboy.button("start")
self.pyboy.tick()
- self.pyboy.send_input(WindowEvent.RELEASE_BUTTON_START)
# Wait for transition to finish (exit start screen, enter level intro screen)
- for _ in range(60):
- self.pyboy.tick()
+ self.pyboy.tick(60, False)
# Skip level intro
- self.pyboy.send_input(WindowEvent.PRESS_BUTTON_START)
+ self.pyboy.button("start")
self.pyboy.tick()
- self.pyboy.send_input(WindowEvent.RELEASE_BUTTON_START)
# Wait for transition to finish (exit level intro screen, enter game)
- for _ in range(60):
- self.pyboy.tick()
-
- self.game_has_started = True
+ self.pyboy.tick(60, False)
self.saved_state.seek(0)
self.pyboy.save_state(self.saved_state)
@@ -340,7 +280,7 @@
Classes
After calling `start_game`, you can call this method at any time to reset the game.
Kwargs:
- timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
+ * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
"""
PyBoyGameWrapper.reset_game(self, timer_div=timer_div)
@@ -382,27 +322,13 @@
Classes
return self._game_over
def __repr__(self):
- adjust = 4
# yapf: disable
return (
f"Kirby Dream Land:\n" +
f"Score: {self.score}\n" +
f"Health: {self.health}\n" +
f"Lives left: {self.lives_left}\n" +
- f"Fitness: {self.fitness}\n" +
- "Sprites on screen:\n" +
- "\n".join([str(s) for s in self._sprites_on_screen()]) +
- "\n" +
- "Tiles on screen:\n" +
- " "*5 + "".join([f"{i: <4}" for i in range(10)]) + "\n" +
- "_"*(adjust*20+4) +
- "\n" +
- "\n".join(
- [
- f"{i: <3}| " + "".join([str(tile).ljust(adjust) for tile in line])
- for i, line in enumerate(self.game_area())
- ]
- )
+ super().__repr__()
)
# yapf: enable
@@ -413,10 +339,6 @@
Ancestors
Instance variables
-
var shape
-
-
The shape of the game area
-
var score
The score provided by the game
@@ -429,11 +351,6 @@
Instance variables
The lives remaining provided by the game
-
var fitness
-
-
A built-in fitness scoring. Taking score, health, and lives left into account.
-
fitness = score \cdot health \cdot lives\_left
-
Methods
@@ -446,7 +363,9 @@
Methods
The state of the emulator is saved, and using reset_game, you can get back to this point of the game
instantly.
Kwargs
-
timer_div (int): Replace timer's DIV register with this value. Use None to randomize.
+
+
timer_div (int): Replace timer's DIV register with this value. Use None to randomize.
+
Expand source code
@@ -460,39 +379,30 @@
Kwargs
instantly.
Kwargs:
- timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
+ * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
"""
PyBoyGameWrapper.start_game(self, timer_div=timer_div)
# Boot screen
while True:
- self.pyboy.tick()
- self.tilemap_background.refresh_lcdc()
+ self.pyboy.tick(1, False)
if self.tilemap_background[0:3, 16] == [231, 224, 235]: # 'HAL' on the first screen
break
# Wait for transition to finish (start screen)
- for _ in range(25):
- self.pyboy.tick()
-
- self.pyboy.send_input(WindowEvent.PRESS_BUTTON_START)
+ self.pyboy.tick(25, False)
+ self.pyboy.button("start")
self.pyboy.tick()
- self.pyboy.send_input(WindowEvent.RELEASE_BUTTON_START)
# Wait for transition to finish (exit start screen, enter level intro screen)
- for _ in range(60):
- self.pyboy.tick()
+ self.pyboy.tick(60, False)
# Skip level intro
- self.pyboy.send_input(WindowEvent.PRESS_BUTTON_START)
+ self.pyboy.button("start")
self.pyboy.tick()
- self.pyboy.send_input(WindowEvent.RELEASE_BUTTON_START)
# Wait for transition to finish (exit level intro screen, enter game)
- for _ in range(60):
- self.pyboy.tick()
-
- self.game_has_started = True
+ self.pyboy.tick(60, False)
self.saved_state.seek(0)
self.pyboy.save_state(self.saved_state)
@@ -506,7 +416,9 @@
Kwargs
After calling start_game, you can call this method at any time to reset the game.
Kwargs
-
timer_div (int): Replace timer's DIV register with this value. Use None to randomize.
+
+
timer_div (int): Replace timer's DIV register with this value. Use None to randomize.
+
Expand source code
@@ -516,7 +428,7 @@
Kwargs
After calling `start_game`, you can call this method at any time to reset the game.
Kwargs:
- timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
+ * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
"""
PyBoyGameWrapper.reset_game(self, timer_div=timer_div)
@@ -595,6 +507,8 @@
if 0 <= amount <= 99:
tens = amount // 10
ones = amount % 10
- self.pyboy.set_memory_value(ADDR_LIVES_LEFT, (tens << 4) | ones)
- self.pyboy.set_memory_value(ADDR_LIVES_LEFT_DISPLAY, tens)
- self.pyboy.set_memory_value(ADDR_LIVES_LEFT_DISPLAY + 1, ones)
+ self.pyboy.memory[ADDR_LIVES_LEFT] = (tens << 4) | ones
+ self.pyboy.memory[ADDR_LIVES_LEFT_DISPLAY] = tens
+ self.pyboy.memory[ADDR_LIVES_LEFT_DISPLAY + 1] = ones
else:
- logger.error(f"{amount} is out of bounds. Only values between 0 and 99 allowed.")
+ logger.error("%d is out of bounds. Only values between 0 and 99 allowed.", amount)
def set_world_level(self, world, level):
"""
Patches the handler for pressing start in the menu. It hardcodes a world and level to always "continue" from.
+ Example:
+ ```python
+ >>> pyboy = PyBoy(supermarioland_rom)
+ >>> pyboy.game_wrapper.set_world_level(3, 2)
+ >>> pyboy.game_wrapper.start_game()
+ >>> pyboy.game_wrapper.world
+ (3, 2)
+ ```
+
Args:
world (int): The world to select a level from, 0-3
level (int): The level to start from, 0-2
"""
for i in range(0x450, 0x461):
- self.pyboy.override_memory_value(0, i, 0x00)
+ self.pyboy.memory[0, i] = 0x00
patch1 = [
0x3E, # LD A, d8
@@ -492,7 +843,7 @@
Classes
]
for i, byte in enumerate(patch1):
- self.pyboy.override_memory_value(0, 0x451 + i, byte)
+ self.pyboy.memory[0, 0x451 + i] = byte
def start_game(self, timer_div=None, world_level=None, unlock_level_select=False):
"""
@@ -508,10 +859,18 @@
Classes
If you're not using the game wrapper for unattended use, you can unlock the level selector for the main menu.
Enabling the selector, will make this function return before entering the game.
+ Example:
+ ```python
+ >>> pyboy = PyBoy(supermarioland_rom)
+ >>> pyboy.game_wrapper.start_game(world_level=(4,1))
+ >>> pyboy.game_wrapper.world
+ (4, 1)
+ ```
+
Kwargs:
- timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
- world_level (tuple): (world, level) to start the game from
- unlock_level_select (bool): Unlock level selector menu
+ * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
+ * world_level (tuple): (world, level) to start the game from
+ * unlock_level_select (bool): Unlock level selector menu
"""
PyBoyGameWrapper.start_game(self, timer_div=timer_div)
@@ -520,28 +879,24 @@
Classes
# Boot screen
while True:
- self.pyboy.tick()
+ self.pyboy.tick(1, False)
if self.tilemap_background[6:11, 13] == [284, 285, 266, 283, 285]: # "START" on the main menu
break
- self.pyboy.tick()
- self.pyboy.tick()
- self.pyboy.tick()
- self.pyboy.send_input(WindowEvent.PRESS_BUTTON_START)
- self.pyboy.tick()
- self.pyboy.send_input(WindowEvent.RELEASE_BUTTON_START)
+ self.pyboy.tick(3, False)
+ self.pyboy.button("start")
+ self.pyboy.tick(1, False)
while True:
if unlock_level_select and self.pyboy.frame_count == 71: # An arbitrary frame count, where the write will work
- self.pyboy.set_memory_value(ADDR_WIN_COUNT, 2 if unlock_level_select else 0)
+ self.pyboy.memory[ADDR_WIN_COUNT] = 2 if unlock_level_select else 0
break
- self.pyboy.tick()
- self.tilemap_background.refresh_lcdc()
+ self.pyboy.tick(1, False)
# "MARIO" in the title bar and 0 is placed at score
if self.tilemap_background[0:5, 0] == [278, 266, 283, 274, 280] and \
self.tilemap_background[5, 1] == 256:
- self.game_has_started = True
+ # Game has started
break
self.saved_state.seek(0)
@@ -551,13 +906,20 @@
Classes
def reset_game(self, timer_div=None):
"""
- After calling `start_game`, use this method to reset Mario to the beginning of world 1-1.
+ After calling `start_game`, use this method to reset Mario to the beginning of `start_game`.
- If you want to reset to later parts of the game -- for example world 1-2 or 3-1 -- use the methods
- `pyboy.PyBoy.save_state` and `pyboy.PyBoy.load_state`.
+ If you want to reset to other worlds or levels of the game -- for example world 1-2 or 3-1 -- reset the entire
+ emulator and provide the `world_level` on `start_game`.
+
+ Example:
+ ```python
+ >>> pyboy = PyBoy(supermarioland_rom)
+ >>> pyboy.game_wrapper.start_game()
+ >>> pyboy.game_wrapper.reset_game()
+ ```
Kwargs:
- timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
+ * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
"""
PyBoyGameWrapper.reset_game(self, timer_div=timer_div)
@@ -571,26 +933,28 @@
def game_over(self):
# Apparantly that address is for game over
# https://datacrystal.romhacking.net/wiki/Super_Mario_Land:RAM_map
- return self.pyboy.get_memory_value(0xC0A4) == 0x39
+ return self.pyboy.memory[0xC0A4] == 0x39
def __repr__(self):
- adjust = 4
# yapf: disable
return (
- f"Super Mario Land: World {'-'.join([str(i) for i in self.world])}\n" +
+ f"Super Mario Land: World {'-'.join([str(i) for i in self.world])}\n"
f"Coins: {self.coins}\n" +
f"lives_left: {self.lives_left}\n" +
f"Score: {self.score}\n" +
f"Time left: {self.time_left}\n" +
f"Level progress: {self.level_progress}\n" +
- f"Fitness: {self.fitness}\n" +
- "Sprites on screen:\n" +
- "\n".join([str(s) for s in self._sprites_on_screen()]) +
- "\n" +
- "Tiles on screen:\n" +
- " "*5 + "".join([f"{i: <4}" for i in range(20)]) + "\n" +
- "_"*(adjust*20+4) +
- "\n" +
- "\n".join(
- [
- f"{i: <3}| " + "".join([str(tile).ljust(adjust) for tile in line])
- for i, line in enumerate(self.game_area())
- ]
- )
+ super().__repr__()
)
# yapf: enable
This should only be called when the game has started.
+ Example:
+ ```python
+ >>> pyboy = PyBoy(supermarioland_rom)
+ >>> pyboy.game_wrapper.start_game()
+ >>> pyboy.game_wrapper.lives_left
+ 2
+ >>> pyboy.game_wrapper.set_lives_left(10)
+ >>> pyboy.tick(1, False)
+ True
+ >>> pyboy.game_wrapper.lives_left
+ 10
+ ```
+
Args:
amount (int): The wanted number of lives
"""
@@ -716,11 +1162,11 @@
Args
if 0 <= amount <= 99:
tens = amount // 10
ones = amount % 10
- self.pyboy.set_memory_value(ADDR_LIVES_LEFT, (tens << 4) | ones)
- self.pyboy.set_memory_value(ADDR_LIVES_LEFT_DISPLAY, tens)
- self.pyboy.set_memory_value(ADDR_LIVES_LEFT_DISPLAY + 1, ones)
+ self.pyboy.memory[ADDR_LIVES_LEFT] = (tens << 4) | ones
+ self.pyboy.memory[ADDR_LIVES_LEFT_DISPLAY] = tens
+ self.pyboy.memory[ADDR_LIVES_LEFT_DISPLAY + 1] = ones
else:
- logger.error(f"{amount} is out of bounds. Only values between 0 and 99 allowed.")
+ logger.error("%d is out of bounds. Only values between 0 and 99 allowed.", amount)
@@ -728,6 +1174,13 @@
Args
Patches the handler for pressing start in the menu. It hardcodes a world and level to always "continue" from.
"""
Patches the handler for pressing start in the menu. It hardcodes a world and level to always "continue" from.
+ Example:
+ ```python
+ >>> pyboy = PyBoy(supermarioland_rom)
+ >>> pyboy.game_wrapper.set_world_level(3, 2)
+ >>> pyboy.game_wrapper.start_game()
+ >>> pyboy.game_wrapper.world
+ (3, 2)
+ ```
+
Args:
world (int): The world to select a level from, 0-3
level (int): The level to start from, 0-2
"""
for i in range(0x450, 0x461):
- self.pyboy.override_memory_value(0, i, 0x00)
+ self.pyboy.memory[0, i] = 0x00
patch1 = [
0x3E, # LD A, d8
@@ -757,7 +1219,7 @@
Args
]
for i, byte in enumerate(patch1):
- self.pyboy.override_memory_value(0, 0x451 + i, byte)
+ self.pyboy.memory[0, 0x451 + i] = byte
@@ -772,10 +1234,18 @@
Args
the optional keyword-argument world_level.
If you're not using the game wrapper for unattended use, you can unlock the level selector for the main menu.
Enabling the selector, will make this function return before entering the game.
timer_div (int): Replace timer's DIV register with this value. Use None to randomize.
-world_level (tuple): (world, level) to start the game from
-unlock_level_select (bool): Unlock level selector menu
+
+
timer_div (int): Replace timer's DIV register with this value. Use None to randomize.
+
world_level (tuple): (world, level) to start the game from
+
unlock_level_select (bool): Unlock level selector menu
+
Expand source code
@@ -794,10 +1264,18 @@
Kwargs
If you're not using the game wrapper for unattended use, you can unlock the level selector for the main menu.
Enabling the selector, will make this function return before entering the game.
+ Example:
+ ```python
+ >>> pyboy = PyBoy(supermarioland_rom)
+ >>> pyboy.game_wrapper.start_game(world_level=(4,1))
+ >>> pyboy.game_wrapper.world
+ (4, 1)
+ ```
+
Kwargs:
- timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
- world_level (tuple): (world, level) to start the game from
- unlock_level_select (bool): Unlock level selector menu
+ * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
+ * world_level (tuple): (world, level) to start the game from
+ * unlock_level_select (bool): Unlock level selector menu
"""
PyBoyGameWrapper.start_game(self, timer_div=timer_div)
@@ -806,28 +1284,24 @@
Kwargs
# Boot screen
while True:
- self.pyboy.tick()
+ self.pyboy.tick(1, False)
if self.tilemap_background[6:11, 13] == [284, 285, 266, 283, 285]: # "START" on the main menu
break
- self.pyboy.tick()
- self.pyboy.tick()
- self.pyboy.tick()
- self.pyboy.send_input(WindowEvent.PRESS_BUTTON_START)
- self.pyboy.tick()
- self.pyboy.send_input(WindowEvent.RELEASE_BUTTON_START)
+ self.pyboy.tick(3, False)
+ self.pyboy.button("start")
+ self.pyboy.tick(1, False)
while True:
if unlock_level_select and self.pyboy.frame_count == 71: # An arbitrary frame count, where the write will work
- self.pyboy.set_memory_value(ADDR_WIN_COUNT, 2 if unlock_level_select else 0)
+ self.pyboy.memory[ADDR_WIN_COUNT] = 2 if unlock_level_select else 0
break
- self.pyboy.tick()
- self.tilemap_background.refresh_lcdc()
+ self.pyboy.tick(1, False)
# "MARIO" in the title bar and 0 is placed at score
if self.tilemap_background[0:5, 0] == [278, 266, 283, 274, 280] and \
self.tilemap_background[5, 1] == 256:
- self.game_has_started = True
+ # Game has started
break
self.saved_state.seek(0)
@@ -840,24 +1314,38 @@
Kwargs
def reset_game(self, timer_div=None)
-
After calling start_game, use this method to reset Mario to the beginning of world 1-1.
After calling start_game, use this method to reset Mario to the beginning of start_game.
+
If you want to reset to other worlds or levels of the game – for example world 1-2 or 3-1 – reset the entire
+emulator and provide the world_level on start_game.
timer_div (int): Replace timer's DIV register with this value. Use None to randomize.
+
+
timer_div (int): Replace timer's DIV register with this value. Use None to randomize.
+
Expand source code
def reset_game(self, timer_div=None):
"""
- After calling `start_game`, use this method to reset Mario to the beginning of world 1-1.
+ After calling `start_game`, use this method to reset Mario to the beginning of `start_game`.
+
+ If you want to reset to other worlds or levels of the game -- for example world 1-2 or 3-1 -- reset the entire
+ emulator and provide the `world_level` on `start_game`.
- If you want to reset to later parts of the game -- for example world 1-2 or 3-1 -- use the methods
- `pyboy.PyBoy.save_state` and `pyboy.PyBoy.load_state`.
+ Example:
+ ```python
+ >>> pyboy = PyBoy(supermarioland_rom)
+ >>> pyboy.game_wrapper.start_game()
+ >>> pyboy.game_wrapper.reset_game()
+ ```
Kwargs:
- timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
+ * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
"""
PyBoyGameWrapper.reset_game(self, timer_div=timer_div)
@@ -872,25 +1360,27 @@
Kwargs
machine learning applications.
In Super Mario Land, this is almost the entire screen, expect for the top part showing the score, lives left
and so on. These values can be found in the variables of this class.
-
In this example, Mario is 0, 1, 16 and 17. He is standing on the ground which is 352 and 353:
"GameWrapperTetris.post_tick": False,
}
-import logging
from array import array
import numpy as np
+
+import pyboy
from pyboy.utils import WindowEvent
from .base_plugin import PyBoyGameWrapper
-logger = logging.getLogger(__name__)
-
-try:
- from cython import compiled
- cythonmode = compiled
-except ImportError:
- cythonmode = False
+logger = pyboy.logging.get_logger(__name__)
# Table for translating game-representation of Tetromino types (8-bit int) to string
tetromino_table = {
@@ -68,57 +63,49 @@
Module pyboy.plugins.game_wrapper_tetris
TILES = 384
# Compressed assigns an ID to each Tetromino type
-tiles_compressed = np.zeros(TILES, dtype=np.uint8)
+mapping_compressed = np.zeros(TILES, dtype=np.uint8)
# BLANK, J, Z, O, L, T, S, I, BLACK
tiles_types = [[47], [129], [130], [131], [132], [133], [134], [128, 136, 137, 138, 139, 143], [135]]
for tiles_type_ID, tiles_type in enumerate(tiles_types):
for tile_ID in tiles_type:
- tiles_compressed[tile_ID] = tiles_type_ID
+ mapping_compressed[tile_ID] = tiles_type_ID
# Minimal has 3 id's: Background, Tetromino and "losing tile" (which fills the board when losing)
-tiles_minimal = np.ones(TILES, dtype=np.uint8) # For minimal everything is 1
-tiles_minimal[47] = 0 # Except BLANK which is 0
-tiles_minimal[135] = 2 # And background losing tiles BLACK which is 2
+mapping_minimal = np.ones(TILES, dtype=np.uint8) # For minimal everything is 1
+mapping_minimal[47] = 0 # Except BLANK which is 0
+mapping_minimal[135] = 2 # And background losing tiles BLACK which is 2
class GameWrapperTetris(PyBoyGameWrapper):
"""
- This class wraps Tetris, and provides easy access to score, lines, level and a "fitness" score for AIs.
+ This class wraps Tetris, and provides easy access to score, lines and level for AIs.
If you call `print` on an instance of this object, it will show an overview of everything this object provides.
"""
cartridge_title = "TETRIS"
- tiles_compressed = tiles_compressed
- tiles_minimal = tiles_minimal
-
+ mapping_compressed = mapping_compressed
+ """
+ Compressed mapping for `pyboy.PyBoy.game_area_mapping`
+ """
+ mapping_minimal = mapping_minimal
+ """
+ Minimal mapping for `pyboy.PyBoy.game_area_mapping`
+ """
def __init__(self, *args, **kwargs):
- self.shape = (10, 18)
- """The shape of the game area"""
self.score = 0
"""The score provided by the game"""
self.level = 0
"""The current level"""
self.lines = 0
"""The number of cleared lines"""
- self.fitness = 0
- """
- A built-in fitness scoring. The scoring is equals to `score`.
-
- .. math::
- fitness = score
- """
- super().__init__(*args, **kwargs)
- ROWS, COLS = self.shape
- self._cached_game_area_tiles_raw = array("B", [0xFF] * (ROWS*COLS*4))
+ # super().__init__(*args, **kwargs)
- if cythonmode:
- self._cached_game_area_tiles = memoryview(self._cached_game_area_tiles_raw).cast("I", shape=(ROWS, COLS))
- else:
- v = memoryview(self._cached_game_area_tiles_raw).cast("I")
- self._cached_game_area_tiles = [v[i:i + COLS] for i in range(0, COLS * ROWS, COLS)]
+ # ROWS, COLS = self.shape
+ # self._cached_game_area_tiles_raw = array("B", [0xFF] * (ROWS*COLS*4))
+ # self._cached_game_area_tiles = memoryview(self._cached_game_area_tiles_raw).cast("I", shape=(ROWS, COLS))
- super().__init__(*args, game_area_section=(2, 0) + self.shape, game_area_wrap_around=True, **kwargs)
+ super().__init__(*args, game_area_section=(2, 0, 10, 18), game_area_follow_scxy=False, **kwargs)
def _game_area_tiles(self):
if self._tile_cache_invalid:
@@ -131,14 +118,10 @@
Module pyboy.plugins.game_wrapper_tetris
self._sprite_cache_invalid = True
blank = 47
- self.tilemap_background.refresh_lcdc()
self.score = self._sum_number_on_screen(13, 3, 6, blank, 0)
self.level = self._sum_number_on_screen(14, 7, 4, blank, 0)
self.lines = self._sum_number_on_screen(14, 10, 4, blank, 0)
- if self.game_has_started:
- self.fitness = self.score
-
def start_game(self, timer_div=None):
"""
Call this function right after initializing PyBoy. This will navigate through menus to start the game at the
@@ -148,32 +131,25 @@
Module pyboy.plugins.game_wrapper_tetris
instantly.
Kwargs:
- timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
+ * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
"""
# We don't supply the timer_div arg here, as it won't have the desired effect
PyBoyGameWrapper.start_game(self)
# Boot screen
while True:
- self.pyboy.tick()
- self.tilemap_background.refresh_lcdc()
+ self.pyboy.tick(1, False)
if self.tilemap_background[2:9, 14] == [89, 25, 21, 10, 34, 14, 27]: # '1PLAYER' on the first screen
break
# Start game. Just press Start when the game allows us.
for i in range(2):
- self.pyboy.send_input(WindowEvent.PRESS_BUTTON_START)
- self.pyboy.tick()
- self.pyboy.send_input(WindowEvent.RELEASE_BUTTON_START)
-
- for _ in range(6):
- self.pyboy.tick()
+ self.pyboy.button("start")
+ self.pyboy.tick(7, False)
self.saved_state.seek(0)
self.pyboy.save_state(self.saved_state)
- self.game_has_started = True
-
self.reset_game(timer_div=timer_div)
def reset_game(self, timer_div=None):
@@ -181,18 +157,13 @@
Module pyboy.plugins.game_wrapper_tetris
After calling `start_game`, you can call this method at any time to reset the game.
Kwargs:
- timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
+ * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
"""
PyBoyGameWrapper.reset_game(self, timer_div=timer_div)
self._set_timer_div(timer_div)
-
- self.pyboy.send_input(WindowEvent.PRESS_BUTTON_START)
- self.pyboy.tick()
- self.pyboy.send_input(WindowEvent.RELEASE_BUTTON_START)
-
- for _ in range(6):
- self.pyboy.tick()
+ self.pyboy.button("start")
+ self.pyboy.tick(7, False)
def game_area(self):
"""
@@ -252,7 +223,7 @@
Module pyboy.plugins.game_wrapper_tetris
* `"T"`: T-shape
"""
# Bitmask, as the last two bits determine the direction
- return inverse_tetromino_table[self.pyboy.get_memory_value(NEXT_TETROMINO_ADDR) & 0b11111100]
+ return inverse_tetromino_table[self.pyboy.memory[NEXT_TETROMINO_ADDR] & 0b11111100]
def set_tetromino(self, shape):
"""
@@ -291,7 +262,7 @@
Module pyboy.plugins.game_wrapper_tetris
]
for i, byte in enumerate(patch1):
- self.pyboy.override_memory_value(0, 0x206E + i, byte)
+ self.pyboy.memory[0, 0x206E + i] = byte
patch2 = [
0x3E, # LD A, Tetromino
@@ -299,7 +270,7 @@
Module pyboy.plugins.game_wrapper_tetris
]
for i, byte in enumerate(patch2):
- self.pyboy.override_memory_value(0, 0x20B0 + i, byte)
+ self.pyboy.memory[0, 0x20B0 + i] = byte
def game_over(self):
"""
@@ -317,20 +288,7 @@
Module pyboy.plugins.game_wrapper_tetris
f"Score: {self.score}\n" +
f"Level: {self.level}\n" +
f"Lines: {self.lines}\n" +
- f"Fitness: {self.fitness}\n" +
- "Sprites on screen:\n" +
- "\n".join([str(s) for s in self._sprites_on_screen()]) +
- "\n" +
- "Tiles on screen:\n" +
- " "*5 + "".join([f"{i: <4}" for i in range(10)]) + "\n" +
- "_"*(adjust*10+4) +
- "\n" +
- "\n".join(
- [
- f"{i: <3}| " + "".join([str(tile).ljust(adjust) for tile in line])
- for i, line in enumerate(self._game_area_np())
- ]
- )
+ super().__repr__()
)
# yapf: enable
@@ -349,7 +307,7 @@
Classes
(*args, **kwargs)
-
This class wraps Tetris, and provides easy access to score, lines, level and a "fitness" score for AIs.
+
This class wraps Tetris, and provides easy access to score, lines and level for AIs.
If you call print on an instance of this object, it will show an overview of everything this object provides.
@@ -357,42 +315,34 @@
Classes
class GameWrapperTetris(PyBoyGameWrapper):
"""
- This class wraps Tetris, and provides easy access to score, lines, level and a "fitness" score for AIs.
+ This class wraps Tetris, and provides easy access to score, lines and level for AIs.
If you call `print` on an instance of this object, it will show an overview of everything this object provides.
"""
cartridge_title = "TETRIS"
- tiles_compressed = tiles_compressed
- tiles_minimal = tiles_minimal
-
+ mapping_compressed = mapping_compressed
+ """
+ Compressed mapping for `pyboy.PyBoy.game_area_mapping`
+ """
+ mapping_minimal = mapping_minimal
+ """
+ Minimal mapping for `pyboy.PyBoy.game_area_mapping`
+ """
def __init__(self, *args, **kwargs):
- self.shape = (10, 18)
- """The shape of the game area"""
self.score = 0
"""The score provided by the game"""
self.level = 0
"""The current level"""
self.lines = 0
"""The number of cleared lines"""
- self.fitness = 0
- """
- A built-in fitness scoring. The scoring is equals to `score`.
-
- .. math::
- fitness = score
- """
- super().__init__(*args, **kwargs)
- ROWS, COLS = self.shape
- self._cached_game_area_tiles_raw = array("B", [0xFF] * (ROWS*COLS*4))
+ # super().__init__(*args, **kwargs)
- if cythonmode:
- self._cached_game_area_tiles = memoryview(self._cached_game_area_tiles_raw).cast("I", shape=(ROWS, COLS))
- else:
- v = memoryview(self._cached_game_area_tiles_raw).cast("I")
- self._cached_game_area_tiles = [v[i:i + COLS] for i in range(0, COLS * ROWS, COLS)]
+ # ROWS, COLS = self.shape
+ # self._cached_game_area_tiles_raw = array("B", [0xFF] * (ROWS*COLS*4))
+ # self._cached_game_area_tiles = memoryview(self._cached_game_area_tiles_raw).cast("I", shape=(ROWS, COLS))
- super().__init__(*args, game_area_section=(2, 0) + self.shape, game_area_wrap_around=True, **kwargs)
+ super().__init__(*args, game_area_section=(2, 0, 10, 18), game_area_follow_scxy=False, **kwargs)
def _game_area_tiles(self):
if self._tile_cache_invalid:
@@ -405,14 +355,10 @@
Classes
self._sprite_cache_invalid = True
blank = 47
- self.tilemap_background.refresh_lcdc()
self.score = self._sum_number_on_screen(13, 3, 6, blank, 0)
self.level = self._sum_number_on_screen(14, 7, 4, blank, 0)
self.lines = self._sum_number_on_screen(14, 10, 4, blank, 0)
- if self.game_has_started:
- self.fitness = self.score
-
def start_game(self, timer_div=None):
"""
Call this function right after initializing PyBoy. This will navigate through menus to start the game at the
@@ -422,32 +368,25 @@
Classes
instantly.
Kwargs:
- timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
+ * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
"""
# We don't supply the timer_div arg here, as it won't have the desired effect
PyBoyGameWrapper.start_game(self)
# Boot screen
while True:
- self.pyboy.tick()
- self.tilemap_background.refresh_lcdc()
+ self.pyboy.tick(1, False)
if self.tilemap_background[2:9, 14] == [89, 25, 21, 10, 34, 14, 27]: # '1PLAYER' on the first screen
break
# Start game. Just press Start when the game allows us.
for i in range(2):
- self.pyboy.send_input(WindowEvent.PRESS_BUTTON_START)
- self.pyboy.tick()
- self.pyboy.send_input(WindowEvent.RELEASE_BUTTON_START)
-
- for _ in range(6):
- self.pyboy.tick()
+ self.pyboy.button("start")
+ self.pyboy.tick(7, False)
self.saved_state.seek(0)
self.pyboy.save_state(self.saved_state)
- self.game_has_started = True
-
self.reset_game(timer_div=timer_div)
def reset_game(self, timer_div=None):
@@ -455,18 +394,13 @@
Classes
After calling `start_game`, you can call this method at any time to reset the game.
Kwargs:
- timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
+ * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
"""
PyBoyGameWrapper.reset_game(self, timer_div=timer_div)
self._set_timer_div(timer_div)
-
- self.pyboy.send_input(WindowEvent.PRESS_BUTTON_START)
- self.pyboy.tick()
- self.pyboy.send_input(WindowEvent.RELEASE_BUTTON_START)
-
- for _ in range(6):
- self.pyboy.tick()
+ self.pyboy.button("start")
+ self.pyboy.tick(7, False)
def game_area(self):
"""
@@ -526,7 +460,7 @@
Classes
* `"T"`: T-shape
"""
# Bitmask, as the last two bits determine the direction
- return inverse_tetromino_table[self.pyboy.get_memory_value(NEXT_TETROMINO_ADDR) & 0b11111100]
+ return inverse_tetromino_table[self.pyboy.memory[NEXT_TETROMINO_ADDR] & 0b11111100]
def set_tetromino(self, shape):
"""
@@ -565,7 +499,7 @@
Classes
]
for i, byte in enumerate(patch1):
- self.pyboy.override_memory_value(0, 0x206E + i, byte)
+ self.pyboy.memory[0, 0x206E + i] = byte
patch2 = [
0x3E, # LD A, Tetromino
@@ -573,7 +507,7 @@
Classes
]
for i, byte in enumerate(patch2):
- self.pyboy.override_memory_value(0, 0x20B0 + i, byte)
+ self.pyboy.memory[0, 0x20B0 + i] = byte
def game_over(self):
"""
@@ -591,20 +525,7 @@
Classes
f"Score: {self.score}\n" +
f"Level: {self.level}\n" +
f"Lines: {self.lines}\n" +
- f"Fitness: {self.fitness}\n" +
- "Sprites on screen:\n" +
- "\n".join([str(s) for s in self._sprites_on_screen()]) +
- "\n" +
- "Tiles on screen:\n" +
- " "*5 + "".join([f"{i: <4}" for i in range(10)]) + "\n" +
- "_"*(adjust*10+4) +
- "\n" +
- "\n".join(
- [
- f"{i: <3}| " + "".join([str(tile).ljust(adjust) for tile in line])
- for i, line in enumerate(self._game_area_np())
- ]
- )
+ super().__repr__()
)
# yapf: enable
A built-in fitness scoring. The scoring is equals to score.
-
fitness = score
-
Methods
@@ -659,7 +571,9 @@
Methods
The state of the emulator is saved, and using reset_game, you can get back to this point of the game
instantly.
Kwargs
-
timer_div (int): Replace timer's DIV register with this value. Use None to randomize.
+
+
timer_div (int): Replace timer's DIV register with this value. Use None to randomize.
+
Expand source code
@@ -673,32 +587,25 @@
Kwargs
instantly.
Kwargs:
- timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
+ * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
"""
# We don't supply the timer_div arg here, as it won't have the desired effect
PyBoyGameWrapper.start_game(self)
# Boot screen
while True:
- self.pyboy.tick()
- self.tilemap_background.refresh_lcdc()
+ self.pyboy.tick(1, False)
if self.tilemap_background[2:9, 14] == [89, 25, 21, 10, 34, 14, 27]: # '1PLAYER' on the first screen
break
# Start game. Just press Start when the game allows us.
for i in range(2):
- self.pyboy.send_input(WindowEvent.PRESS_BUTTON_START)
- self.pyboy.tick()
- self.pyboy.send_input(WindowEvent.RELEASE_BUTTON_START)
-
- for _ in range(6):
- self.pyboy.tick()
+ self.pyboy.button("start")
+ self.pyboy.tick(7, False)
self.saved_state.seek(0)
self.pyboy.save_state(self.saved_state)
- self.game_has_started = True
-
self.reset_game(timer_div=timer_div)
@@ -708,7 +615,9 @@
Kwargs
After calling start_game, you can call this method at any time to reset the game.
Kwargs
-
timer_div (int): Replace timer's DIV register with this value. Use None to randomize.
+
+
timer_div (int): Replace timer's DIV register with this value. Use None to randomize.
+
Expand source code
@@ -718,18 +627,13 @@
Kwargs
After calling `start_game`, you can call this method at any time to reset the game.
Kwargs:
- timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
+ * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
"""
PyBoyGameWrapper.reset_game(self, timer_div=timer_div)
self._set_timer_div(timer_div)
-
- self.pyboy.send_input(WindowEvent.PRESS_BUTTON_START)
- self.pyboy.tick()
- self.pyboy.send_input(WindowEvent.RELEASE_BUTTON_START)
-
- for _ in range(6):
- self.pyboy.tick()
+ self.pyboy.button("start")
+ self.pyboy.tick(7, False)
@@ -852,7 +756,7 @@
Returns
* `"T"`: T-shape
"""
# Bitmask, as the last two bits determine the direction
- return inverse_tetromino_table[self.pyboy.get_memory_value(NEXT_TETROMINO_ADDR) & 0b11111100]
+ return inverse_tetromino_table[self.pyboy.memory[NEXT_TETROMINO_ADDR] & 0b11111100]
@@ -919,7 +823,7 @@
Args
]
for i, byte in enumerate(patch1):
- self.pyboy.override_memory_value(0, 0x206E + i, byte)
+ self.pyboy.memory[0, 0x206E + i] = byte
patch2 = [
0x3E, # LD A, Tetromino
@@ -927,7 +831,7 @@
Args
]
for i, byte in enumerate(patch2):
- self.pyboy.override_memory_value(0, 0x20B0 + i, byte)
+ self.pyboy.memory[0, 0x20B0 + i] = byte
def dec_to_bcd(value, byte_width=1, byteorder="little"):
+ """
+ Converts a decimal value to Binary Coded Decimal (BCD).
+
+ Args:
+ value (int): The integer value to convert.
+ byte_width (int): The number of bytes to consider for each value.
+ byteorder (str): The endian type to use. This is only used for 16-bit values and higher. See [int.from_bytes](https://docs.python.org/3/library/stdtypes.html#int.from_bytes) for more details.
+
+ Example:
+ ```python
+ >>> from pyboy.utils import dec_to_bcd
+ >>> f"{dec_to_bcd(30):08b}"
+ '00110000'
+ >>> f"{dec_to_bcd(32):08b}"
+ '00110010'
+
+ ```
+
+ Returns:
+ int: The BCD equivalent of the decimal value.
+ """
+ bcd_result = []
+ for _ in range(byte_width):
+ tens = ((value%100) // 10) << 4
+ units = value % 10
+ bcd_byte = (tens | units) & 0xFF
+ bcd_result.append(bcd_byte)
+ value //= 100
+ return int.from_bytes(bcd_result, byteorder)
def bcd_to_dec(value, byte_width=1, byteorder="little"):
+ """
+ Converts a Binary Coded Decimal (BCD) value to decimal.
+
+ Args:
+ value (int): The BCD value to convert.
+ byte_width (int): The number of bytes to consider for each value.
+ byteorder (str): The endian type to use. This is only used for 16-bit values and higher. See [int.to_bytes](https://docs.python.org/3/library/stdtypes.html#int.to_bytes) for more details.
+
+ Example:
+ ```python
+ >>> from pyboy.utils import bcd_to_dec
+ >>> bcd_to_dec(0b00110000)
+ 30
+ >>> bcd_to_dec(0b00110010)
+ 32
+
+ ```
+
+ Returns:
+ int: The decimal equivalent of the BCD value.
+ """
+ decimal_value = 0
+ multiplier = 1
+
+ bcd_bytes = value.to_bytes(byte_width, byteorder)
+
+ for bcd_byte in bcd_bytes:
+ decimal_value += ((bcd_byte >> 4) * 10 + (bcd_byte & 0x0F)) * multiplier
+ multiplier *= 100
+
+ return decimal_value
+
+
+
+
+
+
Classes
+
+
+class WindowEvent
+(event)
+
+
+
All supported events can be found in the class description below.
+
It can be used as follows:
+
>>> from pyboy.utils import WindowEvent
+>>> pyboy.send_input(WindowEvent.PAUSE)
+
+