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..76cc41655
--- /dev/null
+++ b/docs/api/memory_scanner.html
@@ -0,0 +1,757 @@
+
+
+
+
+
+
+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):
+ """
+ Initializes the MemoryScanner with a PyBoy instance.
+
+ Args:
+ pyboy (PyBoy): The PyBoy emulator instance.
+ """
+ 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.
+
Initializes the MemoryScanner with a PyBoy instance.
+
Args
+
+
pyboy : PyBoy
+
The PyBoy emulator instance.
+
+
+
+Expand source code
+
+
class MemoryScanner():
+ """A class for scanning memory within a given range."""
+ def __init__(self, pyboy):
+ """
+ Initializes the MemoryScanner with a PyBoy instance.
+
+ Args:
+ pyboy (PyBoy): The PyBoy emulator instance.
+ """
+ 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/botsupport/screen.html b/docs/api/screen.html
similarity index 55%
rename from docs/botsupport/screen.html
rename to docs/api/screen.html
index 27f9864d0..ebb8ccd15 100644
--- a/docs/botsupport/screen.html
+++ b/docs/api/screen.html
@@ -4,7 +4,7 @@
-pyboy.botsupport.screen API documentation
+pyboy.api.screen API documentation
@@ -18,7 +18,7 @@
-
Module pyboy.botsupport.screen
+
Module pyboy.api.screen
This class gives access to the frame buffer and other screen parameters of PyBoy.
@@ -34,13 +34,14 @@
Module pyboy.botsupport.screen
This class gives access to the frame buffer and other screen parameters of PyBoy.
"""
-import logging
-
import numpy as np
+from pyboy import utils
+from pyboy.logging import get_logger
+
from .constants import COLS, ROWS
-logger = logging.getLogger(__name__)
+logger = get_logger(__name__)
try:
from PIL import Image
@@ -54,117 +55,122 @@
Module pyboy.botsupport.screen
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.
+ It's much more efficient to use `pyboy.tilemap_background`, `pyboy.tilemap_window`, and `pyboy.sprite` instead.
"""
def __init__(self, mb):
self.mb = mb
- def tilemap_position(self):
+ self.raw_buffer = self.mb.lcd.renderer._screenbuffer
"""
- 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.
+ 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.
- 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).
+ Use this, only if you need to bypass the overhead of `Screen.image` or `Screen.ndarray`.
Returns
-------
- tuple:
- Returns the tuple of registers ((SCX, SCY), (WX - 7, WY))
+ bytes:
+ 92160 bytes of screen data in a `bytes` object.
"""
- return (self.mb.lcd.getviewport(), self.mb.lcd.getwindowpos())
-
- def tilemap_position_list(self):
+ self.raw_buffer_dims = self.mb.lcd.renderer.buffer_dims
"""
- 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 the dimensions of the raw screen buffer. The screen buffer is row-major.
Returns
-------
- list:
- Nested list of SCX, SCY, WX and WY for each scanline (144x4). Returns (0, 0, 0, 0) when LCD is off.
+ tuple:
+ A two-tuple of the buffer dimensions. E.g. (144, 160).
"""
- 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):
+ self.raw_buffer_format = self.mb.lcd.renderer.color_format
"""
- 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 the color format of the raw screen buffer. **This format is subject to change.**
Returns
-------
- bytes:
- 92160 bytes of screen data in a `bytes` object.
+ str:
+ Color format of the raw screen buffer. E.g. 'RGBA'.
"""
- return self.mb.lcd.renderer._screenbuffer_raw.tobytes()
-
- def raw_screen_buffer_dims(self):
+ self.image = None
"""
- Returns the dimensions of the raw screen buffer.
+ 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.
+
+ The screen buffer is internally row-major, but PIL hides this.
+
+ 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.
Returns
-------
- tuple:
- A two-tuple of the buffer dimensions. E.g. (160, 144).
+ PIL.Image:
+ RGB image of (160, 144) pixels
"""
- return self.mb.lcd.renderer.buffer_dims
+ if not Image:
+ logger.warning("Cannot generate screen image. Missing dependency \"Pillow\".")
- def raw_screen_buffer_format(self):
+ else:
+ self.image = Image.frombuffer(
+ self.mb.lcd.renderer.color_format, self.mb.lcd.renderer.buffer_dims[::-1],
+ self.mb.lcd.renderer._screenbuffer_raw
+ )
+
+ self.ndarray = np.frombuffer(
+ self.mb.lcd.renderer._screenbuffer_raw,
+ dtype=np.uint8,
+ ).reshape(ROWS, COLS, 4)
"""
- Returns the color format of the raw screen buffer.
+ 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.
Returns
-------
- str:
- Color format of the raw screen buffer. E.g. 'RGB'.
+ numpy.ndarray:
+ Screendata in `ndarray` of bytes with shape (144, 160, 3)
"""
- return self.mb.lcd.renderer.color_format
- def screen_ndarray(self):
+ @property
+ def tilemap_position_list(self):
"""
- Provides the screen data in NumPy format. The dataformat is always RGB.
+ 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.
Returns
-------
- numpy.ndarray:
- Screendata in `ndarray` of bytes with shape (160, 144, 3)
+ list:
+ Nested list of SCX, SCY, WX and WY for each scanline (144x4). Returns (0, 0, 0, 0) when LCD is off.
"""
- return np.frombuffer(self.mb.lcd.renderer._screenbuffer_raw, dtype=np.uint8).reshape(ROWS, COLS, 4)[:, :, 1:]
+ # 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
- def screen_image(self):
+ # # 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):
"""
- Generates a PIL Image from the screen buffer.
+ 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.
- 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.
+ 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).
Returns
-------
- PIL.Image:
- RGB image of (160, 144) pixels
+ tuple:
+ Returns the tuple of registers ((SCX, SCY), (WX - 7, WY))
"""
- 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")
+ return (self.mb.lcd.getviewport(), self.mb.lcd.getwindowpos())
@@ -176,7 +182,7 @@
Module pyboy.botsupport.screen
Classes
-
+
class Screen(mb)
@@ -184,8 +190,7 @@
Classes
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.
+It's much more efficient to use pyboy.tilemap_background, pyboy.tilemap_window, and pyboy.sprite instead.
Expand source code
@@ -196,165 +201,131 @@
Classes
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.
+ It's much more efficient to use `pyboy.tilemap_background`, `pyboy.tilemap_window`, and `pyboy.sprite` instead.
"""
def __init__(self, mb):
self.mb = mb
- def tilemap_position(self):
+ self.raw_buffer = self.mb.lcd.renderer._screenbuffer
"""
- 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.
+ 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.
- 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).
+ Use this, only if you need to bypass the overhead of `Screen.image` or `Screen.ndarray`.
Returns
-------
- tuple:
- Returns the tuple of registers ((SCX, SCY), (WX - 7, WY))
+ bytes:
+ 92160 bytes of screen data in a `bytes` object.
"""
- return (self.mb.lcd.getviewport(), self.mb.lcd.getwindowpos())
-
- def tilemap_position_list(self):
+ self.raw_buffer_dims = self.mb.lcd.renderer.buffer_dims
"""
- 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 the dimensions of the raw screen buffer. The screen buffer is row-major.
Returns
-------
- list:
- Nested list of SCX, SCY, WX and WY for each scanline (144x4). Returns (0, 0, 0, 0) when LCD is off.
+ tuple:
+ A two-tuple of the buffer dimensions. E.g. (144, 160).
"""
- 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):
+ self.raw_buffer_format = self.mb.lcd.renderer.color_format
"""
- 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 the color format of the raw screen buffer. **This format is subject to change.**
Returns
-------
- bytes:
- 92160 bytes of screen data in a `bytes` object.
+ str:
+ Color format of the raw screen buffer. E.g. 'RGBA'.
"""
- return self.mb.lcd.renderer._screenbuffer_raw.tobytes()
-
- def raw_screen_buffer_dims(self):
+ self.image = None
"""
- Returns the dimensions of the raw screen buffer.
+ 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.
+
+ The screen buffer is internally row-major, but PIL hides this.
+
+ 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.
Returns
-------
- tuple:
- A two-tuple of the buffer dimensions. E.g. (160, 144).
+ PIL.Image:
+ RGB image of (160, 144) pixels
"""
- return self.mb.lcd.renderer.buffer_dims
+ if not Image:
+ logger.warning("Cannot generate screen image. Missing dependency \"Pillow\".")
- def raw_screen_buffer_format(self):
+ else:
+ self.image = Image.frombuffer(
+ self.mb.lcd.renderer.color_format, self.mb.lcd.renderer.buffer_dims[::-1],
+ self.mb.lcd.renderer._screenbuffer_raw
+ )
+
+ self.ndarray = np.frombuffer(
+ self.mb.lcd.renderer._screenbuffer_raw,
+ dtype=np.uint8,
+ ).reshape(ROWS, COLS, 4)
"""
- Returns the color format of the raw screen buffer.
+ 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.
Returns
-------
- str:
- Color format of the raw screen buffer. E.g. 'RGB'.
+ numpy.ndarray:
+ Screendata in `ndarray` of bytes with shape (144, 160, 3)
"""
- return self.mb.lcd.renderer.color_format
- def screen_ndarray(self):
+ @property
+ def tilemap_position_list(self):
"""
- Provides the screen data in NumPy format. The dataformat is always RGB.
+ 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.
Returns
-------
- numpy.ndarray:
- Screendata in `ndarray` of bytes with shape (160, 144, 3)
+ list:
+ Nested list of SCX, SCY, WX and WY for each scanline (144x4). Returns (0, 0, 0, 0) when LCD is off.
"""
- return np.frombuffer(self.mb.lcd.renderer._screenbuffer_raw, dtype=np.uint8).reshape(ROWS, COLS, 4)[:, :, 1:]
+ # 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
- def screen_image(self):
+ # # 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):
"""
- Generates a PIL Image from the screen buffer.
+ 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.
- 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.
+ 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).
Returns
-------
- PIL.Image:
- RGB image of (160, 144) pixels
+ tuple:
+ Returns the tuple of registers ((SCX, SCY), (WX - 7, WY))
"""
- 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")
+ return (self.mb.lcd.getviewport(), self.mb.lcd.getwindowpos())
-
Methods
+
Instance variables
-
-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)
-
+
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(). For such games, Screen.tilemap_position() becomes useless.
@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()`. For such games, `Screen.tilemap_position` becomes useless.
+ the end of each call to `pyboy.PyBoy.tick()`.
- See `Screen.tilemap_position` for more information.
+ See `Screen.get_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.
"""
+ # 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 raw_screen_buffer(self)
-
+
var raw_buffer
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)
-
+
var raw_buffer_dims
-
Returns the dimensions of the raw screen buffer.
+
Returns the dimensions of the raw screen buffer. The screen buffer is row-major.
Returns
tuple:
-
A two-tuple of the buffer dimensions. E.g. (160, 144).
+
A two-tuple of the buffer dimensions. E.g. (144, 160).
-
-
-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)
-
+
var raw_buffer_format
-
Returns the color format of the raw screen buffer.
+
Returns the color format of the raw screen buffer. This format is subject to change.
Returns
str:
-
Color format of the raw screen buffer. E.g. 'RGB'.
+
Color format of the raw screen buffer. E.g. 'RGBA'.
-
-
-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)
-
+
var image
-
Provides the screen data in NumPy format. The dataformat is always RGB.
+
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.
+
The screen buffer is internally row-major, but PIL hides this.
+
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.
+
Returns
+
+
PIL.Image:
+
RGB image of (160, 144) pixels
+
+
+
var ndarray
+
+
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 (160, 144, 3)
+
Screendata in ndarray of bytes with shape (144, 160, 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)
+
+
Methods
+
+
+def get_tilemap_position(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.
+
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 screen_image(self):
+
def get_tilemap_position(self):
"""
- Generates a PIL Image from the screen buffer.
+ 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.
- 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.
+ 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).
Returns
-------
- PIL.Image:
- RGB image of (160, 144) pixels
+ tuple:
+ Returns the tuple of registers ((SCX, SCY), (WX - 7, WY))
"""
- 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")
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 62%
rename from docs/botsupport/tile.html
rename to docs/api/tile.html
index 54fdfe878..e18332394 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.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,9 +120,19 @@
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`.
@@ -120,13 +144,17 @@
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()))
+
+ 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 image_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`.
@@ -135,9 +163,11 @@
Module pyboy.botsupport.tile
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 +177,25 @@
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
-
+ alpha_mask = 0x00FFFFFF
+ self.data[k // 2][x] = self.mb.lcd.BGP.getcolor(colorcode) & alpha_mask
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 +210,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.
+pyboy.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 +230,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.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 +261,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,9 +288,19 @@
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`.
@@ -263,13 +312,17 @@
Classes
if Image is None:
logger.error(f"{__name__}: Missing dependency \"Pillow\".")
return None
- return Image.frombytes("RGBA", (8, 8), bytes(self.image_data()))
+
+ 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 image_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`.
@@ -278,9 +331,11 @@
Classes
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,67 +345,79 @@
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
-
+ alpha_mask = 0x00FFFFFF
+ self.data[k // 2][x] = self.mb.lcd.BGP.getcolor(colorcode) & alpha_mask
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().
Returns
@@ -363,7 +430,7 @@
Returns
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`.
@@ -375,16 +442,20 @@
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().
Returns
@@ -397,9 +468,9 @@
Returns
def image_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`.
@@ -408,49 +479,9 @@
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/index.html b/docs/index.html
index b7c45692f..cb444dc42 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -32,31 +32,30 @@
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.api 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
@@ -101,6 +100,7 @@
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
+ This object, `pyboy.utils.WindowEvent`, and the `pyboy.api` 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
@@ -146,6 +146,13 @@
Kwargs
raise FileNotFoundError(f"ROM file {gamerom_file} was not found!")
self.gamerom_file = gamerom_file
+ self.rom_symbols = {}
+ if symbols_file is not None:
+ if not os.path.isfile(symbols_file):
+ raise FileNotFoundError(f"Symbols file {symbols_file} was not found!")
+ self.symbols_file = symbols_file
+ self._load_symbols()
+
self.mb = Motherboard(
gamerom_file,
bootrom_file or kwargs.get("bootrom"), # Our current way to provide cli arguments is broken
@@ -169,39 +176,204 @@
Kwargs
self.set_emulation_speed(1)
self.paused = False
self.events = []
+ self.queued_input = []
self.old_events = []
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.
+ Example:
+ ```python
+ >>> values = pyboy.memory[0x0000:0x10000]
+ >>> pyboy.memory[0xC000:0xC0010] = 0
+
+ ```
+
+ """
+
+ 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._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 the 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._hooks = {}
+ 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 +387,85 @@
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
+ >>> values = pyboy.memory[0x0000:0x10000]
+ >>> pyboy.memory[0xC000:0xC0010] = 0
+
+ ```
+
+ 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 frame *without* rendering
+ True
+ >>> pyboy.tick(60, True) # Progress 60 frame 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. -1 is infinite.
+ 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:
self.mb.save_state(IntIOWrapper(f))
elif event == WindowEvent.STATE_LOAD:
state_path = self.gamerom_file + ".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 +481,7 @@
def _post_tick(self):
if self.frame_count % 60 == 0:
self._update_window_title()
- self.plugin_manager.post_tick()
- self.plugin_manager.frame_limiter(self.target_emulationspeed)
+ 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 = []
+ self.events = self.queued_input
+ self.queued_input = []
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 +535,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 +550,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 +558,163 @@
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):
"""
- 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".
- 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).
+ The button will automatically be released at the following call to `PyBoy.tick`.
- 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.
+ 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
- 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.
+ Args:
+ input (str): button to press
"""
- 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)
+ self.queued_input.append(WindowEvent(WindowEvent.RELEASE_ARROW_LEFT))
+ elif input == "right":
+ self.send_input(WindowEvent.PRESS_ARROW_RIGHT)
+ self.queued_input.append(WindowEvent(WindowEvent.RELEASE_ARROW_RIGHT))
+ elif input == "up":
+ self.send_input(WindowEvent.PRESS_ARROW_UP)
+ self.queued_input.append(WindowEvent(WindowEvent.RELEASE_ARROW_UP))
+ elif input == "down":
+ self.send_input(WindowEvent.PRESS_ARROW_DOWN)
+ self.queued_input.append(WindowEvent(WindowEvent.RELEASE_ARROW_DOWN))
+ elif input == "a":
+ self.send_input(WindowEvent.PRESS_BUTTON_A)
+ self.queued_input.append(WindowEvent(WindowEvent.RELEASE_BUTTON_A))
+ elif input == "b":
+ self.send_input(WindowEvent.PRESS_BUTTON_B)
+ self.queued_input.append(WindowEvent(WindowEvent.RELEASE_BUTTON_B))
+ elif input == "start":
+ self.send_input(WindowEvent.PRESS_BUTTON_START)
+ self.queued_input.append(WindowEvent(WindowEvent.RELEASE_BUTTON_START))
+ elif input == "select":
+ self.send_input(WindowEvent.PRESS_BUTTON_SELECT)
+ self.queued_input.append(WindowEvent(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`.
+ raise Exception("Unrecognized input:", input)
- Returns
- -------
- `pyboy.plugins.base_plugin.PyBoyGameWrapper`:
- A game-specific wrapper object.
- """
- return self.plugin_manager.gamewrapper()
-
- def get_memory_value(self, addr):
- """
- Reads a given memory address of the Game Boy's current memory state. This will not directly give you access to
- all switchable memory banks. Open an issue on GitHub if that is needed, or use `PyBoy.set_memory_value` to send
- MBC commands to the virtual cartridge.
-
- Returns
- -------
- int:
- An integer with the value of the memory address
- """
- return self.mb.getitem(addr)
-
- def set_memory_value(self, addr, value):
+ 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.
+ 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.
-
- 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 +724,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.
@@ -494,10 +756,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.
@@ -508,22 +772,135 @@
Kwargs
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_area_dimensions(0, 0, 10, 18, False)
+
+ ```
+
+ 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.game_area_section = (x, y, width, height)
+ self.game_wrapper.game_area_follow_scxy = follow_scrolling
+
+ 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 support 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_minimal, 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
- 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.
+ self.game_wrapper.game_area_mapping(mapping, sprite_offset)
+
+ 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,427 +924,792 @@
Kwargs
Some window types do not implement a frame-limiter, and will always run at full speed.
+ Example:
+ ```python
+ >>> pyboy.set_emulation_speed(0) # No limit
+
+ ```
+
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_file)
+ 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] = {}
+
+ self.rom_symbols[bank][addr] = 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, label in addresses.items():
+ if label == symbol:
+ return bank, addr
+ raise ValueError("Symbol not found: %s" % symbol)
+
+ def hook_register(self, bank, addr, callback, context):
+ """
+ Adds a hook into a specific bank and memory address.
+ When the Game Boy executes this address, the provided callback function will be called.
+
+ By providing an object as `context`, you can later get access to information inside and outside of the callback.
+
+ 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
+
+ ```
+
+ 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(..., symbol_file="file.rom.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)
+
+ 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
-------
- str :
- Game title
+ `pyboy.api.sprite.Sprite`:
+ Sprite corresponding to the given index.
"""
- return self.mb.cartridge.gamename
+ return Sprite(self.mb, sprite_index)
- def _rendering(self, value):
- """
- Disable or enable rendering
+ def get_sprite_by_tile_identifier(self, tile_identifiers, on_screen=True):
"""
- self.mb.lcd.disable_renderer = not value
+ 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.sprite` function to get a `pyboy.api.sprite.Sprite` object.
- 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.
+ Example:
+ ```python
+ >>> print(pyboy.get_sprite_by_tile_identifier([43, 123]))
+ [[0, 2, 4], []]
- 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._
- """
- 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()
+ 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.
- nsecs = t_pre - t_start
- self.avg_pre = 0.9 * self.avg_pre + (0.1*nsecs/1_000_000_000)
+ 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.
- nsecs = t_tick - t_pre
- self.avg_tick = 0.9 * self.avg_tick + (0.1*nsecs/1_000_000_000)
+ Example:
+ ```python
+ >>> t = pyboy.get_tile(2)
+ >>> t
+ Tile: 2
+ >>> t.shape
+ (8, 8)
- nsecs = t_post - t_tick
- self.avg_post = 0.9 * self.avg_post + (0.1*nsecs/1_000_000_000)
+ ```
- return self.quitting
+ 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.
Screen:
+A Screen object with helper functions for reading the screen buffer.
-
-def stop(self, save=True)
-
+
var memory
-
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.
+
Provides a pyboy.PyBoyMemoryView object for reading and writing the memory space of the Game Boy.
+
Example:
+
>>> values = pyboy.memory[0x0000:0x10000]
+>>> pyboy.memory[0xC000:0xC0010] = 0
- 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
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
+[]
- 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)
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
+
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 the 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.
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.
+ Progresses the emulator ahead by `count` frame(s).
- 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).
+ 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.
- 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.
+ 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.
- simultaneous_actions (bool): Allow to inject multiple input at once. This dramatically increases the action_space: \\(n \\rightarrow 2^n\\)
+ 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.
- Returns
- -------
- `pyboy.openai_gym.PyBoyGymEnv`:
- A Gym environment based on the `Pyboy` object.
- """
- if gym_enabled:
- return PyBoyGymEnv(self, observation_type, action_type, simultaneous_actions, **kwargs)
- 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.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.
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.
+ Example:
+ ```python
+ >>> values = pyboy.memory[0x0000:0x10000]
+ >>> pyboy.memory[0xC000:0xC0010] = 0
- To get more information, find the wrapper for your game in `pyboy.plugins`.
+ ```
+ 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 frame *without* rendering
+ True
+ >>> pyboy.tick(60, True) # Progress 60 frame 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. -1 is infinite.
+ render (bool): Whether to render an image for this tick
Returns
-------
- `pyboy.plugins.base_plugin.PyBoyGameWrapper`:
- A game-specific wrapper object.
+ (True or False):
+ False if emulation has ended otherwise True
"""
- return self.plugin_manager.gamewrapper()
+
+ 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 get_memory_value(self, addr)
+
+def stop(self, save=True)
-
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
+
Gently stops the emulator and all sub-modules.
+
Example:
+
>>> pyboy.stop() # Stop emulator and save game progress (cartridge RAM)
+>>> pyboy.stop(False) # Stop emulator and discard game progress (cartridge RAM)
+
+
+
Args
-
int:
-
An integer with the value of the memory address
+
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 get_memory_value(self, addr):
+
def stop(self, save=True):
"""
- 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.
+ Gently stops the emulator and all sub-modules.
- Returns
- -------
- int:
- An integer with the value of the memory address
+ 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.
"""
- return self.mb.getitem(addr)
+ 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 set_memory_value(self, addr, value)
+
+def button(self, input)
-
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.
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
+
+
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(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 automatically be released at the following call to `PyBoy.tick`.
- __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('a') # Press button 'a' and release after `pyboy.tick()`
+ >>> pyboy.tick() # Button 'a' pressed
+ True
+ >>> 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_press(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.
+ The button will remain press until explicitly released with `PyBoy.button_release` 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 press
"""
- # 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 button_release(self, input):
"""
- Send a single input to control the emulator. This is both Game Boy buttons and emulator controls.
+ Send input to PyBoy in the form of "a", "b", "start", "select", "left", "right", "up" and "down".
+
+ This will release a button after a call to `PyBoy.button_press` or `PyBoy.send_input`.
- See `pyboy.WindowEvent` for which events to send.
+ 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
+
+ ```
Args:
- event (pyboy.WindowEvent): The event to send
+ input (str): button to release
"""
- self.events.append(WindowEvent(event))
def send_input(self, event):
"""
- Get current inputs except the events specified in "ignore" tuple.
- 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 get.
+ Consider using `PyBoy.button` instead for easier access.
- Args:
- ignore (tuple): Events this function should ignore
+ 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
- Returns
- -------
- list:
- List of the `pyboy.utils.WindowEvent`s processed for the last call to `pyboy.PyBoy.tick`
+ ```
+
+ Args:
+ 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 +1720,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 +1749,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.
@@ -1030,8 +1782,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 +1805,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.
@@ -1066,40 +1822,266 @@
+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_area_dimensions(0, 0, 10, 18, False)
+
+ ```
+
+ 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.game_area_section = (x, y, width, height)
+ self.game_wrapper.game_area_follow_scxy = follow_scrolling
+
+
+
+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.
Define custom mappings for tile identifiers in the game area.
+
Example of custom mapping:
+
>>> 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)
- 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.
+
+
Some game wrappers will supply mappings as well. See the specific documentation for your game wrapper:
+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 +2093,10 @@
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.set_emulation_speed(0) # No limit
+
+
Args
target_speed : int
@@ -1131,332 +2117,365 @@
Args
Some window types do not implement a frame-limiter, and will always run at full speed.
+ Example:
+ ```python
+ >>> pyboy.set_emulation_speed(0) # No limit
+
+ ```
+
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 hook_register(self, bank, addr, callback, context)
-
Get the title stored on the currently loaded cartridge ROM. The title is all upper-case ASCII and may
-have been truncated to 11 characters.
-
Returns
+
Adds a hook into a specific bank and memory address.
+When the Game Boy executes this address, the provided callback function will be called.
+
By providing an object as context, you can later get access to information inside and outside of the callback.
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(..., symbol_file="file.rom.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
-
str :
-
Game title
+
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
-
def cartridge_title(self):
+
def hook_register(self, bank, addr, callback, context):
"""
- 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.
+ Adds a hook into a specific bank and memory address.
+ When the Game Boy executes this address, the provided callback function will be called.
- Returns
- -------
- str :
- Game title
+ By providing an object as `context`, you can later get access to information inside and outside of the callback.
+
+ 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
+
+ ```
+
+ 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(..., symbol_file="file.rom.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
"""
- return self.mb.cartridge.gamename
+ 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)
-
-
-
-class WindowEvent
-(event)
+
+def hook_deregister(self, bank, addr)
-
All supported events can be found in the class description below.
Address in the Game Boy's address space (str for symbol lookup)
+
Expand source code
-
class WindowEvent:
+
def hook_deregister(self, bank, addr):
"""
- All supported events can be found in the class description below.
+ 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)
- It can be used as follows:
+ ```
- >>> from pyboy import PyBoy, WindowEvent
- >>> pyboy = PyBoy('file.rom')
- >>> pyboy.send_input(WindowEvent.PRESS_ARROW_RIGHT)
+ 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)
- # 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]
+ 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)
-
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
-
-
-
-
var DEBUG_MEMORY_SCROLL_DOWN
-
-
-
-
var DEBUG_MEMORY_SCROLL_UP
-
-
-
-
var MOD_SHIFT_ON
+
+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.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.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 FULL_SCREEN_TOGGLE
+
+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)
#
-# 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..4d8a0e34e 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 +626,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 +636,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,7 +215,7 @@
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.
"""
@@ -257,35 +232,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) + self.shape, 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):
"""
@@ -302,33 +268,24 @@
Classes
# 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)
@@ -389,7 +346,6 @@
Classes
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" +
@@ -429,11 +385,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
@@ -466,33 +417,24 @@
Kwargs
# 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)
@@ -595,6 +537,7 @@
This class wraps Super Mario Land, and provides easy access to score, coins, lives left, time left, world and a
-"fitness" score for AIs.
+
This class wraps Super Mario Land, and provides easy access to score, coins, lives left, time left and world for AIs.
Only world 1-1 is officially supported at the moment. Support for more worlds coming soon.
If you call print on an instance of this object, it will show an overview of everything this object provides.
@@ -394,17 +373,21 @@
Classes
class GameWrapperSuperMarioLand(PyBoyGameWrapper):
"""
- This class wraps Super Mario Land, and provides easy access to score, coins, lives left, time left, world and a
- "fitness" score for AIs.
+ This class wraps Super Mario Land, and provides easy access to score, coins, lives left, time left and world for AIs.
__Only world 1-1 is officially supported at the moment. Support for more worlds coming soon.__
If you call `print` on an instance of this object, it will show an overview of everything this object provides.
"""
cartridge_title = "SUPER MARIOLAN"
- 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 = (20, 16)
"""The shape of the game area"""
@@ -420,39 +403,26 @@
Classes
"""The number of seconds left to finish the level"""
self.level_progress = 0
"""An integer of the current "global" X position in this level. Can be used for AI scoring."""
- self._level_progress_max = 0
- self.fitness = 0
- """
- A built-in fitness scoring. Taking points, level progression, time left, and lives left into account.
-
- .. math::
- fitness = (lives\\_left \\cdot 10000) + (score + time\\_left \\cdot 10) + (\\_level\\_progress\\_max \\cdot 10)
- """
- super().__init__(*args, game_area_section=(0, 2) + self.shape, game_area_wrap_around=True, **kwargs)
+ super().__init__(*args, game_area_section=(0, 2) + self.shape, game_area_follow_scxy=True, **kwargs)
def post_tick(self):
self._tile_cache_invalid = True
self._sprite_cache_invalid = True
- world_level = self.pyboy.get_memory_value(ADDR_WORLD_LEVEL)
+ world_level = self.pyboy.memory[ADDR_WORLD_LEVEL]
self.world = world_level >> 4, world_level & 0x0F
blank = 300
self.coins = self._sum_number_on_screen(9, 1, 2, blank, -256)
- self.lives_left = _bcm_to_dec(self.pyboy.get_memory_value(ADDR_LIVES_LEFT))
+ self.lives_left = _bcm_to_dec(self.pyboy.memory[ADDR_LIVES_LEFT])
self.score = self._sum_number_on_screen(0, 1, 6, blank, -256)
self.time_left = self._sum_number_on_screen(17, 1, 3, blank, -256)
- level_block = self.pyboy.get_memory_value(0xC0AB)
- mario_x = self.pyboy.get_memory_value(0xC202)
- scx = self.pyboy.botsupport_manager().screen().tilemap_position_list()[16][0]
+ level_block = self.pyboy.memory[0xC0AB]
+ mario_x = self.pyboy.memory[0xC202]
+ scx = self.pyboy.screen.tilemap_position_list[16][0]
self.level_progress = level_block*16 + (scx-7) % 16 + mario_x
- if self.game_has_started:
- self._level_progress_max = max(self.level_progress, self._level_progress_max)
- end_score = self.score + self.time_left * 10
- self.fitness = self.lives_left * 10000 + end_score + self._level_progress_max * 10
-
def set_lives_left(self, amount):
"""
Set the amount lives to any number between 0 and 99.
@@ -468,11 +438,11 @@
Classes
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):
"""
@@ -484,7 +454,7 @@
Classes
"""
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 +462,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):
"""
@@ -520,28 +490,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)
@@ -603,7 +569,7 @@
Classes
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
@@ -615,7 +581,6 @@
Classes
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" +
@@ -639,13 +604,9 @@
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)
@@ -749,7 +705,7 @@
Args
"""
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 +713,7 @@
Args
]
for i, byte in enumerate(patch1):
- self.pyboy.override_memory_value(0, 0x451 + i, byte)
+ self.pyboy.memory[0, 0x451 + i] = byte
@@ -806,28 +762,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)
@@ -945,6 +897,7 @@
"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,29 +63,34 @@
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"""
@@ -100,25 +100,14 @@
Module pyboy.plugins.game_wrapper_tetris
"""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))
+ self._cached_game_area_tiles = memoryview(self._cached_game_area_tiles_raw).cast("I", shape=(ROWS, COLS))
- 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)]
-
- super().__init__(*args, game_area_section=(2, 0) + self.shape, game_area_wrap_around=True, **kwargs)
+ super().__init__(*args, game_area_section=(2, 0) + self.shape, game_area_follow_scxy=False, **kwargs)
def _game_area_tiles(self):
if self._tile_cache_invalid:
@@ -131,14 +120,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
@@ -155,25 +140,18 @@
Module pyboy.plugins.game_wrapper_tetris
# 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):
@@ -186,13 +164,8 @@
* `"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 +264,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 +272,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,7 +290,6 @@
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" +
@@ -328,7 +300,7 @@
Module pyboy.plugins.game_wrapper_tetris
"\n".join(
[
f"{i: <3}| " + "".join([str(tile).ljust(adjust) for tile in line])
- for i, line in enumerate(self._game_area_np())
+ for i, line in enumerate(self.game_area())
]
)
)
@@ -349,7 +321,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,14 +329,19 @@
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"""
@@ -374,25 +351,14 @@
Classes
"""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))
+ self._cached_game_area_tiles = memoryview(self._cached_game_area_tiles_raw).cast("I", shape=(ROWS, COLS))
- 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)]
-
- super().__init__(*args, game_area_section=(2, 0) + self.shape, game_area_wrap_around=True, **kwargs)
+ super().__init__(*args, game_area_section=(2, 0) + self.shape, game_area_follow_scxy=False, **kwargs)
def _game_area_tiles(self):
if self._tile_cache_invalid:
@@ -405,14 +371,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
@@ -429,25 +391,18 @@
Classes
# 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):
@@ -460,13 +415,8 @@
* `"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 +515,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 +523,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,7 +541,6 @@
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" +
@@ -602,7 +551,7 @@
Classes
"\n".join(
[
f"{i: <3}| " + "".join([str(tile).ljust(adjust) for tile in line])
- for i, line in enumerate(self._game_area_np())
+ for i, line in enumerate(self.game_area())
]
)
)
@@ -615,13 +564,9 @@
* `"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 +847,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 +855,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)
+
+