From 890bc6a1de2cb155659463058ee95754cde453a0 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Sat, 9 Mar 2024 22:32:46 +0100 Subject: [PATCH 01/65] Improve allocation in rewind --- pyboy/plugins/rewind.pxd | 9 +-------- pyboy/plugins/rewind.py | 24 +++++++----------------- 2 files changed, 8 insertions(+), 25 deletions(-) diff --git a/pyboy/plugins/rewind.pxd b/pyboy/plugins/rewind.pxd index 5fc3a0f26..46f2cde44 100644 --- a/pyboy/plugins/rewind.pxd +++ b/pyboy/plugins/rewind.pxd @@ -5,7 +5,6 @@ cimport cython from libc.stdint cimport int64_t, uint8_t, uint64_t -from libc.stdlib cimport free, malloc from pyboy.logging.logging cimport Logger from pyboy.plugins.base_plugin cimport PyBoyPlugin @@ -30,14 +29,8 @@ cdef int64_t FILL_VALUE DEF FIXED_BUFFER_SIZE = 8 * 1024 * 1024 DEF FIXED_BUFFER_MIN_ALLOC = 256*1024 -cdef inline uint8_t* _malloc(size_t n) noexcept: - return malloc(FIXED_BUFFER_SIZE) - -cdef inline void _free(uint8_t* pointer) noexcept: - free( pointer) - cdef class FixedAllocBuffers(IntIOInterface): - cdef uint8_t* buffer + cdef uint8_t[:] buffer cdef list sections cdef int64_t current_section cdef int64_t tail_pointer diff --git a/pyboy/plugins/rewind.py b/pyboy/plugins/rewind.py index a25c42c08..9b5abe0ab 100644 --- a/pyboy/plugins/rewind.py +++ b/pyboy/plugins/rewind.py @@ -18,7 +18,7 @@ except ImportError: cythonmode = False -FIXED_BUFFER_SIZE = 8 * 1024 * 1024 +FIXED_BUFFER_SIZE = 256 * 1024 * 1024 FIXED_BUFFER_MIN_ALLOC = 256 * 1024 FILL_VALUE = 123 @@ -30,7 +30,10 @@ def __init__(self, *args): super().__init__(*args) self.rewind_speed = 1.0 - self.rewind_buffer = DeltaFixedAllocBuffers() + if self.enabled(): + self.rewind_buffer = DeltaFixedAllocBuffers() + else: + self.rewind_buffer = None def post_tick(self): if not self.pyboy.paused: @@ -85,7 +88,7 @@ def enabled(self): class FixedAllocBuffers(IntIOInterface): def __init__(self): - self.buffer = _malloc(FIXED_BUFFER_SIZE) # NOQA: F821 + self.buffer = array.array("B", [0] * (FIXED_BUFFER_SIZE)) # NOQA: F821 for n in range(FIXED_BUFFER_SIZE): self.buffer[n] = FILL_VALUE self.sections = [0] @@ -98,7 +101,7 @@ def __init__(self): self.avg_section_size = 0.0 def stop(self): - _free(self.buffer) # NOQA: F821 + pass def flush(self): pass @@ -290,16 +293,3 @@ def seek_frame(self, frames): return False return CompressedFixedAllocBuffers.seek_frame(self, frames) - - -# Having this in the top of the file, causes glitces in Vim's syntax highlighting -if not cythonmode: - exec( - """ -def _malloc(n): - return array.array('B', [0]*(FIXED_BUFFER_SIZE)) - -def _free(_): - pass -""", globals(), locals() - ) From 65b610ba07d8b9c8b9aa5629253acb458b4b0158 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Tue, 5 Mar 2024 15:09:14 +0100 Subject: [PATCH 02/65] Add descriptive exception on save/load_state when providing a text-file --- pyboy/pyboy.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index 7735e459a..5c8aaee1f 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -419,6 +419,9 @@ def save_state(self, file_like_object): if isinstance(file_like_object, str): raise Exception("String not allowed. Did you specify a filepath instead of a file-like object?") + if file_like_object.__class__.__name__ == "TextIOWrapper": + raise Exception("Text file not allowed. Did you specify open(..., 'wb')?") + self.mb.save_state(IntIOWrapper(file_like_object)) def load_state(self, file_like_object): @@ -442,6 +445,9 @@ def load_state(self, file_like_object): if isinstance(file_like_object, str): raise Exception("String not allowed. Did you specify a filepath instead of a file-like object?") + if file_like_object.__class__.__name__ == "TextIOWrapper": + raise Exception("Text file not allowed. Did you specify open(..., 'rb')?") + self.mb.load_state(IntIOWrapper(file_like_object)) def screen_image(self): From b49b87e4ada781adda2d741e6ea287c8548a6800 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Sun, 11 Feb 2024 10:59:37 +0100 Subject: [PATCH 03/65] Remove logging from docs --- pyboy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyboy/__init__.py b/pyboy/__init__.py index 5d72d0c98..afa09db57 100644 --- a/pyboy/__init__.py +++ b/pyboy/__init__.py @@ -5,7 +5,7 @@ __pdoc__ = { "core": False, - "logger": False, + "logging": False, "pyboy": False, "utils": False, } From cd5dff32f55078a788c4c47da5b5bdc8e5e5c777 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Thu, 8 Feb 2024 09:23:33 +0100 Subject: [PATCH 04/65] Exclude manager.p(y,xd) from pre-commit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eefc72ec1..4768a98eb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -exclude: opcodes.py +exclude: (opcodes.py|manager.py|manager.pxd) repos: - repo: https://github.com/myint/unify rev: v0.5 From afff884336777ee7e27f69d4443067aeee816aea Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Sun, 4 Feb 2024 17:18:10 +0100 Subject: [PATCH 05/65] Update .gitignore --- .gitignore | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index 29f6cf6ca..a47a7e175 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# macOS +.DS_Store + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -38,11 +41,6 @@ docs/_build/ # PyBuilder target/ -.DS_Store -Report/*.pdf -Report/*.aux -Report/*.log - # Don't want to see any ROMs being uploaded # This project is not for pirating games! *.gb @@ -64,6 +62,7 @@ test_data.encrypted tetris/ PyBoy-RL/ +# Cython /pyboy/**/*.c /pyboy/**/*.h /pyboy/**/*.html @@ -80,24 +79,16 @@ PyBoy-RL/ /site-packages /share -# LaTeX files -Projects/Projects.aux -Projects/Projects.bbl -Projects/Projects.blg -Projects/Projects.out -Projects/Projects.log - -bootrom/bootrom*.map -bootrom/bootrom*.obj -bootrom/bootrom*.sym -bootrom/bootrom*.gb -bootrom/logo.asm -bootrom/pyboy.png -bootrom/PYBOY_ROM.bin +# Bootrom +extras/bootrom/bootrom*.map +extras/bootrom/bootrom*.obj +extras/bootrom/bootrom*.sym +extras/bootrom/bootrom*.gb +extras/bootrom/logo.asm -default_rom/default_rom.map -default_rom/default_rom.obj -default_rom/default_rom.sym +extras/default_rom/default_rom.map +extras/default_rom/default_rom.obj +extras/default_rom/default_rom.sym !default_rom.gb test.replay From ee314ed3af516408908fc6a71501ed419077bda2 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Fri, 27 Oct 2023 09:20:08 +0200 Subject: [PATCH 06/65] Experimenting with color_code --- pyboy/core/lcd.pxd | 1 + pyboy/core/lcd.py | 27 ++++++++++++++++++++------- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/pyboy/core/lcd.pxd b/pyboy/core/lcd.pxd index 4417c0abd..b27187528 100644 --- a/pyboy/core/lcd.pxd +++ b/pyboy/core/lcd.pxd @@ -174,6 +174,7 @@ cdef class Renderer: ) cdef void scanline_sprites(self, LCD, int, uint32_t[:,:], bint) noexcept nogil cdef void sort_sprites(self, int) noexcept nogil + cdef inline uint8_t color_code(self, uint8_t, uint8_t, uint8_t) noexcept nogil cdef void clear_cache(self) noexcept nogil cdef void clear_tilecache0(self) noexcept nogil diff --git a/pyboy/core/lcd.py b/pyboy/core/lcd.py index ba4ed58df..9067e5869 100644 --- a/pyboy/core/lcd.py +++ b/pyboy/core/lcd.py @@ -672,6 +672,19 @@ def clear_spritecache1(self): for i in range(TILES): self._spritecache1_state[i] = 0 + def color_code(self, byte1, byte2, offset): + """Convert 2 bytes into color code at a given offset. + + The colors are 2 bit and are found like this: + + Color of the first pixel is 0b10 + | Color of the second pixel is 0b01 + v v + 1 0 0 1 0 0 0 1 <- byte1 + 0 1 1 1 1 1 0 0 <- byte2 + """ + return (((byte2 >> (offset)) & 0b1) << 1) + ((byte1 >> (offset)) & 0b1) + def update_tilecache0(self, lcd, t, bank): if self._tilecache0_state[t]: return @@ -682,7 +695,7 @@ def update_tilecache0(self, lcd, t, bank): y = (t*16 + k) // 2 for x in range(8): - colorcode = utils.color_code(byte1, byte2, 7 - x) + colorcode = self.color_code(byte1, byte2, 7 - x) self._tilecache0[y, x] = colorcode self._tilecache0_state[t] = 1 @@ -700,7 +713,7 @@ def update_spritecache0(self, lcd, t, bank): y = (t*16 + k) // 2 for x in range(8): - colorcode = utils.color_code(byte1, byte2, 7 - x) + colorcode = self.color_code(byte1, byte2, 7 - x) self._spritecache0[y, x] = colorcode self._spritecache0_state[t] = 1 @@ -715,7 +728,7 @@ def update_spritecache1(self, lcd, t, bank): y = (t*16 + k) // 2 for x in range(8): - colorcode = utils.color_code(byte1, byte2, 7 - x) + colorcode = self.color_code(byte1, byte2, 7 - x) self._spritecache1[y, x] = colorcode self._spritecache1_state[t] = 1 @@ -817,7 +830,7 @@ def update_tilecache0(self, lcd, t, bank): y = (t*16 + k) // 2 for x in range(8): - self._tilecache0[y, x] = utils.color_code(byte1, byte2, 7 - x) + self._tilecache0[y, x] = self.color_code(byte1, byte2, 7 - x) self._tilecache0_state[t] = 1 @@ -835,7 +848,7 @@ def update_tilecache1(self, lcd, t, bank): y = (t*16 + k) // 2 for x in range(8): - self._tilecache1[y, x] = utils.color_code(byte1, byte2, 7 - x) + self._tilecache1[y, x] = self.color_code(byte1, byte2, 7 - x) self._tilecache1_state[t] = 1 @@ -853,7 +866,7 @@ def update_spritecache0(self, lcd, t, bank): y = (t*16 + k) // 2 for x in range(8): - self._spritecache0[y, x] = utils.color_code(byte1, byte2, 7 - x) + self._spritecache0[y, x] = self.color_code(byte1, byte2, 7 - x) self._spritecache0_state[t] = 1 @@ -871,7 +884,7 @@ def update_spritecache1(self, lcd, t, bank): y = (t*16 + k) // 2 for x in range(8): - self._spritecache1[y, x] = utils.color_code(byte1, byte2, 7 - x) + self._spritecache1[y, x] = self.color_code(byte1, byte2, 7 - x) self._spritecache1_state[t] = 1 From 3e04b58858dcd76759d2e7cd105b18894e80fcc4 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Sun, 11 Feb 2024 22:59:53 +0100 Subject: [PATCH 07/65] Remove disable_renderer --- pyboy/__main__.py | 1 - pyboy/core/lcd.py | 8 ++++---- pyboy/core/mb.py | 3 --- pyboy/pyboy.py | 2 -- tests/test_pyboy_lcd.py | 6 +++--- 5 files changed, 7 insertions(+), 13 deletions(-) diff --git a/pyboy/__main__.py b/pyboy/__main__.py index 249c735f5..0e4b9722e 100644 --- a/pyboy/__main__.py +++ b/pyboy/__main__.py @@ -87,7 +87,6 @@ def valid_file_path(path): help="Specify window-type to use" ) parser.add_argument("-s", "--scale", default=defaults["scale"], type=int, help="The scaling multiplier for the window") -parser.add_argument("--disable-renderer", action="store_true", help="Disables screen rendering for higher performance") parser.add_argument("--sound", action="store_true", help="Enable sound (beta)") gameboy_type_parser = parser.add_mutually_exclusive_group() diff --git a/pyboy/core/lcd.py b/pyboy/core/lcd.py index 9067e5869..26ce3492a 100644 --- a/pyboy/core/lcd.py +++ b/pyboy/core/lcd.py @@ -23,10 +23,10 @@ class LCD: - def __init__(self, cgb, cartridge_cgb, disable_renderer, color_palette, cgb_color_palette, randomize=False): + def __init__(self, cgb, cartridge_cgb, color_palette, cgb_color_palette, randomize=False): self.VRAM0 = array("B", [0] * VIDEO_RAM) self.OAM = array("B", [0] * OBJECT_ATTRIBUTE_MEMORY) - self.disable_renderer = disable_renderer + self.disable_renderer = False if randomize: for i in range(VIDEO_RAM): @@ -782,8 +782,8 @@ def load_state(self, f, state_version): class CGBLCD(LCD): - def __init__(self, cgb, cartridge_cgb, disable_renderer, color_palette, cgb_color_palette, randomize=False): - LCD.__init__(self, cgb, cartridge_cgb, disable_renderer, color_palette, cgb_color_palette, randomize=False) + def __init__(self, cgb, cartridge_cgb, color_palette, cgb_color_palette, randomize=False): + LCD.__init__(self, cgb, cartridge_cgb, color_palette, cgb_color_palette, randomize=False) self.VRAM1 = array("B", [0] * VIDEO_RAM) self.vbk = VBKregister() diff --git a/pyboy/core/mb.py b/pyboy/core/mb.py index b288bab34..6d4bde6e3 100644 --- a/pyboy/core/mb.py +++ b/pyboy/core/mb.py @@ -23,7 +23,6 @@ def __init__( bootrom_file, color_palette, cgb_color_palette, - disable_renderer, sound_enabled, sound_emulated, cgb, @@ -48,7 +47,6 @@ def __init__( self.lcd = lcd.CGBLCD( cgb, self.cartridge.cgb, - disable_renderer, color_palette, cgb_color_palette, randomize=randomize, @@ -57,7 +55,6 @@ def __init__( self.lcd = lcd.LCD( cgb, self.cartridge.cgb, - disable_renderer, color_palette, cgb_color_palette, randomize=randomize, diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index 5c8aaee1f..8bef42f89 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -38,7 +38,6 @@ def __init__( gamerom_file, *, bootrom_file=None, - disable_renderer=False, sound=False, sound_emulated=False, cgb=None, @@ -87,7 +86,6 @@ def __init__( bootrom_file or kwargs.get("bootrom"), # Our current way to provide cli arguments is broken kwargs["color_palette"], kwargs["cgb_color_palette"], - disable_renderer, sound, sound_emulated, cgb, diff --git a/tests/test_pyboy_lcd.py b/tests/test_pyboy_lcd.py index d9142baa0..283ef5d3b 100644 --- a/tests/test_pyboy_lcd.py +++ b/tests/test_pyboy_lcd.py @@ -27,7 +27,7 @@ ) class TestLCD: def test_set_stat_mode(self): - lcd = LCD(False, False, False, color_palette, cgb_color_palette) + lcd = LCD(False, False, color_palette, cgb_color_palette) lcd._STAT._mode = 2 # Set mode 2 manually assert lcd._STAT._mode == 2 # Init value assert lcd._STAT.set_mode(2) == 0 # Already set @@ -44,7 +44,7 @@ def test_stat_register(self): # "Bit 7 is unused and always returns '1'. Bits 0-2 return '0' when the LCD is off." # 3 LSB are read-only - lcd = LCD(False, False, False, color_palette, cgb_color_palette) + lcd = LCD(False, False, color_palette, cgb_color_palette) lcd.set_lcdc(0b1000_0000) # Turn on LCD. Don't care about rest of the flags lcd._STAT.value &= 0b11111000 # Force LY=LYC and mode bits to 0 lcd.set_stat( @@ -60,7 +60,7 @@ def test_stat_register(self): # lcd.set_stat(0b0111_1111) # Clear top bit, to check that it still returns 1 def test_check_lyc(self): - lcd = LCD(False, False, False, color_palette, cgb_color_palette) + lcd = LCD(False, False, color_palette, cgb_color_palette) lcd.LYC = 0 lcd.LY = 0 From a529fe728c71919967793c6f386dd7ac141ed2a5 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Wed, 7 Feb 2024 22:19:42 +0100 Subject: [PATCH 08/65] Software breakpoint backend --- pyboy/core/cpu.py | 2 +- pyboy/core/mb.pxd | 14 ++-- pyboy/core/mb.py | 142 +++++++++++++++++++++++++++++--------- pyboy/core/opcodes.pxd | 1 + pyboy/core/opcodes.py | 11 ++- pyboy/core/opcodes_gen.py | 10 +++ pyboy/pyboy.py | 25 +++++-- 7 files changed, 156 insertions(+), 49 deletions(-) diff --git a/pyboy/core/cpu.py b/pyboy/core/cpu.py index e66bc7dad..244dfe6d5 100644 --- a/pyboy/core/cpu.py +++ b/pyboy/core/cpu.py @@ -130,7 +130,7 @@ def tick(self): old_pc = self.PC # If the PC doesn't change, we're likely stuck old_sp = self.SP # Sometimes a RET can go to the same PC, so we check the SP too. cycles = self.fetch_and_execute() - if not self.halted and old_pc == self.PC and old_sp == self.SP and not self.is_stuck: + if not self.halted and old_pc == self.PC and old_sp == self.SP and not self.is_stuck and not self.mb.breakpoint_singlestep: logger.debug("CPU is stuck: %s", self.dump_state("")) self.is_stuck = True self.interrupt_queued = False diff --git a/pyboy/core/mb.pxd b/pyboy/core/mb.pxd index b8535d84d..305bb86c1 100644 --- a/pyboy/core/mb.pxd +++ b/pyboy/core/mb.pxd @@ -46,22 +46,24 @@ cdef class Motherboard: cdef bint double_speed cdef public bint cgb - cdef bint breakpoints_enabled cdef list breakpoints_list - cdef int breakpoint_latch + cdef bint breakpoint_singlestep + cdef bint breakpoint_singlestep_latch + cdef int64_t breakpoint_waiting + cdef int breakpoint_add(self, int64_t, int64_t) except -1 with gil + cdef void breakpoint_remove(self, int64_t) noexcept with gil + cdef inline int breakpoint_reached(self) noexcept with gil + cdef inline void breakpoint_reinject(self) noexcept nogil cdef inline bint processing_frame(self) noexcept nogil cdef void buttonevent(self, WindowEvent) noexcept cdef void stop(self, bint) noexcept - @cython.locals(cycles=int64_t, escape_halt=cython.int, mode0_cycles=int64_t) + @cython.locals(cycles=int64_t, mode0_cycles=int64_t, breakpoint_index=int64_t) cdef bint tick(self) noexcept nogil cdef void switch_speed(self) noexcept nogil - @cython.locals(pc=cython.int, bank=cython.int) - cdef bint breakpoint_reached(self) noexcept with gil - cdef uint8_t getitem(self, uint16_t) noexcept nogil cdef void setitem(self, uint16_t, uint8_t) noexcept nogil diff --git a/pyboy/core/mb.py b/pyboy/core/mb.py index 6d4bde6e3..0f7af27cc 100644 --- a/pyboy/core/mb.py +++ b/pyboy/core/mb.py @@ -4,12 +4,12 @@ # from pyboy import utils -from pyboy.core.opcodes import CPU_COMMANDS from pyboy.utils import STATE_VERSION from . import bootrom, cartridge, cpu, interaction, lcd, ram, sound, timer INTR_VBLANK, INTR_LCDC, INTR_TIMER, INTR_SERIAL, INTR_HIGHTOLOW = [1 << x for x in range(5)] +OPCODE_BRK = 0xDB import pyboy @@ -75,9 +75,10 @@ def __init__( self.serialbuffer = [0] * 1024 self.serialbuffer_count = 0 - self.breakpoints_enabled = False # breakpoints_enabled self.breakpoints_list = [] #[(0, 0x150), (0, 0x0040), (0, 0x0048), (0, 0x0050)] - self.breakpoint_latch = 0 + self.breakpoint_singlestep = False + self.breakpoint_singlestep_latch = False + self.breakpoint_waiting = -1 def switch_speed(self): bit0 = self.key1 & 0b1 @@ -86,14 +87,108 @@ def switch_speed(self): self.lcd.double_speed = self.double_speed self.key1 ^= 0b10000001 - def add_breakpoint(self, bank, addr): - self.breakpoints_enabled = True - self.breakpoints_list.append((bank, addr)) + def breakpoint_add(self, bank, addr): + # Replace instruction at address with OPCODE_BRK and save original opcode + # for later reinsertion and when breakpoint is deleted. + if addr < 0x100 and bank == -1: + opcode = self.bootrom.bootrom[addr] + self.bootrom.bootrom[addr] = OPCODE_BRK + elif addr < 0x4000: + if self.cartridge.external_rom_count < bank: + raise Exception(f"ROM bank out of bounds. Asked for {bank}, max is {self.cartridge.external_rom_count}") + opcode = self.cartridge.rombanks[bank, addr] + self.cartridge.rombanks[bank, addr] = OPCODE_BRK + elif 0x4000 <= addr < 0x8000: + if self.cartridge.external_rom_count < bank: + raise Exception(f"ROM bank out of bounds. Asked for {bank}, max is {self.cartridge.external_rom_count}") + opcode = self.cartridge.rombanks[bank, addr - 0x4000] + self.cartridge.rombanks[bank, addr - 0x4000] = OPCODE_BRK + elif 0x8000 <= addr < 0xA000: + if bank == 0: + opcode = self.lcd.VRAM0[addr - 0x8000] + self.lcd.VRAM0[addr - 0x8000] = OPCODE_BRK + else: + opcode = self.lcd.VRAM1[addr - 0x8000] + self.lcd.VRAM1[addr - 0x8000] = OPCODE_BRK + elif 0xA000 <= addr < 0xC000: + if self.cartridge.external_ram_count < bank: + raise Exception(f"RAM bank out of bounds. Asked for {bank}, max is {self.cartridge.external_ram_count}") + opcode = self.cartridge.rambanks[bank, addr - 0xA000] + self.cartridge.rambanks[bank, addr - 0xA000] = OPCODE_BRK + elif 0xC000 <= addr <= 0xE000: + opcode = self.ram.internal_ram0[addr - 0xC000] + self.ram.internal_ram0[addr - 0xC000] = OPCODE_BRK + else: + raise Exception("Unsupported breakpoint address. If this a mistake, reach out to the developers") + + self.breakpoints_list.append((bank, addr, opcode)) + return len(self.breakpoints_list) - 1 + + def breakpoint_find(self, bank, addr): + for i, (_bank, _addr, _) in enumerate(self.breakpoints_list): + if _bank == bank and _addr == addr: + return i + return -1 + + def breakpoint_remove(self, index): + logger.debug(f"Breakpoint remove: {index}") + if not 0 <= index < len(self.breakpoints_list): + logger.error("Cannot remove breakpoint: Index out of bounds %d/%d", index, len(self.breakpoints_list)) + # return (None, None, None) + bank, addr, opcode = self.breakpoints_list.pop(index) + logger.debug(f"Breakpoint remove: {bank:02x}:{addr:04x} {opcode:02x}") + + # Restore opcode + if addr < 0x100 and bank == -1: + self.bootrom.bootrom[addr] = opcode + elif addr < 0x4000: + self.cartridge.rombanks[bank, addr] = opcode + elif 0x4000 <= addr < 0x8000: + self.cartridge.rombanks[bank, addr - 0x4000] = opcode + elif 0x8000 <= addr < 0xA000: + if bank == 0: + self.lcd.VRAM0[addr - 0x8000] = opcode + else: + self.lcd.VRAM1[addr - 0x8000] = opcode + elif 0xA000 <= addr < 0xC000: + self.cartridge.rambanks[bank, addr - 0xA000] = opcode + elif 0xC000 <= addr <= 0xE000: + self.ram.internal_ram0[addr - 0xC000] = opcode + else: + logger.error("Unsupported breakpoint address. If this a mistake, reach out to the developers") + # return (None, None, None) + # return (bank, addr, opcode) - def remove_breakpoint(self, index): - self.breakpoints_list.pop(index) - if self.breakpoints == []: - self.breakpoints_enabled = False + def breakpoint_reached(self): + for i, (bank, pc, _) in enumerate(self.breakpoints_list): + if self.cpu.PC == pc and ( + (pc < 0x4000 and bank == 0 and not self.bootrom_enabled) or \ + (0x4000 <= pc < 0x8000 and self.cartridge.rombank_selected == bank) or \ + (0xA000 <= pc < 0xC000 and self.cartridge.rambank_selected == bank) or \ + (0xC000 <= pc <= 0xFFFF and bank == -1) or \ + (pc < 0x100 and bank == -1 and self.bootrom_enabled) + ): + # Breakpoint hit + # bank, addr, opcode = self.breakpoint_remove(i) + bank, addr, opcode = self.breakpoints_list[i] + logger.debug(f"Breakpoint reached: {bank:02x}:{addr:04x} {opcode:02x}") + self.breakpoint_waiting = (bank & 0xFF) << 24 | (addr & 0xFFFF) << 8 | (opcode & 0xFF) + logger.debug(f"Breakpoint waiting: {self.breakpoint_waiting:08x}") + return i + logger.debug("Invalid breakpoint reached: %04x", self.cpu.PC) + return -1 + + def breakpoint_reinject(self): + if self.breakpoint_waiting < 0: + return # skip + bank = (self.breakpoint_waiting >> 24) & 0xFF + # TODO: Improve signedness + if bank == 0xFF: + bank = -1 + addr = (self.breakpoint_waiting >> 8) & 0xFFFF + logger.debug("Breakpoint reinjecting: %02x:%02x", bank, addr) + self.breakpoint_add(bank, addr) + self.breakpoint_waiting = -1 def getserial(self): b = "".join([chr(x) for x in self.serialbuffer[:self.serialbuffer_count]]) @@ -171,23 +266,6 @@ def load_state(self, f): # Coordinator # - def breakpoint_reached(self): - if self.breakpoint_latch > 0: - self.breakpoint_latch -= 1 - return True - - for bank, pc in self.breakpoints_list: - if self.cpu.PC == pc and ( - (pc < 0x4000 and bank == 0 and not self.bootrom_enabled) or \ - (0x4000 <= pc < 0x8000 and self.cartridge.rombank_selected == bank) or \ - (0xA000 <= pc < 0xC000 and self.cartridge.rambank_selected == bank) or \ - (0xC000 <= pc <= 0xFFFF and bank == -1) or \ - (pc < 0x100 and bank == -1 and self.bootrom_enabled) - ): - # Breakpoint hit - return True - return False - def processing_frame(self): b = (not self.lcd.frame_done) self.lcd.frame_done = False # Clear vblank flag for next iteration @@ -237,17 +315,13 @@ def tick(self): if lcd_interrupt: self.cpu.set_interruptflag(lcd_interrupt) - # Escape halt. This happens when pressing 'return' in the debugger. It will make us skip breaking on halt - # for every cycle, but do break on the next instruction -- even in an interrupt. - escape_halt = self.cpu.halted and self.breakpoint_latch == 1 - # TODO: Replace with GDB Stub - if self.breakpoints_enabled and (not escape_halt) and self.breakpoint_reached(): - return True + if self.breakpoint_singlestep: + break # TODO: Move SDL2 sync to plugin self.sound.sync() - return False + return self.breakpoint_singlestep ################################################################### # MemoryManager diff --git a/pyboy/core/opcodes.pxd b/pyboy/core/opcodes.pxd index 4370a933d..e14b47bea 100644 --- a/pyboy/core/opcodes.pxd +++ b/pyboy/core/opcodes.pxd @@ -19,6 +19,7 @@ cdef uint8_t[512] OPCODE_LENGTHS cdef int execute_opcode(cpu.CPU, uint16_t) noexcept nogil cdef uint8_t no_opcode(cpu.CPU) noexcept nogil +cdef uint8_t BRK(cpu.CPU) noexcept nogil @cython.locals(v=int, flag=uint8_t, t=int) cdef uint8_t NOP_00(cpu.CPU) noexcept nogil # 00 NOP @cython.locals(v=int, flag=uint8_t, t=int) diff --git a/pyboy/core/opcodes.py b/pyboy/core/opcodes.py index 1762144ff..522bd9214 100644 --- a/pyboy/core/opcodes.py +++ b/pyboy/core/opcodes.py @@ -11,6 +11,11 @@ FLAGC, FLAGH, FLAGN, FLAGZ = range(4, 8) +def BRK(cpu): + cpu.mb.breakpoint_singlestep = 1 + cpu.mb.breakpoint_singlestep_latch = 0 + # NOTE: We do not increment PC + return 0 def NOP_00(cpu): # 00 NOP cpu.PC += 1 @@ -2166,7 +2171,7 @@ def JP_CA(cpu, v): # CA JP Z,a16 def PREFIX_CB(cpu): # CB PREFIX CB - logger.critical("CB cannot be called!") + logger.critical('CB cannot be called!') cpu.PC += 1 cpu.PC &= 0xFFFF return 4 @@ -5684,7 +5689,7 @@ def execute_opcode(cpu, opcode): elif opcode == 0xDA: return JP_DA(cpu, v) elif opcode == 0xDB: - return no_opcode(cpu) + return BRK(cpu) elif opcode == 0xDC: return CALL_DC(cpu, v) elif opcode == 0xDD: @@ -6527,7 +6532,7 @@ def execute_opcode(cpu, opcode): "RET C", "RETI", "JP C,a16", - "", + "Breakpoint/Illegal opcode", "CALL C,a16", "", "SBC A,d8", diff --git a/pyboy/core/opcodes_gen.py b/pyboy/core/opcodes_gen.py index 90566067f..e8e046fc2 100644 --- a/pyboy/core/opcodes_gen.py +++ b/pyboy/core/opcodes_gen.py @@ -25,6 +25,11 @@ FLAGC, FLAGH, FLAGN, FLAGZ = range(4, 8) +def BRK(cpu): + cpu.mb.breakpoint_singlestep = 1 + cpu.mb.breakpoint_singlestep_latch = 0 + # NOTE: We do not increment PC + return 0 """ @@ -42,6 +47,7 @@ cdef int execute_opcode(cpu.CPU, uint16_t) noexcept nogil cdef uint8_t no_opcode(cpu.CPU) noexcept nogil +cdef uint8_t BRK(cpu.CPU) noexcept nogil """ @@ -1204,6 +1210,10 @@ def update(): f_pxd.write(pxd + "\n") f.write(functiontext.replace("\t", " " * 4) + "\n\n\n") + # We create a new opcode to use as a software breakpoint instruction. + # I hope the irony of the opcode number is not lost. + lookuplist[0xDB] = (1, "BRK", "Breakpoint/Illegal opcode") + f.write("def no_opcode(cpu):\n return 0\n\n\n") f.write( diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index 8bef42f89..fdd0a2b99 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -131,11 +131,26 @@ def tick(self): 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 + # 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 + + self.plugin_manager.handle_breakpoint() + else: + if self.mb.breakpoint_singlestep_latch: + 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() From b9545009f52ddde3b9091b8e21d6b85559e32873 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Wed, 7 Feb 2024 22:20:19 +0100 Subject: [PATCH 09/65] Fixing cycles signed int --- pyboy/core/lcd.pxd | 6 +++--- pyboy/core/mb.py | 13 ++++++++----- pyboy/core/timer.pxd | 4 ++-- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/pyboy/core/lcd.pxd b/pyboy/core/lcd.pxd index b27187528..22fbe9550 100644 --- a/pyboy/core/lcd.pxd +++ b/pyboy/core/lcd.pxd @@ -6,7 +6,7 @@ import cython from cpython.array cimport array -from libc.stdint cimport int16_t, uint8_t, uint16_t, uint32_t, uint64_t +from libc.stdint cimport int16_t, int64_t, uint8_t, uint16_t, uint32_t, uint64_t cimport pyboy.utils from pyboy cimport utils @@ -48,14 +48,14 @@ cdef class LCD: @cython.locals(interrupt_flag=uint8_t) cdef uint8_t tick(self, int) noexcept nogil - cdef uint64_t cycles_to_interrupt(self) noexcept nogil + cdef int64_t cycles_to_interrupt(self) noexcept nogil cdef void set_lcdc(self, uint8_t) noexcept nogil cdef uint8_t get_lcdc(self) noexcept nogil cdef void set_stat(self, uint8_t) noexcept nogil cdef uint8_t get_stat(self) noexcept nogil - cdef int cycles_to_mode0(self) noexcept nogil + cdef int64_t cycles_to_mode0(self) noexcept nogil cdef void save_state(self, IntIOInterface) noexcept cdef void load_state(self, IntIOInterface, int) noexcept diff --git a/pyboy/core/mb.py b/pyboy/core/mb.py index 0f7af27cc..7e298c02b 100644 --- a/pyboy/core/mb.py +++ b/pyboy/core/mb.py @@ -291,11 +291,14 @@ def tick(self): if self.cgb and self.hdma.transfer_active: mode0_cycles = self.lcd.cycles_to_mode0() - cycles = min( - self.lcd.cycles_to_interrupt(), - self.timer.cycles_to_interrupt(), - # self.serial.cycles_to_interrupt(), - mode0_cycles + cycles = max( + 0, + min( + self.lcd.cycles_to_interrupt(), + self.timer.cycles_to_interrupt(), + # self.serial.cycles_to_interrupt(), + mode0_cycles + ) ) #TODO: Support General Purpose DMA diff --git a/pyboy/core/timer.pxd b/pyboy/core/timer.pxd index 43aaceb76..37d75b997 100644 --- a/pyboy/core/timer.pxd +++ b/pyboy/core/timer.pxd @@ -3,7 +3,7 @@ # GitHub: https://github.com/Baekalfen/PyBoy # -from libc.stdint cimport uint8_t, uint16_t, uint32_t, uint64_t +from libc.stdint cimport int64_t, uint8_t, uint16_t, uint32_t, uint64_t from pyboy.utils cimport IntIOInterface @@ -23,7 +23,7 @@ cdef class Timer: @cython.locals(divider=cython.int) cdef bint tick(self, uint64_t) noexcept nogil @cython.locals(divider=cython.int, cyclesleft=cython.uint) - cdef uint64_t cycles_to_interrupt(self) noexcept nogil + cdef int64_t cycles_to_interrupt(self) noexcept nogil cdef void save_state(self, IntIOInterface) noexcept cdef void load_state(self, IntIOInterface, int) noexcept From a58a94649cbebed0f638c70facb74bdf5b053608 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Wed, 7 Feb 2024 22:20:37 +0100 Subject: [PATCH 10/65] Remove logging prefix of _ --- pyboy/logging/_logging.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyboy/logging/_logging.py b/pyboy/logging/_logging.py index 7bc431714..1456ad7bc 100644 --- a/pyboy/logging/_logging.py +++ b/pyboy/logging/_logging.py @@ -6,16 +6,16 @@ def __init__(self, name): self.name = name def critical(self, fmt, *args): - _log(self.name, "_CRITICAL", CRITICAL, fmt, args) + _log(self.name, "CRITICAL", CRITICAL, fmt, args) def error(self, fmt, *args): - _log(self.name, "_ERROR", ERROR, fmt, args) + _log(self.name, "ERROR", ERROR, fmt, args) def warning(self, fmt, *args): - _log(self.name, "_WARNING", WARNING, fmt, args) + _log(self.name, "WARNING", WARNING, fmt, args) def info(self, fmt, *args): - _log(self.name, "_INFO", INFO, fmt, args) + _log(self.name, "INFO", INFO, fmt, args) def debug(self, fmt, *args): - _log(self.name, "_DEBUG", DEBUG, fmt, args) + _log(self.name, "DEBUG", DEBUG, fmt, args) From 27a824a37c2f0fab266e643f3c7107f6abdcb157 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Wed, 7 Feb 2024 22:21:30 +0100 Subject: [PATCH 11/65] Implement API for hooks --- pyboy/pyboy.pxd | 5 ++++ pyboy/pyboy.py | 73 ++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/pyboy/pyboy.pxd b/pyboy/pyboy.pxd index 180948c36..ed89de850 100644 --- a/pyboy/pyboy.pxd +++ b/pyboy/pyboy.pxd @@ -55,3 +55,8 @@ cdef class PyBoy: cpdef void _unpause(self) noexcept cdef void _update_window_title(self) noexcept cdef void _post_tick(self) noexcept + + cdef dict _hooks + cpdef bint _handle_hooks(self) + cpdef int hook_register(self, uint16_t, uint16_t, object, object) except -1 + cpdef int hook_deregister(self, uint16_t, uint16_t) except -1 \ No newline at end of file diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index fdd0a2b99..13edcff61 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -112,6 +112,7 @@ def __init__( # Plugins self.plugin_manager = PluginManager(self, self.mb, kwargs) + self._hooks = {} self.initialized = True def tick(self): @@ -143,10 +144,12 @@ def tick(self): self.mb.breakpoint_remove(breakpoint_index) self.mb.breakpoint_singlestep_latch = 0 - self.plugin_manager.handle_breakpoint() + if not self._handle_hooks(): + self.plugin_manager.handle_breakpoint() else: if self.mb.breakpoint_singlestep_latch: - self.plugin_manager.handle_breakpoint() + 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 @@ -507,8 +510,7 @@ def set_emulation_speed(self, target_speed): """ if self.initialized: unsupported_window_types_enabled = [ - self.plugin_manager.window_dummy_enabled, - self.plugin_manager.window_headless_enabled, + self.plugin_manager.window_dummy_enabled, self.plugin_manager.window_headless_enabled, self.plugin_manager.window_open_gl_enabled ] if any(unsupported_window_types_enabled): @@ -540,3 +542,66 @@ def _rendering(self, value): def _is_cpu_stuck(self): return self.mb.cpu.is_stuck + + 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, 0x2000, my_callback, context) + >>> pyboy.tick(60) + Hello from hook + ``` + + Args: + bank (int): ROM or RAM bank + addr (int): Address in the Game Boy's address space + callback (func): A function which takes `context` as argument + context (object): Argument to pass to callback when hook is called + """ + 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) + >>> hook_index = pyboy.hook_register(0, 0x2000, my_callback, context) + >>> pyboy.hook_deregister(hook_index) + ``` + + Args: + bank (int): ROM or RAM bank + addr (int): Address in the Game Boy's address space + """ + 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 \ No newline at end of file From 471c2d303f47e96f23ebc4a2d908707d7eeccaca Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Wed, 7 Feb 2024 22:24:12 +0100 Subject: [PATCH 12/65] Split Debug plugin into DebugPrompt --- pyboy/plugins/__init__.py | 19 +++-- pyboy/plugins/debug.pxd | 1 - pyboy/plugins/debug.py | 138 +------------------------------ pyboy/plugins/debug_prompt.pxd | 14 ++++ pyboy/plugins/debug_prompt.py | 147 +++++++++++++++++++++++++++++++++ pyboy/plugins/manager.pxd | 9 +- pyboy/plugins/manager.py | 16 +++- pyboy/plugins/manager_gen.py | 2 +- 8 files changed, 193 insertions(+), 153 deletions(-) create mode 100644 pyboy/plugins/debug_prompt.pxd create mode 100644 pyboy/plugins/debug_prompt.py diff --git a/pyboy/plugins/__init__.py b/pyboy/plugins/__init__.py index bb88661a2..df8b37600 100644 --- a/pyboy/plugins/__init__.py +++ b/pyboy/plugins/__init__.py @@ -8,18 +8,19 @@ __pdoc__ = { # docs exclude - "window_headless": False, + "window_sdl2": False, "window_open_gl": False, - "screen_recorder": False, - "rewind": False, + "debug": False, "window_dummy": False, - "disable_input": False, - "manager_gen": False, "auto_pause": False, - "manager": False, - "record_replay": False, + "rewind": False, + "window_headless": False, + "screen_recorder": False, + "manager_gen": False, + "disable_input": False, "screenshot_recorder": False, - "debug": False, - "window_sdl2": False, + "debug_prompt": False, + "record_replay": False, + "manager": False, # docs exclude end } diff --git a/pyboy/plugins/debug.pxd b/pyboy/plugins/debug.pxd index a20f2bcdd..335214645 100644 --- a/pyboy/plugins/debug.pxd +++ b/pyboy/plugins/debug.pxd @@ -43,7 +43,6 @@ cdef class Debug(PyBoyWindowPlugin): cdef TileDataWindow tiledata1 cdef MemoryWindow memory cdef bint sdl2_event_pump - cdef void handle_breakpoint(self) noexcept cdef class BaseDebugWindow(PyBoyWindowPlugin): diff --git a/pyboy/plugins/debug.py b/pyboy/plugins/debug.py index a25796526..80d34f82a 100644 --- a/pyboy/plugins/debug.py +++ b/pyboy/plugins/debug.py @@ -75,13 +75,7 @@ def __hash__(self): class Debug(PyBoyWindowPlugin): - argv = [("-d", "--debug", { - "action": "store_true", - "help": "Enable emulator debugging mode" - }), ("--breakpoints", { - "type": str, - "help": "Add breakpoints on start-up (internal use)" - })] + argv = [("-d", "--debug", {"action": "store_true", "help": "Enable emulator debugging mode"})] def __init__(self, pyboy, mb, pyboy_argv): super().__init__(pyboy, mb, pyboy_argv) @@ -91,36 +85,6 @@ def __init__(self, pyboy, mb, pyboy_argv): self.cgb = mb.cgb - self.rom_symbols = {} - if pyboy_argv.get("ROM"): - gamerom_file_no_ext, rom_ext = os.path.splitext(pyboy_argv.get("ROM")) - for sym_ext in [".sym", rom_ext + ".sym"]: - sym_path = gamerom_file_no_ext + sym_ext - if os.path.isfile(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()) - self.sdl2_event_pump = self.pyboy_argv.get("window_type") != "SDL2" if self.sdl2_event_pump: sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO) @@ -218,19 +182,6 @@ def __init__(self, pyboy, mb, pyboy_argv): pos_y=(256 * self.tile1.scale) + 128 ) - for _b in (self.pyboy_argv.get("breakpoints") or "").split(","): - b = _b.strip() - if b == "": - continue - - bank_addr = self.parse_bank_addr_sym_label(b) - if bank_addr is None: - logger.error("Couldn't parse address or label: %s", b) - pass - else: - self.mb.add_breakpoint(*bank_addr) - logger.info("Added breakpoint for address or label: %s", b) - def post_tick(self): self.tile1.post_tick() self.tile2.post_tick() @@ -268,93 +219,6 @@ def enabled(self): else: return False - def parse_bank_addr_sym_label(self, command): - if ":" in command: - bank, addr = command.split(":") - bank = int(bank, 16) - addr = int(addr, 16) - return bank, addr - else: - for bank, addresses in self.rom_symbols.items(): - for addr, label in addresses.items(): - if label == command: - return bank, addr - return None - - def handle_breakpoint(self): - while True: - self.post_tick() - - if self.mb.cpu.PC < 0x4000: - bank = 0 - else: - bank = self.mb.cartridge.rombank_selected - sym_label = self.rom_symbols.get(bank, {}).get(self.mb.cpu.PC, "") - - print(self.mb.cpu.dump_state(sym_label)) - cmd = input() - - if cmd == "c" or cmd.startswith("c "): - # continue - if cmd.startswith("c "): - _, command = cmd.split(" ", 1) - bank_addr = self.parse_bank_addr_sym_label(command) - if bank_addr is None: - print("Couldn't parse address or label!") - else: - # TODO: Possibly add a counter of 1, and remove the breakpoint after hitting it the first time - self.mb.add_breakpoint(*bank_addr) - break - else: - break - elif cmd == "sl": - for bank, addresses in self.rom_symbols.items(): - for addr, label in addresses.items(): - print(f"{bank:02X}:{addr:04X} {label}") - elif cmd == "bl": - for bank, addr in self.mb.breakpoints_list: - print(f"{bank:02X}:{addr:04X} {self.rom_symbols.get(bank, {}).get(addr, '')}") - elif cmd == "b" or cmd.startswith("b "): - if cmd.startswith("b "): - _, command = cmd.split(" ", 1) - else: - command = input( - 'Write address in the format of "00:0150" or search for a symbol label like "Main"\n' - ) - - bank_addr = self.parse_bank_addr_sym_label(command) - if bank_addr is None: - print("Couldn't parse address or label!") - else: - self.mb.add_breakpoint(*bank_addr) - - elif cmd == "d": - # Remove current breakpoint - - # TODO: Share this code with breakpoint_reached - for i, (bank, pc) in enumerate(self.mb.breakpoints_list): - if self.mb.cpu.PC == pc and ( - (pc < 0x4000 and bank == 0 and not self.mb.bootrom_enabled) or \ - (0x4000 <= pc < 0x8000 and self.mb.cartridge.rombank_selected == bank) or \ - (0xA000 <= pc < 0xC000 and self.mb.cartridge.rambank_selected == bank) or \ - (pc < 0x100 and bank == -1 and self.mb.bootrom_enabled) - ): - break - else: - print("Breakpoint couldn't be deleted for current PC. Not Found.") - continue - print(f"Removing breakpoint: {bank}:{pc}") - self.mb.remove_breakpoint(i) - elif cmd == "pdb": - # Start pdb - import pdb - pdb.set_trace() - break - else: - # Step once - self.mb.breakpoint_latch = 1 - self.mb.tick() - def make_buffer(w, h): buf = array("B", [0x55] * (w*h*4)) diff --git a/pyboy/plugins/debug_prompt.pxd b/pyboy/plugins/debug_prompt.pxd new file mode 100644 index 000000000..e45736348 --- /dev/null +++ b/pyboy/plugins/debug_prompt.pxd @@ -0,0 +1,14 @@ +# +# License: See LICENSE.md file +# GitHub: https://github.com/Baekalfen/PyBoy +# + +from pyboy.logging.logging cimport Logger +from pyboy.plugins.base_plugin cimport PyBoyPlugin + + +cdef Logger logger + +cdef class DebugPrompt(PyBoyPlugin): + cdef dict rom_symbols + cdef void handle_breakpoint(self) diff --git a/pyboy/plugins/debug_prompt.py b/pyboy/plugins/debug_prompt.py new file mode 100644 index 000000000..64ad08249 --- /dev/null +++ b/pyboy/plugins/debug_prompt.py @@ -0,0 +1,147 @@ +# +# License: See LICENSE.md file +# GitHub: https://github.com/Baekalfen/PyBoy +# + +import os +import re + +import pyboy +from pyboy.plugins.base_plugin import PyBoyPlugin +from pyboy.utils import WindowEvent + +logger = pyboy.logging.get_logger(__name__) + + +class DebugPrompt(PyBoyPlugin): + argv = [("--breakpoints", {"type": str, "help": "Add breakpoints on start-up (internal use)"})] + + def __init__(self, pyboy, mb, pyboy_argv): + super().__init__(pyboy, mb, pyboy_argv) + + if not self.enabled(): + return + + self.rom_symbols = {} + if pyboy_argv.get("ROM"): + gamerom_file_no_ext, rom_ext = os.path.splitext(pyboy_argv.get("ROM")) + for sym_ext in [".sym", rom_ext + ".sym"]: + sym_path = gamerom_file_no_ext + sym_ext + if os.path.isfile(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()) + + for _b in (self.pyboy_argv.get("breakpoints") or "").split(","): + b = _b.strip() + if b == "": + continue + + bank, addr = self.parse_bank_addr_sym_label(b) + if bank is None or addr is None: + logger.error("Couldn't parse address or label: %s", b) + pass + else: + self.mb.breakpoint_add(bank, addr) + logger.info("Added breakpoint for address or label: %s", b) + + def enabled(self): + return self.pyboy_argv.get("breakpoints") + + def parse_bank_addr_sym_label(self, command): + if ":" in command: + bank, addr = command.split(":") + bank = int(bank, 16) + addr = int(addr, 16) + return bank, addr + else: + for bank, addresses in self.rom_symbols.items(): + for addr, label in addresses.items(): + if label == command: + return bank, addr + return None, None + + def handle_breakpoint(self): + while True: + self.pyboy.plugin_manager.post_tick() + + # Get symbol + if self.mb.cpu.PC < 0x4000: + bank = 0 + else: + bank = self.mb.cartridge.rombank_selected + sym_label = self.rom_symbols.get(bank, {}).get(self.mb.cpu.PC, "") + + # Print state + print(self.mb.cpu.dump_state(sym_label)) + + # REPL + cmd = input() + # breakpoint() + if cmd == "c" or cmd.startswith("c "): + # continue + if cmd.startswith("c "): + _, command = cmd.split(" ", 1) + bank, addr = self.parse_bank_addr_sym_label(command) + if bank is None or addr is None: + print("Couldn't parse address or label!") + else: + # TODO: Possibly add a counter of 1, and remove the breakpoint after hitting it the first time + self.mb.breakpoint_add(bank, addr) + break + else: + self.mb.breakpoint_singlestep_latch = 0 + break + elif cmd == "sl": + for bank, addresses in self.rom_symbols.items(): + for addr, label in addresses.items(): + print(f"{bank:02X}:{addr:04X} {label}") + elif cmd == "bl": + for bank, addr in self.mb.breakpoints_list: + print(f"{bank:02X}:{addr:04X} {self.rom_symbols.get(bank, {}).get(addr, '')}") + elif cmd == "b" or cmd.startswith("b "): + if cmd.startswith("b "): + _, command = cmd.split(" ", 1) + else: + command = input( + 'Write address in the format of "00:0150" or search for a symbol label like "Main"\n' + ) + + bank, addr = self.parse_bank_addr_sym_label(command) + if bank is None or addr is None: + print("Couldn't parse address or label!") + else: + self.mb.breakpoint_add(bank, addr) + + elif cmd == "d": + print(f"Removing breakpoint at current PC") + self.mb.breakpoint_reached() # NOTE: This removes the breakpoint we are currently at + elif cmd == "pdb": + # Start pdb + import pdb + pdb.set_trace() + break + else: + # Step once + self.mb.breakpoint_singlestep_latch = 1 + break diff --git a/pyboy/plugins/manager.pxd b/pyboy/plugins/manager.pxd index a457d47ca..54d418064 100644 --- a/pyboy/plugins/manager.pxd +++ b/pyboy/plugins/manager.pxd @@ -7,9 +7,8 @@ cimport cython from pyboy.logging.logging cimport Logger from pyboy.plugins.auto_pause cimport AutoPause -# imports end -from pyboy.plugins.base_plugin cimport PyBoyPlugin, PyBoyWindowPlugin from pyboy.plugins.debug cimport Debug +from pyboy.plugins.debug_prompt cimport DebugPrompt from pyboy.plugins.disable_input cimport DisableInput from pyboy.plugins.game_wrapper_kirby_dream_land cimport GameWrapperKirbyDreamLand from pyboy.plugins.game_wrapper_pokemon_gen1 cimport GameWrapperPokemonGen1 @@ -22,6 +21,8 @@ from pyboy.plugins.screenshot_recorder cimport ScreenshotRecorder from pyboy.plugins.window_dummy cimport WindowDummy from pyboy.plugins.window_headless cimport WindowHeadless from pyboy.plugins.window_open_gl cimport WindowOpenGL +# imports end +# isort:skip # imports from pyboy.plugins.window_sdl2 cimport WindowSDL2 @@ -44,6 +45,7 @@ cdef class PluginManager: cdef public Rewind rewind cdef public ScreenRecorder screen_recorder cdef public ScreenshotRecorder screenshot_recorder + cdef public DebugPrompt debug_prompt cdef public GameWrapperSuperMarioLand game_wrapper_super_mario_land cdef public GameWrapperTetris game_wrapper_tetris cdef public GameWrapperKirbyDreamLand game_wrapper_kirby_dream_land @@ -59,6 +61,7 @@ cdef class PluginManager: cdef bint rewind_enabled cdef bint screen_recorder_enabled cdef bint screenshot_recorder_enabled + cdef bint debug_prompt_enabled cdef bint game_wrapper_super_mario_land_enabled cdef bint game_wrapper_tetris_enabled cdef bint game_wrapper_kirby_dream_land_enabled @@ -66,7 +69,7 @@ cdef class PluginManager: # plugin_cdef end cdef list handle_events(self, list) noexcept - cdef void post_tick(self) noexcept + cpdef void post_tick(self) noexcept cdef void _post_tick_windows(self) noexcept cdef void frame_limiter(self, int) noexcept cdef str window_title(self) noexcept diff --git a/pyboy/plugins/manager.py b/pyboy/plugins/manager.py index ce37b8983..e329bab15 100644 --- a/pyboy/plugins/manager.py +++ b/pyboy/plugins/manager.py @@ -15,6 +15,7 @@ from pyboy.plugins.rewind import Rewind # isort:skip from pyboy.plugins.screen_recorder import ScreenRecorder # isort:skip from pyboy.plugins.screenshot_recorder import ScreenshotRecorder # isort:skip +from pyboy.plugins.debug_prompt import DebugPrompt # isort:skip from pyboy.plugins.game_wrapper_super_mario_land import GameWrapperSuperMarioLand # isort:skip from pyboy.plugins.game_wrapper_tetris import GameWrapperTetris # isort:skip from pyboy.plugins.game_wrapper_kirby_dream_land import GameWrapperKirbyDreamLand # isort:skip @@ -36,6 +37,7 @@ def parser_arguments(): yield Rewind.argv yield ScreenRecorder.argv yield ScreenshotRecorder.argv + yield DebugPrompt.argv yield GameWrapperSuperMarioLand.argv yield GameWrapperTetris.argv yield GameWrapperKirbyDreamLand.argv @@ -71,6 +73,8 @@ def __init__(self, pyboy, mb, pyboy_argv): self.screen_recorder_enabled = self.screen_recorder.enabled() self.screenshot_recorder = ScreenshotRecorder(pyboy, mb, pyboy_argv) self.screenshot_recorder_enabled = self.screenshot_recorder.enabled() + self.debug_prompt = DebugPrompt(pyboy, mb, pyboy_argv) + self.debug_prompt_enabled = self.debug_prompt.enabled() self.game_wrapper_super_mario_land = GameWrapperSuperMarioLand(pyboy, mb, pyboy_argv) self.game_wrapper_super_mario_land_enabled = self.game_wrapper_super_mario_land.enabled() self.game_wrapper_tetris = GameWrapperTetris(pyboy, mb, pyboy_argv) @@ -120,6 +124,8 @@ def handle_events(self, events): events = self.screen_recorder.handle_events(events) if self.screenshot_recorder_enabled: events = self.screenshot_recorder.handle_events(events) + if self.debug_prompt_enabled: + events = self.debug_prompt.handle_events(events) if self.game_wrapper_super_mario_land_enabled: events = self.game_wrapper_super_mario_land.handle_events(events) if self.game_wrapper_tetris_enabled: @@ -145,6 +151,8 @@ def post_tick(self): self.screen_recorder.post_tick() if self.screenshot_recorder_enabled: self.screenshot_recorder.post_tick() + if self.debug_prompt_enabled: + self.debug_prompt.post_tick() if self.game_wrapper_super_mario_land_enabled: self.game_wrapper_super_mario_land.post_tick() if self.game_wrapper_tetris_enabled: @@ -240,6 +248,8 @@ def window_title(self): title += self.screen_recorder.window_title() if self.screenshot_recorder_enabled: title += self.screenshot_recorder.window_title() + if self.debug_prompt_enabled: + title += self.debug_prompt.window_title() if self.game_wrapper_super_mario_land_enabled: title += self.game_wrapper_super_mario_land.window_title() if self.game_wrapper_tetris_enabled: @@ -277,6 +287,8 @@ def stop(self): self.screen_recorder.stop() if self.screenshot_recorder_enabled: self.screenshot_recorder.stop() + if self.debug_prompt_enabled: + self.debug_prompt.stop() if self.game_wrapper_super_mario_land_enabled: self.game_wrapper_super_mario_land.stop() if self.game_wrapper_tetris_enabled: @@ -289,5 +301,5 @@ def stop(self): pass def handle_breakpoint(self): - if self.debug_enabled: - self.debug.handle_breakpoint() + if self.debug_prompt_enabled: + self.debug_prompt.handle_breakpoint() diff --git a/pyboy/plugins/manager_gen.py b/pyboy/plugins/manager_gen.py index a6660d549..9a8993220 100644 --- a/pyboy/plugins/manager_gen.py +++ b/pyboy/plugins/manager_gen.py @@ -11,7 +11,7 @@ "GameWrapperSuperMarioLand", "GameWrapperTetris", "GameWrapperKirbyDreamLand", "GameWrapperPokemonGen1" ] plugins = [ - "DisableInput", "AutoPause", "RecordReplay", "Rewind", "ScreenRecorder", "ScreenshotRecorder" + "DisableInput", "AutoPause", "RecordReplay", "Rewind", "ScreenRecorder", "ScreenshotRecorder", "DebugPrompt" ] + game_wrappers all_plugins = windows + plugins From cf3bec01e84c6541b9c71d8529e3e61d32fd3c57 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Wed, 7 Feb 2024 22:25:59 +0100 Subject: [PATCH 13/65] Breakpoint, hook and DebugPrompt tests --- tests/test_breakpoints.py | 136 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 tests/test_breakpoints.py diff --git a/tests/test_breakpoints.py b/tests/test_breakpoints.py new file mode 100644 index 000000000..06914d8f0 --- /dev/null +++ b/tests/test_breakpoints.py @@ -0,0 +1,136 @@ +# +# License: See LICENSE.md file +# GitHub: https://github.com/Baekalfen/PyBoy +# + +import io +import platform +from unittest.mock import Mock + +import pytest + +from pyboy import PyBoy + +is_pypy = platform.python_implementation() == "PyPy" + + +def test_debugprompt(default_rom, monkeypatch): + pyboy = PyBoy(default_rom, window_type="null", breakpoints="0:0100,-1:0", debug=False) + pyboy.set_emulation_speed(0) + + # Break at 0, step once, continue, break at 100, continue + monkeypatch.setattr("sys.stdin", io.StringIO("n\nc\nc\n")) + for _ in range(120): + pyboy.tick() + + if is_pypy: + assert not pyboy.mb.bootrom_enabled + + +@pytest.mark.parametrize("commands", ["n\nc\n", "n\nn\nc\n", "c\nc\n", "n\nn\nn\nn\nn\nn\nc\n"]) +def test_debugprompt2(default_rom, monkeypatch, commands): + pyboy = PyBoy(default_rom, window_type="null", breakpoints="-1:0,-1:3", debug=False) + pyboy.set_emulation_speed(0) + + monkeypatch.setattr("sys.stdin", io.StringIO(commands)) + for _ in range(120): + pyboy.tick() + + if is_pypy: + assert not pyboy.mb.bootrom_enabled + + +def test_register_hooks(default_rom): + pyboy = PyBoy(default_rom, window_type="null") + pyboy.set_emulation_speed(0) + + mock = Mock() + pyboy.hook_register(0, 0x100, mock.method1, None) + pyboy.hook_register(0, 0x1000, mock.method2, None) + for _ in range(120): + pyboy.tick() + + mock.method1.assert_called() + mock.method2.assert_not_called() + + +def test_register_hook_context(default_rom): + pyboy = PyBoy(default_rom, window_type="null") + pyboy.set_emulation_speed(0) + + def _inner_callback(context): + context.append(1) + + _context = [] + + bank = -1 # "ROM bank number" for bootrom + addr = 0xFC # Address of last instruction. Expected to execute once. + pyboy.hook_register(bank, addr, _inner_callback, _context) + for _ in range(120): + pyboy.tick() + + assert len(_context) == 1 + + +def test_register_hook_context2(default_rom): + pyboy = PyBoy(default_rom, window_type="null") + pyboy.set_emulation_speed(0) + + def _inner_callback(context): + context.append(1) + + _context = [] + + bank = -1 # "ROM bank number" for bootrom + addr = 0x06 # Erase routine, expected 8192 times + pyboy.hook_register(bank, addr, _inner_callback, _context) + + for _ in range(120): + pyboy.tick() + + assert len(_context) == 0xA000 - 0x8000 + + +def test_register_hooks_double(default_rom): + pyboy = PyBoy(default_rom, window_type="null") + pyboy.set_emulation_speed(0) + + mock = Mock() + with pytest.raises(ValueError): + pyboy.hook_register(0, 0x100, mock.method1, None) + pyboy.hook_register(0, 0x100, mock.method1, None) + + +def test_deregister_hooks(default_rom): + pyboy = PyBoy(default_rom, window_type="null") + pyboy.set_emulation_speed(0) + + mock = Mock() + + pyboy.hook_register(0, 0x100, mock.method1, None) + for _ in range(30): + pyboy.tick() + mock.method1.assert_not_called() + + pyboy.hook_deregister(0, 0x100) + for _ in range(90): + pyboy.tick() + mock.method1.assert_not_called() + + +def test_deregister_hooks2(default_rom): + pyboy = PyBoy(default_rom, window_type="null") + pyboy.set_emulation_speed(0) + + mock = Mock() + # Pop in same order + pyboy.hook_register(0, 0x100, mock.method1, None) + pyboy.hook_register(0, 0x101, mock.method1, None) + pyboy.hook_deregister(0, 0x100) + pyboy.hook_deregister(0, 0x101) + + # Pop in reverse order + pyboy.hook_register(0, 0x100, mock.method1, None) + pyboy.hook_register(0, 0x101, mock.method1, None) + pyboy.hook_deregister(0, 0x101) + pyboy.hook_deregister(0, 0x100) From b204674605991519dbf8c768a2036d835c55710c Mon Sep 17 00:00:00 2001 From: Mads Ynddal <5528170+Baekalfen@users.noreply.github.com> Date: Sat, 9 Dec 2023 19:22:58 +0100 Subject: [PATCH 14/65] Added Which, Whichboot, Pokemon RL (GIF) to test --- tests/conftest.py | 64 +++++++++++- tests/test_pokemon_rl.py | 133 ++++++++++++++++++++++++ tests/test_results/cgb_which.gb.png | Bin 0 -> 1037 bytes tests/test_results/cgb_whichboot.gb.png | Bin 0 -> 2243 bytes tests/test_results/dmg_which.gb.png | Bin 0 -> 1072 bytes tests/test_results/dmg_whichboot.gb.png | Bin 0 -> 2225 bytes tests/test_which.py | 41 ++++++++ tests/test_whichboot.py | 41 ++++++++ 8 files changed, 276 insertions(+), 3 deletions(-) create mode 100644 tests/test_pokemon_rl.py create mode 100644 tests/test_results/cgb_which.gb.png create mode 100644 tests/test_results/cgb_whichboot.gb.png create mode 100644 tests/test_results/dmg_which.gb.png create mode 100644 tests/test_results/dmg_whichboot.gb.png create mode 100644 tests/test_which.py create mode 100644 tests/test_whichboot.py diff --git a/tests/conftest.py b/tests/conftest.py index 716720315..beb61bd0c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -88,6 +88,11 @@ def pokemon_blue_rom(secrets): return locate_sha256(b"2a951313c2640e8c2cb21f25d1db019ae6245d9c7121f754fa61afd7bee6452d") +@pytest.fixture(scope="session") +def pokemon_red_rom(secrets): + return locate_sha256(b"5ca7ba01642a3b27b0cc0b5349b52792795b62d3ed977e98a09390659af96b7b") + + @pytest.fixture(scope="session") def pokemon_gold_rom(secrets): return locate_sha256(b"fb0016d27b1e5374e1ec9fcad60e6628d8646103b5313ca683417f52b97e7e4e") @@ -213,7 +218,6 @@ def cgb_acid_file(): @pytest.fixture(scope="session") def shonumi_dir(): - # Has to be in here. Otherwise all test workers will import this file, and cause an error. path = extra_test_rom_dir / Path("GB Tests") with FileLock(path.with_suffix(".lock")) as lock: if not os.path.isdir(path): @@ -236,6 +240,32 @@ def rtc3test_file(): return str(path) +# https://github.com/mattcurrie/which.gb +@pytest.fixture(scope="session") +def which_file(): + path = extra_test_rom_dir / Path("which.gb") + with FileLock(path.with_suffix(".lock")) as lock: + if not os.path.isfile(path): + print(url_open("https://pyboy.dk/mirror/LICENSE.which.txt")) + which_data = url_open("https://pyboy.dk/mirror/which.gb") + with open(path, "wb") as rom_file: + rom_file.write(which_data) + return str(path) + + +# https://github.com/nitro2k01/whichboot.gb +@pytest.fixture(scope="session") +def whichboot_file(): + path = extra_test_rom_dir / Path("whichboot.gb") + with FileLock(path.with_suffix(".lock")) as lock: + if not os.path.isfile(path): + print(url_open("https://pyboy.dk/mirror/LICENSE.whichboot.txt")) + whichboot_data = url_open("https://pyboy.dk/mirror/whichboot.gb") + with open(path, "wb") as rom_file: + rom_file.write(whichboot_data) + return str(path) + + @pytest.fixture(scope="session") def git_tetris_ai(): if os.path.isfile("extras/README/7.gif") or platform.system() == "Windows": @@ -246,7 +276,8 @@ def git_tetris_ai(): with FileLock(path.with_suffix(".lock")) as lock: if not os.path.isdir(path): # NOTE: No affiliation - git.Git(path).clone("https://github.com/uiucanh/tetris.git") + repo = git.Repo.clone_from("https://github.com/uiucanh/tetris.git", path) + repo.head.reset("a098ba8c328d8e7c406787edf61fcb0130cb4c26") _venv = venv.EnvBuilder(with_pip=True) _venv_path = Path(".venv") _venv.create(path / _venv_path) @@ -269,7 +300,8 @@ def git_pyboy_rl(): with FileLock(path.with_suffix(".lock")) as lock: if not os.path.isdir(path): # NOTE: No affiliation - git.Git(path).clone("https://github.com/lixado/PyBoy-RL.git") + repo = git.Repo.clone_from("https://github.com/lixado/PyBoy-RL.git", path) + repo.head.reset("03034a2d72c19c8cdc96d95b50e446a0ab83b421") _venv = venv.EnvBuilder(with_pip=True) _venv_path = Path(".venv") _venv.create(path / _venv_path) @@ -280,6 +312,30 @@ def git_pyboy_rl(): return str(path) +@pytest.fixture(scope="session") +def git_pokemon_red_experiments(): + if os.path.isfile("README/8.gif") or platform.system() == "Windows": + return None + + import venv + path = Path("PokemonRedExperiments") + with FileLock(path.with_suffix(".lock")) as lock: + if not os.path.isdir(path): + # NOTE: No affiliation + repo = git.Repo.clone_from("https://github.com/PWhiddy/PokemonRedExperiments.git", path) + repo.head.reset("fa01143e4b8d165136199be7155757495d16e56a") + _venv = venv.EnvBuilder(with_pip=True) + _venv_path = Path(".venv") + _venv.create(path / _venv_path) + # _venv_context = _venv.ensure_directories(path / Path('.venv')) + assert os.system( + f'cd {path} && . {_venv_path / "bin" / "activate"} && pip install -r baselines/requirements.txt' + ) == 0 + # Overwrite PyBoy with local version + assert os.system(f'cd {path} && . {_venv_path / "bin" / "activate"} && pip install ../') == 0 + return str(path) + + @pytest.fixture(scope="session") def secrets(): path = extra_test_rom_dir / Path("secrets") @@ -307,6 +363,8 @@ def pack_secrets(): with ZipFile(data, "w") as _zip: for rom in [globals()[x] for x in globals().keys() if x.endswith("_rom") and x != "any_rom"]: _secrets_fixture = None + if rom == default_rom: + continue _rom = rom.__pytest_wrapped__.obj(_secrets_fixture) _zip.write(_rom, os.path.basename(_rom)) diff --git a/tests/test_pokemon_rl.py b/tests/test_pokemon_rl.py new file mode 100644 index 000000000..e87c9c87a --- /dev/null +++ b/tests/test_pokemon_rl.py @@ -0,0 +1,133 @@ +# +# License: See LICENSE.md file +# GitHub: https://github.com/Baekalfen/PyBoy +# + +import os +import platform +from pathlib import Path + +import pytest + +record_gif_py = ''' +# MIT License + +# Copyright (c) 2023 Peter Whidden + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from os.path import exists +from pathlib import Path +import uuid +from red_gym_env import RedGymEnv +from stable_baselines3 import A2C, PPO +from stable_baselines3.common import env_checker +from stable_baselines3.common.vec_env import DummyVecEnv, SubprocVecEnv +from stable_baselines3.common.utils import set_random_seed +from stable_baselines3.common.callbacks import CheckpointCallback +from argparse_pokemon import * + +from pyboy import WindowEvent + +RedGymEnv.add_video_frame = lambda x: None + +def make_env(rank, env_conf, seed=0): + """ + Utility function for multiprocessed env. + :param env_id: (str) the environment ID + :param num_env: (int) the number of environments you wish to have in subprocesses + :param seed: (int) the initial seed for RNG + :param rank: (int) index of the subprocess + """ + def _init(): + env = RedGymEnv(env_conf) + env.pyboy.send_input(WindowEvent.SCREEN_RECORDING_TOGGLE) + #env.seed(seed + rank) + return env + set_random_seed(seed) + return _init + +if __name__ == '__main__': + + sess_path = f'session_{str(uuid.uuid4())[:8]}' + ep_length = 48 # 2**16 + args = get_args(usage_string=None, headless=False, ep_length=ep_length, sess_path=sess_path) + + env_config = { + 'headless': False, 'save_final_state': True, 'early_stop': False, + 'action_freq': 24, 'init_state': '../has_pokedex_nballs.state', 'max_steps': ep_length, + 'print_rewards': True, 'save_video': False, 'fast_video': True, 'session_path': sess_path, + 'gb_path': '../PokemonRed.gb', 'debug': False, 'sim_frame_dist': 2_000_000.0 + } + env_config = change_env(env_config, args) + + num_cpu = 1 #64 #46 # Also sets the number of episodes per training iteration + env = make_env(0, env_config)() #SubprocVecEnv([make_env(i, env_config) for i in range(num_cpu)]) + + #env_checker.check_env(env) + file_name = 'session_4da05e87_main_good/poke_439746560_steps' + + if exists(file_name + '.zip'): + print('\\nloading checkpoint') + model = PPO.load(file_name, env=env, custom_objects={'lr_schedule': 0, 'clip_range': 0}) + model.n_steps = ep_length + model.n_envs = num_cpu + model.rollout_buffer.buffer_size = ep_length + model.rollout_buffer.n_envs = num_cpu + model.rollout_buffer.reset() + else: + model = PPO('CnnPolicy', env, verbose=1, n_steps=ep_length, batch_size=512, n_epochs=1, gamma=0.999) + + #keyboard.on_press_key("M", toggle_agent) + obs, info = env.reset() + env.pyboy.send_input(WindowEvent.SCREEN_RECORDING_TOGGLE) + while True: + action = 7 # pass action + try: + with open("agent_enabled.txt", "r") as f: + agent_enabled = f.readlines()[0].startswith("yes") + except: + agent_enabled = False + if agent_enabled: + action, _states = model.predict(obs, deterministic=False) + obs, rewards, terminated, truncated, info = env.step(action) + env.render() + if truncated: + break + env.pyboy.send_input(WindowEvent.SCREEN_RECORDING_TOGGLE) + env.close() +''' + + +@pytest.mark.skipif( + True or \ + os.path.isfile("extras/README/8.gif") or platform.system() == "Windows", + reason="This test takes too long for regular use" +) +def test_pokemon_rl(git_pokemon_red_experiments, pokemon_red_rom): + script_py = "record_gif.py" + with open(Path(git_pokemon_red_experiments) / "baselines" / script_py, "w") as f: + f.write(record_gif_py) + + root_path = Path("../") + assert os.system(f'rm -rf {Path(git_pokemon_red_experiments) / "recordings"}') == 0 + assert os.system( + f'cp "{pokemon_red_rom}" {git_pokemon_red_experiments}/PokemonRed.gb && cd {git_pokemon_red_experiments}/baselines && . {"../" / Path(".venv") / "bin" / "activate"} && python {script_py}' + ) == 0 + assert os.system(f'mv {Path(git_pokemon_red_experiments) / "recordings" / "SUPER*"} {Path("README/8.gif")}') == 0 diff --git a/tests/test_results/cgb_which.gb.png b/tests/test_results/cgb_which.gb.png new file mode 100644 index 0000000000000000000000000000000000000000..252a05331c339a5158dcf625970c480f592331ce GIT binary patch literal 1037 zcmeAS@N?(olHy`uVBq!ia0vp^3xIe62NRHFxc>b*0|WCFPZ!6KiaBrRc$dvF5MVH^ z+x$WBd528lw8#kpky)G{XNs=~$<6uRJZ%9(;*}2`(v|z~13{_5Qjue?6ZUzXsF>&L zX|;0OvzI#crt>tHX3h%oT7Ny8&-b#)|9Qt>r%n?oF1RP?YQx`FnRUpfbg}opbNl#i z=YB0#IiK}h|HSouSEu!!*ha{hb!oEr_= zA2aTTzT3fVJ4yY1^V-luDt}oIsk!}%PmhxgSazao)sCO>+I? z?GL{HKlpI6^5fMXUtiwS?RVWed7apP)_bmpKdN!}H9gdjz$A`r_GCXfKR;hL^IwVW z=RT{qCAW+Erd&$?7bt$>>cN8*vGKCc-gD$V^}o30T*W-UxjD=2U#71t+kcT;NMxUQ zPVe=nE26VMM5%sqRs5Y$n~?p}cXM6k#D{yAanH|OP*>6UeEN>Cnsd{v-#ypQxVrzX zoIBh0)my9f#h#A(JX=87wORRN|EKCh_WNr}`@THlZ8ng*bn<_u+qnl5!^00v-cee3 zOX|N&z5Ye+waT`x;%5q`Hr4$rG>heYb&Y-d>dRGK$KH2;=2$#;_t{{J?ei*4o%j8@ zw);hM>D~W-gmPWkW#mf^1RBV&9!4hSJiFbher~zkf29Wh0058(0ssI20N3}|000PzNkl z%eL$&2tYl(?jZmFwHfvxeRFID1O#7Uk7`*4iTD6XRetXKPQkhSNhw{|l~Q^weO=e{ znJ&-C>tKaVIr&fn<;aT3TWcthyZ22ZPpA zsz1?$UwuCRILS>8gV6~OJ*6`J`l>yHtp*n+=Pe&UAi$<4>f^!x3=A$x#8xMwxz+V7 zi9IT}O zRXl9PzN2w z^=hYsDWAS7FT^T?@Z)J3sT}?}fmJyO5C^ZkXk)q!JCPs|=^!1cijn;wh=ZLBkI5e} z&P0KOv>%9SSi4r`Ic)=5bu^}jXo)sNy5mTfbi!8vFSjZ$0G4RKsPVx=xB_!cnEfW* zWjc4IpM2jgt|aD=!V7IW3r#Sun_2!%FFNAj!_lM*rO@#-_!UQ#dZbI+wOWCu=s&{1 zg%avG*fw7DUb_7BFEpq3e#d;UE9$s#5v=rY`qJZOFoq%yX2ylLT~Z;*yWU&>#ISB! z!NAfum>(C~F44pm2;!|jQ;q$W=+3xsI)n8e5hktaMN8vv%n2H=Kd2Q(#KDI%XbrbDMwmzoLzmLu4aN#KDKtp21+ilUhL$c*%qA$%MpC9$>n0e164~6|Qpn zu|KM;4OX0NUMJD@%nxjPD0c94u9CmQnK^v}ePkty?c< zv~xNbDonz>)8Z?&S4qg{=);sFntTd9v6Y~y>X@y1#W=Xr@Xh-9RSVv}Ex_L> zdIsov6Wj<1ooRhe%0P2ER&dY(#)X{n*L#$v7nwEl`bFGX+)gg@3)W30aWJQ+KpSIw zsx5`Fcem>U|8}~vN!MREm75X5IuX?3hz6@c!k=jvOf8WRqULwTjcCgoln*vpA+f^~ ztQxcaj_?mme!%myX541tMKitPg2Q&x9yndGVjLV8GW374Vle5Tm%zF_XT%=7V5VUs zdaRpg* z_;A|q56oa&F=8o2}X%%i;IN&<=aP7_k$LqC97ERR+t1< zhFRobXXX;ke;Pz8w&(KuFN524LiURH*Xa-~t(YS-OO6%Ow-^o)2Omyj26N&<&XXR& zEHuwWKVSzPi^qjuL*Qho-RWQ=tVec0KVos`B6wgKxOvnZHeMFTIC#yt@M|=}r0}A5 zC*t74Y5!j|KW~ba6ZupO(aIqW&#dy+wERW#%|NoD37zog=ToKf3q3oXyQX}mJ{XJS zn<*zNZ&)FHaDI0fd*z#fd~h+GpY!x}df6)e#^ez4rA@DpZ*DBn?eAf3E8%WO=~kFjX=?S?i2nd*XgS1kXh?eQADtEM-i7njgPn?H1*8S?IZF zDNb>@dHgyT!9#cNJ>TU2MXQ}7K424uPLPRp$2(;p%Ac-q_8ZPTUcQ4co4!BT)g*^G+oRun2HF_g!bMmaMi-hd%f?5Q1@~X17oK5A zj4!@Fcvibph&cFgHp>SGQg+G*2MW${vI3AT^T7e1E%U(vpAY4OzsY9#;6O^m!G|;A z{lOTK-yOh+_XpENe%BR)IJmPnBfslPSGv&Zygu@~evEs6u$0K}ny~IsW#o5Fpne~W zD?WhIq^ml=-Njiq4xUAQ zHoi5M?@iLh$JmPM7uRz1ImX5j2Om!3uMF!fUj9{G0W6oww<@Rg6v^3__8$D)XMN|G zQ-hUDohfPmVtzI4eD$}xIBf&plm0g<;$0I2uhn6~ccwV)V)GId7&flaA9P?{fEn^# zFh#9^Tfi%@)5gTgbc~Jl1_lrAH{v=g-o+UA3;&3Brx0=Q;W$1vi^*xy*>2P4$2N7H zKA@=lt6V-#eJ3zJ{NI;wP?|SlvD1CCJv^T8j%B8U*44Ojnv!EKj*PEe~HHA5E}CP=U+P47L!L2 Rn3ezl002ovPDHLkV1f+nXs-YO literal 0 HcmV?d00001 diff --git a/tests/test_results/dmg_which.gb.png b/tests/test_results/dmg_which.gb.png new file mode 100644 index 0000000000000000000000000000000000000000..a16cbd8badeea422269f277a3404582a2fe9abf3 GIT binary patch literal 1072 zcmeAS@N?(olHy`uVBq!ia0vp^3xIe62NRHFxc>b*0|WDSPZ!6KiaBrRT%0yrL4@Jt zyg#q&CdbdVOYzo>5%4Kmb$RCUx3*QTZmp+!w{JP|_~VYddASBlMULH0*y(+uX5Lbj z=8)BuY`5q4R(oaM-ZtgZ_wV10x8&ab{NCtZ-sE7L=JS0k9;F(bxh(W7lYiD{{$Ex$ zdF$sISC&cFJMHUxJI#NS&Fz|hsq^I)oC%Jz{n_#URQ$imr}qAF-&ejmL3HK3qK8}U zC#Rcx-dno2=aBQ3+#B20ZvXo^<#br@?c$5C6aMp^EB9F@HSJK~sy8>|<`rztn(kPA z%vLVWFx9|EZrk#jX)%54zkJsay>~=+r}(XdM&Bxv8kE2@5{# zO)|YVZSlHm559UmS?%=goo57nTzTyj-Pgq(f@ZQ%u4S0`WILC z%gVW{onO7Zx-zbE{d0ZAv@W*vNAge85ALt2;a&ITl{j0XP0ED-m(SceXlx(Lng8hC z;Y`zimj7%P8SmA$b>%-(FukkrU*)uUEN?HWpU*D%BGP+bJw5T%FSmQq=l9HBc&DyZ z>~B)+cl&ym=!=ZU#bpabc-#-75X*PV?fG*0|IedW?%KTnwJuEhzkU46o@>p)pAUW0 zRJc>|XO(`)n@~oAshQxxmzl@7R^4e=6r~weH$1=^J_f>)us&BTKgaex?74{dW9zsZX&x0058(0ssI20N3}|000PhNkl z(VF8b2!PG`=KWu}=OViqR{;TmsO7(!u@;FW2$Ck(=kqDax%`w;uInnLJeIz$>;9W= zuc`ZBjYB#0sRqiSX@PPI%}kmFYKE<15(e`i0itXG^x)I{@LKC56JHo`~)n+;p+}{z;b_A>f-wTm>iw_ z9$Jn_{v!$t^*N!Rw7-ZGyih{$=L#0(g^F~@HGc)QBJ`)_(1Xs%A^Kaj_fQ)h&G*$o z2U9-%Em{t-tnl+`8mSz<{#NZEKpwn>)0XZ!G3Cr4(qWi1PPRV?@?a;!WAcgNtO9`7%Z^Vl|N8zBuD1tr)jz_(jS}X0 zur6Qpc3pn@gF8GJj`d(y)Oq0|Sm{5ErQ@z(48uIQY5&+26^eZ7z4b2)>y{M^Y?TN1 z?Y1tOvn@)DPUwlXbGAZegB#zg+p47d zr;7%OaMAU4OcG4Q6+GN>!@lLIl(>|;DVYZ!&hKrrHJm(>zq3Q6Nndgqy~s~N%s{l? z^d%8a|E8rNhEJ@}2U)ju`6dAiWK9RPG6}|1kfo@&fi{-LUkRV+oh;CVpU)>v>%@Nb z;6w!FqHA}`sXj{k>+<77LBJe9Wx;qmJUL;WD-%>rL`MQ;ydF;0; z!TW*hS;jUxnDhn{oock6p#0{99-b&ze_|f|c=@i%VRF0z>iaKOK>4IQqCf-|_!hJx zrY8~fO*)BJ=0SM{6FwH4g&Cjh1m8u2ou$HW;^#q=2TPTE%c%7VI*6p1c9h?S3 z9X4_Ip-EIJy}MBwMc1D)t*g)z^8`axgKm~uDk8XnfPEapu8? zGk$+?4X0e#bSC699}W9k-6=7=_sT?nP<}l2FXaI1!9{YD8`fVchvw>j-z?QaI6fyUt-As)&&gUm?)WNP(j;A)k$AAsR%POW#tUIYg+yy^=6>hFoZ7n^XsWn`Hh~FdG=5~Qy+{)>djP;RS&F? z9$eoIW3PHMP!DcS=R0m%XO^vkI7|*9Upn*(_2z+#e!N6)SZp8t<}=X7CV8rrVPaDAFpk=)ypewK3`1gkUp-2fuqA54?= zT~`e9;K|v{`mQTeOJDOuk&Vco6DtnZpY{XQ70n@Zcg`KtP^lngiz zN027A>#2wd(IJCm5I8 zW<~Li;yeK7ni%w(ZT$Z8qIETY(26&pG^tnTkGnYQ=D}~#?~QMb<$IHK`7!2E{qkCl zKF8R2%g8X{GkBC?U&X6G>N|k#Qu$Wxw4Ne4`!ZgG@8j%`91Ci&c4;sronI`jrh~8f zaTjN7VDow{lgxKbVCBKO9Ag+vamLMjcM|e+8U z_d@z%`7TD>FZ?s#eTB?}56AJgSxnB5$#I7s78WzllE5`gFf$W2d$BL>yioO{3_9=H|h}ZehHok9t3V2-NxGD&*gA^I*R@4ako{ z6#2TyB)xofP>IbyM-(@T_Oo$Jehf+g`|I? Date: Thu, 26 Oct 2023 22:34:01 +0200 Subject: [PATCH 15/65] Add button, button_press and button_release functions --- extras/examples/gamewrapper_kirby.py | 6 +- extras/examples/gamewrapper_tetris.py | 6 +- .../plugins/game_wrapper_kirby_dream_land.py | 10 +- .../plugins/game_wrapper_super_mario_land.py | 10 +- pyboy/plugins/game_wrapper_tetris.py | 17 +--- pyboy/pyboy.py | 96 +++++++++++++++++++ pyboy/utils.py | 7 +- tests/test_basics.py | 19 ++-- tests/test_external_api.py | 34 +++---- tests/test_game_wrapper_mario.py | 2 +- tests/test_openai_gym.py | 47 ++++----- tests/test_rtc3test.py | 12 +-- tests/test_states.py | 8 +- 13 files changed, 162 insertions(+), 112 deletions(-) diff --git a/extras/examples/gamewrapper_kirby.py b/extras/examples/gamewrapper_kirby.py index ba5bcf6f0..f487b8dbb 100644 --- a/extras/examples/gamewrapper_kirby.py +++ b/extras/examples/gamewrapper_kirby.py @@ -31,10 +31,8 @@ assert kirby.lives_left == 4 assert kirby.health == 6 -pyboy.send_input(WindowEvent.PRESS_ARROW_RIGHT) - -for _ in range(280): # Walk for 280 ticks - pyboy.tick() +pyboy.button_press("right") +pyboy.tick(280, True) # Walk for 280 ticks assert kirby.score == 800 assert kirby.health == 5 diff --git a/extras/examples/gamewrapper_tetris.py b/extras/examples/gamewrapper_tetris.py index 08f0f53c8..5824facff 100644 --- a/extras/examples/gamewrapper_tetris.py +++ b/extras/examples/gamewrapper_tetris.py @@ -44,10 +44,8 @@ pyboy.tick() # The playing "technique" is just to move the Tetromino to the right. - if frame % 2 == 0: # Even frames - pyboy.send_input(WindowEvent.PRESS_ARROW_RIGHT) - elif frame % 2 == 1: # Odd frames - pyboy.send_input(WindowEvent.RELEASE_ARROW_RIGHT) + if frame % 2 == 0: # Even frames to let PyBoy release the button on odd frames + pyboy.button("right") # Illustrating how we can extract the game board quite simply. This can be used to read the tile identifiers. game_area = tetris.game_area() diff --git a/pyboy/plugins/game_wrapper_kirby_dream_land.py b/pyboy/plugins/game_wrapper_kirby_dream_land.py index cf61bf8de..022198e0c 100644 --- a/pyboy/plugins/game_wrapper_kirby_dream_land.py +++ b/pyboy/plugins/game_wrapper_kirby_dream_land.py @@ -86,21 +86,17 @@ def start_game(self, timer_div=None): 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() # 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): diff --git a/pyboy/plugins/game_wrapper_super_mario_land.py b/pyboy/plugins/game_wrapper_super_mario_land.py index b2cb05902..82713866f 100644 --- a/pyboy/plugins/game_wrapper_super_mario_land.py +++ b/pyboy/plugins/game_wrapper_super_mario_land.py @@ -227,13 +227,9 @@ def start_game(self, timer_div=None, world_level=None, unlock_level_select=False self.pyboy.tick() 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 diff --git a/pyboy/plugins/game_wrapper_tetris.py b/pyboy/plugins/game_wrapper_tetris.py index c6a54b180..ca700370c 100644 --- a/pyboy/plugins/game_wrapper_tetris.py +++ b/pyboy/plugins/game_wrapper_tetris.py @@ -125,12 +125,8 @@ def start_game(self, timer_div=None): # 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) @@ -149,13 +145,8 @@ def reset_game(self, timer_div=None): PyBoyGameWrapper.reset_game(self, timer_div=timer_div) self._set_timer_div(timer_div) - - self.pyboy.send_input(WindowEvent.PRESS_BUTTON_START) - self.pyboy.tick() - self.pyboy.send_input(WindowEvent.RELEASE_BUTTON_START) - - for _ in range(6): - self.pyboy.tick() + self.pyboy.button("start") + self.pyboy.tick(7, False) def game_area(self): """ diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index 13edcff61..72a230f5e 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -378,6 +378,102 @@ def override_memory_value(self, rom_bank, addr, value): # 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(self, input): + """ + 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`. + + Args: + input (str): button to press + """ + 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.RELEASE_BUTTON_A) + elif input == "b": + self.send_input(WindowEvent.PRESS_BUTTON_B) + self.queued_input.append(WindowEvent.RELEASE_BUTTON_B) + elif input == "start": + self.send_input(WindowEvent.PRESS_BUTTON_START) + self.queued_input.append(WindowEvent.RELEASE_BUTTON_START) + elif input == "select": + self.send_input(WindowEvent.PRESS_BUTTON_SELECT) + self.queued_input.append(WindowEvent.RELEASE_BUTTON_SELECT) + else: + raise Exception("Unrecognized input:", input) + + def button_press(self, input): + """ + Send input to PyBoy in the form of "a", "b", "start", "select", "left", "right", "up" and "down". + + The button will remain press until explicitly released with `PyBoy.button_release` or `PyBoy.send_input`. + + Args: + input (str): button to press + """ + 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 button_release(self, input): + """ + 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`. + + Args: + input (str): button to release + """ + 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. diff --git a/pyboy/utils.py b/pyboy/utils.py index b2348ac4c..a333a1ec9 100644 --- a/pyboy/utils.py +++ b/pyboy/utils.py @@ -153,7 +153,12 @@ class WindowEvent: >>> from pyboy import PyBoy, WindowEvent >>> pyboy = PyBoy('file.rom') - >>> pyboy.send_input(WindowEvent.PRESS_ARROW_RIGHT) + >>> pyboy.send_input(WindowEvent.PAUSE) + >>> + ``` + + Just for button presses, it might be easier to use: `pyboy.PyBoy.button`, + `pyboy.PyBoy.button_press` and `pyboy.PyBoy.button_release`. """ # ONLY ADD NEW EVENTS AT THE END OF THE LIST! diff --git a/tests/test_basics.py b/tests/test_basics.py index bf269e00a..af29470ef 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -25,16 +25,15 @@ def test_record_replay(boot_rom, default_rom): pyboy = PyBoy(default_rom, window_type="headless", bootrom_file=boot_rom, record_input=True) pyboy.set_emulation_speed(0) - pyboy.tick() - pyboy.send_input(WindowEvent.PRESS_ARROW_DOWN) - pyboy.tick() - pyboy.send_input(WindowEvent.PRESS_ARROW_UP) - pyboy.tick() - pyboy.tick() - pyboy.send_input(WindowEvent.PRESS_ARROW_DOWN) - pyboy.tick() - pyboy.send_input(WindowEvent.PRESS_ARROW_UP) - pyboy.tick() + pyboy.tick(1, True) + pyboy.button_press("down") + pyboy.tick(1, True) + pyboy.button_press("up") + pyboy.tick(2, True) + pyboy.button_press("down") + pyboy.tick(1, True) + pyboy.button_press("up") + pyboy.tick(1, True) events = pyboy.plugin_manager.record_replay.recorded_input assert len(events) == 4, "We assumed only 4 frames were recorded, as frames without events are skipped." diff --git a/tests/test_external_api.py b/tests/test_external_api.py index ebf2299b9..317c51da7 100644 --- a/tests/test_external_api.py +++ b/tests/test_external_api.py @@ -132,29 +132,19 @@ def test_tetris(tetris_rom): # Start game. Just press Start and A when the game allows us. # The frames are not 100% accurate. if frame == 144: - pyboy.send_input(WindowEvent.PRESS_BUTTON_START) - elif frame == 145: - pyboy.send_input(WindowEvent.RELEASE_BUTTON_START) + pyboy.button("start") elif frame == 152: - pyboy.send_input(WindowEvent.PRESS_BUTTON_A) - elif frame == 153: - pyboy.send_input(WindowEvent.RELEASE_BUTTON_A) + pyboy.button("a") elif frame == 156: - pyboy.send_input(WindowEvent.PRESS_BUTTON_A) - elif frame == 157: - pyboy.send_input(WindowEvent.RELEASE_BUTTON_A) + pyboy.button("a") elif frame == 162: - pyboy.send_input(WindowEvent.PRESS_BUTTON_A) - elif frame == 163: - pyboy.send_input(WindowEvent.RELEASE_BUTTON_A) + pyboy.button("a") # Play game. When we are passed the 168th frame, the game has begone. # The "technique" is just to move the Tetromino to the right. elif frame > 168: if frame % 2 == 0: - pyboy.send_input(WindowEvent.PRESS_ARROW_RIGHT) - elif frame % 2 == 1: - pyboy.send_input(WindowEvent.RELEASE_ARROW_RIGHT) + pyboy.button("right") # Show how we can read the tile data for the screen. We can use # this to see when one of the Tetrominos touch the bottom. This @@ -296,8 +286,8 @@ def test_tetris(tetris_rom): pyboy.save_state(state_data) break - pyboy.send_input(WindowEvent.RELEASE_ARROW_RIGHT) - pyboy.tick() + pyboy.button_release("right") + pyboy.tick(1, False) pre_load_game_board_matrix = None for frame in range(1016, 1865): @@ -347,14 +337,12 @@ def test_tilemap_position_list(supermarioland_rom): pyboy.tick() # Start the game - pyboy.send_input(WindowEvent.PRESS_BUTTON_START) - pyboy.tick() - pyboy.send_input(WindowEvent.RELEASE_BUTTON_START) + pyboy.button("start") + pyboy.tick(1, False) # Move right for 100 frame - pyboy.send_input(WindowEvent.PRESS_ARROW_RIGHT) - for _ in range(100): - pyboy.tick() + pyboy.button_press("right") + pyboy.tick(100, False) # Get screen positions, and verify the values positions = pyboy.botsupport_manager().screen().tilemap_position_list() diff --git a/tests/test_game_wrapper_mario.py b/tests/test_game_wrapper_mario.py index 5357f2edd..895b8456a 100644 --- a/tests/test_game_wrapper_mario.py +++ b/tests/test_game_wrapper_mario.py @@ -57,7 +57,7 @@ def test_mario_game_over(supermarioland_rom): mario = pyboy.game_wrapper() mario.start_game() mario.set_lives_left(0) - pyboy.send_input(WindowEvent.PRESS_ARROW_RIGHT) + pyboy.button_press("right") for _ in range(500): # Enough to game over correctly, and not long enough it'll work without setting the lives pyboy.tick() if mario.game_over(): diff --git a/tests/test_openai_gym.py b/tests/test_openai_gym.py index 720e8731e..8c0e77a3c 100644 --- a/tests/test_openai_gym.py +++ b/tests/test_openai_gym.py @@ -184,39 +184,32 @@ def test_tetris(self, pyboy): assert tetris.lines == 0 for n in range(3): - pyboy.send_input(WindowEvent.PRESS_ARROW_RIGHT) - pyboy.tick() - pyboy.send_input(WindowEvent.RELEASE_ARROW_RIGHT) - pyboy.tick() + pyboy.button("right") + pyboy.tick(2, True) - pyboy.send_input(WindowEvent.PRESS_ARROW_DOWN) + pyboy.button_press("down") while tetris.score == 0: - pyboy.tick() - pyboy.tick() - pyboy.tick() - pyboy.send_input(WindowEvent.RELEASE_ARROW_DOWN) + pyboy.tick(1, True) + pyboy.tick(2, True) + pyboy.button_release("down") for n in range(3): - pyboy.send_input(WindowEvent.PRESS_ARROW_LEFT) - pyboy.tick() - pyboy.send_input(WindowEvent.RELEASE_ARROW_LEFT) - pyboy.tick() + pyboy.button("left") + pyboy.tick(2, True) tetris.set_tetromino("O") - pyboy.send_input(WindowEvent.PRESS_ARROW_DOWN) + pyboy.button_press("down") while tetris.score == 16: - pyboy.tick() - pyboy.tick() - pyboy.tick() - pyboy.send_input(WindowEvent.RELEASE_ARROW_DOWN) + pyboy.tick(1, True) + pyboy.tick(2, True) + pyboy.button_release("down") - pyboy.tick() - pyboy.send_input(WindowEvent.PRESS_ARROW_DOWN) + pyboy.tick(1, True) + pyboy.button_press("down") while tetris.score == 32: - pyboy.tick() - pyboy.tick() - pyboy.tick() - pyboy.send_input(WindowEvent.RELEASE_ARROW_DOWN) + pyboy.tick(1, True) + pyboy.tick(2, True) + pyboy.button_release("down") while tetris.score == 47: pyboy.tick() @@ -227,10 +220,8 @@ def test_tetris(self, pyboy): assert tetris.lines == 1 while not tetris.game_over(): - pyboy.send_input(WindowEvent.PRESS_ARROW_DOWN) - pyboy.tick() - pyboy.send_input(WindowEvent.RELEASE_ARROW_DOWN) - pyboy.tick() + pyboy.button("down") + pyboy.tick(2, True) pyboy.stop(save=False) diff --git a/tests/test_rtc3test.py b/tests/test_rtc3test.py index fd0d7d270..4a82b2bcc 100644 --- a/tests/test_rtc3test.py +++ b/tests/test_rtc3test.py @@ -27,15 +27,11 @@ def test_rtc3test(subtest, rtc3test_file): pyboy.tick() for n in range(subtest): - pyboy.send_input(WindowEvent.PRESS_ARROW_DOWN) - pyboy.tick() - pyboy.send_input(WindowEvent.RELEASE_ARROW_DOWN) - pyboy.tick() + pyboy.button("down") + pyboy.tick(2, True) - pyboy.send_input(WindowEvent.PRESS_BUTTON_A) - pyboy.tick() - pyboy.send_input(WindowEvent.RELEASE_BUTTON_A) - pyboy.tick() + pyboy.button("a") + pyboy.tick(2, True) while True: # Continue until it says "(A) Return" diff --git a/tests/test_states.py b/tests/test_states.py index 17d6bdef2..447e99c2c 100644 --- a/tests/test_states.py +++ b/tests/test_states.py @@ -34,12 +34,8 @@ def test_load_save_consistency(tetris_rom): # Start game. Just press Start when the game allows us. for i in range(2): - pyboy.send_input(WindowEvent.PRESS_BUTTON_START) - pyboy.tick() - pyboy.send_input(WindowEvent.RELEASE_BUTTON_START) - - for _ in range(6): - pyboy.tick() + pyboy.button("start") + pyboy.tick(7, True) ############################################################## # Verify From 682e420969fd681ac90bbea2ae087f4acf956fdf Mon Sep 17 00:00:00 2001 From: Mads Ynddal <5528170+Baekalfen@users.noreply.github.com> Date: Thu, 26 Oct 2023 22:20:20 +0200 Subject: [PATCH 16/65] tick(count=*, render=*) + remove _rendering() and screen_image() --- README.md | 5 +- extras/examples/gamewrapper_kirby.py | 4 +- extras/examples/gamewrapper_mario.py | 2 +- extras/examples/gamewrapper_tetris.py | 4 +- pyboy/__main__.py | 2 +- pyboy/openai_gym.py | 4 +- .../plugins/game_wrapper_kirby_dream_land.py | 8 +-- .../plugins/game_wrapper_super_mario_land.py | 5 +- pyboy/plugins/game_wrapper_tetris.py | 2 +- pyboy/plugins/window_dummy.py | 2 - pyboy/pyboy.pxd | 4 +- pyboy/pyboy.py | 72 ++++++++++--------- tests/test_acid_cgb.py | 7 +- tests/test_acid_dmg.py | 7 +- tests/test_basics.py | 9 +-- tests/test_blargg.py | 5 +- tests/test_external_api.py | 26 +++---- tests/test_game_wrapper_mario.py | 5 +- tests/test_magen.py | 7 +- tests/test_mario_rl.py | 2 +- tests/test_mooneye.py | 9 +-- tests/test_openai_gym.py | 5 +- tests/test_replay.py | 7 +- tests/test_rtc3test.py | 9 +-- tests/test_samesuite.py | 9 +-- tests/test_shonumi.py | 3 +- tests/test_states.py | 2 +- tests/test_tetris_ai.py | 3 +- tests/test_which.py | 7 +- tests/test_whichboot.py | 7 +- 30 files changed, 107 insertions(+), 136 deletions(-) diff --git a/README.md b/README.md index a565e41e3..8f54069e3 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Now you're ready! Either use PyBoy directly from the terminal __`$ pyboy file.ro ```python from pyboy import PyBoy pyboy = PyBoy('ROMs/gamerom.gb') -while not pyboy.tick(): +while pyboy.tick(1, True): pass pyboy.stop() ``` @@ -69,7 +69,7 @@ Or using the context manager: ```python from pyboy import PyBoy with PyBoy('ROMs/gamerom.gb') as pyboy: - while not pyboy.tick(): + while pyboy.tick(1, True): pass ``` @@ -82,6 +82,7 @@ pyboy.tick() # Process one frame to let the game register the input pyboy.send_input(WindowEvent.RELEASE_ARROW_DOWN) pil_image = pyboy.screen_image() +pyboy.tick(1, True) pil_image.save('screenshot.png') ``` diff --git a/extras/examples/gamewrapper_kirby.py b/extras/examples/gamewrapper_kirby.py index f487b8dbb..80b3a8229 100644 --- a/extras/examples/gamewrapper_kirby.py +++ b/extras/examples/gamewrapper_kirby.py @@ -32,7 +32,9 @@ assert kirby.health == 6 pyboy.button_press("right") -pyboy.tick(280, True) # Walk for 280 ticks +for _ in range(280): # Walk for 280 ticks + # We tick one frame at a time to still render the screen for every step + pyboy.tick(1, True) assert kirby.score == 800 assert kirby.health == 5 diff --git a/extras/examples/gamewrapper_mario.py b/extras/examples/gamewrapper_mario.py index 4acac0edc..dcbe4871a 100644 --- a/extras/examples/gamewrapper_mario.py +++ b/extras/examples/gamewrapper_mario.py @@ -41,7 +41,7 @@ assert mario.fitness >= last_fitness last_fitness = mario.fitness - pyboy.tick() + pyboy.tick(1, True) if mario.lives_left == 1: assert last_fitness == 27700 assert mario.fitness == 17700 # Loosing a live, means 10.000 points in this fitness scoring diff --git a/extras/examples/gamewrapper_tetris.py b/extras/examples/gamewrapper_tetris.py index 5824facff..1b315c534 100644 --- a/extras/examples/gamewrapper_tetris.py +++ b/extras/examples/gamewrapper_tetris.py @@ -40,8 +40,8 @@ blank_tile = 47 first_brick = False -for frame in range(1000): # Enough frames for the test. Otherwise do: `while not pyboy.tick():` - pyboy.tick() +for frame in range(1000): # Enough frames for the test. Otherwise do: `while pyboy.tick():` + pyboy.tick(1, True) # The playing "technique" is just to move the Tetromino to the right. if frame % 2 == 0: # Even frames to let PyBoy release the button on odd frames diff --git a/pyboy/__main__.py b/pyboy/__main__.py index 0e4b9722e..19d0a2904 100644 --- a/pyboy/__main__.py +++ b/pyboy/__main__.py @@ -161,7 +161,7 @@ def main(): with open(state_path, "rb") as f: pyboy.load_state(f) - while not pyboy.tick(): + while pyboy.tick(True): pass pyboy.stop() diff --git a/pyboy/openai_gym.py b/pyboy/openai_gym.py index 52fbc20b5..0c02cad3e 100644 --- a/pyboy/openai_gym.py +++ b/pyboy/openai_gym.py @@ -132,7 +132,7 @@ def step(self, action_id): action = self.actions[action_id] if action == self._DO_NOTHING: - pyboy_done = self.pyboy.tick() + pyboy_done = self.pyboy.tick(1, self.observation_type == "raw") else: if self.action_type == "toggle": if self._button_is_pressed[action]: @@ -142,7 +142,7 @@ def step(self, action_id): self._button_is_pressed[action] = True self.pyboy.send_input(action) - pyboy_done = self.pyboy.tick() + pyboy_done = self.pyboy.tick(1, self.observation_type == "raw") if self.action_type == "press": self.pyboy.send_input(self._release_button[action]) diff --git a/pyboy/plugins/game_wrapper_kirby_dream_land.py b/pyboy/plugins/game_wrapper_kirby_dream_land.py index 022198e0c..62392acf6 100644 --- a/pyboy/plugins/game_wrapper_kirby_dream_land.py +++ b/pyboy/plugins/game_wrapper_kirby_dream_land.py @@ -80,7 +80,7 @@ def start_game(self, timer_div=None): # Boot screen while True: - self.pyboy.tick() + self.pyboy.tick(1, False) self.tilemap_background.refresh_lcdc() if self.tilemap_background[0:3, 16] == [231, 224, 235]: # 'HAL' on the first screen break @@ -91,16 +91,14 @@ def start_game(self, timer_div=None): self.pyboy.tick() # 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.button("start") self.pyboy.tick() # Wait for transition to finish (exit level intro screen, enter game) - for _ in range(60): - self.pyboy.tick() + self.pyboy.tick(60, False) self.game_has_started = True diff --git a/pyboy/plugins/game_wrapper_super_mario_land.py b/pyboy/plugins/game_wrapper_super_mario_land.py index 82713866f..37d81e93f 100644 --- a/pyboy/plugins/game_wrapper_super_mario_land.py +++ b/pyboy/plugins/game_wrapper_super_mario_land.py @@ -224,9 +224,10 @@ def start_game(self, timer_div=None, world_level=None, unlock_level_select=False # 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(3, False) self.pyboy.button("start") self.pyboy.tick(1, False) @@ -235,7 +236,7 @@ def start_game(self, timer_div=None, world_level=None, unlock_level_select=False 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) break - self.pyboy.tick() + self.pyboy.tick(1, False) self.tilemap_background.refresh_lcdc() # "MARIO" in the title bar and 0 is placed at score diff --git a/pyboy/plugins/game_wrapper_tetris.py b/pyboy/plugins/game_wrapper_tetris.py index ca700370c..06e2dc1df 100644 --- a/pyboy/plugins/game_wrapper_tetris.py +++ b/pyboy/plugins/game_wrapper_tetris.py @@ -118,7 +118,7 @@ def start_game(self, timer_div=None): # Boot screen while True: - self.pyboy.tick() + self.pyboy.tick(1, False) self.tilemap_background.refresh_lcdc() if self.tilemap_background[2:9, 14] == [89, 25, 21, 10, 34, 14, 27]: # '1PLAYER' on the first screen break diff --git a/pyboy/plugins/window_dummy.py b/pyboy/plugins/window_dummy.py index 5fc24985a..3a6c81fea 100644 --- a/pyboy/plugins/window_dummy.py +++ b/pyboy/plugins/window_dummy.py @@ -17,8 +17,6 @@ def __init__(self, pyboy, mb, pyboy_argv): if not self.enabled(): return - pyboy._rendering(False) - def enabled(self): return self.pyboy_argv.get("window_type") == "dummy" diff --git a/pyboy/pyboy.pxd b/pyboy/pyboy.pxd index ed89de850..87c794a17 100644 --- a/pyboy/pyboy.pxd +++ b/pyboy/pyboy.pxd @@ -46,7 +46,9 @@ cdef class PyBoy: cdef list external_input @cython.locals(t_start=int64_t, t_pre=int64_t, t_tick=int64_t, t_post=int64_t, nsecs=int64_t) - cpdef bint tick(self) noexcept + cpdef bint _tick(self, bint) noexcept + @cython.locals(running=bint) + cpdef bint tick(self, count=*, render=*) noexcept cpdef void stop(self, save=*) noexcept @cython.locals(state_path=str) diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index 72a230f5e..27e3255f2 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -115,23 +115,15 @@ def __init__( self._hooks = {} self.initialized = True - 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._ - """ + 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: + self.__rendering(render) # Reenter mb.tick until we eventually get a clean exit without breakpoints while self.mb.tick(): # Breakpoint reached @@ -167,7 +159,39 @@ def tick(self): 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 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._ + + Setting `render` to `True` will make PyBoy render the screen for this tick. For AI training, it's adviced to use + this sparingly, as it will 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 the screen was rendered, use `pyboy.api.screen.Screen` to get NumPy buffer or a raw memory buffer. + + Args: + count (int): Number of ticks to process + render (bool): Whether to render an image for this tick + Returns + ------- + (True or False): + False if emulation has ended otherwise True + """ + + running = False + while count != 0: + _render = render and count == 1 # Only render on last tick to improve performance + running = self._tick(_render) + count -= 1 + return running def _handle_events(self, events): # This feeds events into the tick-loop from the window. There might already be events in the list from the API. @@ -562,22 +586,8 @@ def load_state(self, file_like_object): self.mb.load_state(IntIOWrapper(file_like_object)) - def screen_image(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](http://bgb.bircd.org/pandocs.htm) on tiles/sprites, - and join our Discord channel for more help. - - Returns - ------- - PIL.Image: - RGB image of (160, 144) pixels - """ - return self.botsupport_manager().screen().screen_image() + def game_area(self): + raise Exception("game_area not implemented") def _serial(self): """ @@ -630,12 +640,6 @@ def cartridge_title(self): """ return self.mb.cartridge.gamename - def _rendering(self, value): - """ - Disable or enable rendering - """ - self.mb.lcd.disable_renderer = not value - def _is_cpu_stuck(self): return self.mb.cpu.is_stuck diff --git a/tests/test_acid_cgb.py b/tests/test_acid_cgb.py index ec983c74b..de85314df 100644 --- a/tests/test_acid_cgb.py +++ b/tests/test_acid_cgb.py @@ -17,11 +17,8 @@ def test_cgb_acid(cgb_acid_file): pyboy = PyBoy(cgb_acid_file, window_type="headless") pyboy.set_emulation_speed(0) - for _ in range(59): - pyboy.tick() - - for _ in range(25): - pyboy.tick() + pyboy.tick(59, True) + pyboy.tick(25, True) png_path = Path(f"tests/test_results/{os.path.basename(cgb_acid_file)}.png") image = pyboy.botsupport_manager().screen().screen_image() diff --git a/tests/test_acid_dmg.py b/tests/test_acid_dmg.py index 93f1527fe..a52e9e1ae 100644 --- a/tests/test_acid_dmg.py +++ b/tests/test_acid_dmg.py @@ -18,11 +18,8 @@ def test_dmg_acid(cgb, dmg_acid_file): pyboy = PyBoy(dmg_acid_file, window_type="headless", cgb=cgb) pyboy.set_emulation_speed(0) - for _ in range(59): - pyboy.tick() - - for _ in range(25): - pyboy.tick() + pyboy.tick(59, True) + pyboy.tick(25, True) png_path = Path(f"tests/test_results/{'cgb' if cgb else 'dmg'}_{os.path.basename(dmg_acid_file)}.png") image = pyboy.botsupport_manager().screen().screen_image() diff --git a/tests/test_basics.py b/tests/test_basics.py index af29470ef..76c93bd8f 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -98,8 +98,7 @@ def test_argv_parser(*args): def test_tilemaps(kirby_rom): pyboy = PyBoy(kirby_rom, window_type="dummy") pyboy.set_emulation_speed(0) - for _ in range(120): - pyboy.tick() + pyboy.tick(120, False) bck_tilemap = pyboy.botsupport_manager().tilemap_background() wdw_tilemap = pyboy.botsupport_manager().tilemap_window() @@ -174,8 +173,7 @@ def test_randomize_ram(default_rom): def test_not_cgb(pokemon_crystal_rom): pyboy = PyBoy(pokemon_crystal_rom, window_type="dummy", cgb=False) pyboy.set_emulation_speed(0) - for _ in range(60 * 7): - pyboy.tick() + pyboy.tick(60 * 7, False) assert pyboy.botsupport_manager().tilemap_background()[1:16, 16] == [ 134, 160, 172, 164, 383, 129, 174, 184, 383, 130, 174, 171, 174, 177, 232 @@ -199,8 +197,7 @@ def test_all_modes(cgb, _bootrom, frames, rom, any_rom_cgb, boot_cgb_rom): pyboy = PyBoy(rom, window_type="headless", bootrom_file=_bootrom, cgb=cgb) pyboy.set_emulation_speed(0) - for _ in range(frames): - pyboy.tick() + pyboy.tick(frames, True) rom_name = "cgbrom" if rom == any_rom_cgb else "dmgrom" png_path = Path(f"tests/test_results/all_modes/{rom_name}_{cgb}_{os.path.basename(str(_bootrom))}.png") diff --git a/tests/test_blargg.py b/tests/test_blargg.py index 879003d71..653f472b5 100644 --- a/tests/test_blargg.py +++ b/tests/test_blargg.py @@ -25,7 +25,7 @@ def run_rom(rom): pyboy.set_emulation_speed(0) t = time.time() result = "" - while not pyboy.tick(): + while pyboy.tick(1, False): b = pyboy._serial() if b != "": result += b @@ -34,8 +34,7 @@ def run_rom(rom): if pyboy._is_cpu_stuck(): break - for _ in range(10): - pyboy.tick() + pyboy.tick(10, False) pyboy.stop(save=False) result += pyboy._serial() # Getting the absolute last. Some times the tests says "Failed X tests". if result == "": diff --git a/tests/test_external_api.py b/tests/test_external_api.py index 317c51da7..8ad393243 100644 --- a/tests/test_external_api.py +++ b/tests/test_external_api.py @@ -20,15 +20,14 @@ def test_misc(default_rom): pyboy = PyBoy(default_rom, window_type="dummy") pyboy.set_emulation_speed(0) - pyboy.tick() + pyboy.tick(1, False) pyboy.stop(save=False) def test_tiles(default_rom): pyboy = PyBoy(default_rom, window_type="dummy") pyboy.set_emulation_speed(0) - for _ in range(BOOTROM_FRAMES_UNTIL_LOGO): - pyboy.tick() + pyboy.tick(BOOTROM_FRAMES_UNTIL_LOGO, False) tile = pyboy.botsupport_manager().tilemap_window().tile(0, 0) assert isinstance(tile, Tile) @@ -70,8 +69,7 @@ def test_screen_buffer_and_image(tetris_rom, boot_rom): pyboy = PyBoy(tetris_rom, window_type="headless", bootrom_file=boot_rom) pyboy.set_emulation_speed(0) - for n in range(275): # Iterate to boot logo - pyboy.tick() + pyboy.tick(275, True) # Iterate to boot logo assert pyboy.botsupport_manager().screen().raw_screen_buffer_dims() == (144, 160) assert pyboy.botsupport_manager().screen().raw_screen_buffer_format() == cformat @@ -124,8 +122,8 @@ def test_tetris(tetris_rom): first_brick = False tile_map = pyboy.botsupport_manager().tilemap_window() state_data = io.BytesIO() - for frame in range(5282): # Enough frames to get a "Game Over". Otherwise do: `while not pyboy.tick():` - pyboy.tick() + for frame in range(5282): # Enough frames to get a "Game Over". Otherwise do: `while pyboy.tick(False):` + pyboy.tick(1, False) assert pyboy.botsupport_manager().screen().tilemap_position() == ((0, 0), (-7, 0)) @@ -291,7 +289,7 @@ def test_tetris(tetris_rom): pre_load_game_board_matrix = None for frame in range(1016, 1865): - pyboy.tick() + pyboy.tick(1, False) if frame == 1864: game_board_matrix = list(tile_map[2:12, :18]) @@ -319,9 +317,9 @@ def test_tetris(tetris_rom): tmp_state.seek(0) for _f in [tmp_state, state_data]: # Tests both file-written state and in-memory state pyboy.load_state(_f) # Reverts memory state to before we changed the Tetromino - pyboy.tick() + pyboy.tick(1, False) for frame in range(1016, 1865): - pyboy.tick() + pyboy.tick(1, False) if frame == 1864: game_board_matrix = list(tile_map[2:12, :18]) @@ -333,8 +331,7 @@ def test_tetris(tetris_rom): def test_tilemap_position_list(supermarioland_rom): pyboy = PyBoy(supermarioland_rom, window_type="dummy") pyboy.set_emulation_speed(0) - for _ in range(100): - pyboy.tick() + pyboy.tick(100, False) # Start the game pyboy.button("start") @@ -353,8 +350,7 @@ def test_tilemap_position_list(supermarioland_rom): last_y = positions[y][0] # Progress another 10 frames to see and increase in SCX - for _ in range(10): - pyboy.tick() + pyboy.tick(10, False) # Get screen positions, and verify the values positions = pyboy.botsupport_manager().screen().tilemap_position_list() @@ -369,7 +365,7 @@ def test_tilemap_position_list(supermarioland_rom): def get_set_override(default_rom): pyboy = PyBoy(default_rom, window_type="dummy") pyboy.set_emulation_speed(0) - pyboy.tick() + pyboy.tick(1, False) assert pyboy.get_memory_value(0xFF40) == 0x91 assert pyboy.set_memory_value(0xFF40) == 0x12 diff --git a/tests/test_game_wrapper_mario.py b/tests/test_game_wrapper_mario.py index 895b8456a..4bf4f7893 100644 --- a/tests/test_game_wrapper_mario.py +++ b/tests/test_game_wrapper_mario.py @@ -9,6 +9,7 @@ import numpy as np import pytest + from pyboy import PyBoy, WindowEvent py_version = platform.python_version()[:3] @@ -40,7 +41,7 @@ def test_mario_advanced(supermarioland_rom): mario.start_game(world_level=(3, 2)) lives = 99 mario.set_lives_left(lives) - pyboy.tick() + pyboy.tick(1, False) assert mario.score == 0 assert mario.lives_left == lives @@ -59,7 +60,7 @@ def test_mario_game_over(supermarioland_rom): mario.set_lives_left(0) pyboy.button_press("right") for _ in range(500): # Enough to game over correctly, and not long enough it'll work without setting the lives - pyboy.tick() + pyboy.tick(1, False) if mario.game_over(): break pyboy.stop() diff --git a/tests/test_magen.py b/tests/test_magen.py index 08a74d8fe..c9fc8d786 100644 --- a/tests/test_magen.py +++ b/tests/test_magen.py @@ -17,11 +17,8 @@ def test_magen_test(magen_test_file): pyboy = PyBoy(magen_test_file, window_type="headless") pyboy.set_emulation_speed(0) - for _ in range(59): - pyboy.tick() - - for _ in range(25): - pyboy.tick() + pyboy.tick(59, True) + pyboy.tick(25, True) png_path = Path(f"tests/test_results/{os.path.basename(magen_test_file)}.png") image = pyboy.botsupport_manager().screen().screen_image() diff --git a/tests/test_mario_rl.py b/tests/test_mario_rl.py index c7ed60f6c..0d9aff093 100644 --- a/tests/test_mario_rl.py +++ b/tests/test_mario_rl.py @@ -178,7 +178,7 @@ break pyboy.send_input(WindowEvent.SCREEN_RECORDING_TOGGLE) -pyboy.tick() +pyboy.tick(1, True) env.close() """ diff --git a/tests/test_mooneye.py b/tests/test_mooneye.py index 9c004d27f..b5ff7cb70 100644 --- a/tests/test_mooneye.py +++ b/tests/test_mooneye.py @@ -155,8 +155,7 @@ def test_mooneye(clean, rom, mooneye_dir, default_rom): pyboy = PyBoy(default_rom, window_type="headless", cgb=False, sound_emulated=True) pyboy.set_emulation_speed(0) saved_state = io.BytesIO() - for _ in range(59): - pyboy.tick() + pyboy.tick(59, True) pyboy.save_state(saved_state) pyboy.stop(save=False) @@ -164,13 +163,11 @@ def test_mooneye(clean, rom, mooneye_dir, default_rom): pyboy.set_emulation_speed(0) saved_state.seek(0) if clean: - for _ in range(59): - pyboy.tick() + pyboy.tick(59, True) else: pyboy.load_state(saved_state) - for _ in range(180 if "div_write" in rom else 40): - pyboy.tick() + pyboy.tick(180 if "div_write" in rom else 40, True) png_path = Path(f"tests/test_results/mooneye/{rom}.png") image = pyboy.botsupport_manager().screen().screen_image() diff --git a/tests/test_openai_gym.py b/tests/test_openai_gym.py index 8c0e77a3c..9ecea7844 100644 --- a/tests/test_openai_gym.py +++ b/tests/test_openai_gym.py @@ -212,10 +212,9 @@ def test_tetris(self, pyboy): pyboy.button_release("down") while tetris.score == 47: - pyboy.tick() + pyboy.tick(1, True) - pyboy.tick() - pyboy.tick() + pyboy.tick(2, True) assert tetris.score == 87 assert tetris.lines == 1 diff --git a/tests/test_replay.py b/tests/test_replay.py index 75146c628..c9d1c7447 100644 --- a/tests/test_replay.py +++ b/tests/test_replay.py @@ -102,8 +102,7 @@ def replay( if state_data is not None: pyboy.load_state(state_data) else: - for _ in range(padding_frames): - pyboy.tick() + pyboy.tick(padding_frames, True) # Filters out the blacklisted events recorded_input = list( @@ -134,14 +133,14 @@ def replay( # if frame_count % 30 == 0: # print(frame_count) # breakpoint() - pyboy.tick() + pyboy.tick(1, True) print(frame_count) # If end-frame in record_gif is high than frame counter if recording: pyboy.send_input(WindowEvent.SCREEN_RECORDING_TOGGLE) # We need to run an extra cycle for the screen recording to save - pyboy.tick() + pyboy.tick(1, True) print(frame_count) recording ^= True diff --git a/tests/test_rtc3test.py b/tests/test_rtc3test.py index 4a82b2bcc..075126351 100644 --- a/tests/test_rtc3test.py +++ b/tests/test_rtc3test.py @@ -20,11 +20,8 @@ def test_rtc3test(subtest, rtc3test_file): pyboy = PyBoy(rtc3test_file, window_type="headless") pyboy.set_emulation_speed(0) - for _ in range(59): - pyboy.tick() - - for _ in range(25): - pyboy.tick() + pyboy.tick(59, True) + pyboy.tick(25, True) for n in range(subtest): pyboy.button("down") @@ -37,7 +34,7 @@ def test_rtc3test(subtest, rtc3test_file): # Continue until it says "(A) Return" if pyboy.botsupport_manager().tilemap_background()[6:14, 17] == [193, 63, 27, 40, 55, 56, 53, 49]: break - pyboy.tick() + pyboy.tick(1, True) png_path = Path(f"tests/test_results/{rtc3test_file}_{subtest}.png") image = pyboy.botsupport_manager().screen().screen_image() diff --git a/tests/test_samesuite.py b/tests/test_samesuite.py index d1983a786..81a0b972e 100644 --- a/tests/test_samesuite.py +++ b/tests/test_samesuite.py @@ -121,8 +121,7 @@ def test_samesuite(clean, gb_type, rom, samesuite_dir, boot_cgb_rom, boot_rom, d ) pyboy.set_emulation_speed(0) saved_state = io.BytesIO() - for _ in range(180 if gb_type == "cgb" else 350): - pyboy.tick() + pyboy.tick(180 if gb_type == "cgb" else 350, True) pyboy.save_state(saved_state) pyboy.stop(save=False) @@ -136,15 +135,13 @@ def test_samesuite(clean, gb_type, rom, samesuite_dir, boot_cgb_rom, boot_rom, d pyboy.set_emulation_speed(0) saved_state.seek(0) if clean: - for _ in range(180 if gb_type == "cgb" else 350): - pyboy.tick() + pyboy.tick(180 if gb_type == "cgb" else 350, True) else: pyboy.load_state(saved_state) for _ in range(10): if np.all(pyboy.botsupport_manager().screen().screen_ndarray() > 240): - for _ in range(20): - pyboy.tick() + pyboy.tick(20, True) else: break diff --git a/tests/test_shonumi.py b/tests/test_shonumi.py index 8467a27b9..c01feea51 100644 --- a/tests/test_shonumi.py +++ b/tests/test_shonumi.py @@ -26,8 +26,7 @@ def test_shonumi(rom, shonumi_dir): # 60 PyBoy Boot # 23 Loading # 48 Progress to screenshot - for _ in range(60 + 23 + 48): - pyboy.tick() + pyboy.tick(60 + 23 + 48, True) png_path = Path(f"tests/test_results/GB Tests/{rom}.png") png_path.parents[0].mkdir(parents=True, exist_ok=True) diff --git a/tests/test_states.py b/tests/test_states.py index 447e99c2c..4fbbfa831 100644 --- a/tests/test_states.py +++ b/tests/test_states.py @@ -27,7 +27,7 @@ def test_load_save_consistency(tetris_rom): # Boot screen while True: - pyboy.tick() + pyboy.tick(1, True) tilemap_background = pyboy.botsupport_manager().tilemap_background() if tilemap_background[2:9, 14] == [89, 25, 21, 10, 34, 14, 27]: # '1PLAYER' on the first screen break diff --git a/tests/test_tetris_ai.py b/tests/test_tetris_ai.py index 887e69a10..9e1f45e72 100644 --- a/tests/test_tetris_ai.py +++ b/tests/test_tetris_ai.py @@ -124,8 +124,7 @@ def eval_network(epoch, child_index, child_model, record_to): for _ in range(best_action['Right']): do_sideway(pyboy, 'Right') drop_down(pyboy) - pyboy._rendering(True) - pyboy.tick() + pyboy.tick(1, True) frames.append(pyboy.screen_image()) pyboy._rendering(False) if len(frames) >= record_to: diff --git a/tests/test_which.py b/tests/test_which.py index 6dabee8ea..99ea6cad3 100644 --- a/tests/test_which.py +++ b/tests/test_which.py @@ -18,11 +18,8 @@ def test_which(cgb, which_file): pyboy = PyBoy(which_file, window_type="headless", cgb=cgb) pyboy.set_emulation_speed(0) - for _ in range(59): - pyboy.tick() - - for _ in range(25): - pyboy.tick() + pyboy.tick(59, True) + pyboy.tick(25, True) png_path = Path(f"tests/test_results/{'cgb' if cgb else 'dmg'}_{os.path.basename(which_file)}.png") image = pyboy.botsupport_manager().screen().screen_image() diff --git a/tests/test_whichboot.py b/tests/test_whichboot.py index c89d0a740..7ca11fa70 100644 --- a/tests/test_whichboot.py +++ b/tests/test_whichboot.py @@ -18,11 +18,8 @@ def test_which(cgb, whichboot_file): pyboy = PyBoy(whichboot_file, window_type="headless", cgb=cgb) pyboy.set_emulation_speed(0) - for _ in range(59): - pyboy.tick() - - for _ in range(25): - pyboy.tick() + pyboy.tick(59, True) + pyboy.tick(25, True) png_path = Path(f"tests/test_results/{'cgb' if cgb else 'dmg'}_{os.path.basename(whichboot_file)}.png") image = pyboy.botsupport_manager().screen().screen_image() From e21611cb3e312be8c01024abf0f855a249607d57 Mon Sep 17 00:00:00 2001 From: Mads Ynddal <5528170+Baekalfen@users.noreply.github.com> Date: Sat, 9 Dec 2023 22:32:45 +0100 Subject: [PATCH 17/65] Overhauled API: Move botsupport->api, etc. --- CONTRIBUTING.md | 2 +- README.md | 7 +- pyboy/__main__.py | 2 +- pyboy/api/__init__.py | 19 + pyboy/{botsupport => api}/constants.py | 0 pyboy/{botsupport => api}/screen.pxd | 2 +- pyboy/{botsupport => api}/screen.py | 5 +- pyboy/{botsupport => api}/sprite.pxd | 2 +- pyboy/{botsupport => api}/sprite.py | 6 +- pyboy/{botsupport => api}/tile.pxd | 4 +- pyboy/{botsupport => api}/tile.py | 26 +- pyboy/{botsupport => api}/tilemap.pxd | 1 - pyboy/{botsupport => api}/tilemap.py | 17 +- pyboy/botsupport/__init__.py | 15 - pyboy/botsupport/manager.pxd | 22 -- pyboy/botsupport/manager.py | 137 ------- pyboy/openai_gym.py | 8 +- pyboy/plugins/base_plugin.pxd | 6 +- pyboy/plugins/base_plugin.py | 8 +- pyboy/plugins/debug.pxd | 10 +- pyboy/plugins/debug.py | 19 +- pyboy/plugins/game_wrapper_pokemon_gen1.py | 4 +- .../plugins/game_wrapper_super_mario_land.py | 2 +- pyboy/plugins/record_replay.py | 4 +- pyboy/plugins/screen_recorder.py | 2 +- pyboy/plugins/screenshot_recorder.py | 2 +- pyboy/pyboy.pxd | 17 +- pyboy/pyboy.py | 343 +++++++++++++++++- tests/test_acid_cgb.py | 2 +- tests/test_acid_dmg.py | 2 +- tests/test_basics.py | 10 +- tests/test_external_api.py | 61 ++-- tests/test_magen.py | 2 +- tests/test_mooneye.py | 2 +- tests/test_openai_gym.py | 3 +- tests/test_replay.py | 7 +- tests/test_rtc3test.py | 4 +- tests/test_samesuite.py | 4 +- tests/test_shonumi.py | 2 +- tests/test_states.py | 2 +- tests/test_which.py | 2 +- tests/test_whichboot.py | 2 +- 42 files changed, 480 insertions(+), 317 deletions(-) create mode 100644 pyboy/api/__init__.py rename pyboy/{botsupport => api}/constants.py (100%) rename pyboy/{botsupport => api}/screen.pxd (100%) rename pyboy/{botsupport => api}/screen.py (93%) rename pyboy/{botsupport => api}/sprite.pxd (100%) rename pyboy/{botsupport => api}/sprite.py (96%) rename pyboy/{botsupport => api}/tile.pxd (99%) rename pyboy/{botsupport => api}/tile.py (85%) rename pyboy/{botsupport => api}/tilemap.pxd (99%) rename pyboy/{botsupport => api}/tilemap.py (93%) delete mode 100644 pyboy/botsupport/__init__.py delete mode 100644 pyboy/botsupport/manager.pxd delete mode 100644 pyboy/botsupport/manager.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 37ffe81fd..58cdc91ea 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,7 +3,7 @@ Any contribution is appreciated. The currently known errors are registered in th For the more major features, there are the following that you can give a try. They are also described in more detail in the [project list](https://github.com/Baekalfen/PyBoy/raw/master/extras/Projects/Projects.pdf): * Color * Link Cable -* _(Experimental)_ AI - use the `botsupport` or game wrappers to train a neural network +* _(Experimental)_ AI - use the API or game wrappers to train a neural network * _(Experimental)_ Game Wrappers - make wrappers for popular games If you want to implement something which is not on the list, feel free to do so anyway. If you want to merge it into our repo, then just send a pull request and we will have a look at it. diff --git a/README.md b/README.md index 8f54069e3..7511864ad 100644 --- a/README.md +++ b/README.md @@ -65,17 +65,18 @@ while pyboy.tick(1, True): pyboy.stop() ``` -Or using the context manager: + When the emulator is running, you can easily access [PyBoy's API](https://baekalfen.github.io/PyBoy/index.html): ```python -from pyboy import WindowEvent +pyboy.button('down') +pyboy.button('a') pyboy.send_input(WindowEvent.PRESS_ARROW_DOWN) pyboy.tick() # Process one frame to let the game register the input diff --git a/pyboy/__main__.py b/pyboy/__main__.py index 19d0a2904..40c1853ba 100644 --- a/pyboy/__main__.py +++ b/pyboy/__main__.py @@ -161,7 +161,7 @@ def main(): with open(state_path, "rb") as f: pyboy.load_state(f) - while pyboy.tick(True): + while pyboy._tick(True): pass pyboy.stop() diff --git a/pyboy/api/__init__.py b/pyboy/api/__init__.py new file mode 100644 index 000000000..2676363fd --- /dev/null +++ b/pyboy/api/__init__.py @@ -0,0 +1,19 @@ +# +# License: See LICENSE.md file +# GitHub: https://github.com/Baekalfen/PyBoy +# +""" +Tools to help interfacing with the Game Boy hardware +""" + +from . import constants +from .screen import Screen +from .sprite import Sprite +from .tile import Tile +from .tilemap import TileMap + +# __pdoc__ = { +# "constants": False, +# "manager": False, +# } +# __all__ = ["API"] diff --git a/pyboy/botsupport/constants.py b/pyboy/api/constants.py similarity index 100% rename from pyboy/botsupport/constants.py rename to pyboy/api/constants.py diff --git a/pyboy/botsupport/screen.pxd b/pyboy/api/screen.pxd similarity index 100% rename from pyboy/botsupport/screen.pxd rename to pyboy/api/screen.pxd index 07ef44cfb..0a6c2d33a 100644 --- a/pyboy/botsupport/screen.pxd +++ b/pyboy/api/screen.pxd @@ -4,8 +4,8 @@ # cimport cython cimport numpy as np -from pyboy.core.mb cimport Motherboard +from pyboy.core.mb cimport Motherboard cdef class Screen: diff --git a/pyboy/botsupport/screen.py b/pyboy/api/screen.py similarity index 93% rename from pyboy/botsupport/screen.py rename to pyboy/api/screen.py index 8dfa35554..8d182c229 100644 --- a/pyboy/botsupport/screen.py +++ b/pyboy/api/screen.py @@ -27,8 +27,7 @@ class 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 @@ -121,7 +120,7 @@ def screen_image(self): Generates a PIL Image from the screen buffer. Convenient for screen captures, but might be a bottleneck, if you use it to train a neural network. In which - case, read up on the `pyboy.botsupport` features, [Pan Docs](http://bgb.bircd.org/pandocs.htm) on tiles/sprites, + case, read up on the `pyboy.api` features, [Pan Docs](http://bgb.bircd.org/pandocs.htm) on tiles/sprites, and join our Discord channel for more help. Returns diff --git a/pyboy/botsupport/sprite.pxd b/pyboy/api/sprite.pxd similarity index 100% rename from pyboy/botsupport/sprite.pxd rename to pyboy/api/sprite.pxd index eebb6dd60..4aeb2b8da 100644 --- a/pyboy/botsupport/sprite.pxd +++ b/pyboy/api/sprite.pxd @@ -3,11 +3,11 @@ # GitHub: https://github.com/Baekalfen/PyBoy # cimport cython + cimport pyboy.utils from pyboy.core.mb cimport Motherboard - cdef class Sprite: cdef Motherboard mb cdef public int _offset diff --git a/pyboy/botsupport/sprite.py b/pyboy/api/sprite.py similarity index 96% rename from pyboy/botsupport/sprite.py rename to pyboy/api/sprite.py index 6fd6d41c4..aca875f71 100644 --- a/pyboy/botsupport/sprite.py +++ b/pyboy/api/sprite.py @@ -28,7 +28,7 @@ def __init__(self, mb, sprite_index): 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 @@ -71,7 +71,7 @@ def __init__(self, mb, sprite_index): 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`). @@ -153,7 +153,7 @@ def __init__(self, mb, sprite_index): 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)] diff --git a/pyboy/botsupport/tile.pxd b/pyboy/api/tile.pxd similarity index 99% rename from pyboy/botsupport/tile.pxd rename to pyboy/api/tile.pxd index 0df665801..47e668127 100644 --- a/pyboy/botsupport/tile.pxd +++ b/pyboy/api/tile.pxd @@ -4,10 +4,12 @@ # import cython + from libc.stdint cimport uint8_t, uint16_t, uint32_t -from pyboy.core.mb cimport Motherboard from pyboy cimport utils +from pyboy.core.mb cimport Motherboard + cdef uint16_t VRAM_OFFSET, LOW_TILEDATA diff --git a/pyboy/botsupport/tile.py b/pyboy/api/tile.py similarity index 85% rename from pyboy/botsupport/tile.py rename to pyboy/api/tile.py index 36a8f0353..187b81914 100644 --- a/pyboy/botsupport/tile.py +++ b/pyboy/api/tile.py @@ -4,7 +4,7 @@ # """ 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 numpy as np @@ -26,12 +26,11 @@ 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`. @@ -43,7 +42,7 @@ def __init__(self, mb, identifier): self.data_address = LOW_TILEDATA + (16*identifier) """ 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 @@ -114,6 +113,20 @@ def image_data(self): 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. + """ + return self._image_data() + + 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 : @@ -129,7 +142,6 @@ def image_data(self): # 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 def __eq__(self, other): diff --git a/pyboy/botsupport/tilemap.pxd b/pyboy/api/tilemap.pxd similarity index 99% rename from pyboy/botsupport/tilemap.pxd rename to pyboy/api/tilemap.pxd index 9b7d67149..410deaf83 100644 --- a/pyboy/botsupport/tilemap.pxd +++ b/pyboy/api/tilemap.pxd @@ -6,7 +6,6 @@ from pyboy.core.mb cimport Motherboard - cdef class TileMap: cdef Motherboard mb cdef bint signed_tile_data diff --git a/pyboy/botsupport/tilemap.py b/pyboy/api/tilemap.py similarity index 93% rename from pyboy/botsupport/tilemap.py rename to pyboy/api/tilemap.py index bfa74ed44..7dc814c54 100644 --- a/pyboy/botsupport/tilemap.py +++ b/pyboy/api/tilemap.py @@ -7,6 +7,7 @@ """ import numpy as np + from pyboy.core.lcd import LCDCRegister from .constants import HIGH_TILEMAP, LCDC_OFFSET, LOW_TILEDATA_NTILES, LOW_TILEMAP @@ -19,8 +20,8 @@ def __init__(self, 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. @@ -107,7 +108,7 @@ def _tile_address(self, column, row): """ 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. @@ -116,7 +117,7 @@ def _tile_address(self, column, row): 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). @@ -138,8 +139,8 @@ def _tile_address(self, column, row): 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: @@ -148,7 +149,7 @@ def tile(self, column, row): Returns ------- - `pyboy.botsupport.tile.Tile`: + `pyboy.api.tile.Tile`: Tile object corresponding to the tile index at the given coordinate in the tile map. """ @@ -207,7 +208,7 @@ def use_tile_objects(self, switch): 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 diff --git a/pyboy/botsupport/__init__.py b/pyboy/botsupport/__init__.py deleted file mode 100644 index 565372b79..000000000 --- a/pyboy/botsupport/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# -# License: See LICENSE.md file -# GitHub: https://github.com/Baekalfen/PyBoy -# -""" -Tools to help interfacing with the Game Boy hardware -""" - -__pdoc__ = { - "constants": False, - "manager": False, -} -__all__ = ["BotSupportManager"] - -from .manager import BotSupportManager diff --git a/pyboy/botsupport/manager.pxd b/pyboy/botsupport/manager.pxd deleted file mode 100644 index 40fcffa0d..000000000 --- a/pyboy/botsupport/manager.pxd +++ /dev/null @@ -1,22 +0,0 @@ -# -# License: See LICENSE.md file -# GitHub: https://github.com/Baekalfen/PyBoy -# -cimport cython -from pyboy.core.mb cimport Motherboard -from pyboy.botsupport.screen cimport Screen -from pyboy.botsupport.sprite cimport Sprite -from pyboy.botsupport.tile cimport Tile -from pyboy.botsupport.tilemap cimport TileMap - - - -cdef class BotSupportManager: - cdef object pyboy - cdef Motherboard mb - cpdef Screen screen(self) noexcept - cpdef Sprite sprite(self, int) noexcept - cpdef list sprite_by_tile_identifier(self, list, on_screen=*) noexcept - cpdef Tile tile(self, int) noexcept - cpdef TileMap tilemap_background(self) noexcept - cpdef TileMap tilemap_window(self) noexcept diff --git a/pyboy/botsupport/manager.py b/pyboy/botsupport/manager.py deleted file mode 100644 index 711d2a0b8..000000000 --- a/pyboy/botsupport/manager.py +++ /dev/null @@ -1,137 +0,0 @@ -# -# License: See LICENSE.md file -# GitHub: https://github.com/Baekalfen/PyBoy -# -from . import constants as _constants -from . import screen as _screen -from . import sprite as _sprite -from . import tile as _tile -from . import tilemap as _tilemap - -try: - from cython import compiled - cythonmode = compiled -except ImportError: - cythonmode = False - - -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") diff --git a/pyboy/openai_gym.py b/pyboy/openai_gym.py index 0c02cad3e..9351030de 100644 --- a/pyboy/openai_gym.py +++ b/pyboy/openai_gym.py @@ -5,12 +5,12 @@ import numpy as np -from .botsupport.constants import TILES +from .api.constants import TILES from .utils import WindowEvent try: from gym import Env - from gym.spaces import Discrete, MultiDiscrete, Box + from gym.spaces import Box, Discrete, MultiDiscrete enabled = True except ImportError: @@ -91,7 +91,7 @@ def __init__(self, pyboy, observation_type="tiles", action_type="toggle", simult # Building the observation_space if observation_type == "raw": - screen = np.asarray(self.pyboy.botsupport_manager().screen().screen_ndarray()) + screen = np.asarray(self.pyboy.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 @@ -120,7 +120,7 @@ def __init__(self, pyboy, observation_type="tiles", action_type="toggle", simult def _get_observation(self): if self.observation_type == "raw": - observation = np.asarray(self.pyboy.botsupport_manager().screen().screen_ndarray(), dtype=np.uint8) + observation = np.asarray(self.pyboy.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: diff --git a/pyboy/plugins/base_plugin.pxd b/pyboy/plugins/base_plugin.pxd index f6535738d..4e43c02a7 100644 --- a/pyboy/plugins/base_plugin.pxd +++ b/pyboy/plugins/base_plugin.pxd @@ -7,7 +7,6 @@ cimport cython from cpython.array cimport array from libc.stdint cimport uint8_t, uint16_t, uint32_t -from pyboy.botsupport.tilemap cimport TileMap from pyboy.core.lcd cimport Renderer from pyboy.core.mb cimport Motherboard from pyboy.logging.logging cimport Logger @@ -15,7 +14,6 @@ from pyboy.utils cimport WindowEvent cdef Logger logger - cdef int ROWS, COLS @@ -46,8 +44,8 @@ cdef class PyBoyWindowPlugin(PyBoyPlugin): cdef class PyBoyGameWrapper(PyBoyPlugin): cdef public shape cdef bint game_has_started - cdef TileMap tilemap_background - cdef TileMap tilemap_window + cdef object tilemap_background + cdef object tilemap_window cdef bint tilemap_use_background cdef uint16_t sprite_offset diff --git a/pyboy/plugins/base_plugin.py b/pyboy/plugins/base_plugin.py index 3427ff318..be1d1907a 100644 --- a/pyboy/plugins/base_plugin.py +++ b/pyboy/plugins/base_plugin.py @@ -18,7 +18,7 @@ import numpy as np import pyboy -from pyboy.botsupport.sprite import Sprite +from pyboy.api.sprite import Sprite logger = pyboy.logging.get_logger(__name__) @@ -99,8 +99,8 @@ class PyBoyGameWrapper(PyBoyPlugin): def __init__(self, *args, game_area_section=(0, 0, 32, 32), game_area_wrap_around=False, **kwargs): super().__init__(*args, **kwargs) - self.tilemap_background = self.pyboy.botsupport_manager().tilemap_background() - self.tilemap_window = self.pyboy.botsupport_manager().tilemap_window() + self.tilemap_background = self.pyboy.tilemap_background + self.tilemap_window = self.pyboy.tilemap_window self.tilemap_use_background = True self.sprite_offset = 0 self.game_has_started = False @@ -183,7 +183,7 @@ def _game_area_tiles(self): yy = self.game_area_section[1] width = self.game_area_section[2] height = self.game_area_section[3] - scanline_parameters = self.pyboy.botsupport_manager().screen().tilemap_position_list() + scanline_parameters = self.pyboy.screen.tilemap_position_list() if self.game_area_wrap_around: self._cached_game_area_tiles = np.ndarray(shape=(height, width), dtype=np.uint32) diff --git a/pyboy/plugins/debug.pxd b/pyboy/plugins/debug.pxd index 335214645..ca55a3461 100644 --- a/pyboy/plugins/debug.pxd +++ b/pyboy/plugins/debug.pxd @@ -8,8 +8,6 @@ from cpython.array cimport array from libc.stdint cimport uint8_t, uint16_t, uint32_t, uint64_t cimport pyboy.plugins.window_sdl2 -from pyboy.botsupport.sprite cimport Sprite -from pyboy.botsupport.tilemap cimport TileMap from pyboy.core.mb cimport Motherboard from pyboy.logging.logging cimport Logger from pyboy.plugins.base_plugin cimport PyBoyWindowPlugin @@ -73,7 +71,7 @@ cdef class BaseDebugWindow(PyBoyWindowPlugin): cdef class TileViewWindow(BaseDebugWindow): cdef int scanline_x cdef int scanline_y - cdef TileMap tilemap + cdef object tilemap cdef uint32_t color cdef uint32_t[:,:] tilecache # Fixing Cython locals @@ -105,10 +103,10 @@ cdef class TileDataWindow(BaseDebugWindow): cdef class SpriteWindow(BaseDebugWindow): - @cython.locals(tile_x=int, tile_y=int, sprite_identifier=int, sprite=Sprite) + @cython.locals(tile_x=int, tile_y=int, sprite_identifier=int, sprite=object) cdef list handle_events(self, list) noexcept - @cython.locals(t=MarkedTile, xx=int, yy=int, sprite=Sprite, i=int) + @cython.locals(t=MarkedTile, xx=int, yy=int, sprite=object, i=int) cdef void draw_overlay(self) noexcept @cython.locals(title=str) @@ -121,7 +119,7 @@ cdef class SpriteViewWindow(BaseDebugWindow): @cython.locals(t=int, x=int, y=int) cdef void post_tick(self) noexcept - @cython.locals(t=MarkedTile, sprite=Sprite, i=int) + @cython.locals(t=MarkedTile, sprite=object, i=int) cdef void draw_overlay(self) noexcept @cython.locals(title=str) diff --git a/pyboy/plugins/debug.py b/pyboy/plugins/debug.py index 80d34f82a..b675a4381 100644 --- a/pyboy/plugins/debug.py +++ b/pyboy/plugins/debug.py @@ -10,9 +10,7 @@ from base64 import b64decode from ctypes import c_void_p -from pyboy import utils -from pyboy.botsupport import constants, tilemap -from pyboy.botsupport.sprite import Sprite +from pyboy.api import Sprite, TileMap, constants from pyboy.plugins.base_plugin import PyBoyWindowPlugin from pyboy.plugins.window_sdl2 import sdl2_event_pump from pyboy.utils import WindowEvent @@ -313,12 +311,7 @@ def __init__(self, *args, window_map, scanline_x, scanline_y, **kwargs): super().__init__(*args, **kwargs) self.scanline_x, self.scanline_y = scanline_x, scanline_y self.color = COLOR_WINDOW if window_map else COLOR_BACKGROUND - - if not cythonmode: - self.tilemap = tilemap.TileMap(self.mb, "WINDOW" if window_map else "BACKGROUND") - - def __cinit__(self, pyboy, mb, *args, window_map, **kwargs): - self.tilemap = tilemap.TileMap(self.mb, "WINDOW" if window_map else "BACKGROUND") + self.tilemap = TileMap(self.mb, "WINDOW" if window_map else "BACKGROUND") def post_tick(self): # Updating screen buffer by copying tiles from cache @@ -400,7 +393,7 @@ def update_title(self): def draw_overlay(self): global marked_tiles - scanlineparameters = self.pyboy.botsupport_manager().screen().tilemap_position_list() + scanlineparameters = self.pyboy.screen.tilemap_position_list() background_view = self.scanline_x == 0 @@ -599,8 +592,7 @@ def draw_overlay(self): sprite_height = 16 if self.mb.lcd._LCDC.sprite_height else 8 # Mark selected tiles for m, matched_sprites in zip( - marked_tiles, - self.pyboy.botsupport_manager().sprite_by_tile_identifier([m.tile_identifier for m in marked_tiles]) + marked_tiles, self.pyboy.get_sprite_by_tile_identifier([m.tile_identifier for m in marked_tiles]) ): for sprite_index in matched_sprites: xx = (sprite_index*8) % self.width @@ -632,8 +624,7 @@ def draw_overlay(self): sprite_height = 16 if self.mb.lcd._LCDC.sprite_height else 8 # Mark selected tiles for m, matched_sprites in zip( - marked_tiles, - self.pyboy.botsupport_manager().sprite_by_tile_identifier([m.tile_identifier for m in marked_tiles]) + marked_tiles, self.pyboy.get_sprite_by_tile_identifier([m.tile_identifier for m in marked_tiles]) ): for sprite_index in matched_sprites: sprite = Sprite(self.mb, sprite_index) diff --git a/pyboy/plugins/game_wrapper_pokemon_gen1.py b/pyboy/plugins/game_wrapper_pokemon_gen1.py index 96de505f6..3a1dda9dc 100644 --- a/pyboy/plugins/game_wrapper_pokemon_gen1.py +++ b/pyboy/plugins/game_wrapper_pokemon_gen1.py @@ -39,7 +39,7 @@ def post_tick(self): self._tile_cache_invalid = True self._sprite_cache_invalid = True - scanline_parameters = self.pyboy.botsupport_manager().screen().tilemap_position_list() + scanline_parameters = self.pyboy.screen.tilemap_position_list() WX = scanline_parameters[0][2] WY = scanline_parameters[0][3] self.use_background(WY != 0) @@ -48,7 +48,7 @@ def _get_screen_background_tilemap(self): ### SIMILAR TO CURRENT pyboy.game_wrapper()._game_area_np(), BUT ONLY FOR BACKGROUND TILEMAP, SO NPC ARE SKIPPED bsm = self.pyboy.botsupport_manager() ((scx, scy), (wx, wy)) = bsm.screen().tilemap_position() - tilemap = np.array(bsm.tilemap_background()[:, :]) + tilemap = np.array(bsm.tilemap_background[:, :]) return np.roll(np.roll(tilemap, -scy // 8, axis=0), -scx // 8, axis=1)[:18, :20] def _get_screen_walkable_matrix(self): diff --git a/pyboy/plugins/game_wrapper_super_mario_land.py b/pyboy/plugins/game_wrapper_super_mario_land.py index 37d81e93f..2421e67c4 100644 --- a/pyboy/plugins/game_wrapper_super_mario_land.py +++ b/pyboy/plugins/game_wrapper_super_mario_land.py @@ -149,7 +149,7 @@ def post_tick(self): 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] + 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: diff --git a/pyboy/plugins/record_replay.py b/pyboy/plugins/record_replay.py index 92aa0bebd..7b06a7d6c 100644 --- a/pyboy/plugins/record_replay.py +++ b/pyboy/plugins/record_replay.py @@ -12,7 +12,6 @@ import numpy as np import pyboy -from pyboy import utils from pyboy.plugins.base_plugin import PyBoyPlugin logger = pyboy.logging.get_logger(__name__) @@ -41,8 +40,7 @@ def handle_events(self, events): if len(events) != 0: self.recorded_input.append(( self.pyboy.frame_count, [e.event for e in events], - base64.b64encode(np.ascontiguousarray(self.pyboy.botsupport_manager().screen().screen_ndarray()) - ).decode("utf8") + base64.b64encode(np.ascontiguousarray(self.pyboy.screen.screen_ndarray())).decode("utf8") )) return events diff --git a/pyboy/plugins/screen_recorder.py b/pyboy/plugins/screen_recorder.py index 7b11f5474..7103f9c8c 100644 --- a/pyboy/plugins/screen_recorder.py +++ b/pyboy/plugins/screen_recorder.py @@ -42,7 +42,7 @@ def handle_events(self, events): def post_tick(self): # Plugin: Screen Recorder if self.recording: - self.add_frame(self.pyboy.botsupport_manager().screen().screen_image()) + self.add_frame(self.pyboy.screen.screen_image()) def add_frame(self, frame): # Pillow makes artifacts in the output, if we use 'RGB', which is PyBoy's default format diff --git a/pyboy/plugins/screenshot_recorder.py b/pyboy/plugins/screenshot_recorder.py index 0daaedf22..5c798b580 100644 --- a/pyboy/plugins/screenshot_recorder.py +++ b/pyboy/plugins/screenshot_recorder.py @@ -37,7 +37,7 @@ def save(self, path=None): os.makedirs(directory, mode=0o755) path = os.path.join(directory, time.strftime(f"{self.pyboy.cartridge_title()}-%Y.%m.%d-%H.%M.%S.png")) - self.pyboy.botsupport_manager().screen().screen_image().save(path) + self.pyboy.screen.screen_image().save(path) logger.info("Screenshot saved in {}".format(path)) diff --git a/pyboy/pyboy.pxd b/pyboy/pyboy.pxd index 87c794a17..3f993717a 100644 --- a/pyboy/pyboy.pxd +++ b/pyboy/pyboy.pxd @@ -21,8 +21,8 @@ cdef double SPF cdef class PyBoy: cdef Motherboard mb cdef public PluginManager plugin_manager - cdef public uint64_t frame_count - cdef public str gamerom_file + cdef readonly uint64_t frame_count + cdef readonly str gamerom_file cdef readonly bint paused cdef double avg_pre @@ -31,6 +31,7 @@ cdef class PyBoy: cdef list old_events cdef list events + cdef list queued_input cdef bint quitting cdef bint stopped cdef bint initialized @@ -38,7 +39,6 @@ cdef class PyBoy: cdef bint limit_emulationspeed cdef int emulationspeed, target_emulationspeed, save_target_emulationspeed - cdef object screen_recorder cdef bint record_input cdef bint disable_input cdef str record_input_file @@ -61,4 +61,13 @@ cdef class PyBoy: cdef dict _hooks cpdef bint _handle_hooks(self) cpdef int hook_register(self, uint16_t, uint16_t, object, object) except -1 - cpdef int hook_deregister(self, uint16_t, uint16_t) except -1 \ No newline at end of file + cpdef int hook_deregister(self, uint16_t, uint16_t) except -1 + + cpdef bint _is_cpu_stuck(self) + cdef void __rendering(self, int) noexcept + + cpdef object get_sprite(self, int) noexcept + cpdef list get_sprite_by_tile_identifier(self, list, on_screen=*) noexcept + cpdef object get_tile(self, int) noexcept + cpdef object tilemap_background(self) noexcept + cpdef object tilemap_window(self) noexcept diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index 27e3255f2..57be749b4 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -9,6 +9,8 @@ import os import time +import numpy as np + from pyboy import utils from pyboy.logging import get_logger from pyboy.openai_gym import PyBoyGymEnv @@ -16,7 +18,8 @@ from pyboy.plugins.manager import PluginManager from pyboy.utils import IntIOWrapper, WindowEvent -from . import botsupport +from . import utils +from .api import Screen, Sprite, Tile, TileMap, constants from .core.mb import Motherboard logger = get_logger(__name__) @@ -49,7 +52,7 @@ def __init__( 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.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 @@ -103,6 +106,7 @@ def __init__( self.set_emulation_speed(1) self.paused = False self.events = [] + self.queued_input = [] self.old_events = [] self.quitting = False self.stopped = False @@ -254,7 +258,8 @@ def _post_tick(self): # 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 @@ -294,16 +299,6 @@ def stop(self, save=True): # 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): """ For Reinforcement learning, it is often easier to use the standard gym environment. This method will provide one. @@ -426,16 +421,16 @@ def button(self, input): self.queued_input.append(WindowEvent(WindowEvent.RELEASE_ARROW_DOWN)) elif input == "a": self.send_input(WindowEvent.PRESS_BUTTON_A) - self.queued_input.append(WindowEvent.RELEASE_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.RELEASE_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.RELEASE_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.RELEASE_BUTTON_SELECT) + self.queued_input.append(WindowEvent(WindowEvent.RELEASE_BUTTON_SELECT)) else: raise Exception("Unrecognized input:", input) @@ -640,6 +635,12 @@ def cartridge_title(self): """ return self.mb.cartridge.gamename + def __rendering(self, value): + """ + Disable or enable rendering + """ + self.mb.lcd.disable_renderer = not value + def _is_cpu_stuck(self): return self.mb.cpu.is_stuck @@ -704,4 +705,310 @@ def _handle_hooks(self): (callback, context) = _handler callback(context) return True - return False \ No newline at end of file + 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). + + Args: + index (int): Sprite index from 0 to 39. + Returns + ------- + `pyboy.api.sprite.Sprite`: + Sprite corresponding to the given index. + """ + return Sprite(self.mb, sprite_index) + + def get_sprite_by_tile_identifier(self, tile_identifiers, on_screen=True): + """ + Provided a list of tile identifiers, this function will find all occurrences of sprites using the tile + identifiers and return the sprite indexes where each identifier is found. Use the sprite indexes in the + `pyboy.sprite` function to get a `pyboy.api.sprite.Sprite` object. + + Example: + ``` + >>> print(pyboy.get_sprite_by_tile_identifier([43, 123])) + [[0, 2, 4], []] + ``` + + Meaning, that tile identifier `43` is found at the sprite indexes: 0, 2, and 4, while tile identifier + `123` was not found anywhere. + + Args: + identifiers (list): List of tile identifiers (int) + on_screen (bool): Require that the matched sprite is on screen + + Returns + ------- + list: + list of sprite matches for every tile identifier in the input + """ + + matches = [] + for i in tile_identifiers: + match = [] + for s in range(constants.SPRITES): + sprite = Sprite(self.mb, s) + for t in sprite.tiles: + if t.tile_identifier == i and (not on_screen or (on_screen and sprite.on_screen)): + match.append(s) + matches.append(match) + return matches + + def get_tile(self, identifier): + """ + The Game Boy can have 384 tiles loaded in memory at once. 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. + + Returns + ------- + `pyboy.api.tile.Tile`: + A Tile object for the given identifier. + """ + return Tile(self.mb, identifier=identifier) + + @property + 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.api.tilemap.TileMap`: + A TileMap object for the tile map. + """ + return TileMap(self.mb, "BACKGROUND") + + @property + 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.api.tilemap.TileMap`: + A TileMap object for the tile map. + """ + return TileMap(self.mb, "WINDOW") + + +class PyBoyMemoryView: + def __init__(self, mb): + self.mb = mb + + def _fix_slice(self, addr): + if addr.start is None: + return (-1, 0, 0) + if addr.stop is None: + return (0, -1, 0) + start = addr.start + stop = addr.stop + if addr.step is None: + step = 1 + else: + step = addr.step + return start, stop, step + + def __getitem__(self, addr): + is_bank = isinstance(addr, tuple) + bank = 0 + if is_bank: + bank, addr = addr + assert isinstance(bank, int), "Bank has to be integer. Slicing is not supported." + is_single = isinstance(addr, int) + if not is_single: + start, stop, step = self._fix_slice(addr) + assert start >= 0, "Start address required" + assert stop >= 0, "End address required" + return self.__getitem(start, stop, step, bank, is_single, is_bank) + else: + return self.__getitem(addr, 0, 0, bank, is_single, is_bank) + + def __getitem(self, start, stop, step, bank, is_single, is_bank): + if is_bank: + # Reading a specific bank + if start < 0x8000: + if start >= 0x4000: + start -= 0x4000 + stop -= 0x4000 + # Cartridge ROM Banks + assert stop < 0x4000, "Out of bounds for reading ROM bank" + assert bank <= self.mb.cartridge.external_rom_count, "ROM Bank out of range" + if not is_single: + return [self.mb.cartridge.rombanks[bank][x] for x in range(start, stop, step)] + else: + return self.mb.cartridge.rombanks[bank][start] + elif start < 0xA000: + start -= 0x8000 + stop -= 0x8000 + # CGB VRAM Banks + assert self.mb.cgb, "Selecting bank of VRAM is only supported for CGB mode" + assert stop < 0x2000, "Out of bounds for reading VRAM bank" + assert bank <= 1, "VRAM Bank out of range" + + if bank == 0: + if not is_single: + return [self.mb.lcd.VRAM0[x] for x in range(start, stop, step)] + else: + return self.mb.lcd.VRAM0[start] + else: + if not is_single: + return [self.mb.lcd.VRAM1[x] for x in range(start, stop, step)] + else: + return self.mb.lcd.VRAM1[start] + elif start < 0xC000: + start -= 0xA000 + stop -= 0xA000 + # Cartridge RAM banks + assert stop < 0x2000, "Out of bounds for reading cartridge RAM bank" + assert bank <= self.mb.cartridge.external_ram_count, "ROM Bank out of range" + if not is_single: + return [self.mb.cartridge.rambanks[bank][x] for x in range(start, stop, step)] + else: + return self.mb.cartridge.rambanks[bank][start] + elif start < 0xE000: + start -= 0xC000 + stop -= 0xC000 + if start >= 0x1000: + start -= 0x1000 + stop -= 0x1000 + # CGB VRAM banks + assert self.mb.cgb, "Selecting bank of WRAM is only supported for CGB mode" + assert stop < 0x1000, "Out of bounds for reading VRAM bank" + assert bank <= 7, "WRAM Bank out of range" + if not is_single: + return [self.mb.ram.internal_ram0[x + bank*0x1000] for x in range(start, stop, step)] + else: + return self.mb.ram.internal_ram0[start + bank*0x1000] + else: + assert None, "Invalid memory address for bank" + elif not is_single: + # Reading slice of memory space + return [self.mb.getitem(x) for x in range(start, stop, step)] + else: + # Reading specific address of memory space + return self.mb.getitem(start) + + def __setitem__(self, addr, v): + is_bank = isinstance(addr, tuple) + bank = 0 + if is_bank: + bank, addr = addr + assert isinstance(bank, int), "Bank has to be integer. Slicing is not supported." + is_single = isinstance(addr, int) + if not is_single: + start, stop, step = self._fix_slice(addr) + assert start >= 0, "Start address required" + assert stop >= 0, "End address required" + self.__setitem(start, stop, step, v, bank, is_single, is_bank) + else: + self.__setitem(addr, 0, 0, v, bank, is_single, is_bank) + + def __setitem(self, start, stop, step, v, bank, is_single, is_bank): + if is_bank: + # Writing a specific bank + if start < 0x8000: + assert None, "Cannot write to ROM banks" + elif start < 0xA000: + start -= 0x8000 + stop -= 0x8000 + # CGB VRAM Banks + assert self.mb.cgb, "Selecting bank of VRAM is only supported for CGB mode" + assert stop < 0x2000, "Out of bounds for reading VRAM bank" + assert bank <= 1, "VRAM Bank out of range" + + if bank == 0: + if not is_single: + # Writing slice of memory space + if hasattr(v, "__iter__"): + assert (stop-start) // step == len(v), "slice does not match length of data" + _v = iter(v) + for x in range(start, stop, step): + self.mb.lcd.VRAM0[x] = next(_v) + else: + for x in range(start, stop, step): + self.mb.lcd.VRAM0[x] = v + else: + self.mb.lcd.VRAM0[start] = v + else: + if not is_single: + # Writing slice of memory space + if hasattr(v, "__iter__"): + assert (stop-start) // step == len(v), "slice does not match length of data" + _v = iter(v) + for x in range(start, stop, step): + self.mb.lcd.VRAM1[x] = next(_v) + else: + for x in range(start, stop, step): + self.mb.lcd.VRAM1[x] = v + else: + self.mb.lcd.VRAM1[start] = v + elif start < 0xC000: + start -= 0xA000 + stop -= 0xA000 + # Cartridge RAM banks + assert stop < 0x2000, "Out of bounds for reading cartridge RAM bank" + assert bank <= self.mb.cartridge.external_ram_count, "ROM Bank out of range" + if not is_single: + # Writing slice of memory space + if hasattr(v, "__iter__"): + assert (stop-start) // step == len(v), "slice does not match length of data" + _v = iter(v) + for x in range(start, stop, step): + self.mb.cartridge.rambanks[bank][x] = next(_v) + else: + for x in range(start, stop, step): + self.mb.cartridge.rambanks[bank][x] = v + else: + self.mb.cartridge.rambanks[bank][start] = v + elif start < 0xE000: + start -= 0xC000 + stop -= 0xC000 + if start >= 0x1000: + start -= 0x1000 + stop -= 0x1000 + # CGB VRAM banks + assert self.mb.cgb, "Selecting bank of WRAM is only supported for CGB mode" + assert stop < 0x1000, "Out of bounds for reading VRAM bank" + assert bank <= 7, "WRAM Bank out of range" + if not is_single: + # Writing slice of memory space + if hasattr(v, "__iter__"): + assert (stop-start) // step == len(v), "slice does not match length of data" + _v = iter(v) + for x in range(start, stop, step): + self.mb.ram.internal_ram0[x + bank*0x1000] = next(_v) + else: + for x in range(start, stop, step): + self.mb.ram.internal_ram0[x + bank*0x1000] = v + else: + self.mb.ram.internal_ram0[start + bank*0x1000] = v + else: + assert None, "Invalid memory address for bank" + elif not is_single: + # Writing slice of memory space + if hasattr(v, "__iter__"): + assert (stop-start) // step == len(v), "slice does not match length of data" + _v = iter(v) + for x in range(start, stop, step): + self.mb.setitem(x, next(_v)) + else: + for x in range(start, stop, step): + self.mb.setitem(x, v) + else: + # Writing specific address of memory space + self.mb.setitem(start, v) diff --git a/tests/test_acid_cgb.py b/tests/test_acid_cgb.py index de85314df..79e52c0c8 100644 --- a/tests/test_acid_cgb.py +++ b/tests/test_acid_cgb.py @@ -21,7 +21,7 @@ def test_cgb_acid(cgb_acid_file): pyboy.tick(25, True) png_path = Path(f"tests/test_results/{os.path.basename(cgb_acid_file)}.png") - image = pyboy.botsupport_manager().screen().screen_image() + image = pyboy.screen.screen_image() if OVERWRITE_PNGS: png_path.parents[0].mkdir(parents=True, exist_ok=True) image.save(png_path) diff --git a/tests/test_acid_dmg.py b/tests/test_acid_dmg.py index a52e9e1ae..3f359f063 100644 --- a/tests/test_acid_dmg.py +++ b/tests/test_acid_dmg.py @@ -22,7 +22,7 @@ def test_dmg_acid(cgb, dmg_acid_file): pyboy.tick(25, True) png_path = Path(f"tests/test_results/{'cgb' if cgb else 'dmg'}_{os.path.basename(dmg_acid_file)}.png") - image = pyboy.botsupport_manager().screen().screen_image() + image = pyboy.screen.screen_image() if OVERWRITE_PNGS: png_path.parents[0].mkdir(parents=True, exist_ok=True) image.save(png_path) diff --git a/tests/test_basics.py b/tests/test_basics.py index 76c93bd8f..438dcf77f 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -17,7 +17,7 @@ from pyboy import PyBoy, WindowEvent from pyboy import __main__ as main -from pyboy.botsupport.tile import Tile +from pyboy.api.tile import Tile is_pypy = platform.python_implementation() == "PyPy" @@ -100,8 +100,8 @@ def test_tilemaps(kirby_rom): pyboy.set_emulation_speed(0) pyboy.tick(120, False) - bck_tilemap = pyboy.botsupport_manager().tilemap_background() - wdw_tilemap = pyboy.botsupport_manager().tilemap_window() + bck_tilemap = pyboy.tilemap_background + wdw_tilemap = pyboy.tilemap_window assert bck_tilemap[0, 0] == 256 assert bck_tilemap[:5, 0] == [256, 256, 256, 256, 170] @@ -175,7 +175,7 @@ def test_not_cgb(pokemon_crystal_rom): pyboy.set_emulation_speed(0) pyboy.tick(60 * 7, False) - assert pyboy.botsupport_manager().tilemap_background()[1:16, 16] == [ + assert pyboy.tilemap_background[1:16, 16] == [ 134, 160, 172, 164, 383, 129, 174, 184, 383, 130, 174, 171, 174, 177, 232 ] # Assert that the screen says "Game Boy Color." at the bottom. @@ -201,7 +201,7 @@ def test_all_modes(cgb, _bootrom, frames, rom, any_rom_cgb, boot_cgb_rom): rom_name = "cgbrom" if rom == any_rom_cgb else "dmgrom" png_path = Path(f"tests/test_results/all_modes/{rom_name}_{cgb}_{os.path.basename(str(_bootrom))}.png") - image = pyboy.botsupport_manager().screen().screen_image() + image = pyboy.screen.screen_image() if OVERWRITE_PNGS: png_path.parents[0].mkdir(parents=True, exist_ok=True) png_buf = BytesIO() diff --git a/tests/test_external_api.py b/tests/test_external_api.py index 8ad393243..8267310c4 100644 --- a/tests/test_external_api.py +++ b/tests/test_external_api.py @@ -11,8 +11,9 @@ import PIL import pytest from PIL import ImageChops + from pyboy import PyBoy, WindowEvent -from pyboy.botsupport.tile import Tile +from pyboy.api.tile import Tile from .conftest import BOOTROM_FRAMES_UNTIL_LOGO @@ -29,10 +30,10 @@ def test_tiles(default_rom): pyboy.set_emulation_speed(0) pyboy.tick(BOOTROM_FRAMES_UNTIL_LOGO, False) - tile = pyboy.botsupport_manager().tilemap_window().tile(0, 0) + tile = pyboy.tilemap_window.tile(0, 0) assert isinstance(tile, Tile) - tile = pyboy.botsupport_manager().tile(1) + tile = pyboy.get_tile(1) image = tile.image() assert isinstance(image, PIL.Image.Image) ndarray = tile.image_ndarray() @@ -53,12 +54,12 @@ def test_tiles(default_rom): [0xffffffff, 0xff000000, 0xff000000, 0xffffffff, 0xffffffff, 0xff000000, 0xff000000, 0xffffffff]] for identifier in range(384): - t = pyboy.botsupport_manager().tile(identifier) + t = pyboy.get_tile(identifier) assert t.tile_identifier == identifier with pytest.raises(Exception): - pyboy.botsupport_manager().tile(-1) + pyboy.get_tile(-1) with pytest.raises(Exception): - pyboy.botsupport_manager().tile(385) + pyboy.get_tile(385) pyboy.stop(save=False) @@ -71,13 +72,13 @@ def test_screen_buffer_and_image(tetris_rom, boot_rom): pyboy.set_emulation_speed(0) pyboy.tick(275, True) # Iterate to boot logo - assert pyboy.botsupport_manager().screen().raw_screen_buffer_dims() == (144, 160) - assert pyboy.botsupport_manager().screen().raw_screen_buffer_format() == cformat + assert pyboy.screen.raw_screen_buffer_dims() == (144, 160) + assert pyboy.screen.raw_screen_buffer_format() == cformat boot_logo_hash = hashlib.sha256() - boot_logo_hash.update(pyboy.botsupport_manager().screen().raw_screen_buffer()) + boot_logo_hash.update(pyboy.screen.raw_screen_buffer()) assert boot_logo_hash.digest() == boot_logo_hash_predigested - assert isinstance(pyboy.botsupport_manager().screen().raw_screen_buffer(), bytes) + assert isinstance(pyboy.screen.raw_screen_buffer(), bytes) # The output of `screen_image` is supposed to be homogeneous, which means a shared hash between versions. boot_logo_png_hash_predigested = ( @@ -85,29 +86,33 @@ def test_screen_buffer_and_image(tetris_rom, boot_rom): b"\xa4\x0eR&\xda9\xfcg\xf7\x0f|\xba}\x08\xb6$" ) boot_logo_png_hash = hashlib.sha256() - image = pyboy.botsupport_manager().screen().screen_image() + image = pyboy.screen.screen_image() assert isinstance(image, PIL.Image.Image) image_data = io.BytesIO() image.save(image_data, format="BMP") boot_logo_png_hash.update(image_data.getvalue()) assert boot_logo_png_hash.digest() == boot_logo_png_hash_predigested - # Screenshot shortcut - image1 = pyboy.botsupport_manager().screen().screen_image() - image2 = pyboy.screen_image() - diff = ImageChops.difference(image1, image2) - assert not diff.getbbox() - # screen_ndarray numpy_hash = hashlib.sha256() - numpy_array = np.ascontiguousarray(pyboy.botsupport_manager().screen().screen_ndarray()) - assert isinstance(pyboy.botsupport_manager().screen().screen_ndarray(), np.ndarray) + numpy_array = np.ascontiguousarray(pyboy.screen.screen_ndarray()) + assert isinstance(pyboy.screen.screen_ndarray(), np.ndarray) assert numpy_array.shape == (144, 160, 3) numpy_hash.update(numpy_array.tobytes()) assert numpy_hash.digest( ) == (b"\r\t\x87\x131\xe8\x06\x82\xcaO=\n\x1e\xa2K$" b"\xd6\x8e\x91R( H7\xd8a*B+\xc7\x1f\x19") + # Check PIL image is reference for performance + pyboy.tick(1, True) + new_image1 = pyboy.screen.screen_image() + nd_image = pyboy.screen.screen_ndarray() + nd_image[:, :] = 0 + pyboy.tick(1, True) + new_image2 = pyboy.screen.screen_image() + diff = ImageChops.difference(new_image1, new_image2) + assert not diff.getbbox() + pyboy.stop(save=False) @@ -120,12 +125,12 @@ def test_tetris(tetris_rom): tetris.set_tetromino("T") first_brick = False - tile_map = pyboy.botsupport_manager().tilemap_window() + tile_map = pyboy.tilemap_window state_data = io.BytesIO() for frame in range(5282): # Enough frames to get a "Game Over". Otherwise do: `while pyboy.tick(False):` pyboy.tick(1, False) - assert pyboy.botsupport_manager().screen().tilemap_position() == ((0, 0), (-7, 0)) + assert pyboy.screen.tilemap_position() == ((0, 0), (-7, 0)) # Start game. Just press Start and A when the game allows us. # The frames are not 100% accurate. @@ -218,21 +223,21 @@ def test_tetris(tetris_rom): if frame == 1014: assert first_brick - s1 = pyboy.botsupport_manager().sprite(0) - s2 = pyboy.botsupport_manager().sprite(1) + s1 = pyboy.get_sprite(0) + s2 = pyboy.get_sprite(1) assert s1 == s1 assert s1 != s2 assert s1.tiles[0] == s2.tiles[0], "Testing equal tiles of two different sprites" # Test that both ways of getting identifiers work and provides the same result. all_sprites = [(s.x, s.y, s.tiles[0].tile_identifier, s.on_screen) - for s in [pyboy.botsupport_manager().sprite(n) for n in range(40)]] + for s in [pyboy.get_sprite(n) for n in range(40)]] all_sprites2 = [(s.x, s.y, s.tile_identifier, s.on_screen) - for s in [pyboy.botsupport_manager().sprite(n) for n in range(40)]] + for s in [pyboy.get_sprite(n) for n in range(40)]] assert all_sprites == all_sprites2 # Verify data with known reference - # pyboy.botsupport_manager().screen().screen_image().show() + # pyboy.screen.screen_image().show() assert all_sprites == ([ (-8, -16, 0, False), (-8, -16, 0, False), @@ -342,7 +347,7 @@ def test_tilemap_position_list(supermarioland_rom): pyboy.tick(100, False) # Get screen positions, and verify the values - positions = pyboy.botsupport_manager().screen().tilemap_position_list() + positions = pyboy.screen.tilemap_position_list() for y in range(1, 16): assert positions[y][0] == 0 # HUD for y in range(16, 144): @@ -353,7 +358,7 @@ def test_tilemap_position_list(supermarioland_rom): pyboy.tick(10, False) # Get screen positions, and verify the values - positions = pyboy.botsupport_manager().screen().tilemap_position_list() + positions = pyboy.screen.tilemap_position_list() for y in range(1, 16): assert positions[y][0] == 0 # HUD for y in range(16, 144): diff --git a/tests/test_magen.py b/tests/test_magen.py index c9fc8d786..a4dc2d2fa 100644 --- a/tests/test_magen.py +++ b/tests/test_magen.py @@ -21,7 +21,7 @@ def test_magen_test(magen_test_file): pyboy.tick(25, True) png_path = Path(f"tests/test_results/{os.path.basename(magen_test_file)}.png") - image = pyboy.botsupport_manager().screen().screen_image() + image = pyboy.screen.screen_image() if OVERWRITE_PNGS: png_path.parents[0].mkdir(parents=True, exist_ok=True) image.save(png_path) diff --git a/tests/test_mooneye.py b/tests/test_mooneye.py index b5ff7cb70..eaa7da1b1 100644 --- a/tests/test_mooneye.py +++ b/tests/test_mooneye.py @@ -170,7 +170,7 @@ def test_mooneye(clean, rom, mooneye_dir, default_rom): pyboy.tick(180 if "div_write" in rom else 40, True) png_path = Path(f"tests/test_results/mooneye/{rom}.png") - image = pyboy.botsupport_manager().screen().screen_image() + image = pyboy.screen.screen_image() if OVERWRITE_PNGS: png_path.parents[0].mkdir(parents=True, exist_ok=True) image.save(png_path) diff --git a/tests/test_openai_gym.py b/tests/test_openai_gym.py index 9ecea7844..d018c9100 100644 --- a/tests/test_openai_gym.py +++ b/tests/test_openai_gym.py @@ -9,8 +9,9 @@ import numpy as np import pytest + from pyboy import PyBoy, WindowEvent -from pyboy.botsupport.constants import COLS, ROWS +from pyboy.api.constants import COLS, ROWS py_version = platform.python_version()[:3] is_pypy = platform.python_implementation() == "PyPy" diff --git a/tests/test_replay.py b/tests/test_replay.py index c9d1c7447..95a6374f6 100644 --- a/tests/test_replay.py +++ b/tests/test_replay.py @@ -29,15 +29,12 @@ def verify_screen_image_np(pyboy, saved_array): - match = np.all( - np.frombuffer(saved_array, dtype=np.uint8).reshape(144, 160, 3) == - pyboy.botsupport_manager().screen().screen_ndarray() - ) + match = np.all(np.frombuffer(saved_array, dtype=np.uint8).reshape(144, 160, 3) == pyboy.screen.screen_ndarray()) if not match and not os.environ.get("TEST_CI"): from PIL import Image original = Image.frombytes("RGB", (160, 144), np.frombuffer(saved_array, dtype=np.uint8).reshape(144, 160, 3)) original.show() - new = pyboy.botsupport_manager().screen().screen_image() + new = pyboy.screen.screen_image() new.show() import PIL.ImageChops PIL.ImageChops.difference(original, new).show() diff --git a/tests/test_rtc3test.py b/tests/test_rtc3test.py index 075126351..a4702173b 100644 --- a/tests/test_rtc3test.py +++ b/tests/test_rtc3test.py @@ -32,12 +32,12 @@ def test_rtc3test(subtest, rtc3test_file): while True: # Continue until it says "(A) Return" - if pyboy.botsupport_manager().tilemap_background()[6:14, 17] == [193, 63, 27, 40, 55, 56, 53, 49]: + if pyboy[6:14, 17] == [193, 63, 27, 40, 55, 56, 53, 49]: break pyboy.tick(1, True) png_path = Path(f"tests/test_results/{rtc3test_file}_{subtest}.png") - image = pyboy.botsupport_manager().screen().screen_image() + image = pyboy.screen.screen_image() if OVERWRITE_PNGS: png_path.parents[0].mkdir(parents=True, exist_ok=True) image.save(png_path) diff --git a/tests/test_samesuite.py b/tests/test_samesuite.py index 81a0b972e..d521942ea 100644 --- a/tests/test_samesuite.py +++ b/tests/test_samesuite.py @@ -140,13 +140,13 @@ def test_samesuite(clean, gb_type, rom, samesuite_dir, boot_cgb_rom, boot_rom, d pyboy.load_state(saved_state) for _ in range(10): - if np.all(pyboy.botsupport_manager().screen().screen_ndarray() > 240): + if np.all(pyboy.screen.screen_ndarray() > 240): pyboy.tick(20, True) else: break png_path = Path(f"tests/test_results/SameSuite/{rom}.png") - image = pyboy.botsupport_manager().screen().screen_image() + image = pyboy.screen.screen_image() if OVERWRITE_PNGS: png_path.parents[0].mkdir(parents=True, exist_ok=True) image.save(png_path) diff --git a/tests/test_shonumi.py b/tests/test_shonumi.py index c01feea51..70cc0a36d 100644 --- a/tests/test_shonumi.py +++ b/tests/test_shonumi.py @@ -30,7 +30,7 @@ def test_shonumi(rom, shonumi_dir): png_path = Path(f"tests/test_results/GB Tests/{rom}.png") png_path.parents[0].mkdir(parents=True, exist_ok=True) - image = pyboy.botsupport_manager().screen().screen_image() + image = pyboy.screen.screen_image() old_image = PIL.Image.open(png_path) old_image = old_image.resize(image.size, resample=PIL.Image.Dither.NONE) diff --git a/tests/test_states.py b/tests/test_states.py index 4fbbfa831..0f1ae7358 100644 --- a/tests/test_states.py +++ b/tests/test_states.py @@ -28,7 +28,7 @@ def test_load_save_consistency(tetris_rom): # Boot screen while True: pyboy.tick(1, True) - tilemap_background = pyboy.botsupport_manager().tilemap_background() + tilemap_background = pyboy.tilemap_background if tilemap_background[2:9, 14] == [89, 25, 21, 10, 34, 14, 27]: # '1PLAYER' on the first screen break diff --git a/tests/test_which.py b/tests/test_which.py index 99ea6cad3..1583e7c65 100644 --- a/tests/test_which.py +++ b/tests/test_which.py @@ -22,7 +22,7 @@ def test_which(cgb, which_file): pyboy.tick(25, True) png_path = Path(f"tests/test_results/{'cgb' if cgb else 'dmg'}_{os.path.basename(which_file)}.png") - image = pyboy.botsupport_manager().screen().screen_image() + image = pyboy.screen.screen_image() if OVERWRITE_PNGS: png_path.parents[0].mkdir(parents=True, exist_ok=True) image.save(png_path) diff --git a/tests/test_whichboot.py b/tests/test_whichboot.py index 7ca11fa70..a507d92fd 100644 --- a/tests/test_whichboot.py +++ b/tests/test_whichboot.py @@ -22,7 +22,7 @@ def test_which(cgb, whichboot_file): pyboy.tick(25, True) png_path = Path(f"tests/test_results/{'cgb' if cgb else 'dmg'}_{os.path.basename(whichboot_file)}.png") - image = pyboy.botsupport_manager().screen().screen_image() + image = pyboy.screen.screen_image() if OVERWRITE_PNGS: png_path.parents[0].mkdir(parents=True, exist_ok=True) image.save(png_path) From 9ceabe7295fb501dc47bdc271150c25afbfd2b4a Mon Sep 17 00:00:00 2001 From: Mads Ynddal <5528170+Baekalfen@users.noreply.github.com> Date: Sun, 12 Nov 2023 17:38:20 +0100 Subject: [PATCH 18/65] Implement pyboy.memory interface to replace get/set_memory_value --- README.md | 1 + .../plugins/game_wrapper_kirby_dream_land.py | 6 +- pyboy/plugins/game_wrapper_pokemon_gen1.py | 8 +- .../plugins/game_wrapper_super_mario_land.py | 22 +- pyboy/plugins/game_wrapper_tetris.py | 6 +- pyboy/pyboy.pxd | 11 + pyboy/pyboy.py | 212 ++++++++++++------ tests/test_basics.py | 24 +- tests/test_blargg.py | 2 +- tests/test_external_api.py | 26 ++- tests/test_memoryview.py | 137 +++++++++++ tests/test_states.py | 2 +- tests/test_tetris_ai.py | 4 +- 13 files changed, 348 insertions(+), 113 deletions(-) create mode 100644 tests/test_memoryview.py diff --git a/README.md b/README.md index 7511864ad..c6247ce14 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ When the emulator is running, you can easily access [PyBoy's API](https://baekal ```python pyboy.button('down') pyboy.button('a') +some_value = pyboy.memory[0xC345] pyboy.send_input(WindowEvent.PRESS_ARROW_DOWN) pyboy.tick() # Process one frame to let the game register the input diff --git a/pyboy/plugins/game_wrapper_kirby_dream_land.py b/pyboy/plugins/game_wrapper_kirby_dream_land.py index 62392acf6..94c9bff82 100644 --- a/pyboy/plugins/game_wrapper_kirby_dream_land.py +++ b/pyboy/plugins/game_wrapper_kirby_dream_land.py @@ -51,16 +51,16 @@ def post_tick(self): self.score = 0 score_digits = 5 for n in range(score_digits): - self.score += self.pyboy.get_memory_value(0xD06F + n) * 10**(score_digits - n) + self.score += self.pyboy.memory[0xD06F + 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 + self.lives_left = self.pyboy.memory[0xD089] - 1 if self.game_has_started: self.fitness = self.score * self.health * self.lives_left diff --git a/pyboy/plugins/game_wrapper_pokemon_gen1.py b/pyboy/plugins/game_wrapper_pokemon_gen1.py index 3a1dda9dc..f1b58f46b 100644 --- a/pyboy/plugins/game_wrapper_pokemon_gen1.py +++ b/pyboy/plugins/game_wrapper_pokemon_gen1.py @@ -53,14 +53,14 @@ def _get_screen_background_tilemap(self): def _get_screen_walkable_matrix(self): walkable_tiles_indexes = [] - collision_ptr = self.pyboy.get_memory_value(0xD530) + (self.pyboy.get_memory_value(0xD531) << 8) - tileset_type = self.pyboy.get_memory_value(0xFFD7) + collision_ptr = self.pyboy.memory[0xD530] + (self.pyboy.memory[0xD531] << 8) + tileset_type = self.pyboy.memory[0xFFD7] if tileset_type > 0: - grass_tile_index = self.pyboy.get_memory_value(0xD535) + grass_tile_index = self.pyboy.memory[0xD535] if grass_tile_index != 0xFF: walkable_tiles_indexes.append(grass_tile_index + 0x100) for i in range(0x180): - tile_index = self.pyboy.get_memory_value(collision_ptr + i) + tile_index = self.pyboy.memory[collision_ptr + i] if tile_index == 0xFF: break else: diff --git a/pyboy/plugins/game_wrapper_super_mario_land.py b/pyboy/plugins/game_wrapper_super_mario_land.py index 2421e67c4..915ead15e 100644 --- a/pyboy/plugins/game_wrapper_super_mario_land.py +++ b/pyboy/plugins/game_wrapper_super_mario_land.py @@ -139,16 +139,16 @@ 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) + 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 @@ -172,9 +172,9 @@ def set_lives_left(self, amount): 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("%d is out of bounds. Only values between 0 and 99 allowed.", amount) @@ -188,7 +188,7 @@ def set_world_level(self, world, level): """ 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 @@ -196,7 +196,7 @@ def set_world_level(self, world, level): ] 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): """ @@ -234,7 +234,7 @@ def start_game(self, timer_div=None, world_level=None, unlock_level_select=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(1, False) self.tilemap_background.refresh_lcdc() @@ -304,7 +304,7 @@ def game_area(self): 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 diff --git a/pyboy/plugins/game_wrapper_tetris.py b/pyboy/plugins/game_wrapper_tetris.py index 06e2dc1df..b25d912f6 100644 --- a/pyboy/plugins/game_wrapper_tetris.py +++ b/pyboy/plugins/game_wrapper_tetris.py @@ -206,7 +206,7 @@ def next_tetromino(self): * `"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): """ @@ -245,7 +245,7 @@ def set_tetromino(self, shape): ] 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 @@ -253,7 +253,7 @@ def set_tetromino(self, shape): ] 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): """ diff --git a/pyboy/pyboy.pxd b/pyboy/pyboy.pxd index 3f993717a..711019165 100644 --- a/pyboy/pyboy.pxd +++ b/pyboy/pyboy.pxd @@ -18,6 +18,16 @@ cdef Logger logger cdef double SPF +cdef class PyBoyMemoryView: + cdef Motherboard mb + + @cython.locals(start=int,stop=int,step=int) + cpdef (int,int,int) _fix_slice(self, slice) + @cython.locals(start=int,stop=int,step=int) + cdef object __getitem(self, int, int, int, int, bint, bint) + @cython.locals(start=int,stop=int,step=int,x=int, bank=int) + cdef int __setitem(self, int, int, int, object, int, bint, bint) except -1 + cdef class PyBoy: cdef Motherboard mb cdef public PluginManager plugin_manager @@ -36,6 +46,7 @@ cdef class PyBoy: cdef bint stopped cdef bint initialized cdef public str window_title + cdef readonly PyBoyMemoryView memory cdef bint limit_emulationspeed cdef int emulationspeed, target_emulationspeed, save_target_emulationspeed diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index 57be749b4..0b71e3e0a 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -13,13 +13,15 @@ from pyboy import utils from pyboy.logging import get_logger +from pyboy.api.screen import Screen +from pyboy.api.tilemap import TileMap +from pyboy.logging import get_logger from pyboy.openai_gym import PyBoyGymEnv from pyboy.openai_gym import enabled as gym_enabled from pyboy.plugins.manager import PluginManager from pyboy.utils import IntIOWrapper, WindowEvent -from . import utils -from .api import Screen, Sprite, Tile, TileMap, constants +from .api import Sprite, Tile, constants from .core.mb import Motherboard logger = get_logger(__name__) @@ -112,6 +114,46 @@ def __init__( self.stopped = False self.window_title = "PyBoy" + ################### + # API attributes + + self.memory = PyBoyMemoryView(self.mb) + """ + 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 + ``` + """ + + 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](http://bgb.bircd.org/pandocs.htm#vrambackgroundmaps). + + 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](http://bgb.bircd.org/pandocs.htm#vrambackgroundmaps). + + Returns + ------- + `pyboy.api.tilemap.TileMap`: + A TileMap object for the tile map. + """ + ################### # Plugins @@ -346,57 +388,6 @@ def game_wrapper(self): """ return self.plugin_manager.gamewrapper() - def get_memory_value(self, addr): - """ - Reads a given memory address of the Game Boy's current memory state. This will not directly give you access to - all switchable memory banks. Open an issue on GitHub if that is needed, or use `PyBoy.set_memory_value` to send - MBC commands to the virtual cartridge. - - Returns - ------- - int: - An integer with the value of the memory address - """ - return self.mb.getitem(addr) - - def set_memory_value(self, addr, value): - """ - Write one byte to a given memory address of the Game Boy's current memory state. - - This will not directly give you access to all switchable memory banks. - - __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). - - 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 - """ - self.mb.setitem(addr, value) - - def override_memory_value(self, rom_bank, 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. - - 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 - """ - # 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(self, input): """ Send input to PyBoy in the form of "a", "b", "start", "select", "left", "right", "up" and "down". @@ -817,6 +808,8 @@ def _fix_slice(self, addr): return (0, -1, 0) start = addr.start stop = addr.stop + if start > stop: + return (-1, -1, 0) if addr.step is None: step = 1 else: @@ -832,13 +825,15 @@ def __getitem__(self, addr): is_single = isinstance(addr, int) if not is_single: start, stop, step = self._fix_slice(addr) + assert start >= 0 or stop >= 0, "Start address has to come before end address" assert start >= 0, "Start address required" assert stop >= 0, "End address required" return self.__getitem(start, stop, step, bank, is_single, is_bank) else: - return self.__getitem(addr, 0, 0, bank, is_single, is_bank) + return self.__getitem(addr, 0, 1, bank, is_single, is_bank) def __getitem(self, start, stop, step, bank, is_single, is_bank): + slice_length = (stop-start) // step if is_bank: # Reading a specific bank if start < 0x8000: @@ -847,11 +842,25 @@ def __getitem(self, start, stop, step, bank, is_single, is_bank): stop -= 0x4000 # Cartridge ROM Banks assert stop < 0x4000, "Out of bounds for reading ROM bank" - assert bank <= self.mb.cartridge.external_rom_count, "ROM Bank out of range" - if not is_single: - return [self.mb.cartridge.rombanks[bank][x] for x in range(start, stop, step)] + if bank == -1: + assert start <= 0xFF, "Start address out of range for bootrom" + assert stop <= 0xFF, "Start address out of range for bootrom" + if not is_single: + mem_slice = [0] * slice_length + for x in range(start, stop, step): + mem_slice[(x-start) // step] = self.mb.bootrom.bootrom[x] + return mem_slice + else: + return self.mb.bootrom.bootrom[start] else: - return self.mb.cartridge.rombanks[bank][start] + assert bank <= self.mb.cartridge.external_rom_count, "ROM Bank out of range" + if not is_single: + mem_slice = [0] * slice_length + for x in range(start, stop, step): + mem_slice[(x-start) // step] = self.mb.cartridge.rombanks[bank, x] + return mem_slice + else: + return self.mb.cartridge.rombanks[bank, start] elif start < 0xA000: start -= 0x8000 stop -= 0x8000 @@ -862,12 +871,18 @@ def __getitem(self, start, stop, step, bank, is_single, is_bank): if bank == 0: if not is_single: - return [self.mb.lcd.VRAM0[x] for x in range(start, stop, step)] + mem_slice = [0] * slice_length + for x in range(start, stop, step): + mem_slice[(x-start) // step] = self.mb.lcd.VRAM0[x] + return mem_slice else: return self.mb.lcd.VRAM0[start] else: if not is_single: - return [self.mb.lcd.VRAM1[x] for x in range(start, stop, step)] + mem_slice = [0] * slice_length + for x in range(start, stop, step): + mem_slice[(x-start) // step] = self.mb.lcd.VRAM1[x] + return mem_slice else: return self.mb.lcd.VRAM1[start] elif start < 0xC000: @@ -877,9 +892,12 @@ def __getitem(self, start, stop, step, bank, is_single, is_bank): assert stop < 0x2000, "Out of bounds for reading cartridge RAM bank" assert bank <= self.mb.cartridge.external_ram_count, "ROM Bank out of range" if not is_single: - return [self.mb.cartridge.rambanks[bank][x] for x in range(start, stop, step)] + mem_slice = [0] * slice_length + for x in range(start, stop, step): + mem_slice[(x-start) // step] = self.mb.cartridge.rambanks[bank, x] + return mem_slice else: - return self.mb.cartridge.rambanks[bank][start] + return self.mb.cartridge.rambanks[bank, start] elif start < 0xE000: start -= 0xC000 stop -= 0xC000 @@ -891,14 +909,20 @@ def __getitem(self, start, stop, step, bank, is_single, is_bank): assert stop < 0x1000, "Out of bounds for reading VRAM bank" assert bank <= 7, "WRAM Bank out of range" if not is_single: - return [self.mb.ram.internal_ram0[x + bank*0x1000] for x in range(start, stop, step)] + mem_slice = [0] * slice_length + for x in range(start, stop, step): + mem_slice[(x-start) // step] = self.mb.ram.internal_ram0[x + bank*0x1000] + return mem_slice else: return self.mb.ram.internal_ram0[start + bank*0x1000] else: assert None, "Invalid memory address for bank" elif not is_single: # Reading slice of memory space - return [self.mb.getitem(x) for x in range(start, stop, step)] + mem_slice = [0] * slice_length + for x in range(start, stop, step): + mem_slice[(x-start) // step] = self.mb.getitem(x) + return mem_slice else: # Reading specific address of memory space return self.mb.getitem(start) @@ -922,7 +946,59 @@ def __setitem(self, start, stop, step, v, bank, is_single, is_bank): if is_bank: # Writing a specific bank if start < 0x8000: - assert None, "Cannot write to ROM banks" + """ + Override one byte at a given memory address of the Game Boy's ROM. + + This will let you override data in the ROM at any given bank. This is the memory allocated at 0x0000 to 0x8000, where 0x4000 to 0x8000 can be changed from the MBC. + + __NOTE__: Any changes here are not saved or loaded to game states! Use this function with caution and reapply + any overrides when reloading the ROM. + + If you need to change a RAM address, see `pyboy.PyBoy.memory`. + + Args: + rom_bank (int): ROM bank to do the overwrite in + addr (int): Address to write the byte inside the ROM bank + value (int): A byte of data + """ + if start >= 0x4000: + start -= 0x4000 + stop -= 0x4000 + # Cartridge ROM Banks + assert stop < 0x4000, "Out of bounds for reading ROM bank" + assert bank <= self.mb.cartridge.external_rom_count, "ROM Bank out of range" + + # TODO: If you change a RAM value outside of the ROM banks above, the memory value will stay the same no matter + # what the game writes to the address. This can be used so freeze the value for health, cash etc. + if bank == -1: + assert start <= 0xFF, "Start address out of range for bootrom" + assert stop <= 0xFF, "Start address out of range for bootrom" + if not is_single: + # Writing slice of memory space + if hasattr(v, "__iter__"): + assert (stop-start) // step == len(v), "slice does not match length of data" + _v = iter(v) + for x in range(start, stop, step): + self.mb.bootrom.bootrom[x] = next(_v) + else: + for x in range(start, stop, step): + self.mb.bootrom.bootrom[x] = v + else: + self.mb.bootrom.bootrom[start] = v + else: + if not is_single: + # Writing slice of memory space + if hasattr(v, "__iter__"): + assert (stop-start) // step == len(v), "slice does not match length of data" + _v = iter(v) + for x in range(start, stop, step): + self.mb.cartridge.overrideitem(bank, x, next(_v)) + else: + for x in range(start, stop, step): + self.mb.cartridge.overrideitem(bank, x, v) + else: + self.mb.cartridge.overrideitem(bank, start, v) + elif start < 0xA000: start -= 0x8000 stop -= 0x8000 @@ -969,12 +1045,12 @@ def __setitem(self, start, stop, step, v, bank, is_single, is_bank): assert (stop-start) // step == len(v), "slice does not match length of data" _v = iter(v) for x in range(start, stop, step): - self.mb.cartridge.rambanks[bank][x] = next(_v) + self.mb.cartridge.rambanks[bank, x] = next(_v) else: for x in range(start, stop, step): - self.mb.cartridge.rambanks[bank][x] = v + self.mb.cartridge.rambanks[bank, x] = v else: - self.mb.cartridge.rambanks[bank][start] = v + self.mb.cartridge.rambanks[bank, start] = v elif start < 0xE000: start -= 0xC000 stop -= 0xC000 diff --git a/tests/test_basics.py b/tests/test_basics.py index 438dcf77f..0c7db82e2 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -151,22 +151,22 @@ def test_tilemaps(kirby_rom): def test_randomize_ram(default_rom): pyboy = PyBoy(default_rom, window_type="dummy", randomize=False) # RAM banks should all be 0 by default - assert not any([pyboy.get_memory_value(x) for x in range(0x8000, 0xA000)]), "VRAM not zeroed" - assert not any([pyboy.get_memory_value(x) for x in range(0xC000, 0xE000)]), "Internal RAM 0 not zeroed" - assert not any([pyboy.get_memory_value(x) for x in range(0xFE00, 0xFEA0)]), "OAM not zeroed" - assert not any([pyboy.get_memory_value(x) for x in range(0xFEA0, 0xFF00)]), "Non-IO internal RAM 0 not zeroed" - assert not any([pyboy.get_memory_value(x) for x in range(0xFF4C, 0xFF80)]), "Non-IO internal RAM 1 not zeroed" - assert not any([pyboy.get_memory_value(x) for x in range(0xFF80, 0xFFFF)]), "Internal RAM 1 not zeroed" + assert not any(pyboy.memory[0x8000:0xA000]), "VRAM not zeroed" + assert not any(pyboy.memory[0xC000:0xE000]), "Internal RAM 0 not zeroed" + assert not any(pyboy.memory[0xFE00:0xFEA0]), "OAM not zeroed" + assert not any(pyboy.memory[0xFEA0:0xFF00]), "Non-IO internal RAM 0 not zeroed" + assert not any(pyboy.memory[0xFF4C:0xFF80]), "Non-IO internal RAM 1 not zeroed" + assert not any(pyboy.memory[0xFF80:0xFFFF]), "Internal RAM 1 not zeroed" pyboy.stop(save=False) pyboy = PyBoy(default_rom, window_type="dummy", randomize=True) # RAM banks should have at least one nonzero value now - assert any([pyboy.get_memory_value(x) for x in range(0x8000, 0xA000)]), "VRAM not randomized" - assert any([pyboy.get_memory_value(x) for x in range(0xC000, 0xE000)]), "Internal RAM 0 not randomized" - assert any([pyboy.get_memory_value(x) for x in range(0xFE00, 0xFEA0)]), "OAM not randomized" - assert any([pyboy.get_memory_value(x) for x in range(0xFEA0, 0xFF00)]), "Non-IO internal RAM 0 not randomized" - assert any([pyboy.get_memory_value(x) for x in range(0xFF4C, 0xFF80)]), "Non-IO internal RAM 1 not randomized" - assert any([pyboy.get_memory_value(x) for x in range(0xFF80, 0xFFFF)]), "Internal RAM 1 not randomized" + assert any(pyboy.memory[0x8000:0xA000]), "VRAM not randomized" + assert any(pyboy.memory[0xC000:0xE000]), "Internal RAM 0 not randomized" + assert any(pyboy.memory[0xFE00:0xFEA0]), "OAM not randomized" + assert any(pyboy.memory[0xFEA0:0xFF00]), "Non-IO internal RAM 0 not randomized" + assert any(pyboy.memory[0xFF4C:0xFF80]), "Non-IO internal RAM 1 not randomized" + assert any(pyboy.memory[0xFF80:0xFFFF]), "Internal RAM 1 not randomized" pyboy.stop(save=False) diff --git a/tests/test_blargg.py b/tests/test_blargg.py index 653f472b5..8855a9979 100644 --- a/tests/test_blargg.py +++ b/tests/test_blargg.py @@ -40,7 +40,7 @@ def run_rom(rom): if result == "": n = 0 while True: - char = pyboy.get_memory_value(0xA004 + n) + char = pyboy.memory[0xA004 + n] if char != 0: result += chr(char) n += 1 diff --git a/tests/test_external_api.py b/tests/test_external_api.py index 8267310c4..9a80ea45c 100644 --- a/tests/test_external_api.py +++ b/tests/test_external_api.py @@ -281,7 +281,7 @@ def test_tetris(tetris_rom): (-8, -16, 0, False), ]) - assert pyboy.get_memory_value(NEXT_TETROMINO) == 24 + assert pyboy.memory[NEXT_TETROMINO] == 24 assert tetris.next_tetromino() == "T" tmp_state = io.BytesIO() @@ -367,17 +367,27 @@ def test_tilemap_position_list(supermarioland_rom): pyboy.stop(save=False) -def get_set_override(default_rom): +def test_get_set_override(default_rom): pyboy = PyBoy(default_rom, window_type="dummy") pyboy.set_emulation_speed(0) pyboy.tick(1, False) - assert pyboy.get_memory_value(0xFF40) == 0x91 - assert pyboy.set_memory_value(0xFF40) == 0x12 - assert pyboy.get_memory_value(0xFF40) == 0x12 + assert pyboy.memory[0xFF40] == 0x00 + pyboy.memory[0xFF40] = 0x12 + assert pyboy.memory[0xFF40] == 0x12 - assert pyboy.get_memory_value(0x0002) == 0xFE - assert pyboy.override_memory_value(0x0002) == 0x12 - assert pyboy.get_memory_value(0x0002) == 0x12 + assert pyboy.memory[0, 0x0002] == 0x42 # Taken from ROM bank 0 + assert pyboy.memory[0x0002] == 0xFF # Taken from bootrom + assert pyboy.memory[-1, 0x0002] == 0xFF # Taken from bootrom + pyboy.memory[-1, 0x0002] = 0x01 # Change bootrom + assert pyboy.memory[-1, 0x0002] == 0x01 # New value in bootrom + assert pyboy.memory[0, 0x0002] == 0x42 # Taken from ROM bank 0 + + pyboy.memory[0xFF50] = 1 # Disable bootrom + assert pyboy.memory[0x0002] == 0x42 # Taken from ROM bank 0 + + pyboy.memory[0, 0x0002] = 0x12 + assert pyboy.memory[0x0002] == 0x12 + assert pyboy.memory[0, 0x0002] == 0x12 pyboy.stop(save=False) diff --git a/tests/test_memoryview.py b/tests/test_memoryview.py new file mode 100644 index 000000000..7a5889523 --- /dev/null +++ b/tests/test_memoryview.py @@ -0,0 +1,137 @@ +# +# License: See LICENSE.md file +# GitHub: https://github.com/Baekalfen/PyBoy +# + +import pytest + +from pyboy import PyBoy + + +def test_memoryview(default_rom, boot_rom): + p = PyBoy(default_rom, bootrom_file=boot_rom) + + with open(default_rom, "rb") as f: + rom_bytes = [ord(f.read(1)) for x in range(16)] + + with open(boot_rom, "rb") as f: + bootrom_bytes = [ord(f.read(1)) for x in range(16)] + + assert p.memory[0] == 49 + assert p.memory[0:10] == bootrom_bytes[:10] + assert p.memory[0:10:2] == bootrom_bytes[:10:2] + + assert p.memory[0xFFF0:0x10000] == [0] * 16 + p.memory[0xFFFF] = 1 + assert p.memory[0xFFF0:0x10000] == [0] * 15 + [1] + assert p.memory[0xFFFF] == 1 + + # Requires start and end address + with pytest.raises(AssertionError): + p.memory[:10] == [] + with pytest.raises(AssertionError): + p.memory[20:10] == [] + with pytest.raises(AssertionError): + p.memory[:10:] == [] + with pytest.raises(AssertionError): + p.memory[0xFF00:] == [] + with pytest.raises(AssertionError): + p.memory[0xFF00::] == [] + with pytest.raises(AssertionError): + p.memory[:10] = 0 + with pytest.raises(AssertionError): + p.memory[:10:] = 0 + with pytest.raises(AssertionError): + p.memory[0xFF00:] = 0 + with pytest.raises(AssertionError): + p.memory[0xFF00::] = 0 + + # Attempt write to ROM area + p.memory[0:10] = 1 + assert p.memory[0:10] == bootrom_bytes[:10] + + # Actually do write to RAM area + assert p.memory[0xC000:0xC00a] == [0] * 10 + p.memory[0xC000:0xC00a] = 123 + assert p.memory[0xC000:0xC00a] == [123] * 10 + + # Attempt to write slice to ROM area + p.memory[0:5] = [0, 1, 2, 3, 4] + assert p.memory[0:10] == bootrom_bytes[:10] + + # Actually do write slice to RAM area + p.memory[0xC000:0xC00a] = [0] * 10 + assert p.memory[0xC000:0xC00a] == [0] * 10 + p.memory[0xC000:0xC00a] = [1] * 10 + assert p.memory[0xC000:0xC00a] == [1] * 10 + + # Attempt to write too large memory slice into memory area + with pytest.raises(AssertionError): + p.memory[0xC000:0xC001] = [1] * 10 + + # Attempt to write too small memory slice into memory area + with pytest.raises(AssertionError): + p.memory[0xC000:0xC00a] = [1] * 2 + + # Read specific ROM bank + assert p.memory[0, 0x00:0x10] == rom_bytes[:16] + assert p.memory[1, 0x00] == 0 + with pytest.raises(AssertionError): + # Slicing currently unsupported + assert p.memory[0:2, 0x00:0x10] == [] + + # Write to RAM bank + p.memory[1, 0xA000] = 0 + assert p.memory[1, 0xA000] == 0 + assert p.memory[1, 0xA001] == 0 + p.memory[1, 0xA000] = 1 + assert p.memory[1, 0xA000] == 1 + p.memory[1, 0xA000:0xA010] = 2 + assert p.memory[1, 0xA000] == 2 + assert p.memory[1, 0xA001] == 2 + + with pytest.raises(AssertionError): + # Out of bounds + p.memory[0, 0x00:0x9000] + + +def test_cgb_banks(cgb_acid_file): # Any CGB file + p = PyBoy(cgb_acid_file) + + # Read VRAM banks through both aliases + assert p.memory[0, 0x8000:0x8010] == [0] * 16 + assert p.memory[1, 0x8000:0x8010] == [0] * 16 + + # Set some value in VRAM + p.memory[0, 0x8000:0x8010] = [1] * 16 + p.memory[1, 0x8000:0x8010] = [2] * 16 + + # Read same value from both VRAM banks through both aliases + assert p.memory[0, 0x8000:0x8010] == [1] * 16 + assert p.memory[1, 0x8000:0x8010] == [2] * 16 + + # Read WRAM banks through both aliases + assert p.memory[0, 0xC000:0xC010] == [0] * 16 + assert p.memory[7, 0xC000:0xC010] == [0] * 16 + assert p.memory[0, 0xD000:0xD010] == [0] * 16 + assert p.memory[7, 0xD000:0xD010] == [0] * 16 + + # Set some value in WRAM + p.memory[0, 0xC000:0xC010] = [1] * 16 + p.memory[7, 0xC000:0xC010] = [2] * 16 + + # Read same value from both WRAM banks through both aliases + assert p.memory[0, 0xC000:0xC010] == [1] * 16 + assert p.memory[7, 0xC000:0xC010] == [2] * 16 + assert p.memory[0, 0xD000:0xD010] == [1] * 16 + assert p.memory[7, 0xD000:0xD010] == [2] * 16 + + with pytest.raises(AssertionError): + # Slicing currently unsupported + p.memory[0:2, 0xD000:0xD010] = 1 + + with pytest.raises(AssertionError): + p.memory[8, 0xD000] # Only bank 0-7 + + with pytest.raises(AssertionError): + p.memory[8, 0xD000] = 1 # Only bank 0-7 diff --git a/tests/test_states.py b/tests/test_states.py index 0f1ae7358..7c1e5472b 100644 --- a/tests/test_states.py +++ b/tests/test_states.py @@ -14,7 +14,7 @@ def test_load_save_consistency(tetris_rom): pyboy = PyBoy(tetris_rom, window_type="headless", game_wrapper=True) assert pyboy.cartridge_title() == "TETRIS" pyboy.set_emulation_speed(0) - pyboy.get_memory_value(NEXT_TETROMINO_ADDR) + pyboy.memory[NEXT_TETROMINO_ADDR] ############################################################## # Set up some kind of state, where not all registers are reset diff --git a/tests/test_tetris_ai.py b/tests/test_tetris_ai.py index 9e1f45e72..6302d5960 100644 --- a/tests/test_tetris_ai.py +++ b/tests/test_tetris_ai.py @@ -58,7 +58,7 @@ def eval_network(epoch, child_index, child_model, record_to): pyboy.send_input(WindowEvent.SCREEN_RECORDING_TOGGLE) # Set block animation to fall instantly - pyboy.set_memory_value(0xff9a, 2) + pyboy.memory[0xff9a] = 2 run = 0 scores = [] @@ -76,7 +76,7 @@ def eval_network(epoch, child_index, child_model, record_to): s_lines = tetris.lines # Determine how many possible rotations we need to check for the block - block_tile = pyboy.get_memory_value(0xc203) + block_tile = pyboy.memory[0xc203] turns_needed = check_needed_turn(block_tile) lefts_needed, rights_needed = check_needed_dirs(block_tile) From 5f3660eb258b7541368501a57b3e3b8498864581 Mon Sep 17 00:00:00 2001 From: Mads Ynddal <5528170+Baekalfen@users.noreply.github.com> Date: Fri, 27 Oct 2023 23:36:29 +0200 Subject: [PATCH 19/65] Removed "headless" and "dummy" windows in favor of "null" --- extras/examples/gamewrapper_kirby.py | 2 +- extras/examples/gamewrapper_mario.py | 2 +- extras/examples/gamewrapper_tetris.py | 2 +- pyboy/__main__.py | 6 +- pyboy/core/sound.py | 2 +- pyboy/plugins/__init__.py | 17 ++--- pyboy/plugins/manager.pxd | 30 ++++---- pyboy/plugins/manager.py | 74 ++++++------------- pyboy/plugins/manager_gen.py | 2 +- pyboy/plugins/window_headless.pxd | 13 ---- pyboy/plugins/window_headless.py | 24 ------ .../{window_dummy.pxd => window_null.pxd} | 5 +- .../{window_dummy.py => window_null.py} | 9 ++- pyboy/pyboy.py | 13 +--- tests/test_acid_cgb.py | 2 +- tests/test_acid_dmg.py | 2 +- tests/test_basics.py | 12 +-- tests/test_blargg.py | 2 +- tests/test_external_api.py | 12 +-- tests/test_game_wrapper_mario.py | 12 +-- tests/test_magen.py | 2 +- tests/test_mario_rl.py | 2 +- tests/test_mooneye.py | 4 +- tests/test_openai_gym.py | 2 +- tests/test_replay.py | 4 +- tests/test_rtc3test.py | 2 +- tests/test_samesuite.py | 4 +- tests/test_shonumi.py | 2 +- tests/test_states.py | 2 +- tests/test_tetris_ai.py | 2 +- tests/test_which.py | 2 +- tests/test_whichboot.py | 2 +- tests/test_windows.py | 9 +-- 33 files changed, 107 insertions(+), 175 deletions(-) delete mode 100644 pyboy/plugins/window_headless.pxd delete mode 100644 pyboy/plugins/window_headless.py rename pyboy/plugins/{window_dummy.pxd => window_null.pxd} (74%) rename pyboy/plugins/{window_dummy.py => window_null.py} (53%) diff --git a/extras/examples/gamewrapper_kirby.py b/extras/examples/gamewrapper_kirby.py index 80b3a8229..f8a4ccc8f 100644 --- a/extras/examples/gamewrapper_kirby.py +++ b/extras/examples/gamewrapper_kirby.py @@ -20,7 +20,7 @@ exit(1) quiet = "--quiet" in sys.argv -pyboy = PyBoy(filename, window_type="headless" if quiet else "SDL2", window_scale=3, debug=not quiet, game_wrapper=True) +pyboy = PyBoy(filename, window_type="null" if quiet else "SDL2", window_scale=3, debug=not quiet, game_wrapper=True) pyboy.set_emulation_speed(0) assert pyboy.cartridge_title() == "KIRBY DREAM LA" diff --git a/extras/examples/gamewrapper_mario.py b/extras/examples/gamewrapper_mario.py index dcbe4871a..9acda0bb8 100644 --- a/extras/examples/gamewrapper_mario.py +++ b/extras/examples/gamewrapper_mario.py @@ -20,7 +20,7 @@ exit(1) quiet = "--quiet" in sys.argv -pyboy = PyBoy(filename, window_type="headless" if quiet else "SDL2", window_scale=3, debug=not quiet, game_wrapper=True) +pyboy = PyBoy(filename, window_type="null" if quiet else "SDL2", window_scale=3, debug=not quiet, game_wrapper=True) pyboy.set_emulation_speed(0) assert pyboy.cartridge_title() == "SUPER MARIOLAN" diff --git a/extras/examples/gamewrapper_tetris.py b/extras/examples/gamewrapper_tetris.py index 1b315c534..226721543 100644 --- a/extras/examples/gamewrapper_tetris.py +++ b/extras/examples/gamewrapper_tetris.py @@ -20,7 +20,7 @@ exit(1) quiet = "--quiet" in sys.argv -pyboy = PyBoy(filename, window_type="headless" if quiet else "SDL2", window_scale=3, debug=not quiet, game_wrapper=True) +pyboy = PyBoy(filename, window_type="null" if quiet else "SDL2", window_scale=3, debug=not quiet, game_wrapper=True) pyboy.set_emulation_speed(0) assert pyboy.cartridge_title() == "TETRIS" diff --git a/pyboy/__main__.py b/pyboy/__main__.py index 40c1853ba..1bd2a86ad 100644 --- a/pyboy/__main__.py +++ b/pyboy/__main__.py @@ -83,11 +83,12 @@ def valid_file_path(path): "--window", default=defaults["window_type"], type=str, - choices=["SDL2", "OpenGL", "headless", "dummy"], + choices=["SDL2", "OpenGL", "null"], help="Specify window-type to use" ) parser.add_argument("-s", "--scale", default=defaults["scale"], type=int, help="The scaling multiplier for the window") parser.add_argument("--sound", action="store_true", help="Enable sound (beta)") +parser.add_argument("--no-renderer", action="store_true", help="Disable rendering (internal use)") gameboy_type_parser = parser.add_mutually_exclusive_group() gameboy_type_parser.add_argument( @@ -161,7 +162,8 @@ def main(): with open(state_path, "rb") as f: pyboy.load_state(f) - while pyboy._tick(True): + render = not argv.no_renderer + while pyboy._tick(render): pass pyboy.stop() diff --git a/pyboy/core/sound.py b/pyboy/core/sound.py index be3ff2b23..b69de626c 100644 --- a/pyboy/core/sound.py +++ b/pyboy/core/sound.py @@ -38,7 +38,7 @@ def __init__(self, enabled, emulate): self.device = sdl2.SDL_OpenAudioDevice(None, 0, spec_want, spec_have, 0) if self.device > 1: - # Start playback (move out of __init__ if needed, maybe for headless) + # Start playback (move out of __init__ if needed, maybe for null) sdl2.SDL_PauseAudioDevice(self.device, 0) self.sample_rate = spec_have.freq diff --git a/pyboy/plugins/__init__.py b/pyboy/plugins/__init__.py index df8b37600..0bcf22e5e 100644 --- a/pyboy/plugins/__init__.py +++ b/pyboy/plugins/__init__.py @@ -9,18 +9,17 @@ __pdoc__ = { # docs exclude "window_sdl2": False, - "window_open_gl": False, - "debug": False, - "window_dummy": False, - "auto_pause": False, - "rewind": False, - "window_headless": False, "screen_recorder": False, - "manager_gen": False, "disable_input": False, - "screenshot_recorder": False, + "auto_pause": False, + "debug": False, "debug_prompt": False, - "record_replay": False, + "window_null": False, + "manager_gen": False, "manager": False, + "screenshot_recorder": False, + "window_open_gl": False, + "record_replay": False, + "rewind": False, # docs exclude end } diff --git a/pyboy/plugins/manager.pxd b/pyboy/plugins/manager.pxd index 54d418064..cf5d03ce1 100644 --- a/pyboy/plugins/manager.pxd +++ b/pyboy/plugins/manager.pxd @@ -5,26 +5,28 @@ cimport cython + from pyboy.logging.logging cimport Logger from pyboy.plugins.auto_pause cimport AutoPause + +# isort:skip +# imports +from pyboy.plugins.window_sdl2 cimport WindowSDL2 +from pyboy.plugins.window_open_gl cimport WindowOpenGL +from pyboy.plugins.window_null cimport WindowNull from pyboy.plugins.debug cimport Debug -from pyboy.plugins.debug_prompt cimport DebugPrompt from pyboy.plugins.disable_input cimport DisableInput -from pyboy.plugins.game_wrapper_kirby_dream_land cimport GameWrapperKirbyDreamLand -from pyboy.plugins.game_wrapper_pokemon_gen1 cimport GameWrapperPokemonGen1 -from pyboy.plugins.game_wrapper_super_mario_land cimport GameWrapperSuperMarioLand -from pyboy.plugins.game_wrapper_tetris cimport GameWrapperTetris +from pyboy.plugins.auto_pause cimport AutoPause from pyboy.plugins.record_replay cimport RecordReplay from pyboy.plugins.rewind cimport Rewind from pyboy.plugins.screen_recorder cimport ScreenRecorder from pyboy.plugins.screenshot_recorder cimport ScreenshotRecorder -from pyboy.plugins.window_dummy cimport WindowDummy -from pyboy.plugins.window_headless cimport WindowHeadless -from pyboy.plugins.window_open_gl cimport WindowOpenGL +from pyboy.plugins.debug_prompt cimport DebugPrompt +from pyboy.plugins.game_wrapper_super_mario_land cimport GameWrapperSuperMarioLand +from pyboy.plugins.game_wrapper_tetris cimport GameWrapperTetris +from pyboy.plugins.game_wrapper_kirby_dream_land cimport GameWrapperKirbyDreamLand +from pyboy.plugins.game_wrapper_pokemon_gen1 cimport GameWrapperPokemonGen1 # imports end -# isort:skip -# imports -from pyboy.plugins.window_sdl2 cimport WindowSDL2 cdef Logger logger @@ -36,8 +38,7 @@ cdef class PluginManager: # plugin_cdef cdef public WindowSDL2 window_sdl2 cdef public WindowOpenGL window_open_gl - cdef public WindowHeadless window_headless - cdef public WindowDummy window_dummy + cdef public WindowNull window_null cdef public Debug debug cdef public DisableInput disable_input cdef public AutoPause auto_pause @@ -52,8 +53,7 @@ cdef class PluginManager: cdef public GameWrapperPokemonGen1 game_wrapper_pokemon_gen1 cdef bint window_sdl2_enabled cdef bint window_open_gl_enabled - cdef bint window_headless_enabled - cdef bint window_dummy_enabled + cdef bint window_null_enabled cdef bint debug_enabled cdef bint disable_input_enabled cdef bint auto_pause_enabled diff --git a/pyboy/plugins/manager.py b/pyboy/plugins/manager.py index e329bab15..ebf020223 100644 --- a/pyboy/plugins/manager.py +++ b/pyboy/plugins/manager.py @@ -6,8 +6,7 @@ # imports from pyboy.plugins.window_sdl2 import WindowSDL2 # isort:skip from pyboy.plugins.window_open_gl import WindowOpenGL # isort:skip -from pyboy.plugins.window_headless import WindowHeadless # isort:skip -from pyboy.plugins.window_dummy import WindowDummy # isort:skip +from pyboy.plugins.window_null import WindowNull # isort:skip from pyboy.plugins.debug import Debug # isort:skip from pyboy.plugins.disable_input import DisableInput # isort:skip from pyboy.plugins.auto_pause import AutoPause # isort:skip @@ -28,8 +27,7 @@ def parser_arguments(): # yield_plugins yield WindowSDL2.argv yield WindowOpenGL.argv - yield WindowHeadless.argv - yield WindowDummy.argv + yield WindowNull.argv yield Debug.argv yield DisableInput.argv yield AutoPause.argv @@ -55,10 +53,8 @@ def __init__(self, pyboy, mb, pyboy_argv): self.window_sdl2_enabled = self.window_sdl2.enabled() self.window_open_gl = WindowOpenGL(pyboy, mb, pyboy_argv) self.window_open_gl_enabled = self.window_open_gl.enabled() - self.window_headless = WindowHeadless(pyboy, mb, pyboy_argv) - self.window_headless_enabled = self.window_headless.enabled() - self.window_dummy = WindowDummy(pyboy, mb, pyboy_argv) - self.window_dummy_enabled = self.window_dummy.enabled() + self.window_null = WindowNull(pyboy, mb, pyboy_argv) + self.window_null_enabled = self.window_null.enabled() self.debug = Debug(pyboy, mb, pyboy_argv) self.debug_enabled = self.debug.enabled() self.disable_input = DisableInput(pyboy, mb, pyboy_argv) @@ -87,14 +83,10 @@ def __init__(self, pyboy, mb, pyboy_argv): def gamewrapper(self): # gamewrapper - if self.game_wrapper_super_mario_land_enabled: - return self.game_wrapper_super_mario_land - if self.game_wrapper_tetris_enabled: - return self.game_wrapper_tetris - if self.game_wrapper_kirby_dream_land_enabled: - return self.game_wrapper_kirby_dream_land - if self.game_wrapper_pokemon_gen1_enabled: - return self.game_wrapper_pokemon_gen1 + if self.game_wrapper_super_mario_land_enabled: return self.game_wrapper_super_mario_land + if self.game_wrapper_tetris_enabled: return self.game_wrapper_tetris + if self.game_wrapper_kirby_dream_land_enabled: return self.game_wrapper_kirby_dream_land + if self.game_wrapper_pokemon_gen1_enabled: return self.game_wrapper_pokemon_gen1 # gamewrapper end return None @@ -104,10 +96,8 @@ def handle_events(self, events): events = self.window_sdl2.handle_events(events) if self.window_open_gl_enabled: events = self.window_open_gl.handle_events(events) - if self.window_headless_enabled: - events = self.window_headless.handle_events(events) - if self.window_dummy_enabled: - events = self.window_dummy.handle_events(events) + if self.window_null_enabled: + events = self.window_null.handle_events(events) if self.debug_enabled: events = self.debug.handle_events(events) # foreach end @@ -171,10 +161,8 @@ def _set_title(self): self.window_sdl2.set_title(self.pyboy.window_title) if self.window_open_gl_enabled: self.window_open_gl.set_title(self.pyboy.window_title) - if self.window_headless_enabled: - self.window_headless.set_title(self.pyboy.window_title) - if self.window_dummy_enabled: - self.window_dummy.set_title(self.pyboy.window_title) + if self.window_null_enabled: + self.window_null.set_title(self.pyboy.window_title) if self.debug_enabled: self.debug.set_title(self.pyboy.window_title) # foreach end @@ -186,10 +174,8 @@ def _post_tick_windows(self): self.window_sdl2.post_tick() if self.window_open_gl_enabled: self.window_open_gl.post_tick() - if self.window_headless_enabled: - self.window_headless.post_tick() - if self.window_dummy_enabled: - self.window_dummy.post_tick() + if self.window_null_enabled: + self.window_null.post_tick() if self.debug_enabled: self.debug.post_tick() # foreach end @@ -201,24 +187,16 @@ def frame_limiter(self, speed): # foreach windows done = [].frame_limiter(speed), if done: return if self.window_sdl2_enabled: done = self.window_sdl2.frame_limiter(speed) - if done: - return + if done: return if self.window_open_gl_enabled: done = self.window_open_gl.frame_limiter(speed) - if done: - return - if self.window_headless_enabled: - done = self.window_headless.frame_limiter(speed) - if done: - return - if self.window_dummy_enabled: - done = self.window_dummy.frame_limiter(speed) - if done: - return + if done: return + if self.window_null_enabled: + done = self.window_null.frame_limiter(speed) + if done: return if self.debug_enabled: done = self.debug.frame_limiter(speed) - if done: - return + if done: return # foreach end def window_title(self): @@ -228,10 +206,8 @@ def window_title(self): title += self.window_sdl2.window_title() if self.window_open_gl_enabled: title += self.window_open_gl.window_title() - if self.window_headless_enabled: - title += self.window_headless.window_title() - if self.window_dummy_enabled: - title += self.window_dummy.window_title() + if self.window_null_enabled: + title += self.window_null.window_title() if self.debug_enabled: title += self.debug.window_title() # foreach end @@ -267,10 +243,8 @@ def stop(self): self.window_sdl2.stop() if self.window_open_gl_enabled: self.window_open_gl.stop() - if self.window_headless_enabled: - self.window_headless.stop() - if self.window_dummy_enabled: - self.window_dummy.stop() + if self.window_null_enabled: + self.window_null.stop() if self.debug_enabled: self.debug.stop() # foreach end diff --git a/pyboy/plugins/manager_gen.py b/pyboy/plugins/manager_gen.py index 9a8993220..dfb73a500 100644 --- a/pyboy/plugins/manager_gen.py +++ b/pyboy/plugins/manager_gen.py @@ -6,7 +6,7 @@ # Plugins and priority! # E.g. DisableInput first -windows = ["WindowSDL2", "WindowOpenGL", "WindowHeadless", "WindowDummy", "Debug"] +windows = ["WindowSDL2", "WindowOpenGL", "WindowNull", "Debug"] game_wrappers = [ "GameWrapperSuperMarioLand", "GameWrapperTetris", "GameWrapperKirbyDreamLand", "GameWrapperPokemonGen1" ] diff --git a/pyboy/plugins/window_headless.pxd b/pyboy/plugins/window_headless.pxd deleted file mode 100644 index e3e503f1d..000000000 --- a/pyboy/plugins/window_headless.pxd +++ /dev/null @@ -1,13 +0,0 @@ -# -# License: See LICENSE.md file -# GitHub: https://github.com/Baekalfen/PyBoy -# - -from pyboy.logging.logging cimport Logger -from pyboy.plugins.base_plugin cimport PyBoyWindowPlugin - - -cdef Logger logger - -cdef class WindowHeadless(PyBoyWindowPlugin): - pass diff --git a/pyboy/plugins/window_headless.py b/pyboy/plugins/window_headless.py deleted file mode 100644 index 1959af439..000000000 --- a/pyboy/plugins/window_headless.py +++ /dev/null @@ -1,24 +0,0 @@ -# -# License: See LICENSE.md file -# GitHub: https://github.com/Baekalfen/PyBoy -# - -import pyboy -from pyboy import utils -from pyboy.plugins.base_plugin import PyBoyWindowPlugin - -logger = pyboy.logging.get_logger(__name__) - - -class WindowHeadless(PyBoyWindowPlugin): - def __init__(self, pyboy, mb, pyboy_argv): - super().__init__(pyboy, mb, pyboy_argv) - - if not self.enabled(): - return - - def enabled(self): - return self.pyboy_argv.get("window_type") == "headless" - - def set_title(self, title): - logger.debug(title) diff --git a/pyboy/plugins/window_dummy.pxd b/pyboy/plugins/window_null.pxd similarity index 74% rename from pyboy/plugins/window_dummy.pxd rename to pyboy/plugins/window_null.pxd index 95a5cf555..a12cb4a85 100644 --- a/pyboy/plugins/window_dummy.pxd +++ b/pyboy/plugins/window_null.pxd @@ -6,8 +6,5 @@ from pyboy.logging.logging cimport Logger from pyboy.plugins.base_plugin cimport PyBoyWindowPlugin - -cdef Logger logger - -cdef class WindowDummy(PyBoyWindowPlugin): +cdef class WindowNull(PyBoyWindowPlugin): pass diff --git a/pyboy/plugins/window_dummy.py b/pyboy/plugins/window_null.py similarity index 53% rename from pyboy/plugins/window_dummy.py rename to pyboy/plugins/window_null.py index 3a6c81fea..6e53d4c7a 100644 --- a/pyboy/plugins/window_dummy.py +++ b/pyboy/plugins/window_null.py @@ -10,15 +10,20 @@ logger = pyboy.logging.get_logger(__name__) -class WindowDummy(PyBoyWindowPlugin): +class WindowNull(PyBoyWindowPlugin): def __init__(self, pyboy, mb, pyboy_argv): super().__init__(pyboy, mb, pyboy_argv) if not self.enabled(): return + if pyboy_argv.get("window_type") in ["headless", "dummy"]: + logger.error( + 'Deprecated use of "headless" or "dummy" window. Change to "null" window instead. https://github.com/Baekalfen/PyBoy/wiki/Migrating-from-v1.x.x-to-v2.0.0' + ) + def enabled(self): - return self.pyboy_argv.get("window_type") == "dummy" + return self.pyboy_argv.get("window_type") in ["null", "headless", "dummy"] def set_title(self, title): logger.debug(title) diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index 0b71e3e0a..cb3a7ae99 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -600,15 +600,10 @@ def set_emulation_speed(self, target_speed): Args: target_speed (int): Target emulation speed as multiplier of real-time. """ - if self.initialized: - unsupported_window_types_enabled = [ - self.plugin_manager.window_dummy_enabled, self.plugin_manager.window_headless_enabled, - self.plugin_manager.window_open_gl_enabled - ] - if any(unsupported_window_types_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 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") diff --git a/tests/test_acid_cgb.py b/tests/test_acid_cgb.py index 79e52c0c8..641280987 100644 --- a/tests/test_acid_cgb.py +++ b/tests/test_acid_cgb.py @@ -15,7 +15,7 @@ # https://github.com/mattcurrie/cgb-acid2 def test_cgb_acid(cgb_acid_file): - pyboy = PyBoy(cgb_acid_file, window_type="headless") + pyboy = PyBoy(cgb_acid_file, window_type="null") pyboy.set_emulation_speed(0) pyboy.tick(59, True) pyboy.tick(25, True) diff --git a/tests/test_acid_dmg.py b/tests/test_acid_dmg.py index 3f359f063..8588ed337 100644 --- a/tests/test_acid_dmg.py +++ b/tests/test_acid_dmg.py @@ -16,7 +16,7 @@ @pytest.mark.parametrize("cgb", [False, True]) def test_dmg_acid(cgb, dmg_acid_file): - pyboy = PyBoy(dmg_acid_file, window_type="headless", cgb=cgb) + pyboy = PyBoy(dmg_acid_file, window_type="null", cgb=cgb) pyboy.set_emulation_speed(0) pyboy.tick(59, True) pyboy.tick(25, True) diff --git a/tests/test_basics.py b/tests/test_basics.py index 0c7db82e2..a3b16e373 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -23,7 +23,7 @@ def test_record_replay(boot_rom, default_rom): - pyboy = PyBoy(default_rom, window_type="headless", bootrom_file=boot_rom, record_input=True) + pyboy = PyBoy(default_rom, window_type="null", bootrom_file=boot_rom, record_input=True) pyboy.set_emulation_speed(0) pyboy.tick(1, True) pyboy.button_press("down") @@ -96,7 +96,7 @@ def test_argv_parser(*args): def test_tilemaps(kirby_rom): - pyboy = PyBoy(kirby_rom, window_type="dummy") + pyboy = PyBoy(kirby_rom, window_type="null") pyboy.set_emulation_speed(0) pyboy.tick(120, False) @@ -149,7 +149,7 @@ def test_tilemaps(kirby_rom): def test_randomize_ram(default_rom): - pyboy = PyBoy(default_rom, window_type="dummy", randomize=False) + pyboy = PyBoy(default_rom, window_type="null", randomize=False) # RAM banks should all be 0 by default assert not any(pyboy.memory[0x8000:0xA000]), "VRAM not zeroed" assert not any(pyboy.memory[0xC000:0xE000]), "Internal RAM 0 not zeroed" @@ -159,7 +159,7 @@ def test_randomize_ram(default_rom): assert not any(pyboy.memory[0xFF80:0xFFFF]), "Internal RAM 1 not zeroed" pyboy.stop(save=False) - pyboy = PyBoy(default_rom, window_type="dummy", randomize=True) + pyboy = PyBoy(default_rom, window_type="null", randomize=True) # RAM banks should have at least one nonzero value now assert any(pyboy.memory[0x8000:0xA000]), "VRAM not randomized" assert any(pyboy.memory[0xC000:0xE000]), "Internal RAM 0 not randomized" @@ -171,7 +171,7 @@ def test_randomize_ram(default_rom): def test_not_cgb(pokemon_crystal_rom): - pyboy = PyBoy(pokemon_crystal_rom, window_type="dummy", cgb=False) + pyboy = PyBoy(pokemon_crystal_rom, window_type="null", cgb=False) pyboy.set_emulation_speed(0) pyboy.tick(60 * 7, False) @@ -195,7 +195,7 @@ def test_all_modes(cgb, _bootrom, frames, rom, any_rom_cgb, boot_cgb_rom): if cgb == None and _bootrom == boot_cgb_rom and rom != any_rom_cgb: pytest.skip("Invalid combination") - pyboy = PyBoy(rom, window_type="headless", bootrom_file=_bootrom, cgb=cgb) + pyboy = PyBoy(rom, window_type="null", bootrom_file=_bootrom, cgb=cgb) pyboy.set_emulation_speed(0) pyboy.tick(frames, True) diff --git a/tests/test_blargg.py b/tests/test_blargg.py index 8855a9979..02dfa8d9d 100644 --- a/tests/test_blargg.py +++ b/tests/test_blargg.py @@ -21,7 +21,7 @@ def run_rom(rom): - pyboy = PyBoy(str(rom), window_type="dummy", cgb="cgb" in rom, sound_emulated=True) + pyboy = PyBoy(str(rom), window_type="null", cgb="cgb" in rom, sound_emulated=True) pyboy.set_emulation_speed(0) t = time.time() result = "" diff --git a/tests/test_external_api.py b/tests/test_external_api.py index 9a80ea45c..a7d123476 100644 --- a/tests/test_external_api.py +++ b/tests/test_external_api.py @@ -19,14 +19,14 @@ def test_misc(default_rom): - pyboy = PyBoy(default_rom, window_type="dummy") + pyboy = PyBoy(default_rom, window_type="null") pyboy.set_emulation_speed(0) pyboy.tick(1, False) pyboy.stop(save=False) def test_tiles(default_rom): - pyboy = PyBoy(default_rom, window_type="dummy") + pyboy = PyBoy(default_rom, window_type="null") pyboy.set_emulation_speed(0) pyboy.tick(BOOTROM_FRAMES_UNTIL_LOGO, False) @@ -68,7 +68,7 @@ def test_screen_buffer_and_image(tetris_rom, boot_rom): cformat = "RGBA" boot_logo_hash_predigested = b"_M\x0e\xd9\xe2\xdb\\o]\x83U\x93\xebZm\x1e\xaaFR/Q\xa52\x1c{8\xe7g\x95\xbcIz" - pyboy = PyBoy(tetris_rom, window_type="headless", bootrom_file=boot_rom) + pyboy = PyBoy(tetris_rom, window_type="null", bootrom_file=boot_rom) pyboy.set_emulation_speed(0) pyboy.tick(275, True) # Iterate to boot logo @@ -119,7 +119,7 @@ def test_screen_buffer_and_image(tetris_rom, boot_rom): def test_tetris(tetris_rom): NEXT_TETROMINO = 0xC213 - pyboy = PyBoy(tetris_rom, bootrom_file="pyboy_fast", window_type="dummy", game_wrapper=True) + pyboy = PyBoy(tetris_rom, bootrom_file="pyboy_fast", window_type="null", game_wrapper=True) pyboy.set_emulation_speed(0) tetris = pyboy.game_wrapper() tetris.set_tetromino("T") @@ -334,7 +334,7 @@ def test_tetris(tetris_rom): def test_tilemap_position_list(supermarioland_rom): - pyboy = PyBoy(supermarioland_rom, window_type="dummy") + pyboy = PyBoy(supermarioland_rom, window_type="null") pyboy.set_emulation_speed(0) pyboy.tick(100, False) @@ -368,7 +368,7 @@ def test_tilemap_position_list(supermarioland_rom): def test_get_set_override(default_rom): - pyboy = PyBoy(default_rom, window_type="dummy") + pyboy = PyBoy(default_rom, window_type="null") pyboy.set_emulation_speed(0) pyboy.tick(1, False) diff --git a/tests/test_game_wrapper_mario.py b/tests/test_game_wrapper_mario.py index 4bf4f7893..94ebe106e 100644 --- a/tests/test_game_wrapper_mario.py +++ b/tests/test_game_wrapper_mario.py @@ -17,7 +17,7 @@ def test_mario_basics(supermarioland_rom): - pyboy = PyBoy(supermarioland_rom, window_type="dummy", game_wrapper=True) + pyboy = PyBoy(supermarioland_rom, window_type="null", game_wrapper=True) pyboy.set_emulation_speed(0) assert pyboy.cartridge_title() == "SUPER MARIOLAN" @@ -33,7 +33,7 @@ def test_mario_basics(supermarioland_rom): def test_mario_advanced(supermarioland_rom): - pyboy = PyBoy(supermarioland_rom, window_type="dummy", game_wrapper=True) + pyboy = PyBoy(supermarioland_rom, window_type="null", game_wrapper=True) pyboy.set_emulation_speed(0) assert pyboy.cartridge_title() == "SUPER MARIOLAN" @@ -52,7 +52,7 @@ def test_mario_advanced(supermarioland_rom): def test_mario_game_over(supermarioland_rom): - pyboy = PyBoy(supermarioland_rom, window_type="dummy", game_wrapper=True) + pyboy = PyBoy(supermarioland_rom, window_type="null", game_wrapper=True) pyboy.set_emulation_speed(0) mario = pyboy.game_wrapper() @@ -69,7 +69,7 @@ def test_mario_game_over(supermarioland_rom): @pytest.mark.skipif(is_pypy, reason="This requires gym, which doesn't work on this platform") class TestOpenAIGym: def test_observation_type_compressed(self, supermarioland_rom): - pyboy = PyBoy(supermarioland_rom, window_type="dummy", game_wrapper=True) + pyboy = PyBoy(supermarioland_rom, window_type="null", game_wrapper=True) pyboy.set_emulation_speed(0) env = pyboy.openai_gym(observation_type="compressed") @@ -88,7 +88,7 @@ def test_observation_type_compressed(self, supermarioland_rom): assert np.all(observation == expected_observation) def test_observation_type_minimal(self, supermarioland_rom): - pyboy = PyBoy(supermarioland_rom, window_type="dummy", game_wrapper=True) + pyboy = PyBoy(supermarioland_rom, window_type="null", game_wrapper=True) pyboy.set_emulation_speed(0) env = pyboy.openai_gym(observation_type="minimal") @@ -107,7 +107,7 @@ def test_observation_type_minimal(self, supermarioland_rom): assert np.all(observation == expected_observation) def test_start_level(self, supermarioland_rom): - pyboy = PyBoy(supermarioland_rom, window_type="dummy", game_wrapper=True) + pyboy = PyBoy(supermarioland_rom, window_type="null", game_wrapper=True) pyboy.set_emulation_speed(0) starting_level = (2, 1) diff --git a/tests/test_magen.py b/tests/test_magen.py index a4dc2d2fa..c87b54776 100644 --- a/tests/test_magen.py +++ b/tests/test_magen.py @@ -15,7 +15,7 @@ # https://github.com/alloncm/MagenTests def test_magen_test(magen_test_file): - pyboy = PyBoy(magen_test_file, window_type="headless") + pyboy = PyBoy(magen_test_file, window_type="null") pyboy.set_emulation_speed(0) pyboy.tick(59, True) pyboy.tick(25, True) diff --git a/tests/test_mario_rl.py b/tests/test_mario_rl.py index 0d9aff093..65159f32a 100644 --- a/tests/test_mario_rl.py +++ b/tests/test_mario_rl.py @@ -85,7 +85,7 @@ ### # Load emulator ### -pyboy = PyBoy(game, window_type="headless", window_scale=3, debug=False, game_wrapper=True) +pyboy = PyBoy(game, window_type="null", window_scale=3, debug=False, game_wrapper=True) ### # Load enviroment diff --git a/tests/test_mooneye.py b/tests/test_mooneye.py index eaa7da1b1..875eab86a 100644 --- a/tests/test_mooneye.py +++ b/tests/test_mooneye.py @@ -152,14 +152,14 @@ def test_mooneye(clean, rom, mooneye_dir, default_rom): if saved_state is None: # HACK: We load any rom and load it until the last frame in the boot rom. # Then we save it, so we won't need to redo it. - pyboy = PyBoy(default_rom, window_type="headless", cgb=False, sound_emulated=True) + pyboy = PyBoy(default_rom, window_type="null", cgb=False, sound_emulated=True) pyboy.set_emulation_speed(0) saved_state = io.BytesIO() pyboy.tick(59, True) pyboy.save_state(saved_state) pyboy.stop(save=False) - pyboy = PyBoy(mooneye_dir + rom, window_type="headless", cgb=False) + pyboy = PyBoy(mooneye_dir + rom, window_type="null", cgb=False) pyboy.set_emulation_speed(0) saved_state.seek(0) if clean: diff --git a/tests/test_openai_gym.py b/tests/test_openai_gym.py index d018c9100..0b0e3613e 100644 --- a/tests/test_openai_gym.py +++ b/tests/test_openai_gym.py @@ -19,7 +19,7 @@ @pytest.fixture def pyboy(tetris_rom): - pyboy = PyBoy(tetris_rom, window_type="dummy", disable_input=True, game_wrapper=True) + pyboy = PyBoy(tetris_rom, window_type="null", disable_input=True, game_wrapper=True) pyboy.set_emulation_speed(0) return pyboy diff --git a/tests/test_replay.py b/tests/test_replay.py index 95a6374f6..9f72e3fed 100644 --- a/tests/test_replay.py +++ b/tests/test_replay.py @@ -66,7 +66,7 @@ def move_gif(game, dest): def replay( ROM, replay, - window="headless", + window="null", verify=False, record_gif=None, gif_destination=None, @@ -93,7 +93,7 @@ def replay( rewind=rewind, randomize=randomize, cgb=cgb, - record_input=(RESET_REPLAYS and window in ["SDL2", "headless", "OpenGL"]), + record_input=(RESET_REPLAYS and window in ["SDL2", "null", "OpenGL"]), ) pyboy.set_emulation_speed(0) if state_data is not None: diff --git a/tests/test_rtc3test.py b/tests/test_rtc3test.py index a4702173b..817ef1ec4 100644 --- a/tests/test_rtc3test.py +++ b/tests/test_rtc3test.py @@ -18,7 +18,7 @@ @pytest.mark.skip("RTC is too unstable") @pytest.mark.parametrize("subtest", [0, 1, 2]) def test_rtc3test(subtest, rtc3test_file): - pyboy = PyBoy(rtc3test_file, window_type="headless") + pyboy = PyBoy(rtc3test_file, window_type="null") pyboy.set_emulation_speed(0) pyboy.tick(59, True) pyboy.tick(25, True) diff --git a/tests/test_samesuite.py b/tests/test_samesuite.py index d521942ea..4d96309b4 100644 --- a/tests/test_samesuite.py +++ b/tests/test_samesuite.py @@ -114,7 +114,7 @@ def test_samesuite(clean, gb_type, rom, samesuite_dir, boot_cgb_rom, boot_rom, d # Then we save it, so we won't need to redo it. pyboy = PyBoy( default_rom, - window_type="headless", + window_type="null", cgb=gb_type == "cgb", bootrom=boot_cgb_rom if gb_type == "cgb" else boot_rom, sound_emulated=True, @@ -127,7 +127,7 @@ def test_samesuite(clean, gb_type, rom, samesuite_dir, boot_cgb_rom, boot_rom, d pyboy = PyBoy( samesuite_dir + rom, - window_type="headless", + window_type="null", cgb=gb_type == "cgb", bootrom=boot_cgb_rom if gb_type == "cgb" else boot_rom, sound_emulated=True, diff --git a/tests/test_shonumi.py b/tests/test_shonumi.py index 70cc0a36d..6c3225c2d 100644 --- a/tests/test_shonumi.py +++ b/tests/test_shonumi.py @@ -19,7 +19,7 @@ "sprite_suite.gb", ]) def test_shonumi(rom, shonumi_dir): - pyboy = PyBoy(shonumi_dir + rom, window_type="headless", color_palette=(0xFFFFFF, 0x999999, 0x606060, 0x000000)) + pyboy = PyBoy(shonumi_dir + rom, window_type="null", color_palette=(0xFFFFFF, 0x999999, 0x606060, 0x000000)) pyboy.set_emulation_speed(0) # sprite_suite.gb diff --git a/tests/test_states.py b/tests/test_states.py index 7c1e5472b..f5cede064 100644 --- a/tests/test_states.py +++ b/tests/test_states.py @@ -11,7 +11,7 @@ def test_load_save_consistency(tetris_rom): - pyboy = PyBoy(tetris_rom, window_type="headless", game_wrapper=True) + pyboy = PyBoy(tetris_rom, window_type="null", game_wrapper=True) assert pyboy.cartridge_title() == "TETRIS" pyboy.set_emulation_speed(0) pyboy.memory[NEXT_TETROMINO_ADDR] diff --git a/tests/test_tetris_ai.py b/tests/test_tetris_ai.py index 6302d5960..d6f31c87f 100644 --- a/tests/test_tetris_ai.py +++ b/tests/test_tetris_ai.py @@ -50,7 +50,7 @@ def eval_network(epoch, child_index, child_model, record_to): - pyboy = PyBoy('tetris_1.1.gb', game_wrapper=True, window_type="headless") + pyboy = PyBoy('tetris_1.1.gb', game_wrapper=True, window_type="null") pyboy.set_emulation_speed(0) tetris = pyboy.game_wrapper() tetris.start_game() diff --git a/tests/test_which.py b/tests/test_which.py index 1583e7c65..5c6f425d8 100644 --- a/tests/test_which.py +++ b/tests/test_which.py @@ -16,7 +16,7 @@ @pytest.mark.parametrize("cgb", [False, True]) def test_which(cgb, which_file): - pyboy = PyBoy(which_file, window_type="headless", cgb=cgb) + pyboy = PyBoy(which_file, window_type="null", cgb=cgb) pyboy.set_emulation_speed(0) pyboy.tick(59, True) pyboy.tick(25, True) diff --git a/tests/test_whichboot.py b/tests/test_whichboot.py index a507d92fd..40a5000e0 100644 --- a/tests/test_whichboot.py +++ b/tests/test_whichboot.py @@ -16,7 +16,7 @@ @pytest.mark.parametrize("cgb", [False, True]) def test_which(cgb, whichboot_file): - pyboy = PyBoy(whichboot_file, window_type="headless", cgb=cgb) + pyboy = PyBoy(whichboot_file, window_type="null", cgb=cgb) pyboy.set_emulation_speed(0) pyboy.tick(59, True) pyboy.tick(25, True) diff --git a/tests/test_windows.py b/tests/test_windows.py index 0d375c119..ec5f3287a 100644 --- a/tests/test_windows.py +++ b/tests/test_windows.py @@ -6,6 +6,7 @@ import os import pytest + from tests.test_replay import replay from .conftest import BOOTROM_FRAMES_UNTIL_LOGO @@ -13,12 +14,8 @@ replay_file = "tests/replays/default_rom.replay" -def test_headless(default_rom): - replay(default_rom, replay_file, "headless", bootrom_file=None, padding_frames=BOOTROM_FRAMES_UNTIL_LOGO) - - -def test_dummy(default_rom): - replay(default_rom, replay_file, "dummy", bootrom_file=None, verify=False) +def test_null(default_rom): + replay(default_rom, replay_file, "null", bootrom_file=None, padding_frames=BOOTROM_FRAMES_UNTIL_LOGO) @pytest.mark.skipif(os.environ.get("TEST_NO_UI"), reason="Skipping test, as there is no UI") From 73fb59cf61be003f3c78638ad54cb5d89525f036 Mon Sep 17 00:00:00 2001 From: Mads Ynddal <5528170+Baekalfen@users.noreply.github.com> Date: Sat, 9 Dec 2023 22:58:59 +0100 Subject: [PATCH 20/65] Refactor tilemaps --- pyboy/api/tilemap.pxd | 10 +- pyboy/api/tilemap.py | 17 ++- pyboy/plugins/debug.pxd | 3 +- pyboy/plugins/debug.py | 7 +- .../plugins/game_wrapper_kirby_dream_land.py | 1 - .../plugins/game_wrapper_super_mario_land.py | 1 - pyboy/plugins/game_wrapper_tetris.py | 2 - pyboy/pyboy.pxd | 6 +- pyboy/pyboy.py | 30 ----- tests/test_basics.py | 3 + tests/test_external_api.py | 115 +++++++++--------- tests/test_rtc3test.py | 2 +- 12 files changed, 91 insertions(+), 106 deletions(-) diff --git a/pyboy/api/tilemap.pxd b/pyboy/api/tilemap.pxd index 410deaf83..6415b0332 100644 --- a/pyboy/api/tilemap.pxd +++ b/pyboy/api/tilemap.pxd @@ -3,13 +3,19 @@ # GitHub: https://github.com/Baekalfen/PyBoy # +from libc.stdint cimport uint64_t + from pyboy.core.mb cimport Motherboard cdef class TileMap: + cdef object pyboy cdef Motherboard mb cdef bint signed_tile_data cdef bint _use_tile_objects - cdef int map_offset + cdef uint64_t frame_count_update + cpdef int _refresh_lcdc(self) except -1 + cdef int __refresh_lcdc(self) except -1 + cdef readonly int map_offset cdef str _select - cdef public tuple shape + cdef readonly tuple shape diff --git a/pyboy/api/tilemap.py b/pyboy/api/tilemap.py index 7dc814c54..676223602 100644 --- a/pyboy/api/tilemap.py +++ b/pyboy/api/tilemap.py @@ -15,7 +15,7 @@ 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". @@ -44,10 +44,12 @@ def __init__(self, mb, select): 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) """ @@ -59,7 +61,12 @@ def __init__(self, mb, select): 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. @@ -130,7 +137,6 @@ def _tile_address(self, column, row): 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: @@ -175,7 +181,7 @@ def tile_identifier(self, column, row): int: Tile identifier. """ - + self._refresh_lcdc() tile = self.mb.getitem(self._tile_address(column, row)) if self.signed_tile_data: return ((tile ^ 0x80) - 128) + LOW_TILEDATA_NTILES @@ -183,6 +189,7 @@ def tile_identifier(self, column, row): return tile def __repr__(self): + self._refresh_lcdc() adjust = 4 _use_tile_objects = self._use_tile_objects self.use_tile_objects(False) diff --git a/pyboy/plugins/debug.pxd b/pyboy/plugins/debug.pxd index ca55a3461..a719e5e79 100644 --- a/pyboy/plugins/debug.pxd +++ b/pyboy/plugins/debug.pxd @@ -8,6 +8,7 @@ from cpython.array cimport array from libc.stdint cimport uint8_t, uint16_t, uint32_t, uint64_t cimport pyboy.plugins.window_sdl2 +from pyboy.api.tilemap cimport TileMap from pyboy.core.mb cimport Motherboard from pyboy.logging.logging cimport Logger from pyboy.plugins.base_plugin cimport PyBoyWindowPlugin @@ -71,7 +72,7 @@ cdef class BaseDebugWindow(PyBoyWindowPlugin): cdef class TileViewWindow(BaseDebugWindow): cdef int scanline_x cdef int scanline_y - cdef object tilemap + cdef TileMap tilemap cdef uint32_t color cdef uint32_t[:,:] tilecache # Fixing Cython locals diff --git a/pyboy/plugins/debug.py b/pyboy/plugins/debug.py index b675a4381..9dc6de360 100644 --- a/pyboy/plugins/debug.py +++ b/pyboy/plugins/debug.py @@ -10,7 +10,8 @@ from base64 import b64decode from ctypes import c_void_p -from pyboy.api import Sprite, TileMap, constants +from pyboy.api import Sprite, constants +from pyboy.api.tilemap import TileMap from pyboy.plugins.base_plugin import PyBoyWindowPlugin from pyboy.plugins.window_sdl2 import sdl2_event_pump from pyboy.utils import WindowEvent @@ -311,7 +312,7 @@ def __init__(self, *args, window_map, scanline_x, scanline_y, **kwargs): super().__init__(*args, **kwargs) self.scanline_x, self.scanline_y = scanline_x, scanline_y self.color = COLOR_WINDOW if window_map else COLOR_BACKGROUND - self.tilemap = TileMap(self.mb, "WINDOW" if window_map else "BACKGROUND") + self.tilemap = TileMap(self.pyboy, self.mb, "WINDOW" if window_map else "BACKGROUND") def post_tick(self): # Updating screen buffer by copying tiles from cache @@ -357,8 +358,6 @@ def post_tick(self): def handle_events(self, events): global mark_counter, marked_tiles - self.tilemap.refresh_lcdc() - # Feed events into the loop events = BaseDebugWindow.handle_events(self, events) for event in events: diff --git a/pyboy/plugins/game_wrapper_kirby_dream_land.py b/pyboy/plugins/game_wrapper_kirby_dream_land.py index 94c9bff82..e98f2069d 100644 --- a/pyboy/plugins/game_wrapper_kirby_dream_land.py +++ b/pyboy/plugins/game_wrapper_kirby_dream_land.py @@ -81,7 +81,6 @@ def start_game(self, timer_div=None): # Boot screen while True: self.pyboy.tick(1, False) - self.tilemap_background.refresh_lcdc() if self.tilemap_background[0:3, 16] == [231, 224, 235]: # 'HAL' on the first screen break diff --git a/pyboy/plugins/game_wrapper_super_mario_land.py b/pyboy/plugins/game_wrapper_super_mario_land.py index 915ead15e..3fdf9d83a 100644 --- a/pyboy/plugins/game_wrapper_super_mario_land.py +++ b/pyboy/plugins/game_wrapper_super_mario_land.py @@ -237,7 +237,6 @@ def start_game(self, timer_div=None, world_level=None, unlock_level_select=False self.pyboy.memory[ADDR_WIN_COUNT] = 2 if unlock_level_select else 0 break self.pyboy.tick(1, False) - self.tilemap_background.refresh_lcdc() # "MARIO" in the title bar and 0 is placed at score if self.tilemap_background[0:5, 0] == [278, 266, 283, 274, 280] and \ diff --git a/pyboy/plugins/game_wrapper_tetris.py b/pyboy/plugins/game_wrapper_tetris.py index b25d912f6..cd792d9b3 100644 --- a/pyboy/plugins/game_wrapper_tetris.py +++ b/pyboy/plugins/game_wrapper_tetris.py @@ -94,7 +94,6 @@ def post_tick(self): 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) @@ -119,7 +118,6 @@ def start_game(self, timer_div=None): # Boot screen while True: self.pyboy.tick(1, False) - self.tilemap_background.refresh_lcdc() if self.tilemap_background[2:9, 14] == [89, 25, 21, 10, 34, 14, 27]: # '1PLAYER' on the first screen break diff --git a/pyboy/pyboy.pxd b/pyboy/pyboy.pxd index 711019165..9c143a3f8 100644 --- a/pyboy/pyboy.pxd +++ b/pyboy/pyboy.pxd @@ -8,6 +8,7 @@ cimport cython from libc cimport time from libc.stdint cimport int64_t, uint64_t +from pyboy.api.tilemap cimport TileMap from pyboy.core.mb cimport Motherboard from pyboy.logging.logging cimport Logger from pyboy.plugins.manager cimport PluginManager @@ -48,6 +49,9 @@ cdef class PyBoy: cdef public str window_title cdef readonly PyBoyMemoryView memory + cdef readonly TileMap tilemap_background + cdef readonly TileMap tilemap_window + cdef bint limit_emulationspeed cdef int emulationspeed, target_emulationspeed, save_target_emulationspeed cdef bint record_input @@ -80,5 +84,3 @@ cdef class PyBoy: cpdef object get_sprite(self, int) noexcept cpdef list get_sprite_by_tile_identifier(self, list, on_screen=*) noexcept cpdef object get_tile(self, int) noexcept - cpdef object tilemap_background(self) noexcept - cpdef object tilemap_window(self) noexcept diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index cb3a7ae99..d5bf3c6b4 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -761,36 +761,6 @@ def get_tile(self, identifier): """ return Tile(self.mb, identifier=identifier) - @property - 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.api.tilemap.TileMap`: - A TileMap object for the tile map. - """ - return TileMap(self.mb, "BACKGROUND") - - @property - 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.api.tilemap.TileMap`: - A TileMap object for the tile map. - """ - return TileMap(self.mb, "WINDOW") - class PyBoyMemoryView: def __init__(self, mb): diff --git a/tests/test_basics.py b/tests/test_basics.py index a3b16e373..7d7dcb030 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -102,6 +102,9 @@ def test_tilemaps(kirby_rom): bck_tilemap = pyboy.tilemap_background wdw_tilemap = pyboy.tilemap_window + bck_tilemap._refresh_lcdc() + wdw_tilemap._refresh_lcdc() + assert bck_tilemap.map_offset != wdw_tilemap.map_offset assert bck_tilemap[0, 0] == 256 assert bck_tilemap[:5, 0] == [256, 256, 256, 256, 170] diff --git a/tests/test_external_api.py b/tests/test_external_api.py index a7d123476..68c9ca2b8 100644 --- a/tests/test_external_api.py +++ b/tests/test_external_api.py @@ -125,7 +125,7 @@ def test_tetris(tetris_rom): tetris.set_tetromino("T") first_brick = False - tile_map = pyboy.tilemap_window + tile_map = pyboy.tilemap_background state_data = io.BytesIO() for frame in range(5282): # Enough frames to get a "Game Over". Otherwise do: `while pyboy.tick(False):` pyboy.tick(1, False) @@ -162,31 +162,32 @@ def test_tetris(tetris_rom): if not first_brick: # 17 for the bottom tile when zero-indexed # 2 because we skip the border on the left side. Then we take a slice of 10 more tiles - # 303 is the white background tile index - if any(filter(lambda x: x != 303, tile_map[2:12, 17])): + # 47 is the white background tile index + # breakpoint() + if any(filter(lambda x: x != 47, tile_map[2:12, 17])): first_brick = True print(frame) print("First brick touched the bottom!") game_board_matrix = list(tile_map[2:12, :18]) - assert game_board_matrix == ([[303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 133, 133, 133], - [303, 303, 303, 303, 303, 303, 303, 303, 133, 303]]) + assert game_board_matrix == ([[47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 133, 133, 133], + [47, 47, 47, 47, 47, 47, 47, 47, 133, 47]]) tile_map.use_tile_objects(True) @@ -198,24 +199,24 @@ def test_tetris(tetris_rom): game_board_matrix = [[x.tile_identifier for x in row] for row in tile_map[2:12, :18]] tile_map.use_tile_objects(False) - assert game_board_matrix == ([[303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 133, 133, 133], - [303, 303, 303, 303, 303, 303, 303, 303, 133, 303]]) + assert game_board_matrix == ([[47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 133, 133, 133], + [47, 47, 47, 47, 47, 47, 47, 47, 133, 47]]) if frame == 1012: assert not first_brick @@ -298,24 +299,24 @@ def test_tetris(tetris_rom): if frame == 1864: game_board_matrix = list(tile_map[2:12, :18]) - assert game_board_matrix == ([[303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 303, 303, 303, 303, 303, 303, 303], - [303, 303, 303, 133, 133, 133, 303, 133, 133, 133], - [303, 303, 303, 303, 133, 303, 303, 303, 133, 303]]) + assert game_board_matrix == ([[47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [47, 47, 47, 133, 133, 133, 47, 133, 133, 133], + [47, 47, 47, 47, 133, 47, 47, 47, 133, 47]]) pre_load_game_board_matrix = game_board_matrix state_data.seek(0) # Reset to the start of the buffer. Otherwise, we call `load_state` at end of file diff --git a/tests/test_rtc3test.py b/tests/test_rtc3test.py index 817ef1ec4..bca74c3d6 100644 --- a/tests/test_rtc3test.py +++ b/tests/test_rtc3test.py @@ -32,7 +32,7 @@ def test_rtc3test(subtest, rtc3test_file): while True: # Continue until it says "(A) Return" - if pyboy[6:14, 17] == [193, 63, 27, 40, 55, 56, 53, 49]: + if pyboy.tilemap_background[6:14, 17] == [193, 63, 27, 40, 55, 56, 53, 49]: break pyboy.tick(1, True) From f29e31e021e99a5b6162dff4a7294adc759f75eb Mon Sep 17 00:00:00 2001 From: Mads Ynddal <5528170+Baekalfen@users.noreply.github.com> Date: Sat, 9 Dec 2023 20:33:05 +0100 Subject: [PATCH 21/65] pyboy.screen as instance variable instead of property --- pyboy/pyboy.pxd | 2 ++ pyboy/pyboy.py | 18 +++++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/pyboy/pyboy.pxd b/pyboy/pyboy.pxd index 9c143a3f8..4d744fbfd 100644 --- a/pyboy/pyboy.pxd +++ b/pyboy/pyboy.pxd @@ -8,6 +8,7 @@ cimport cython from libc cimport time from libc.stdint cimport int64_t, uint64_t +from pyboy.api.screen cimport Screen from pyboy.api.tilemap cimport TileMap from pyboy.core.mb cimport Motherboard from pyboy.logging.logging cimport Logger @@ -49,6 +50,7 @@ cdef class PyBoy: cdef public str window_title cdef readonly PyBoyMemoryView memory + cdef readonly Screen screen cdef readonly TileMap tilemap_background cdef readonly TileMap tilemap_window diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index d5bf3c6b4..da6338754 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -9,18 +9,14 @@ import os import time -import numpy as np - -from pyboy import utils from pyboy.logging import get_logger from pyboy.api.screen import Screen -from pyboy.api.tilemap import TileMap -from pyboy.logging import get_logger from pyboy.openai_gym import PyBoyGymEnv from pyboy.openai_gym import enabled as gym_enabled from pyboy.plugins.manager import PluginManager from pyboy.utils import IntIOWrapper, WindowEvent +from pyboy.api.tilemap import TileMap from .api import Sprite, Tile, constants from .core.mb import Motherboard @@ -116,7 +112,19 @@ def __init__( ################### # 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. + 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` for more information. + + Returns + ------- + `pyboy.api.screen.Screen`: + A Screen object with helper functions for reading the screen buffer. + """ self.memory = PyBoyMemoryView(self.mb) """ Provides a `pyboy.PyBoyMemoryView` object for reading and writing the memory space of the Game Boy. From 0478d803aeaec1e60646e046b7cfb70198bb34d6 Mon Sep 17 00:00:00 2001 From: Mads Ynddal <5528170+Baekalfen@users.noreply.github.com> Date: Thu, 16 Nov 2023 15:50:12 +0100 Subject: [PATCH 22/65] Convert Screen API to properties instead of functions --- pyboy/api/screen.pxd | 11 +- pyboy/api/screen.py | 122 +++++++++--------- pyboy/core/lcd.pxd | 2 +- pyboy/openai_gym.py | 4 +- pyboy/plugins/base_plugin.py | 2 +- pyboy/plugins/debug.py | 2 +- pyboy/plugins/game_wrapper_pokemon_gen1.py | 2 +- .../plugins/game_wrapper_super_mario_land.py | 2 +- pyboy/plugins/record_replay.py | 2 +- pyboy/plugins/screen_recorder.py | 2 +- pyboy/plugins/screenshot_recorder.py | 2 +- tests/test_acid_cgb.py | 2 +- tests/test_acid_dmg.py | 2 +- tests/test_basics.py | 2 +- tests/test_external_api.py | 58 ++++++--- tests/test_magen.py | 2 +- tests/test_mooneye.py | 2 +- tests/test_replay.py | 4 +- tests/test_rtc3test.py | 2 +- tests/test_samesuite.py | 4 +- tests/test_shonumi.py | 2 +- tests/test_which.py | 2 +- tests/test_whichboot.py | 2 +- 23 files changed, 129 insertions(+), 108 deletions(-) diff --git a/pyboy/api/screen.pxd b/pyboy/api/screen.pxd index 0a6c2d33a..ce0818724 100644 --- a/pyboy/api/screen.pxd +++ b/pyboy/api/screen.pxd @@ -10,4 +10,13 @@ from pyboy.core.mb cimport Motherboard cdef class Screen: cdef Motherboard mb - cpdef np.ndarray[np.uint8_t, ndim=3] screen_ndarray(self) + cpdef ((int, int), (int, int)) get_tilemap_position(self) + # cdef readonly list tilemap_position_list + # cdef readonly uint8_t[:,:] tilemap_position_list + cdef readonly uint32_t[:,:] raw_buffer + cdef readonly (int, int) raw_buffer_dims + cdef readonly str raw_buffer_format + # cdef readonly uint8_t[144][160][4] memoryview + # cpdef np.ndarray[np.uint8_t, ndim=3] get_ndarray(self) + cdef readonly object ndarray + cdef readonly object image diff --git a/pyboy/api/screen.py b/pyboy/api/screen.py index 8d182c229..855f32adb 100644 --- a/pyboy/api/screen.py +++ b/pyboy/api/screen.py @@ -32,57 +32,20 @@ class Screen: def __init__(self, mb): self.mb = mb - def tilemap_position(self): - """ - These coordinates define the offset in the tile map from where the top-left corner of the screen is place. Note - that the tile map defines 256x256 pixels, but the screen can only show 160x144 pixels. When the offset is closer - to the right or bottom edge than 160x144 pixels, the screen will wrap around and render from the opposite site - of the tile map. - - For more details, see "7.4 Viewport" in the [report](https://github.com/Baekalfen/PyBoy/raw/master/extras/PyBoy.pdf), - or the Pan Docs under [LCD Position and Scrolling](http://bgb.bircd.org/pandocs.htm#lcdpositionandscrolling). - - Returns - ------- - tuple: - Returns the tuple of registers ((SCX, SCY), (WX - 7, WY)) - """ - return (self.mb.lcd.getviewport(), self.mb.lcd.getwindowpos()) - - def tilemap_position_list(self): - """ - This function provides the screen (SCX, SCY) and window (WX. WY) position for each horizontal line in the - screen buffer. These parameters are often used for visual effects, and some games will reset the registers at - the end of each call to `pyboy.PyBoy.tick()`. For such games, `Screen.tilemap_position` becomes useless. - - See `Screen.tilemap_position` for more information. - - Returns - ------- - list: - Nested list of SCX, SCY, WX and WY for each scanline (144x4). Returns (0, 0, 0, 0) when LCD is off. - """ - if self.mb.lcd._LCDC.lcd_enable: - return [[line[0], line[1], line[2], line[3]] for line in self.mb.lcd.renderer._scanlineparameters] - else: - return [[0, 0, 0, 0] for line in range(144)] - - def raw_screen_buffer(self): + self.raw_buffer = self.mb.lcd.renderer._screenbuffer """ 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 + `Screen.raw_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`. + Use this, only if you need to bypass the overhead of `Screen.image` or `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): + self.raw_buffer_dims = self.mb.lcd.renderer.buffer_dims """ Returns the dimensions of the raw screen buffer. @@ -91,9 +54,7 @@ def raw_screen_buffer_dims(self): 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): + self.raw_buffer_format = self.mb.lcd.renderer.color_format """ Returns the color format of the raw screen buffer. @@ -102,9 +63,28 @@ def raw_screen_buffer_format(self): str: Color format of the raw screen buffer. E.g. 'RGB'. """ - return self.mb.lcd.renderer.color_format + if not Image: + logger.warning("Cannot generate screen image. Missing dependency \"Pillow\".") + self.image = None + else: + self.image = Image.frombuffer( + self.mb.lcd.renderer.color_format, self.mb.lcd.renderer.buffer_dims[::-1], + self.mb.lcd.renderer._screenbuffer_raw + ) + """ + 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.api` features, [Pan Docs](http://bgb.bircd.org/pandocs.htm) on tiles/sprites, + and join our Discord channel for more help. - def screen_ndarray(self): + Returns + ------- + PIL.Image: + RGB image of (160, 144) pixels + """ + self.ndarray = np.frombuffer(self.mb.lcd.renderer._screenbuffer_raw, dtype=np.uint8).reshape(ROWS, COLS, + 4)[:, :, 1:] """ Provides the screen data in NumPy format. The dataformat is always RGB. @@ -113,27 +93,43 @@ def screen_ndarray(self): 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): + @property + def tilemap_position_list(self): """ - Generates a PIL Image from the screen buffer. + 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. - 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](http://bgb.bircd.org/pandocs.htm) on tiles/sprites, - and join our Discord channel for more help. + See `Screen.tilemap_position` for more information. Returns ------- - PIL.Image: - RGB image of (160, 144) pixels + list: + Nested list of SCX, SCY, WX and WY for each scanline (144x4). Returns (0, 0, 0, 0) when LCD is off. """ - 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") + # self.tilemap_position_list = np.asarray(self.mb.lcd.renderer._scanlineparameters, dtype=np.uint8).reshape(144, 5)[:, :4] + # self.tilemap_position_list = self.mb.lcd.renderer._scanlineparameters + + # # return self.mb.lcd.renderer._scanlineparameters + if self.mb.lcd._LCDC.lcd_enable: + return [[line[0], line[1], line[2], line[3]] for line in self.mb.lcd.renderer._scanlineparameters] + else: + return [[0, 0, 0, 0] for line in range(144)] + + def get_tilemap_position(self): + """ + These coordinates define the offset in the tile map from where the top-left corner of the screen is place. Note + that the tile map defines 256x256 pixels, but the screen can only show 160x144 pixels. When the offset is closer + to the right or bottom edge than 160x144 pixels, the screen will wrap around and render from the opposite site + of the tile map. + + For more details, see "7.4 Viewport" in the [report](https://github.com/Baekalfen/PyBoy/raw/master/extras/PyBoy.pdf), + or the Pan Docs under [LCD Position and Scrolling](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()) diff --git a/pyboy/core/lcd.pxd b/pyboy/core/lcd.pxd index 22fbe9550..b857cb1bc 100644 --- a/pyboy/core/lcd.pxd +++ b/pyboy/core/lcd.pxd @@ -125,7 +125,7 @@ cdef class Renderer: cdef int ly_window cdef void invalidate_tile(self, int, int) noexcept nogil - cdef int[144][5] _scanlineparameters + cdef uint8_t[144][5] _scanlineparameters cdef void blank_screen(self, LCD) noexcept nogil diff --git a/pyboy/openai_gym.py b/pyboy/openai_gym.py index 9351030de..84ba6720a 100644 --- a/pyboy/openai_gym.py +++ b/pyboy/openai_gym.py @@ -91,7 +91,7 @@ def __init__(self, pyboy, observation_type="tiles", action_type="toggle", simult # Building the observation_space if observation_type == "raw": - screen = np.asarray(self.pyboy.screen.screen_ndarray()) + screen = np.asarray(self.pyboy.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 @@ -120,7 +120,7 @@ def __init__(self, pyboy, observation_type="tiles", action_type="toggle", simult def _get_observation(self): if self.observation_type == "raw": - observation = np.asarray(self.pyboy.screen.screen_ndarray(), dtype=np.uint8) + observation = np.asarray(self.pyboy.screen.ndarray, dtype=np.uint8) elif self.observation_type in ["tiles", "compressed", "minimal"]: observation = self.game_wrapper._game_area_np(self.observation_type) else: diff --git a/pyboy/plugins/base_plugin.py b/pyboy/plugins/base_plugin.py index be1d1907a..f495ffc94 100644 --- a/pyboy/plugins/base_plugin.py +++ b/pyboy/plugins/base_plugin.py @@ -183,7 +183,7 @@ def _game_area_tiles(self): yy = self.game_area_section[1] width = self.game_area_section[2] height = self.game_area_section[3] - scanline_parameters = self.pyboy.screen.tilemap_position_list() + scanline_parameters = self.pyboy.screen.tilemap_position_list if self.game_area_wrap_around: self._cached_game_area_tiles = np.ndarray(shape=(height, width), dtype=np.uint32) diff --git a/pyboy/plugins/debug.py b/pyboy/plugins/debug.py index 9dc6de360..dbd378be2 100644 --- a/pyboy/plugins/debug.py +++ b/pyboy/plugins/debug.py @@ -392,7 +392,7 @@ def update_title(self): def draw_overlay(self): global marked_tiles - scanlineparameters = self.pyboy.screen.tilemap_position_list() + scanlineparameters = self.pyboy.screen.tilemap_position_list background_view = self.scanline_x == 0 diff --git a/pyboy/plugins/game_wrapper_pokemon_gen1.py b/pyboy/plugins/game_wrapper_pokemon_gen1.py index f1b58f46b..d64f735d5 100644 --- a/pyboy/plugins/game_wrapper_pokemon_gen1.py +++ b/pyboy/plugins/game_wrapper_pokemon_gen1.py @@ -39,7 +39,7 @@ def post_tick(self): self._tile_cache_invalid = True self._sprite_cache_invalid = True - scanline_parameters = self.pyboy.screen.tilemap_position_list() + scanline_parameters = self.pyboy.screen.tilemap_position_list WX = scanline_parameters[0][2] WY = scanline_parameters[0][3] self.use_background(WY != 0) diff --git a/pyboy/plugins/game_wrapper_super_mario_land.py b/pyboy/plugins/game_wrapper_super_mario_land.py index 3fdf9d83a..14398a737 100644 --- a/pyboy/plugins/game_wrapper_super_mario_land.py +++ b/pyboy/plugins/game_wrapper_super_mario_land.py @@ -149,7 +149,7 @@ def post_tick(self): level_block = self.pyboy.memory[0xC0AB] mario_x = self.pyboy.memory[0xC202] - scx = self.pyboy.screen.tilemap_position_list()[16][0] + 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: diff --git a/pyboy/plugins/record_replay.py b/pyboy/plugins/record_replay.py index 7b06a7d6c..43eb192f7 100644 --- a/pyboy/plugins/record_replay.py +++ b/pyboy/plugins/record_replay.py @@ -40,7 +40,7 @@ def handle_events(self, events): if len(events) != 0: self.recorded_input.append(( self.pyboy.frame_count, [e.event for e in events], - base64.b64encode(np.ascontiguousarray(self.pyboy.screen.screen_ndarray())).decode("utf8") + base64.b64encode(np.ascontiguousarray(self.pyboy.screen.ndarray)).decode("utf8") )) return events diff --git a/pyboy/plugins/screen_recorder.py b/pyboy/plugins/screen_recorder.py index 7103f9c8c..8f38b5a14 100644 --- a/pyboy/plugins/screen_recorder.py +++ b/pyboy/plugins/screen_recorder.py @@ -42,7 +42,7 @@ def handle_events(self, events): def post_tick(self): # Plugin: Screen Recorder if self.recording: - self.add_frame(self.pyboy.screen.screen_image()) + self.add_frame(self.pyboy.screen.image.convert(mode="RGBA")) def add_frame(self, frame): # Pillow makes artifacts in the output, if we use 'RGB', which is PyBoy's default format diff --git a/pyboy/plugins/screenshot_recorder.py b/pyboy/plugins/screenshot_recorder.py index 5c798b580..b72cd4517 100644 --- a/pyboy/plugins/screenshot_recorder.py +++ b/pyboy/plugins/screenshot_recorder.py @@ -37,7 +37,7 @@ def save(self, path=None): os.makedirs(directory, mode=0o755) path = os.path.join(directory, time.strftime(f"{self.pyboy.cartridge_title()}-%Y.%m.%d-%H.%M.%S.png")) - self.pyboy.screen.screen_image().save(path) + self.pyboy.screen.image.save(path) logger.info("Screenshot saved in {}".format(path)) diff --git a/tests/test_acid_cgb.py b/tests/test_acid_cgb.py index 641280987..7c57630a6 100644 --- a/tests/test_acid_cgb.py +++ b/tests/test_acid_cgb.py @@ -21,7 +21,7 @@ def test_cgb_acid(cgb_acid_file): pyboy.tick(25, True) png_path = Path(f"tests/test_results/{os.path.basename(cgb_acid_file)}.png") - image = pyboy.screen.screen_image() + image = pyboy.screen.image if OVERWRITE_PNGS: png_path.parents[0].mkdir(parents=True, exist_ok=True) image.save(png_path) diff --git a/tests/test_acid_dmg.py b/tests/test_acid_dmg.py index 8588ed337..e2c53eecb 100644 --- a/tests/test_acid_dmg.py +++ b/tests/test_acid_dmg.py @@ -22,7 +22,7 @@ def test_dmg_acid(cgb, dmg_acid_file): pyboy.tick(25, True) png_path = Path(f"tests/test_results/{'cgb' if cgb else 'dmg'}_{os.path.basename(dmg_acid_file)}.png") - image = pyboy.screen.screen_image() + image = pyboy.screen.image if OVERWRITE_PNGS: png_path.parents[0].mkdir(parents=True, exist_ok=True) image.save(png_path) diff --git a/tests/test_basics.py b/tests/test_basics.py index 7d7dcb030..2cab163a4 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -204,7 +204,7 @@ def test_all_modes(cgb, _bootrom, frames, rom, any_rom_cgb, boot_cgb_rom): rom_name = "cgbrom" if rom == any_rom_cgb else "dmgrom" png_path = Path(f"tests/test_results/all_modes/{rom_name}_{cgb}_{os.path.basename(str(_bootrom))}.png") - image = pyboy.screen.screen_image() + image = pyboy.screen.image if OVERWRITE_PNGS: png_path.parents[0].mkdir(parents=True, exist_ok=True) png_buf = BytesIO() diff --git a/tests/test_external_api.py b/tests/test_external_api.py index 68c9ca2b8..6c7ee2a68 100644 --- a/tests/test_external_api.py +++ b/tests/test_external_api.py @@ -72,47 +72,63 @@ def test_screen_buffer_and_image(tetris_rom, boot_rom): pyboy.set_emulation_speed(0) pyboy.tick(275, True) # Iterate to boot logo - assert pyboy.screen.raw_screen_buffer_dims() == (144, 160) - assert pyboy.screen.raw_screen_buffer_format() == cformat + assert pyboy.screen.raw_buffer_dims == (144, 160) + assert pyboy.screen.raw_buffer_format == cformat boot_logo_hash = hashlib.sha256() - boot_logo_hash.update(pyboy.screen.raw_screen_buffer()) - assert boot_logo_hash.digest() == boot_logo_hash_predigested - assert isinstance(pyboy.screen.raw_screen_buffer(), bytes) - # The output of `screen_image` is supposed to be homogeneous, which means a shared hash between versions. + if hasattr(pyboy.screen.raw_buffer, "tobytes"): + boot_logo_hash.update(pyboy.screen.raw_buffer.tobytes()) # PyPy + else: + boot_logo_hash.update(pyboy.screen.raw_buffer.base) # Cython + # assert boot_logo_hash.digest() == boot_logo_hash_predigested + # assert isinstance(pyboy.screen.raw_buffer, bytes) + + # The output of `image` is supposed to be homogeneous, which means a shared hash between versions. boot_logo_png_hash_predigested = ( b"\x1b\xab\x90r^\xfb\x0e\xef\xf1\xdb\xf8\xba\xb6:^\x01" b"\xa4\x0eR&\xda9\xfcg\xf7\x0f|\xba}\x08\xb6$" ) boot_logo_png_hash = hashlib.sha256() - image = pyboy.screen.screen_image() + image = pyboy.screen.image assert isinstance(image, PIL.Image.Image) image_data = io.BytesIO() - image.save(image_data, format="BMP") + image.convert(mode="RGB").save(image_data, format="BMP") boot_logo_png_hash.update(image_data.getvalue()) assert boot_logo_png_hash.digest() == boot_logo_png_hash_predigested # screen_ndarray numpy_hash = hashlib.sha256() - numpy_array = np.ascontiguousarray(pyboy.screen.screen_ndarray()) - assert isinstance(pyboy.screen.screen_ndarray(), np.ndarray) + numpy_array = np.ascontiguousarray(pyboy.screen.ndarray) + assert isinstance(pyboy.screen.ndarray, np.ndarray) assert numpy_array.shape == (144, 160, 3) numpy_hash.update(numpy_array.tobytes()) - assert numpy_hash.digest( - ) == (b"\r\t\x87\x131\xe8\x06\x82\xcaO=\n\x1e\xa2K$" - b"\xd6\x8e\x91R( H7\xd8a*B+\xc7\x1f\x19") + # assert numpy_hash.digest( + # ) == (b"\r\t\x87\x131\xe8\x06\x82\xcaO=\n\x1e\xa2K$" + # b"\xd6\x8e\x91R( H7\xd8a*B+\xc7\x1f\x19") # Check PIL image is reference for performance pyboy.tick(1, True) - new_image1 = pyboy.screen.screen_image() - nd_image = pyboy.screen.screen_ndarray() + new_image1 = pyboy.screen.image + _new_image1 = new_image1.copy() + diff = ImageChops.difference(new_image1, _new_image1) + assert not diff.getbbox() + + nd_image = pyboy.screen.ndarray nd_image[:, :] = 0 + diff = ImageChops.difference(new_image1, _new_image1) + assert diff.getbbox() + pyboy.tick(1, True) - new_image2 = pyboy.screen.screen_image() + new_image2 = pyboy.screen.image diff = ImageChops.difference(new_image1, new_image2) assert not diff.getbbox() + new_image3 = new_image1.copy() + nd_image[:, :] = 0xFF + diff = ImageChops.difference(new_image1, new_image3) + assert diff.getbbox() + pyboy.stop(save=False) @@ -128,9 +144,9 @@ def test_tetris(tetris_rom): tile_map = pyboy.tilemap_background state_data = io.BytesIO() for frame in range(5282): # Enough frames to get a "Game Over". Otherwise do: `while pyboy.tick(False):` - pyboy.tick(1, False) + image = pyboy.tick(1, True) - assert pyboy.screen.tilemap_position() == ((0, 0), (-7, 0)) + assert pyboy.screen.get_tilemap_position() == ((0, 0), (-7, 0)) # Start game. Just press Start and A when the game allows us. # The frames are not 100% accurate. @@ -238,7 +254,7 @@ def test_tetris(tetris_rom): assert all_sprites == all_sprites2 # Verify data with known reference - # pyboy.screen.screen_image().show() + # pyboy.screen.image.show() assert all_sprites == ([ (-8, -16, 0, False), (-8, -16, 0, False), @@ -348,7 +364,7 @@ def test_tilemap_position_list(supermarioland_rom): pyboy.tick(100, False) # Get screen positions, and verify the values - positions = pyboy.screen.tilemap_position_list() + positions = pyboy.screen.tilemap_position_list for y in range(1, 16): assert positions[y][0] == 0 # HUD for y in range(16, 144): @@ -359,7 +375,7 @@ def test_tilemap_position_list(supermarioland_rom): pyboy.tick(10, False) # Get screen positions, and verify the values - positions = pyboy.screen.tilemap_position_list() + positions = pyboy.screen.tilemap_position_list for y in range(1, 16): assert positions[y][0] == 0 # HUD for y in range(16, 144): diff --git a/tests/test_magen.py b/tests/test_magen.py index c87b54776..51997492a 100644 --- a/tests/test_magen.py +++ b/tests/test_magen.py @@ -21,7 +21,7 @@ def test_magen_test(magen_test_file): pyboy.tick(25, True) png_path = Path(f"tests/test_results/{os.path.basename(magen_test_file)}.png") - image = pyboy.screen.screen_image() + image = pyboy.screen.image if OVERWRITE_PNGS: png_path.parents[0].mkdir(parents=True, exist_ok=True) image.save(png_path) diff --git a/tests/test_mooneye.py b/tests/test_mooneye.py index 875eab86a..ee388ddc4 100644 --- a/tests/test_mooneye.py +++ b/tests/test_mooneye.py @@ -170,7 +170,7 @@ def test_mooneye(clean, rom, mooneye_dir, default_rom): pyboy.tick(180 if "div_write" in rom else 40, True) png_path = Path(f"tests/test_results/mooneye/{rom}.png") - image = pyboy.screen.screen_image() + image = pyboy.screen.image if OVERWRITE_PNGS: png_path.parents[0].mkdir(parents=True, exist_ok=True) image.save(png_path) diff --git a/tests/test_replay.py b/tests/test_replay.py index 9f72e3fed..354397b97 100644 --- a/tests/test_replay.py +++ b/tests/test_replay.py @@ -29,12 +29,12 @@ def verify_screen_image_np(pyboy, saved_array): - match = np.all(np.frombuffer(saved_array, dtype=np.uint8).reshape(144, 160, 3) == pyboy.screen.screen_ndarray()) + match = np.all(np.frombuffer(saved_array, dtype=np.uint8).reshape(144, 160, 3) == pyboy.screen.ndarray) if not match and not os.environ.get("TEST_CI"): from PIL import Image original = Image.frombytes("RGB", (160, 144), np.frombuffer(saved_array, dtype=np.uint8).reshape(144, 160, 3)) original.show() - new = pyboy.screen.screen_image() + new = pyboy.screen.image new.show() import PIL.ImageChops PIL.ImageChops.difference(original, new).show() diff --git a/tests/test_rtc3test.py b/tests/test_rtc3test.py index bca74c3d6..6f80144d7 100644 --- a/tests/test_rtc3test.py +++ b/tests/test_rtc3test.py @@ -37,7 +37,7 @@ def test_rtc3test(subtest, rtc3test_file): pyboy.tick(1, True) png_path = Path(f"tests/test_results/{rtc3test_file}_{subtest}.png") - image = pyboy.screen.screen_image() + image = pyboy.screen.image if OVERWRITE_PNGS: png_path.parents[0].mkdir(parents=True, exist_ok=True) image.save(png_path) diff --git a/tests/test_samesuite.py b/tests/test_samesuite.py index 4d96309b4..25c0a58f7 100644 --- a/tests/test_samesuite.py +++ b/tests/test_samesuite.py @@ -140,13 +140,13 @@ def test_samesuite(clean, gb_type, rom, samesuite_dir, boot_cgb_rom, boot_rom, d pyboy.load_state(saved_state) for _ in range(10): - if np.all(pyboy.screen.screen_ndarray() > 240): + if np.all(pyboy.screen.ndarray > 240): pyboy.tick(20, True) else: break png_path = Path(f"tests/test_results/SameSuite/{rom}.png") - image = pyboy.screen.screen_image() + image = pyboy.screen.image if OVERWRITE_PNGS: png_path.parents[0].mkdir(parents=True, exist_ok=True) image.save(png_path) diff --git a/tests/test_shonumi.py b/tests/test_shonumi.py index 6c3225c2d..db77f11c8 100644 --- a/tests/test_shonumi.py +++ b/tests/test_shonumi.py @@ -30,7 +30,7 @@ def test_shonumi(rom, shonumi_dir): png_path = Path(f"tests/test_results/GB Tests/{rom}.png") png_path.parents[0].mkdir(parents=True, exist_ok=True) - image = pyboy.screen.screen_image() + image = pyboy.screen.image old_image = PIL.Image.open(png_path) old_image = old_image.resize(image.size, resample=PIL.Image.Dither.NONE) diff --git a/tests/test_which.py b/tests/test_which.py index 5c6f425d8..3c0cc78a8 100644 --- a/tests/test_which.py +++ b/tests/test_which.py @@ -22,7 +22,7 @@ def test_which(cgb, which_file): pyboy.tick(25, True) png_path = Path(f"tests/test_results/{'cgb' if cgb else 'dmg'}_{os.path.basename(which_file)}.png") - image = pyboy.screen.screen_image() + image = pyboy.screen.image if OVERWRITE_PNGS: png_path.parents[0].mkdir(parents=True, exist_ok=True) image.save(png_path) diff --git a/tests/test_whichboot.py b/tests/test_whichboot.py index 40a5000e0..1a174fc8f 100644 --- a/tests/test_whichboot.py +++ b/tests/test_whichboot.py @@ -22,7 +22,7 @@ def test_which(cgb, whichboot_file): pyboy.tick(25, True) png_path = Path(f"tests/test_results/{'cgb' if cgb else 'dmg'}_{os.path.basename(whichboot_file)}.png") - image = pyboy.screen.screen_image() + image = pyboy.screen.image if OVERWRITE_PNGS: png_path.parents[0].mkdir(parents=True, exist_ok=True) image.save(png_path) From 242edaf8c11e34589fc257202170596837b8cd58 Mon Sep 17 00:00:00 2001 From: Mads Ynddal <5528170+Baekalfen@users.noreply.github.com> Date: Tue, 14 Nov 2023 22:25:51 +0100 Subject: [PATCH 23/65] Convert color scheme to RGBX for Pillow, BGRA for SDL2, RGBA for OpenGL --- pyboy/api/screen.pxd | 1 + pyboy/api/screen.py | 28 +++++++------ pyboy/api/sprite.py | 2 +- pyboy/api/tile.pxd | 5 ++- pyboy/api/tile.py | 55 ++++++++++++++----------- pyboy/core/lcd.pxd | 13 +++--- pyboy/core/lcd.py | 35 ++++++++++------ pyboy/plugins/debug.py | 4 +- pyboy/plugins/record_replay.py | 2 +- pyboy/plugins/window_open_gl.py | 4 +- pyboy/plugins/window_sdl2.py | 2 +- tests/test_acid_cgb.py | 2 +- tests/test_acid_dmg.py | 2 +- tests/test_basics.py | 4 +- tests/test_external_api.py | 72 ++++++++++++++++++++++++++------- tests/test_magen.py | 2 +- tests/test_mooneye.py | 6 ++- tests/test_openai_gym.py | 4 +- tests/test_rtc3test.py | 2 +- tests/test_samesuite.py | 4 +- tests/test_shonumi.py | 2 +- tests/test_which.py | 2 +- tests/test_whichboot.py | 2 +- 23 files changed, 163 insertions(+), 92 deletions(-) diff --git a/pyboy/api/screen.pxd b/pyboy/api/screen.pxd index ce0818724..5e95ad9d9 100644 --- a/pyboy/api/screen.pxd +++ b/pyboy/api/screen.pxd @@ -4,6 +4,7 @@ # cimport cython cimport numpy as np +from libc.stdint cimport uint8_t, uint32_t from pyboy.core.mb cimport Motherboard diff --git a/pyboy/api/screen.py b/pyboy/api/screen.py index 855f32adb..5607be0b2 100644 --- a/pyboy/api/screen.py +++ b/pyboy/api/screen.py @@ -61,16 +61,9 @@ def __init__(self, mb): Returns ------- str: - Color format of the raw screen buffer. E.g. 'RGB'. + Color format of the raw screen buffer. E.g. 'RGBX'. """ - if not Image: - logger.warning("Cannot generate screen image. Missing dependency \"Pillow\".") - self.image = None - 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.image = None """ Generates a PIL Image from the screen buffer. @@ -83,10 +76,21 @@ def __init__(self, mb): PIL.Image: RGB image of (160, 144) pixels """ - self.ndarray = np.frombuffer(self.mb.lcd.renderer._screenbuffer_raw, dtype=np.uint8).reshape(ROWS, COLS, - 4)[:, :, 1:] + if not Image: + logger.warning("Cannot generate screen image. Missing dependency \"Pillow\".") + + 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) """ - Provides the screen data in NumPy format. The dataformat is always RGB. + Provides the screen data in NumPy format. The format is given by `pyboy.api.screen.Screen.raw_buffer_format`. Returns ------- diff --git a/pyboy/api/sprite.py b/pyboy/api/sprite.py index aca875f71..1d6f9cdc4 100644 --- a/pyboy/api/sprite.py +++ b/pyboy/api/sprite.py @@ -128,7 +128,7 @@ def __init__(self, mb, sprite_index): """ 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 diff --git a/pyboy/api/tile.pxd b/pyboy/api/tile.pxd index 47e668127..ddd413e9e 100644 --- a/pyboy/api/tile.pxd +++ b/pyboy/api/tile.pxd @@ -16,6 +16,7 @@ cdef uint16_t VRAM_OFFSET, LOW_TILEDATA cdef class Tile: cdef Motherboard mb + cdef public str raw_buffer_format cdef public int tile_identifier cdef public int data_address cdef public tuple shape @@ -23,5 +24,5 @@ cdef class Tile: cpdef object image_ndarray(self) noexcept cdef uint32_t[:,:] data # TODO: Add to locals instead - @cython.locals(byte1=uint8_t, byte2=uint8_t, old_A_format=uint32_t, colorcode=uint32_t) - cpdef uint32_t[:,:] image_data(self) noexcept + @cython.locals(byte1=uint8_t, byte2=uint8_t, colorcode=uint32_t) + cdef uint32_t[:,:] _image_data(self) noexcept diff --git a/pyboy/api/tile.py b/pyboy/api/tile.py index 187b81914..3faae41f7 100644 --- a/pyboy/api/tile.py +++ b/pyboy/api/tile.py @@ -21,6 +21,12 @@ except ImportError: Image = None +try: + from cython import compiled + cythonmode = compiled +except ImportError: + cythonmode = False + class Tile: def __init__(self, mb, identifier): @@ -75,9 +81,19 @@ def __init__(self, mb, identifier): 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. 'RGBX'. + """ + 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`. @@ -89,13 +105,17 @@ def image(self): 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="RGBX") + else: + return Image.frombytes("RGBX", (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`. @@ -104,21 +124,9 @@ def image_ndarray(self): 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.PyBoy.tick`. - - Returns - ------- - memoryview : - Image data of tile in 8x8 pixels and RGBA colors. - """ - return self._image_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) def _image_data(self): """ @@ -130,7 +138,7 @@ def _image_data(self): 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 @@ -139,9 +147,8 @@ def _image_data(self): 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): diff --git a/pyboy/core/lcd.pxd b/pyboy/core/lcd.pxd index b857cb1bc..07068733d 100644 --- a/pyboy/core/lcd.pxd +++ b/pyboy/core/lcd.pxd @@ -10,19 +10,21 @@ from libc.stdint cimport int16_t, int64_t, uint8_t, uint16_t, uint32_t, uint64_t cimport pyboy.utils from pyboy cimport utils +from pyboy.logging.logging cimport Logger from pyboy.utils cimport IntIOInterface cdef uint8_t INTR_VBLANK, INTR_LCDC, INTR_TIMER, INTR_SERIAL, INTR_HIGHTOLOW cdef uint16_t LCDC, STAT, SCY, SCX, LY, LYC, DMA, BGP, OBP0, OBP1, WY, WX cdef int ROWS, COLS, TILES, FRAME_CYCLES, VIDEO_RAM, OBJECT_ATTRIBUTE_MEMORY -cdef uint32_t COL0_FLAG -cdef uint8_t BG_PRIORITY_FLAG, CGB_NUM_PALETTES -from pyboy.logging.logging cimport Logger - +cdef uint32_t COL0_FLAG, BG_PRIORITY_FLAG +cdef uint8_t CGB_NUM_PALETTES cdef Logger logger +@cython.locals(a=uint32_t, r=uint32_t, g=uint32_t, b=uint32_t) +cpdef uint32_t rgb_to_bgr(uint32_t) noexcept + cdef class LCD: cdef bint disable_renderer cdef uint8_t[8 * 1024] VRAM0 @@ -106,6 +108,7 @@ cdef class LCDCRegister: cdef public bint background_enable cdef public bint cgb_master_priority + cpdef int _get_sprite_height(self) cdef class Renderer: cdef uint8_t[:] _tilecache0_state, _tilecache1_state, _spritecache0_state, _spritecache1_state @@ -146,7 +149,7 @@ cdef class Renderer: xx=int, yy=int, tilecache=uint32_t[:,:], - bg_priority_apply=uint8_t, + bg_priority_apply=uint32_t, ) cdef void scanline(self, LCD, int) noexcept nogil diff --git a/pyboy/core/lcd.py b/pyboy/core/lcd.py index 26ce3492a..d97b16169 100644 --- a/pyboy/core/lcd.py +++ b/pyboy/core/lcd.py @@ -22,6 +22,13 @@ FRAME_CYCLES = 70224 +def rgb_to_bgr(color): + r = (color >> 16) & 0xFF + g = (color >> 8) & 0xFF + b = color & 0xFF + return (b << 16) | (g << 8) | r + + class LCD: def __init__(self, cgb, cartridge_cgb, color_palette, cgb_color_palette, randomize=False): self.VRAM0 = array("B", [0] * VIDEO_RAM) @@ -61,15 +68,15 @@ def __init__(self, cgb, cartridge_cgb, color_palette, cgb_color_palette, randomi logger.debug("Starting CGB renderer in DMG-mode") # Running DMG ROM on CGB hardware use the default palettes bg_pal, obj0_pal, obj1_pal = cgb_color_palette - self.BGP.palette_mem_rgb = [(c << 8) for c in bg_pal] - self.OBP0.palette_mem_rgb = [(c << 8) for c in obj0_pal] - self.OBP1.palette_mem_rgb = [(c << 8) for c in obj1_pal] + self.BGP.palette_mem_rgb = [(rgb_to_bgr(c)) for c in bg_pal] + self.OBP0.palette_mem_rgb = [(rgb_to_bgr(c)) for c in obj0_pal] + self.OBP1.palette_mem_rgb = [(rgb_to_bgr(c)) for c in obj1_pal] self.renderer = Renderer(False) else: logger.debug("Starting DMG renderer") - self.BGP.palette_mem_rgb = [(c << 8) for c in color_palette] - self.OBP0.palette_mem_rgb = [(c << 8) for c in color_palette] - self.OBP1.palette_mem_rgb = [(c << 8) for c in color_palette] + self.BGP.palette_mem_rgb = [(rgb_to_bgr(c)) for c in color_palette] + self.OBP0.palette_mem_rgb = [(rgb_to_bgr(c)) for c in color_palette] + self.OBP1.palette_mem_rgb = [(rgb_to_bgr(c)) for c in color_palette] self.renderer = Renderer(False) def get_lcdc(self): @@ -360,15 +367,18 @@ def set(self, value): self.cgb_master_priority = self.background_enable # Different meaning on CGB # yapf: enable + def _get_sprite_height(self): + return self.sprite_height + -COL0_FLAG = 0b01 -BG_PRIORITY_FLAG = 0b10 +COL0_FLAG = 0b01 << 24 +BG_PRIORITY_FLAG = 0b10 << 24 class Renderer: def __init__(self, cgb): self.cgb = cgb - self.color_format = "RGBA" + self.color_format = "RGBX" self.buffer_dims = (ROWS, COLS) @@ -444,7 +454,7 @@ def scanline(self, lcd, y): # add 256 for offset (reduces to + 128) wt = (wt ^ 0x80) + 128 - bg_priority_apply = 0x00 + bg_priority_apply = 0 if self.cgb: palette, vbank, horiflip, vertflip, bg_priority = self._cgb_get_background_map_attributes( lcd, tile_addr @@ -480,7 +490,7 @@ def scanline(self, lcd, y): # add 256 for offset (reduces to + 128) bt = (bt ^ 0x80) + 128 - bg_priority_apply = 0x00 + bg_priority_apply = 0 if self.cgb: palette, vbank, horiflip, vertflip, bg_priority = self._cgb_get_background_map_attributes( lcd, tile_addr @@ -964,7 +974,8 @@ def cgb_to_rgb(self, cgb_color, index): red = (cgb_color & 0x1F) << 3 green = ((cgb_color >> 5) & 0x1F) << 3 blue = ((cgb_color >> 10) & 0x1F) << 3 - rgb_color = ((red << 16) | (green << 8) | blue) << 8 + # NOTE: Actually BGR, not RGB + rgb_color = ((blue << 16) | (green << 8) | red) if index % 4 == 0: rgb_color |= COL0_FLAG return rgb_color diff --git a/pyboy/plugins/debug.py b/pyboy/plugins/debug.py index dbd378be2..a428e2d77 100644 --- a/pyboy/plugins/debug.py +++ b/pyboy/plugins/debug.py @@ -35,7 +35,7 @@ COLOR = 0x00000000 # MASK = 0x00C0C000 COLOR_BACKGROUND = 0x00C0C000 -COLOR_WINDOW = 0xC179D400 +COLOR_WINDOW = 0x00D479C1 # Additive colors HOVER = 0xFF0000 @@ -246,7 +246,7 @@ def __init__(self, pyboy, mb, pyboy_argv, *, scale, title, width, height, pos_x, self._sdlrenderer = sdl2.SDL_CreateRenderer(self._window, -1, sdl2.SDL_RENDERER_ACCELERATED) sdl2.SDL_RenderSetLogicalSize(self._sdlrenderer, width, height) self._sdltexturebuffer = sdl2.SDL_CreateTexture( - self._sdlrenderer, sdl2.SDL_PIXELFORMAT_RGBA8888, sdl2.SDL_TEXTUREACCESS_STATIC, width, height + self._sdlrenderer, sdl2.SDL_PIXELFORMAT_ABGR8888, sdl2.SDL_TEXTUREACCESS_STATIC, width, height ) def handle_events(self, events): diff --git a/pyboy/plugins/record_replay.py b/pyboy/plugins/record_replay.py index 43eb192f7..6cd525d89 100644 --- a/pyboy/plugins/record_replay.py +++ b/pyboy/plugins/record_replay.py @@ -40,7 +40,7 @@ def handle_events(self, events): if len(events) != 0: self.recorded_input.append(( self.pyboy.frame_count, [e.event for e in events], - base64.b64encode(np.ascontiguousarray(self.pyboy.screen.ndarray)).decode("utf8") + base64.b64encode(np.ascontiguousarray(self.pyboy.screen.ndarray[:, :, :-1])).decode("utf8") )) return events diff --git a/pyboy/plugins/window_open_gl.py b/pyboy/plugins/window_open_gl.py index 77b0745a9..63a2e1324 100644 --- a/pyboy/plugins/window_open_gl.py +++ b/pyboy/plugins/window_open_gl.py @@ -15,7 +15,7 @@ try: import OpenGL.GLUT.freeglut - from OpenGL.GL import (GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, glClear, + from OpenGL.GL import (GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV, glClear, glDrawPixels, glFlush, glPixelZoom) from OpenGL.GLUT import (GLUT_KEY_DOWN, GLUT_KEY_LEFT, GLUT_KEY_RIGHT, GLUT_KEY_UP, GLUT_RGBA, GLUT_SINGLE, glutCreateWindow, glutDestroyWindow, glutDisplayFunc, glutGetWindow, glutInit, @@ -135,7 +135,7 @@ def _glreshape(self, width, height): def _gldraw(self): glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) buf = np.asarray(self.renderer._screenbuffer)[::-1, :] - glDrawPixels(COLS, ROWS, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, buf) + glDrawPixels(COLS, ROWS, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV, buf) glFlush() def frame_limiter(self, speed): diff --git a/pyboy/plugins/window_sdl2.py b/pyboy/plugins/window_sdl2.py index 84dedaa3c..2e7b950f8 100644 --- a/pyboy/plugins/window_sdl2.py +++ b/pyboy/plugins/window_sdl2.py @@ -169,7 +169,7 @@ def __init__(self, pyboy, mb, pyboy_argv): self._sdlrenderer = sdl2.SDL_CreateRenderer(self._window, -1, sdl2.SDL_RENDERER_ACCELERATED) sdl2.SDL_RenderSetLogicalSize(self._sdlrenderer, COLS, ROWS) self._sdltexturebuffer = sdl2.SDL_CreateTexture( - self._sdlrenderer, sdl2.SDL_PIXELFORMAT_RGBA8888, sdl2.SDL_TEXTUREACCESS_STATIC, COLS, ROWS + self._sdlrenderer, sdl2.SDL_PIXELFORMAT_ABGR8888, sdl2.SDL_TEXTUREACCESS_STATIC, COLS, ROWS ) sdl2.SDL_ShowWindow(self._window) diff --git a/tests/test_acid_cgb.py b/tests/test_acid_cgb.py index 7c57630a6..5fb9388b9 100644 --- a/tests/test_acid_cgb.py +++ b/tests/test_acid_cgb.py @@ -27,7 +27,7 @@ def test_cgb_acid(cgb_acid_file): image.save(png_path) else: old_image = PIL.Image.open(png_path) - diff = PIL.ImageChops.difference(image, old_image) + diff = PIL.ImageChops.difference(image.convert(mode="RGB"), old_image) if diff.getbbox() and not os.environ.get("TEST_CI"): image.show() old_image.show() diff --git a/tests/test_acid_dmg.py b/tests/test_acid_dmg.py index e2c53eecb..dd31d1a8c 100644 --- a/tests/test_acid_dmg.py +++ b/tests/test_acid_dmg.py @@ -28,7 +28,7 @@ def test_dmg_acid(cgb, dmg_acid_file): image.save(png_path) else: old_image = PIL.Image.open(png_path) - diff = PIL.ImageChops.difference(image, old_image) + diff = PIL.ImageChops.difference(image.convert(mode="RGB"), old_image) if diff.getbbox() and not os.environ.get("TEST_CI"): image.show() old_image.show() diff --git a/tests/test_basics.py b/tests/test_basics.py index 2cab163a4..27480d1e9 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -40,7 +40,7 @@ def test_record_replay(boot_rom, default_rom): frame_no, keys, frame_data = events[0] assert frame_no == 1, "We inserted the key on the second frame" assert keys[0] == WindowEvent.PRESS_ARROW_DOWN, "Check we have the right keypress" - assert sum(base64.b64decode(frame_data)) / 0xFF == 144 * 160 * 3, "Frame does not contain 160x144 of RGB data" + assert len(base64.b64decode(frame_data)) == 144 * 160 * 3, "Frame does not contain 160x144 of RGB data" pyboy.stop(save=False) @@ -219,7 +219,7 @@ def test_all_modes(cgb, _bootrom, frames, rom, any_rom_cgb, boot_cgb_rom): png_buf.seek(0) old_image = PIL.Image.open(png_buf) - diff = PIL.ImageChops.difference(image, old_image) + diff = PIL.ImageChops.difference(image.convert(mode="RGB"), old_image) if diff.getbbox() and not os.environ.get("TEST_CI"): image.show() old_image.show() diff --git a/tests/test_external_api.py b/tests/test_external_api.py index 6c7ee2a68..8c8263049 100644 --- a/tests/test_external_api.py +++ b/tests/test_external_api.py @@ -17,6 +17,9 @@ from .conftest import BOOTROM_FRAMES_UNTIL_LOGO +NDARRAY_COLOR_DEPTH = 4 +NDARRAY_COLOR_FORMAT = "RGBX" + def test_misc(default_rom): pyboy = PyBoy(default_rom, window_type="null") @@ -38,20 +41,59 @@ def test_tiles(default_rom): assert isinstance(image, PIL.Image.Image) ndarray = tile.image_ndarray() assert isinstance(ndarray, np.ndarray) - assert ndarray.shape == (8, 8, 4) + assert ndarray.shape == (8, 8, NDARRAY_COLOR_DEPTH) + assert ndarray.dtype == np.uint8 + # data = tile.image_data() + # assert data.shape == (8, 8) + + assert [[x for x in y] for y in ndarray.view(dtype=np.uint32).reshape(8, 8) + ] == [[0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF], + [0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF], + [0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF], + [0xFFFFFF, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0xFFFFFF, 0xFFFFFF], + [0xFFFFFF, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0xFFFFFF], + [0xFFFFFF, 0x000000, 0x000000, 0xFFFFFF, 0xFFFFFF, 0x000000, 0x000000, 0xFFFFFF], + [0xFFFFFF, 0x000000, 0x000000, 0xFFFFFF, 0xFFFFFF, 0x000000, 0x000000, 0xFFFFFF], + [0xFFFFFF, 0x000000, 0x000000, 0xFFFFFF, 0xFFFFFF, 0x000000, 0x000000, 0xFFFFFF]] + + for identifier in range(384): + t = pyboy.get_tile(identifier) + assert t.tile_identifier == identifier + with pytest.raises(Exception): + pyboy.get_tile(-1) + with pytest.raises(Exception): + pyboy.get_tile(385) + + pyboy.stop(save=False) + + +def test_tiles_cgb(any_rom_cgb): + pyboy = PyBoy(any_rom_cgb, window_type="null") + pyboy.set_emulation_speed(0) + pyboy.tick(BOOTROM_FRAMES_UNTIL_LOGO, False) + + tile = pyboy.tilemap_window.tile(0, 0) + assert isinstance(tile, Tile) + + tile = pyboy.get_tile(1) + image = tile.image() + assert isinstance(image, PIL.Image.Image) + ndarray = tile.image_ndarray() + assert isinstance(ndarray, np.ndarray) + assert ndarray.shape == (8, 8, NDARRAY_COLOR_DEPTH) assert ndarray.dtype == np.uint8 - data = tile.image_data() - assert data.shape == (8, 8) - - assert [[x for x in y] for y in data - ] == [[0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff], - [0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff], - [0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff], - [0xffffffff, 0xff000000, 0xff000000, 0xff000000, 0xff000000, 0xff000000, 0xffffffff, 0xffffffff], - [0xffffffff, 0xff000000, 0xff000000, 0xff000000, 0xff000000, 0xff000000, 0xff000000, 0xffffffff], - [0xffffffff, 0xff000000, 0xff000000, 0xffffffff, 0xffffffff, 0xff000000, 0xff000000, 0xffffffff], - [0xffffffff, 0xff000000, 0xff000000, 0xffffffff, 0xffffffff, 0xff000000, 0xff000000, 0xffffffff], - [0xffffffff, 0xff000000, 0xff000000, 0xffffffff, 0xffffffff, 0xff000000, 0xff000000, 0xffffffff]] + # data = tile.image_data() + # assert data.shape == (8, 8) + + assert [[x for x in y] + for y in ndarray] == [[0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF], + [0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF], + [0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF], + [0xFFFFFF, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0xFFFFFF, 0xFFFFFF], + [0xFFFFFF, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0xFFFFFF], + [0xFFFFFF, 0x000000, 0x000000, 0xFFFFFF, 0xFFFFFF, 0x000000, 0x000000, 0xFFFFFF], + [0xFFFFFF, 0x000000, 0x000000, 0xFFFFFF, 0xFFFFFF, 0x000000, 0x000000, 0xFFFFFF], + [0xFFFFFF, 0x000000, 0x000000, 0xFFFFFF, 0xFFFFFF, 0x000000, 0x000000, 0xFFFFFF]] for identifier in range(384): t = pyboy.get_tile(identifier) @@ -65,7 +107,7 @@ def test_tiles(default_rom): def test_screen_buffer_and_image(tetris_rom, boot_rom): - cformat = "RGBA" + cformat = "RGBX" boot_logo_hash_predigested = b"_M\x0e\xd9\xe2\xdb\\o]\x83U\x93\xebZm\x1e\xaaFR/Q\xa52\x1c{8\xe7g\x95\xbcIz" pyboy = PyBoy(tetris_rom, window_type="null", bootrom_file=boot_rom) @@ -101,7 +143,7 @@ def test_screen_buffer_and_image(tetris_rom, boot_rom): numpy_hash = hashlib.sha256() numpy_array = np.ascontiguousarray(pyboy.screen.ndarray) assert isinstance(pyboy.screen.ndarray, np.ndarray) - assert numpy_array.shape == (144, 160, 3) + assert numpy_array.shape == (144, 160, NDARRAY_COLOR_DEPTH) numpy_hash.update(numpy_array.tobytes()) # assert numpy_hash.digest( # ) == (b"\r\t\x87\x131\xe8\x06\x82\xcaO=\n\x1e\xa2K$" diff --git a/tests/test_magen.py b/tests/test_magen.py index 51997492a..ee4650f28 100644 --- a/tests/test_magen.py +++ b/tests/test_magen.py @@ -27,7 +27,7 @@ def test_magen_test(magen_test_file): image.save(png_path) else: old_image = PIL.Image.open(png_path) - diff = PIL.ImageChops.difference(image, old_image) + diff = PIL.ImageChops.difference(image.convert(mode="RGB"), old_image) if diff.getbbox() and not os.environ.get("TEST_CI"): image.show() old_image.show() diff --git a/tests/test_mooneye.py b/tests/test_mooneye.py index ee388ddc4..363f28e94 100644 --- a/tests/test_mooneye.py +++ b/tests/test_mooneye.py @@ -178,9 +178,11 @@ def test_mooneye(clean, rom, mooneye_dir, default_rom): old_image = PIL.Image.open(png_path) if "acceptance" in rom: # The registers are too volatile to depend on. We crop the top out, and only match the assertions. - diff = PIL.ImageChops.difference(image.crop((0, 72, 160, 144)), old_image.crop((0, 72, 160, 144))) + diff = PIL.ImageChops.difference( + image.crop((0, 72, 160, 144)).convert(mode="RGB"), old_image.crop((0, 72, 160, 144)) + ) else: - diff = PIL.ImageChops.difference(image, old_image) + diff = PIL.ImageChops.difference(image.convert(mode="RGB"), old_image) if diff.getbbox() and not os.environ.get("TEST_CI"): image.show() diff --git a/tests/test_openai_gym.py b/tests/test_openai_gym.py index 0b0e3613e..3048af651 100644 --- a/tests/test_openai_gym.py +++ b/tests/test_openai_gym.py @@ -44,11 +44,11 @@ class TestOpenAIGym: def test_raw(self, pyboy): env = pyboy.openai_gym(observation_type="raw", action_type="press") observation = env.reset() - assert observation.shape == (ROWS, COLS, 3) + assert observation.shape == (ROWS, COLS, 4) assert observation.dtype == np.uint8 observation, _, _, _ = env.step(0) - assert observation.shape == (ROWS, COLS, 3) + assert observation.shape == (ROWS, COLS, 4) assert observation.dtype == np.uint8 def test_tiles(self, pyboy, tiles_id, id0_block, id1_block): diff --git a/tests/test_rtc3test.py b/tests/test_rtc3test.py index 6f80144d7..8bc8d5fb4 100644 --- a/tests/test_rtc3test.py +++ b/tests/test_rtc3test.py @@ -43,7 +43,7 @@ def test_rtc3test(subtest, rtc3test_file): image.save(png_path) else: old_image = PIL.Image.open(png_path) - diff = PIL.ImageChops.difference(image, old_image) + diff = PIL.ImageChops.difference(image.convert(mode="RGB"), old_image) if diff.getbbox() and not os.environ.get("TEST_CI"): image.show() old_image.show() diff --git a/tests/test_samesuite.py b/tests/test_samesuite.py index 25c0a58f7..35815ddea 100644 --- a/tests/test_samesuite.py +++ b/tests/test_samesuite.py @@ -140,7 +140,7 @@ def test_samesuite(clean, gb_type, rom, samesuite_dir, boot_cgb_rom, boot_rom, d pyboy.load_state(saved_state) for _ in range(10): - if np.all(pyboy.screen.ndarray > 240): + if np.all(pyboy.screen.ndarray[:, :, :-1] > 240): pyboy.tick(20, True) else: break @@ -152,7 +152,7 @@ def test_samesuite(clean, gb_type, rom, samesuite_dir, boot_cgb_rom, boot_rom, d image.save(png_path) else: old_image = PIL.Image.open(png_path) - diff = PIL.ImageChops.difference(image, old_image) + diff = PIL.ImageChops.difference(image.convert(mode="RGB"), old_image) if diff.getbbox() and not os.environ.get("TEST_CI"): image.show() diff --git a/tests/test_shonumi.py b/tests/test_shonumi.py index db77f11c8..b63d82680 100644 --- a/tests/test_shonumi.py +++ b/tests/test_shonumi.py @@ -34,7 +34,7 @@ def test_shonumi(rom, shonumi_dir): old_image = PIL.Image.open(png_path) old_image = old_image.resize(image.size, resample=PIL.Image.Dither.NONE) - diff = PIL.ImageChops.difference(image, old_image) + diff = PIL.ImageChops.difference(image.convert(mode="RGB"), old_image) if diff.getbbox() and not os.environ.get("TEST_CI"): image.show() diff --git a/tests/test_which.py b/tests/test_which.py index 3c0cc78a8..daa7eee60 100644 --- a/tests/test_which.py +++ b/tests/test_which.py @@ -28,7 +28,7 @@ def test_which(cgb, which_file): image.save(png_path) else: old_image = PIL.Image.open(png_path) - diff = PIL.ImageChops.difference(image, old_image) + diff = PIL.ImageChops.difference(image.convert(mode="RGB"), old_image) if diff.getbbox() and not os.environ.get("TEST_CI"): image.show() old_image.show() diff --git a/tests/test_whichboot.py b/tests/test_whichboot.py index 1a174fc8f..ded627915 100644 --- a/tests/test_whichboot.py +++ b/tests/test_whichboot.py @@ -28,7 +28,7 @@ def test_which(cgb, whichboot_file): image.save(png_path) else: old_image = PIL.Image.open(png_path) - diff = PIL.ImageChops.difference(image, old_image) + diff = PIL.ImageChops.difference(image.convert(mode="RGB"), old_image) if diff.getbbox() and not os.environ.get("TEST_CI"): image.show() old_image.show() From eef31e29cad1ce1e33c5da1674a2208266030614 Mon Sep 17 00:00:00 2001 From: Mads Ynddal <5528170+Baekalfen@users.noreply.github.com> Date: Sat, 9 Dec 2023 20:33:40 +0100 Subject: [PATCH 24/65] Pyboy-object: readonly window title --- pyboy/pyboy.pxd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyboy/pyboy.pxd b/pyboy/pyboy.pxd index 4d744fbfd..b3f7de630 100644 --- a/pyboy/pyboy.pxd +++ b/pyboy/pyboy.pxd @@ -47,7 +47,7 @@ cdef class PyBoy: cdef bint quitting cdef bint stopped cdef bint initialized - cdef public str window_title + cdef readonly str window_title cdef readonly PyBoyMemoryView memory cdef readonly Screen screen From e4fce576ad79c5cc0e78d0c712691e140ca8c4ea Mon Sep 17 00:00:00 2001 From: Mads Ynddal <5528170+Baekalfen@users.noreply.github.com> Date: Sat, 9 Dec 2023 20:34:40 +0100 Subject: [PATCH 25/65] Pyboy-object: readonly memory --- pyboy/pyboy.pxd | 1 - 1 file changed, 1 deletion(-) diff --git a/pyboy/pyboy.pxd b/pyboy/pyboy.pxd index b3f7de630..70ed6687e 100644 --- a/pyboy/pyboy.pxd +++ b/pyboy/pyboy.pxd @@ -49,7 +49,6 @@ cdef class PyBoy: cdef bint initialized cdef readonly str window_title cdef readonly PyBoyMemoryView memory - cdef readonly Screen screen cdef readonly TileMap tilemap_background cdef readonly TileMap tilemap_window From 4aa0ef6c1fd6b28a0add43557d2de6bb3d146c61 Mon Sep 17 00:00:00 2001 From: Mads Ynddal <5528170+Baekalfen@users.noreply.github.com> Date: Sat, 9 Dec 2023 23:27:08 +0100 Subject: [PATCH 26/65] Allow 768 tiles on CGB in API --- pyboy/api/constants.py | 1 + pyboy/api/tile.pxd | 1 + pyboy/api/tile.py | 29 +++++++++++++++++++++-------- pyboy/pyboy.py | 2 +- tests/test_external_api.py | 10 ++++++++++ 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/pyboy/api/constants.py b/pyboy/api/constants.py index 0cc2ac985..7d4dd06d4 100644 --- a/pyboy/api/constants.py +++ b/pyboy/api/constants.py @@ -15,5 +15,6 @@ LOW_TILEDATA_NTILES = 0x100 HIGH_TILEDATA = 0x800 + VRAM_OFFSET TILES = 384 +TILES_CGB = 768 SPRITES = 40 ROWS, COLS = 144, 160 diff --git a/pyboy/api/tile.pxd b/pyboy/api/tile.pxd index ddd413e9e..cfb584c47 100644 --- a/pyboy/api/tile.pxd +++ b/pyboy/api/tile.pxd @@ -16,6 +16,7 @@ cdef uint16_t VRAM_OFFSET, LOW_TILEDATA cdef class Tile: cdef Motherboard mb + cdef public int vram_bank cdef public str raw_buffer_format cdef public int tile_identifier cdef public int data_address diff --git a/pyboy/api/tile.py b/pyboy/api/tile.py index 3faae41f7..6922dd15e 100644 --- a/pyboy/api/tile.py +++ b/pyboy/api/tile.py @@ -12,7 +12,7 @@ import pyboy from pyboy import utils -from .constants import LOW_TILEDATA, VRAM_OFFSET +from .constants import LOW_TILEDATA, TILES, TILES_CGB, VRAM_OFFSET logger = pyboy.logging.get_logger(__name__) @@ -43,9 +43,12 @@ def __init__(self, mb, identifier): """ 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.api.tile.Tile.image` or one of the @@ -60,10 +63,16 @@ def __init__(self, mb, identifier): 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 ------- @@ -142,8 +151,12 @@ def _image_data(self): """ 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) @@ -152,7 +165,7 @@ def _image_data(self): 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}" diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index da6338754..c99415214 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -756,7 +756,7 @@ def get_sprite_by_tile_identifier(self, tile_identifiers, on_screen=True): def get_tile(self, identifier): """ - The Game Boy can have 384 tiles loaded in memory at once. Use this method to get a + 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 diff --git a/tests/test_external_api.py b/tests/test_external_api.py index 8c8263049..65da31e54 100644 --- a/tests/test_external_api.py +++ b/tests/test_external_api.py @@ -36,6 +36,8 @@ def test_tiles(default_rom): tile = pyboy.tilemap_window.tile(0, 0) assert isinstance(tile, Tile) + with pytest.raises(AssertionError): + pyboy.get_tile(384) # Not CGB tile = pyboy.get_tile(1) image = tile.image() assert isinstance(image, PIL.Image.Image) @@ -106,6 +108,14 @@ def test_tiles_cgb(any_rom_cgb): pyboy.stop(save=False) +def test_tiles_cgb(any_rom_cgb): + pyboy = PyBoy(any_rom_cgb, window_type="null") + pyboy.set_emulation_speed(0) + pyboy.tick(BOOTROM_FRAMES_UNTIL_LOGO, False) + + pyboy.get_tile(384) # Allowed on CGB + + def test_screen_buffer_and_image(tetris_rom, boot_rom): cformat = "RGBX" boot_logo_hash_predigested = b"_M\x0e\xd9\xe2\xdb\\o]\x83U\x93\xebZm\x1e\xaaFR/Q\xa52\x1c{8\xe7g\x95\xbcIz" From c495084e88c9091b5b5aeac3b5bc8f9fe24f79c8 Mon Sep 17 00:00:00 2001 From: Mads Ynddal <5528170+Baekalfen@users.noreply.github.com> Date: Wed, 22 Nov 2023 20:34:05 +0100 Subject: [PATCH 27/65] Update pan docs links --- pyboy/api/screen.py | 4 ++-- pyboy/api/sprite.py | 10 +++++----- pyboy/api/tile.py | 2 +- pyboy/api/tilemap.py | 4 ++-- pyboy/pyboy.py | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pyboy/api/screen.py b/pyboy/api/screen.py index 5607be0b2..1d48d7991 100644 --- a/pyboy/api/screen.py +++ b/pyboy/api/screen.py @@ -68,7 +68,7 @@ def __init__(self, mb): 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.api` features, [Pan Docs](http://bgb.bircd.org/pandocs.htm) on tiles/sprites, + 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 @@ -129,7 +129,7 @@ def get_tilemap_position(self): 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). + or the Pan Docs under [LCD Position and Scrolling](https://gbdev.io/pandocs/Scrolling.html). Returns ------- diff --git a/pyboy/api/sprite.py b/pyboy/api/sprite.py index 1d6f9cdc4..99eb0dfd8 100644 --- a/pyboy/api/sprite.py +++ b/pyboy/api/sprite.py @@ -86,7 +86,7 @@ def __init__(self, mb, sprite_index): 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 ------- @@ -97,7 +97,7 @@ def __init__(self, mb, sprite_index): 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 ------- @@ -108,7 +108,7 @@ def __init__(self, mb, sprite_index): 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 ------- @@ -119,7 +119,7 @@ def __init__(self, mb, sprite_index): self.attr_palette_number = _bit(attr, 4) """ 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 ------- @@ -148,7 +148,7 @@ def __init__(self, mb, sprite_index): 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 ------- diff --git a/pyboy/api/tile.py b/pyboy/api/tile.py index 6922dd15e..7fb98dacc 100644 --- a/pyboy/api/tile.py +++ b/pyboy/api/tile.py @@ -55,7 +55,7 @@ def __init__(self, mb, identifier): 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 ------- diff --git a/pyboy/api/tilemap.py b/pyboy/api/tilemap.py index 676223602..510c82778 100644 --- a/pyboy/api/tilemap.py +++ b/pyboy/api/tilemap.py @@ -126,7 +126,7 @@ def _tile_address(self, column, row): 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.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. @@ -170,7 +170,7 @@ def tile_identifier(self, column, row): 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. diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index c99415214..4cdc90476 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -141,7 +141,7 @@ def __init__( 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). + Read more details about it, in the [Pan Docs](https://gbdev.io/pandocs/Tile_Maps.html). Returns ------- @@ -154,7 +154,7 @@ def __init__( 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). + Read more details about it, in the [Pan Docs](https://gbdev.io/pandocs/Tile_Maps.html). Returns ------- From a0a331fdef6dcf5a1dd826b3a9a6fe5a23b25a2c Mon Sep 17 00:00:00 2001 From: Mads Ynddal <5528170+Baekalfen@users.noreply.github.com> Date: Wed, 22 Nov 2023 20:53:45 +0100 Subject: [PATCH 28/65] Change .pxd public to readonly --- pyboy/api/sprite.pxd | 24 +++++++++---------- pyboy/api/tile.pxd | 10 ++++---- pyboy/api/tilemap.pxd | 5 ++++ pyboy/core/lcd.pxd | 18 +++++++------- pyboy/plugins/base_plugin.pxd | 2 +- .../plugins/game_wrapper_kirby_dream_land.pxd | 10 ++++---- .../plugins/game_wrapper_super_mario_land.pxd | 16 ++++++------- pyboy/plugins/game_wrapper_tetris.pxd | 8 +++---- pyboy/utils.pxd | 14 +++++------ 9 files changed, 56 insertions(+), 51 deletions(-) diff --git a/pyboy/api/sprite.pxd b/pyboy/api/sprite.pxd index 4aeb2b8da..bfbba208c 100644 --- a/pyboy/api/sprite.pxd +++ b/pyboy/api/sprite.pxd @@ -10,17 +10,17 @@ from pyboy.core.mb cimport Motherboard cdef class Sprite: cdef Motherboard mb - cdef public int _offset + cdef readonly int _offset - cdef public int sprite_index - cdef public int y - cdef public int x - cdef public int tile_identifier - cdef public bint attr_obj_bg_priority - cdef public bint attr_y_flip - cdef public bint attr_x_flip - cdef public bint attr_palette_number - cdef public tuple shape - cdef public list tiles - cdef public bint on_screen + cdef readonly int sprite_index + cdef readonly int y + cdef readonly int x + cdef readonly int tile_identifier + cdef readonly bint attr_obj_bg_priority + cdef readonly bint attr_y_flip + cdef readonly bint attr_x_flip + cdef readonly bint attr_palette_number + cdef readonly tuple shape + cdef readonly list tiles + cdef readonly bint on_screen cdef int _sprite_index diff --git a/pyboy/api/tile.pxd b/pyboy/api/tile.pxd index cfb584c47..a5f3bb420 100644 --- a/pyboy/api/tile.pxd +++ b/pyboy/api/tile.pxd @@ -16,11 +16,11 @@ cdef uint16_t VRAM_OFFSET, LOW_TILEDATA cdef class Tile: cdef Motherboard mb - cdef public int vram_bank - cdef public str raw_buffer_format - cdef public int tile_identifier - cdef public int data_address - cdef public tuple shape + cdef readonly int vram_bank + cdef readonly str raw_buffer_format + cdef readonly int tile_identifier + cdef readonly int data_address + cdef readonly tuple shape cpdef object image(self) noexcept cpdef object image_ndarray(self) noexcept diff --git a/pyboy/api/tilemap.pxd b/pyboy/api/tilemap.pxd index 6415b0332..950e2b2d9 100644 --- a/pyboy/api/tilemap.pxd +++ b/pyboy/api/tilemap.pxd @@ -3,11 +3,15 @@ # GitHub: https://github.com/Baekalfen/PyBoy # +cimport cython from libc.stdint cimport uint64_t +from pyboy.core.lcd cimport LCDCRegister from pyboy.core.mb cimport Motherboard +cdef int HIGH_TILEMAP, LCDC_OFFSET, LOW_TILEDATA_NTILES, LOW_TILEMAP + cdef class TileMap: cdef object pyboy cdef Motherboard mb @@ -15,6 +19,7 @@ cdef class TileMap: cdef bint _use_tile_objects cdef uint64_t frame_count_update cpdef int _refresh_lcdc(self) except -1 + @cython.locals(LCDC=LCDCRegister) cdef int __refresh_lcdc(self) except -1 cdef readonly int map_offset cdef str _select diff --git a/pyboy/core/lcd.pxd b/pyboy/core/lcd.pxd index 07068733d..0d1ec33d2 100644 --- a/pyboy/core/lcd.pxd +++ b/pyboy/core/lcd.pxd @@ -98,15 +98,15 @@ cdef class LCDCRegister: cdef void set(self, uint64_t) noexcept nogil - cdef public bint lcd_enable - cdef public bint windowmap_select - cdef public bint window_enable - cdef public bint tiledata_select - cdef public bint backgroundmap_select - cdef public bint sprite_height - cdef public bint sprite_enable - cdef public bint background_enable - cdef public bint cgb_master_priority + cdef bint lcd_enable + cdef bint windowmap_select + cdef bint window_enable + cdef bint tiledata_select + cdef bint backgroundmap_select + cdef bint sprite_height + cdef bint sprite_enable + cdef bint background_enable + cdef bint cgb_master_priority cpdef int _get_sprite_height(self) diff --git a/pyboy/plugins/base_plugin.pxd b/pyboy/plugins/base_plugin.pxd index 4e43c02a7..40d4fcdc7 100644 --- a/pyboy/plugins/base_plugin.pxd +++ b/pyboy/plugins/base_plugin.pxd @@ -42,7 +42,7 @@ cdef class PyBoyWindowPlugin(PyBoyPlugin): cdef class PyBoyGameWrapper(PyBoyPlugin): - cdef public shape + cdef readonly shape cdef bint game_has_started cdef object tilemap_background cdef object tilemap_window diff --git a/pyboy/plugins/game_wrapper_kirby_dream_land.pxd b/pyboy/plugins/game_wrapper_kirby_dream_land.pxd index bc5a781bf..ef48d9586 100644 --- a/pyboy/plugins/game_wrapper_kirby_dream_land.pxd +++ b/pyboy/plugins/game_wrapper_kirby_dream_land.pxd @@ -15,8 +15,8 @@ cdef int ROWS, COLS cdef class GameWrapperKirbyDreamLand(PyBoyGameWrapper): - cdef public int score - cdef public int health - cdef public int lives_left - cdef public int fitness - cdef public int _game_over \ No newline at end of file + cdef readonly int score + cdef readonly int health + cdef readonly int lives_left + cdef readonly int fitness + cdef readonly int _game_over \ No newline at end of file diff --git a/pyboy/plugins/game_wrapper_super_mario_land.pxd b/pyboy/plugins/game_wrapper_super_mario_land.pxd index 236838f49..12681072b 100644 --- a/pyboy/plugins/game_wrapper_super_mario_land.pxd +++ b/pyboy/plugins/game_wrapper_super_mario_land.pxd @@ -15,14 +15,14 @@ cdef int ROWS, COLS cdef class GameWrapperSuperMarioLand(PyBoyGameWrapper): - cdef public tuple world - cdef public int coins - cdef public int lives_left - cdef public int score - cdef public int time_left - cdef public int level_progress - cdef public int _level_progress_max - cdef public int fitness + cdef readonly tuple world + cdef readonly int coins + cdef readonly int lives_left + cdef readonly int score + cdef readonly int time_left + cdef readonly int level_progress + cdef readonly int _level_progress_max + cdef readonly int fitness cpdef void start_game(self, timer_div=*, world_level=*, unlock_level_select=*) noexcept cpdef void reset_game(self, timer_div=*) noexcept diff --git a/pyboy/plugins/game_wrapper_tetris.pxd b/pyboy/plugins/game_wrapper_tetris.pxd index a323ec6af..0209da08f 100644 --- a/pyboy/plugins/game_wrapper_tetris.pxd +++ b/pyboy/plugins/game_wrapper_tetris.pxd @@ -15,10 +15,10 @@ cdef int ROWS, COLS cdef int NEXT_TETROMINO_ADDR cdef class GameWrapperTetris(PyBoyGameWrapper): - cdef public int score - cdef public int level - cdef public int lines - cdef public int fitness + cdef readonly int score + cdef readonly int level + cdef readonly int lines + cdef readonly int fitness cpdef void set_tetromino(self, str) noexcept cpdef str next_tetromino(self) noexcept diff --git a/pyboy/utils.pxd b/pyboy/utils.pxd index 30527a263..ebf9de7ad 100644 --- a/pyboy/utils.pxd +++ b/pyboy/utils.pxd @@ -38,12 +38,12 @@ cdef uint8_t color_code(uint8_t, uint8_t, uint8_t) noexcept nogil # Temporarily placed here to not be exposed on public API cdef class WindowEvent: - cdef public int event + cdef readonly int event cdef class WindowEventMouse(WindowEvent): - cdef public int window_id - cdef public int mouse_x - cdef public int mouse_y - cdef public int mouse_scroll_x - cdef public int mouse_scroll_y - cdef public int mouse_button + cdef readonly int window_id + cdef readonly int mouse_x + cdef readonly int mouse_y + cdef readonly int mouse_scroll_x + cdef readonly int mouse_scroll_y + cdef readonly int mouse_button From 995cfbea7b55af4c1770070c9835638f397ffc28 Mon Sep 17 00:00:00 2001 From: Mads Ynddal <5528170+Baekalfen@users.noreply.github.com> Date: Thu, 23 Nov 2023 22:42:24 +0100 Subject: [PATCH 29/65] Introduce exceptions for loading a corrupt save-state --- pyboy/core/mb.pxd | 4 ++-- pyboy/core/mb.py | 3 +-- pyboy/plugins/rewind.pxd | 2 +- pyboy/pyboy.pxd | 2 ++ pyboy/utils.pxd | 22 +++++++++++----------- tests/test_external_api.py | 24 ++++++++++++++++++++++++ 6 files changed, 41 insertions(+), 16 deletions(-) diff --git a/pyboy/core/mb.pxd b/pyboy/core/mb.pxd index 305bb86c1..43ab9ffdf 100644 --- a/pyboy/core/mb.pxd +++ b/pyboy/core/mb.pxd @@ -69,8 +69,8 @@ cdef class Motherboard: @cython.locals(offset=cython.int, dst=cython.int, n=cython.int) cdef void transfer_DMA(self, uint8_t) noexcept nogil - cdef void save_state(self, IntIOInterface) noexcept - cdef void load_state(self, IntIOInterface) noexcept + cdef int save_state(self, IntIOInterface) except -1 + cdef int load_state(self, IntIOInterface) except -1 cdef class HDMA: cdef uint8_t hdma1 diff --git a/pyboy/core/mb.py b/pyboy/core/mb.py index 7e298c02b..81057b02d 100644 --- a/pyboy/core/mb.py +++ b/pyboy/core/mb.py @@ -240,8 +240,7 @@ def load_state(self, f): self.double_speed = f.read() _cgb = f.read() if self.cgb != _cgb: - logger.critical("Loading state which is not CGB, but PyBoy is loaded in CGB mode!") - return + raise Exception("Loading state which is not CGB, but PyBoy is loaded in CGB mode!") self.cgb = _cgb if self.cgb: self.hdma.load_state(f, state_version) diff --git a/pyboy/plugins/rewind.pxd b/pyboy/plugins/rewind.pxd index 46f2cde44..f66a26920 100644 --- a/pyboy/plugins/rewind.pxd +++ b/pyboy/plugins/rewind.pxd @@ -50,7 +50,7 @@ cdef class CompressedFixedAllocBuffers(FixedAllocBuffers): cdef uint64_t zeros @cython.locals(chunks=int64_t, rest=int64_t) - cdef void flush(self) noexcept + cpdef int flush(self) except -1 cdef class DeltaFixedAllocBuffers(CompressedFixedAllocBuffers): cdef int64_t internal_pointer diff --git a/pyboy/pyboy.pxd b/pyboy/pyboy.pxd index 70ed6687e..7e9cb826e 100644 --- a/pyboy/pyboy.pxd +++ b/pyboy/pyboy.pxd @@ -66,6 +66,8 @@ cdef class PyBoy: @cython.locals(running=bint) cpdef bint tick(self, count=*, render=*) noexcept cpdef void stop(self, save=*) noexcept + cpdef int save_state(self, object) except -1 + cpdef int load_state(self, object) except -1 @cython.locals(state_path=str) cdef void _handle_events(self, list) noexcept diff --git a/pyboy/utils.pxd b/pyboy/utils.pxd index ebf9de7ad..f88f1b365 100644 --- a/pyboy/utils.pxd +++ b/pyboy/utils.pxd @@ -10,19 +10,19 @@ from libc.stdint cimport int64_t, uint8_t, uint16_t, uint32_t, uint64_t # Buffer classes cdef class IntIOInterface: - cdef int64_t write(self, uint8_t) noexcept - cdef int64_t write_16bit(self, uint16_t) noexcept - cdef int64_t write_32bit(self, uint32_t) noexcept - cdef int64_t write_64bit(self, uint64_t) noexcept - cdef uint8_t read(self) noexcept - cdef uint16_t read_16bit(self) noexcept - cdef uint32_t read_32bit(self) noexcept + cpdef int64_t write(self, uint8_t) except -1 + cpdef int64_t write_16bit(self, uint16_t) except -1 + cpdef int64_t write_32bit(self, uint32_t) except -1 + cpdef int64_t write_64bit(self, uint64_t) except -1 + cpdef uint8_t read(self) except? -1 + cpdef uint16_t read_16bit(self) except? -1 + cpdef uint32_t read_32bit(self) except? -1 @cython.locals(a=uint64_t, b=uint64_t, c=uint64_t,d=uint64_t,e=uint64_t,f=uint64_t,g=uint64_t,h=uint64_t,ret=uint64_t, ret2=uint64_t, ret1=uint64_t) - cdef uint64_t read_64bit(self) noexcept - cdef void seek(self, int64_t) noexcept - cdef void flush(self) noexcept - cdef uint64_t tell(self) noexcept + cpdef uint64_t read_64bit(self) except? -1 + cpdef int seek(self, int64_t) except -1 + cpdef int flush(self) except -1 + cpdef int64_t tell(self) except -1 cdef class IntIOWrapper(IntIOInterface): diff --git a/tests/test_external_api.py b/tests/test_external_api.py index 65da31e54..2d86ec4d3 100644 --- a/tests/test_external_api.py +++ b/tests/test_external_api.py @@ -14,6 +14,7 @@ from pyboy import PyBoy, WindowEvent from pyboy.api.tile import Tile +from pyboy.utils import IntIOWrapper from .conftest import BOOTROM_FRAMES_UNTIL_LOGO @@ -28,6 +29,29 @@ def test_misc(default_rom): pyboy.stop(save=False) +def test_faulty_state(default_rom): + pyboy = PyBoy(default_rom, window_type="null") + + a = IntIOWrapper(io.BytesIO(b"abcd")) + a.seek(0) + a.write(0xFF) + # Cython causes OverflowError, PyPy causes AssertionError + with pytest.raises((OverflowError, AssertionError)): + a.write(0x100) + a.seek(0) + assert a.read() == 0xFF + assert a.read() == ord("b") + assert a.read() == ord("c") + assert a.read() == ord("d") + with pytest.raises(AssertionError): + a.read() + + b = io.BytesIO(b"1234") + with pytest.raises(AssertionError): + pyboy.load_state(b) + pyboy.stop(save=False) + + def test_tiles(default_rom): pyboy = PyBoy(default_rom, window_type="null") pyboy.set_emulation_speed(0) From b3c3ecf84aa723c211cad5b2cd699b9109159b79 Mon Sep 17 00:00:00 2001 From: Mads Ynddal <5528170+Baekalfen@users.noreply.github.com> Date: Mon, 4 Dec 2023 09:27:45 +0100 Subject: [PATCH 30/65] Correct docs for api/screen.py --- pyboy/api/screen.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pyboy/api/screen.py b/pyboy/api/screen.py index 1d48d7991..7bd9d7800 100644 --- a/pyboy/api/screen.py +++ b/pyboy/api/screen.py @@ -35,8 +35,8 @@ def __init__(self, mb): self.raw_buffer = self.mb.lcd.renderer._screenbuffer """ Provides a raw, unfiltered `bytes` object with the data from the screen. Check - `Screen.raw_buffer_format` to see which dataformat is used. The returned type and dataformat are - subject to change. + `Screen.raw_buffer_format` to see which dataformat is used. **The returned type and dataformat are + subject to change.** The screen buffer is row-major. Use this, only if you need to bypass the overhead of `Screen.image` or `Screen.ndarray`. @@ -47,16 +47,16 @@ def __init__(self, mb): """ self.raw_buffer_dims = self.mb.lcd.renderer.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). """ self.raw_buffer_format = self.mb.lcd.renderer.color_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 ------- @@ -65,7 +65,7 @@ def __init__(self, mb): """ self.image = None """ - Generates a PIL Image from the screen buffer. + Generates a PIL Image from the screen buffer. 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, @@ -91,11 +91,12 @@ def __init__(self, mb): ).reshape(ROWS, COLS, 4) """ Provides the screen data in NumPy format. The format is given by `pyboy.api.screen.Screen.raw_buffer_format`. + The screen buffer is row-major. Returns ------- numpy.ndarray: - Screendata in `ndarray` of bytes with shape (160, 144, 3) + Screendata in `ndarray` of bytes with shape (144, 160, 3) """ @property From c2ea115c17defe2db8dc7df7ab29c4e1c9cabd22 Mon Sep 17 00:00:00 2001 From: Mads Ynddal <5528170+Baekalfen@users.noreply.github.com> Date: Tue, 12 Dec 2023 10:13:03 +0100 Subject: [PATCH 31/65] Hide pyboy.plugin_manager and make readonly --- pyboy/plugins/debug_prompt.py | 2 +- pyboy/pyboy.pxd | 2 +- pyboy/pyboy.py | 36 +++++++++++++++++++---------------- tests/test_basics.py | 2 +- tests/test_replay.py | 2 +- 5 files changed, 24 insertions(+), 20 deletions(-) diff --git a/pyboy/plugins/debug_prompt.py b/pyboy/plugins/debug_prompt.py index 64ad08249..bf6a016c1 100644 --- a/pyboy/plugins/debug_prompt.py +++ b/pyboy/plugins/debug_prompt.py @@ -83,7 +83,7 @@ def parse_bank_addr_sym_label(self, command): def handle_breakpoint(self): while True: - self.pyboy.plugin_manager.post_tick() + self.pyboy._plugin_manager.post_tick() # Get symbol if self.mb.cpu.PC < 0x4000: diff --git a/pyboy/pyboy.pxd b/pyboy/pyboy.pxd index 7e9cb826e..e9b71ad74 100644 --- a/pyboy/pyboy.pxd +++ b/pyboy/pyboy.pxd @@ -32,7 +32,7 @@ cdef class PyBoyMemoryView: cdef class PyBoy: cdef Motherboard mb - cdef public PluginManager plugin_manager + cdef readonly PluginManager _plugin_manager cdef readonly uint64_t frame_count cdef readonly str gamerom_file cdef readonly bint paused diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index 4cdc90476..59dd43db4 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -9,14 +9,14 @@ import os import time -from pyboy.logging import get_logger from pyboy.api.screen import Screen +from pyboy.api.tilemap import TileMap +from pyboy.logging import get_logger from pyboy.openai_gym import PyBoyGymEnv from pyboy.openai_gym import enabled as gym_enabled from pyboy.plugins.manager import PluginManager from pyboy.utils import IntIOWrapper, WindowEvent -from pyboy.api.tilemap import TileMap from .api import Sprite, Tile, constants from .core.mb import Motherboard @@ -162,10 +162,14 @@ def __init__( A TileMap object for the tile map. """ - ################### - # Plugins + self._plugin_manager = PluginManager(self, self.mb, kwargs) + """ + Returns + ------- + `pyboy.plugins.manager.PluginManager`: + Object for handling plugins in PyBoy + """ - self.plugin_manager = PluginManager(self, self.mb, kwargs) self._hooks = {} self.initialized = True @@ -191,11 +195,11 @@ def _tick(self, render): self.mb.breakpoint_singlestep_latch = 0 if not self._handle_hooks(): - self.plugin_manager.handle_breakpoint() + self._plugin_manager.handle_breakpoint() else: if self.mb.breakpoint_singlestep_latch: if not self._handle_hooks(): - self.plugin_manager.handle_breakpoint() + self._plugin_manager.handle_breakpoint() # Keep singlestepping on, if that's what we're doing self.mb.breakpoint_singlestep = self.mb.breakpoint_singlestep_latch @@ -249,7 +253,7 @@ def tick(self, count=1, render=True): 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 @@ -279,7 +283,7 @@ def _handle_events(self, events): elif event == WindowEvent.UNPAUSE: self._unpause() elif event == WindowEvent._INTERNAL_RENDERER_FLUSH: - self.plugin_manager._post_tick_windows() + self._plugin_manager._post_tick_windows() else: self.mb.buttonevent(event) @@ -303,8 +307,8 @@ def _unpause(self): 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 @@ -317,8 +321,8 @@ def _update_window_title(self): 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) @@ -341,7 +345,7 @@ def stop(self, save=True): 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 @@ -394,7 +398,7 @@ def game_wrapper(self): `pyboy.plugins.base_plugin.PyBoyGameWrapper`: A game-specific wrapper object. """ - return self.plugin_manager.gamewrapper() + return self._plugin_manager.gamewrapper() def button(self, input): """ @@ -608,7 +612,7 @@ def set_emulation_speed(self, target_speed): Args: target_speed (int): Target emulation speed as multiplier of real-time. """ - if self.initialized and self.plugin_manager.window_null_enabled: + 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.' ) diff --git a/tests/test_basics.py b/tests/test_basics.py index 27480d1e9..4412843fa 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -35,7 +35,7 @@ def test_record_replay(boot_rom, default_rom): pyboy.button_press("up") pyboy.tick(1, True) - events = pyboy.plugin_manager.record_replay.recorded_input + events = pyboy._plugin_manager.record_replay.recorded_input assert len(events) == 4, "We assumed only 4 frames were recorded, as frames without events are skipped." frame_no, keys, frame_data = events[0] assert frame_no == 1, "We inserted the key on the second frame" diff --git a/tests/test_replay.py b/tests/test_replay.py index 354397b97..0bbdf158a 100644 --- a/tests/test_replay.py +++ b/tests/test_replay.py @@ -150,7 +150,7 @@ def replay( with open(replay, "wb") as f: f.write( zlib.compress( - json.dumps((pyboy.plugin_manager.record_replay.recorded_input, b64_romhash, b64_state)).encode() + json.dumps((pyboy._plugin_manager.record_replay.recorded_input, b64_romhash, b64_state)).encode() ) ) From 7f37371c3698fbb0fdd4442f135f8cb8b7161d31 Mon Sep 17 00:00:00 2001 From: Mads Ynddal <5528170+Baekalfen@users.noreply.github.com> Date: Tue, 12 Dec 2023 12:54:59 +0100 Subject: [PATCH 32/65] game_wrapper as property, and auto-initialized --- extras/examples/gamewrapper_kirby.py | 2 +- extras/examples/gamewrapper_mario.py | 2 +- extras/examples/gamewrapper_tetris.py | 2 +- pyboy/openai_gym.py | 2 +- pyboy/plugins/__init__.py | 14 +++++----- pyboy/plugins/base_plugin.py | 3 ++- pyboy/plugins/game_wrapper_pokemon_gen1.py | 5 ++-- pyboy/plugins/manager.pxd | 4 +-- pyboy/plugins/manager.py | 3 ++- pyboy/pyboy.pxd | 1 + pyboy/pyboy.py | 31 +++++++++++----------- tests/test_external_api.py | 4 +-- tests/test_game_wrapper.py | 26 ++++++++++++++++++ tests/test_game_wrapper_mario.py | 18 ++++++------- tests/test_mario_rl.py | 6 ++--- tests/test_openai_gym.py | 26 +++++++++--------- tests/test_states.py | 4 +-- tests/test_tetris_ai.py | 4 +-- 18 files changed, 91 insertions(+), 66 deletions(-) create mode 100644 tests/test_game_wrapper.py diff --git a/extras/examples/gamewrapper_kirby.py b/extras/examples/gamewrapper_kirby.py index f8a4ccc8f..41aa5ab26 100644 --- a/extras/examples/gamewrapper_kirby.py +++ b/extras/examples/gamewrapper_kirby.py @@ -24,7 +24,7 @@ pyboy.set_emulation_speed(0) assert pyboy.cartridge_title() == "KIRBY DREAM LA" -kirby = pyboy.game_wrapper() +kirby = pyboy.game_wrapper kirby.start_game() assert kirby.score == 0 diff --git a/extras/examples/gamewrapper_mario.py b/extras/examples/gamewrapper_mario.py index 9acda0bb8..5cd678e8d 100644 --- a/extras/examples/gamewrapper_mario.py +++ b/extras/examples/gamewrapper_mario.py @@ -24,7 +24,7 @@ pyboy.set_emulation_speed(0) assert pyboy.cartridge_title() == "SUPER MARIOLAN" -mario = pyboy.game_wrapper() +mario = pyboy.game_wrapper mario.start_game() assert mario.score == 0 diff --git a/extras/examples/gamewrapper_tetris.py b/extras/examples/gamewrapper_tetris.py index 226721543..7d807eadd 100644 --- a/extras/examples/gamewrapper_tetris.py +++ b/extras/examples/gamewrapper_tetris.py @@ -24,7 +24,7 @@ pyboy.set_emulation_speed(0) assert pyboy.cartridge_title() == "TETRIS" -tetris = pyboy.game_wrapper() +tetris = pyboy.game_wrapper tetris.start_game(timer_div=0x00) # The timer_div works like a random seed in Tetris tetromino_at_0x00 = tetris.next_tetromino() diff --git a/pyboy/openai_gym.py b/pyboy/openai_gym.py index 84ba6720a..d6e405851 100644 --- a/pyboy/openai_gym.py +++ b/pyboy/openai_gym.py @@ -54,7 +54,7 @@ def __init__(self, pyboy, observation_type="tiles", action_type="toggle", simult raise TypeError("pyboy must be a Pyboy object") # Build game_wrapper - self.game_wrapper = pyboy.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." diff --git a/pyboy/plugins/__init__.py b/pyboy/plugins/__init__.py index 0bcf22e5e..b8afa8093 100644 --- a/pyboy/plugins/__init__.py +++ b/pyboy/plugins/__init__.py @@ -8,18 +8,18 @@ __pdoc__ = { # docs exclude + "window_open_gl": False, "window_sdl2": False, - "screen_recorder": False, - "disable_input": False, "auto_pause": False, + "disable_input": False, + "rewind": False, + "manager_gen": False, "debug": False, - "debug_prompt": False, + "record_replay": False, "window_null": False, - "manager_gen": False, "manager": False, + "debug_prompt": False, "screenshot_recorder": False, - "window_open_gl": False, - "record_replay": False, - "rewind": False, + "screen_recorder": False, # docs exclude end } diff --git a/pyboy/plugins/base_plugin.py b/pyboy/plugins/base_plugin.py index f495ffc94..663ff7688 100644 --- a/pyboy/plugins/base_plugin.py +++ b/pyboy/plugins/base_plugin.py @@ -95,6 +95,7 @@ class PyBoyGameWrapper(PyBoyPlugin): , which shows both sprites and tiles on the screen as a simple matrix. """ + cartridge_title = None argv = [("--game-wrapper", {"action": "store_true", "help": "Enable game wrapper for the current game"})] def __init__(self, *args, game_area_section=(0, 0, 32, 32), game_area_wrap_around=False, **kwargs): @@ -118,7 +119,7 @@ def __init__(self, *args, game_area_section=(0, 0, 32, 32), game_area_wrap_aroun self._cached_game_area_tiles = memoryview(self._cached_game_area_tiles_raw).cast("I", shape=(width, height)) def enabled(self): - return self.pyboy_argv.get("game_wrapper") and self.pyboy.cartridge_title() == self.cartridge_title + return self.cartridge_title is None or self.pyboy.cartridge_title() == self.cartridge_title def post_tick(self): raise NotImplementedError("post_tick not implemented in game wrapper") diff --git a/pyboy/plugins/game_wrapper_pokemon_gen1.py b/pyboy/plugins/game_wrapper_pokemon_gen1.py index d64f735d5..12fa4cd76 100644 --- a/pyboy/plugins/game_wrapper_pokemon_gen1.py +++ b/pyboy/plugins/game_wrapper_pokemon_gen1.py @@ -32,8 +32,7 @@ def __init__(self, *args, **kwargs): self.sprite_offset = 0x1000 def enabled(self): - return self.pyboy_argv.get("game_wrapper") and ((self.pyboy.cartridge_title() == "POKEMON RED") or - (self.pyboy.cartridge_title() == "POKEMON BLUE")) + return (self.pyboy.cartridge_title() == "POKEMON RED") or (self.pyboy.cartridge_title() == "POKEMON BLUE") def post_tick(self): self._tile_cache_invalid = True @@ -45,7 +44,7 @@ def post_tick(self): self.use_background(WY != 0) def _get_screen_background_tilemap(self): - ### SIMILAR TO CURRENT pyboy.game_wrapper()._game_area_np(), BUT ONLY FOR BACKGROUND TILEMAP, SO NPC ARE SKIPPED + ### SIMILAR TO CURRENT pyboy.game_wrapper._game_area_np(), BUT ONLY FOR BACKGROUND TILEMAP, SO NPC ARE SKIPPED bsm = self.pyboy.botsupport_manager() ((scx, scy), (wx, wy)) = bsm.screen().tilemap_position() tilemap = np.array(bsm.tilemap_background[:, :]) diff --git a/pyboy/plugins/manager.pxd b/pyboy/plugins/manager.pxd index cf5d03ce1..8095f230d 100644 --- a/pyboy/plugins/manager.pxd +++ b/pyboy/plugins/manager.pxd @@ -4,6 +4,7 @@ # cimport cython +from pyboy.plugins.base_plugin cimport PyBoyGameWrapper from pyboy.logging.logging cimport Logger @@ -29,9 +30,6 @@ from pyboy.plugins.game_wrapper_pokemon_gen1 cimport GameWrapperPokemonGen1 # imports end -cdef Logger logger - - cdef class PluginManager: cdef object pyboy diff --git a/pyboy/plugins/manager.py b/pyboy/plugins/manager.py index ebf020223..b2af42a8a 100644 --- a/pyboy/plugins/manager.py +++ b/pyboy/plugins/manager.py @@ -3,6 +3,8 @@ # GitHub: https://github.com/Baekalfen/PyBoy # +from pyboy.plugins.base_plugin import PyBoyGameWrapper + # imports from pyboy.plugins.window_sdl2 import WindowSDL2 # isort:skip from pyboy.plugins.window_open_gl import WindowOpenGL # isort:skip @@ -20,7 +22,6 @@ from pyboy.plugins.game_wrapper_kirby_dream_land import GameWrapperKirbyDreamLand # isort:skip from pyboy.plugins.game_wrapper_pokemon_gen1 import GameWrapperPokemonGen1 # isort:skip # imports end -from pyboy.plugins.base_plugin import PyBoyGameWrapper def parser_arguments(): diff --git a/pyboy/pyboy.pxd b/pyboy/pyboy.pxd index e9b71ad74..95ef75fb7 100644 --- a/pyboy/pyboy.pxd +++ b/pyboy/pyboy.pxd @@ -52,6 +52,7 @@ cdef class PyBoy: cdef readonly Screen screen cdef readonly TileMap tilemap_background cdef readonly TileMap tilemap_window + cdef readonly object game_wrapper cdef bint limit_emulationspeed cdef int emulationspeed, target_emulationspeed, save_target_emulationspeed diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index 59dd43db4..3e164e36b 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -170,6 +170,21 @@ def __init__( 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`. + + Returns + ------- + `pyboy.plugins.base_plugin.PyBoyGameWrapper`: + A game-specific wrapper object. + """ + self._hooks = {} self.initialized = True @@ -384,22 +399,6 @@ def openai_gym(self, observation_type="tiles", action_type="press", simultaneous logger.error("%s: Missing dependency \"gym\". ", __name__) return None - def game_wrapper(self): - """ - Provides an instance of a game-specific wrapper. The game is detected by the cartridge's hard-coded game title - (see `pyboy.PyBoy.cartridge_title`). - - If the game isn't supported, None will be returned. - - To get more information, find the wrapper for your game in `pyboy.plugins`. - - Returns - ------- - `pyboy.plugins.base_plugin.PyBoyGameWrapper`: - A game-specific wrapper object. - """ - return self._plugin_manager.gamewrapper() - def button(self, input): """ Send input to PyBoy in the form of "a", "b", "start", "select", "left", "right", "up" and "down". diff --git a/tests/test_external_api.py b/tests/test_external_api.py index 2d86ec4d3..f392b8c16 100644 --- a/tests/test_external_api.py +++ b/tests/test_external_api.py @@ -211,9 +211,9 @@ def test_screen_buffer_and_image(tetris_rom, boot_rom): def test_tetris(tetris_rom): NEXT_TETROMINO = 0xC213 - pyboy = PyBoy(tetris_rom, bootrom_file="pyboy_fast", window_type="null", game_wrapper=True) + pyboy = PyBoy(tetris_rom, bootrom_file="pyboy_fast", window_type="null") pyboy.set_emulation_speed(0) - tetris = pyboy.game_wrapper() + tetris = pyboy.game_wrapper tetris.set_tetromino("T") first_brick = False diff --git a/tests/test_game_wrapper.py b/tests/test_game_wrapper.py new file mode 100644 index 000000000..ff75eb9be --- /dev/null +++ b/tests/test_game_wrapper.py @@ -0,0 +1,26 @@ +# +# License: See LICENSE.md file +# GitHub: https://github.com/Baekalfen/PyBoy +# + +import os +import platform +import sys + +import numpy as np +import pytest + +from pyboy import PyBoy, WindowEvent + +py_version = platform.python_version()[:3] +is_pypy = platform.python_implementation() == "PyPy" + + +def test_game_wrapper_basics(default_rom): + pyboy = PyBoy(default_rom, window_type="null") + pyboy.set_emulation_speed(0) + + generic_wrapper = pyboy.game_wrapper + assert generic_wrapper is not None + # pyboy.game_area() + pyboy.stop() diff --git a/tests/test_game_wrapper_mario.py b/tests/test_game_wrapper_mario.py index 94ebe106e..b8cfaee7b 100644 --- a/tests/test_game_wrapper_mario.py +++ b/tests/test_game_wrapper_mario.py @@ -17,11 +17,11 @@ def test_mario_basics(supermarioland_rom): - pyboy = PyBoy(supermarioland_rom, window_type="null", game_wrapper=True) + pyboy = PyBoy(supermarioland_rom, window_type="null") pyboy.set_emulation_speed(0) assert pyboy.cartridge_title() == "SUPER MARIOLAN" - mario = pyboy.game_wrapper() + mario = pyboy.game_wrapper mario.start_game(world_level=(1, 1)) assert mario.score == 0 @@ -33,11 +33,11 @@ def test_mario_basics(supermarioland_rom): def test_mario_advanced(supermarioland_rom): - pyboy = PyBoy(supermarioland_rom, window_type="null", game_wrapper=True) + pyboy = PyBoy(supermarioland_rom, window_type="null") pyboy.set_emulation_speed(0) assert pyboy.cartridge_title() == "SUPER MARIOLAN" - mario = pyboy.game_wrapper() + mario = pyboy.game_wrapper mario.start_game(world_level=(3, 2)) lives = 99 mario.set_lives_left(lives) @@ -52,10 +52,10 @@ def test_mario_advanced(supermarioland_rom): def test_mario_game_over(supermarioland_rom): - pyboy = PyBoy(supermarioland_rom, window_type="null", game_wrapper=True) + pyboy = PyBoy(supermarioland_rom, window_type="null") pyboy.set_emulation_speed(0) - mario = pyboy.game_wrapper() + mario = pyboy.game_wrapper mario.start_game() mario.set_lives_left(0) pyboy.button_press("right") @@ -69,7 +69,7 @@ def test_mario_game_over(supermarioland_rom): @pytest.mark.skipif(is_pypy, reason="This requires gym, which doesn't work on this platform") class TestOpenAIGym: def test_observation_type_compressed(self, supermarioland_rom): - pyboy = PyBoy(supermarioland_rom, window_type="null", game_wrapper=True) + pyboy = PyBoy(supermarioland_rom, window_type="null") pyboy.set_emulation_speed(0) env = pyboy.openai_gym(observation_type="compressed") @@ -88,7 +88,7 @@ def test_observation_type_compressed(self, supermarioland_rom): assert np.all(observation == expected_observation) def test_observation_type_minimal(self, supermarioland_rom): - pyboy = PyBoy(supermarioland_rom, window_type="null", game_wrapper=True) + pyboy = PyBoy(supermarioland_rom, window_type="null") pyboy.set_emulation_speed(0) env = pyboy.openai_gym(observation_type="minimal") @@ -107,7 +107,7 @@ def test_observation_type_minimal(self, supermarioland_rom): assert np.all(observation == expected_observation) def test_start_level(self, supermarioland_rom): - pyboy = PyBoy(supermarioland_rom, window_type="null", game_wrapper=True) + pyboy = PyBoy(supermarioland_rom, window_type="null") pyboy.set_emulation_speed(0) starting_level = (2, 1) diff --git a/tests/test_mario_rl.py b/tests/test_mario_rl.py index 65159f32a..cd06a30ef 100644 --- a/tests/test_mario_rl.py +++ b/tests/test_mario_rl.py @@ -85,15 +85,15 @@ ### # Load emulator ### -pyboy = PyBoy(game, window_type="null", window_scale=3, debug=False, game_wrapper=True) +pyboy = PyBoy(game, window_type="null", window_scale=3, debug=False) ### # Load enviroment ### aiSettings = AISettingsInterface() -if pyboy.game_wrapper().cartridge_title == "SUPER MARIOLAN": +if pyboy.game_wrapper.cartridge_title == "SUPER MARIOLAN": aiSettings = MarioAI() -if pyboy.game_wrapper().cartridge_title == "KIRBY DREAM LA": +if pyboy.game_wrapper.cartridge_title == "KIRBY DREAM LA": aiSettings = KirbyAI() env = CustomPyBoyGym(pyboy, observation_type=observation_type) diff --git a/tests/test_openai_gym.py b/tests/test_openai_gym.py index 3048af651..2a2da5c35 100644 --- a/tests/test_openai_gym.py +++ b/tests/test_openai_gym.py @@ -19,7 +19,7 @@ @pytest.fixture def pyboy(tetris_rom): - pyboy = PyBoy(tetris_rom, window_type="null", disable_input=True, game_wrapper=True) + pyboy = PyBoy(tetris_rom, window_type="null", disable_input=True) pyboy.set_emulation_speed(0) return pyboy @@ -53,12 +53,12 @@ def test_raw(self, pyboy): def test_tiles(self, pyboy, tiles_id, id0_block, id1_block): env = pyboy.openai_gym(observation_type="tiles") - tetris = pyboy.game_wrapper() + tetris = pyboy.game_wrapper tetris.set_tetromino("Z") observation = env.reset() # Build the expected first observation - game_area_shape = pyboy.game_wrapper().shape[::-1] + game_area_shape = pyboy.game_wrapper.shape[::-1] expected_observation = tiles_id["BLANK"] * np.ones(game_area_shape, dtype=np.uint16) expected_observation[id0_block, id1_block] = tiles_id["Z"] print(observation, expected_observation) @@ -77,12 +77,12 @@ def test_tiles(self, pyboy, tiles_id, id0_block, id1_block): def test_compressed(self, pyboy, tiles_id, id0_block, id1_block): env = pyboy.openai_gym(observation_type="compressed") - tetris = pyboy.game_wrapper() + tetris = pyboy.game_wrapper tetris.set_tetromino("Z") observation = env.reset() # Build the expected first observation - game_area_shape = pyboy.game_wrapper().shape[::-1] + game_area_shape = pyboy.game_wrapper.shape[::-1] expected_observation = np.zeros(game_area_shape, dtype=np.uint16) expected_observation[id0_block, id1_block] = 2 print(observation, expected_observation) @@ -101,12 +101,12 @@ def test_compressed(self, pyboy, tiles_id, id0_block, id1_block): def test_minimal(self, pyboy, tiles_id, id0_block, id1_block): env = pyboy.openai_gym(observation_type="minimal") - tetris = pyboy.game_wrapper() + tetris = pyboy.game_wrapper tetris.set_tetromino("Z") observation = env.reset() # Build the expected first observation - game_area_shape = pyboy.game_wrapper().shape[::-1] + game_area_shape = pyboy.game_wrapper.shape[::-1] expected_observation = np.zeros(game_area_shape, dtype=np.uint16) expected_observation[id0_block, id1_block] = 1 print(observation, expected_observation) @@ -125,7 +125,7 @@ def test_minimal(self, pyboy, tiles_id, id0_block, id1_block): def test_press(self, pyboy, tiles_id, id0_block, id1_block): env = pyboy.openai_gym(observation_type="tiles", action_type="press") - tetris = pyboy.game_wrapper() + tetris = pyboy.game_wrapper tetris.set_tetromino("Z") assert env.action_space.n == 9 @@ -134,7 +134,7 @@ def test_press(self, pyboy, tiles_id, id0_block, id1_block): observation, _, _, _ = env.step(action) # Press RIGHT observation, _, _, _ = env.step(0) # Press NOTHING - game_area_shape = pyboy.game_wrapper().shape[::-1] + game_area_shape = pyboy.game_wrapper.shape[::-1] expected_observation = tiles_id["BLANK"] * np.ones(game_area_shape, dtype=np.uint16) expected_observation[id0_block, id1_block + 1] = tiles_id["Z"] print(observation, expected_observation) @@ -148,7 +148,7 @@ def test_press(self, pyboy, tiles_id, id0_block, id1_block): def test_toggle(self, pyboy, tiles_id, id0_block, id1_block): env = pyboy.openai_gym(observation_type="tiles", action_type="toggle") - tetris = pyboy.game_wrapper() + tetris = pyboy.game_wrapper tetris.set_tetromino("Z") assert env.action_space.n == 9 @@ -157,7 +157,7 @@ def test_toggle(self, pyboy, tiles_id, id0_block, id1_block): observation, _, _, _ = env.step(action) # Press RIGHT observation, _, _, _ = env.step(0) # Press NOTHING - game_area_shape = pyboy.game_wrapper().shape[::-1] + game_area_shape = pyboy.game_wrapper.shape[::-1] expected_observation = tiles_id["BLANK"] * np.ones(game_area_shape, dtype=np.uint16) expected_observation[id0_block, id1_block + 1] = tiles_id["Z"] print(observation, expected_observation) @@ -177,7 +177,7 @@ def test_all(self, pyboy): assert env.action_space.n == 17 def test_tetris(self, pyboy): - tetris = pyboy.game_wrapper() + tetris = pyboy.game_wrapper tetris.set_tetromino("I") tetris.start_game() @@ -226,7 +226,7 @@ def test_tetris(self, pyboy): pyboy.stop(save=False) def test_reward(self, pyboy): - tetris = pyboy.game_wrapper() + tetris = pyboy.game_wrapper tetris.set_tetromino("I") env = pyboy.openai_gym(action_type="all") diff --git a/tests/test_states.py b/tests/test_states.py index f5cede064..6d4042ac5 100644 --- a/tests/test_states.py +++ b/tests/test_states.py @@ -11,7 +11,7 @@ def test_load_save_consistency(tetris_rom): - pyboy = PyBoy(tetris_rom, window_type="null", game_wrapper=True) + pyboy = PyBoy(tetris_rom, window_type="null") assert pyboy.cartridge_title() == "TETRIS" pyboy.set_emulation_speed(0) pyboy.memory[NEXT_TETROMINO_ADDR] @@ -20,7 +20,7 @@ def test_load_save_consistency(tetris_rom): # Set up some kind of state, where not all registers are reset ############################################################## - tetris = pyboy.game_wrapper() + tetris = pyboy.game_wrapper timer_div = 0x00 saved_state = io.BytesIO() diff --git a/tests/test_tetris_ai.py b/tests/test_tetris_ai.py index d6f31c87f..57252de9c 100644 --- a/tests/test_tetris_ai.py +++ b/tests/test_tetris_ai.py @@ -50,9 +50,9 @@ def eval_network(epoch, child_index, child_model, record_to): - pyboy = PyBoy('tetris_1.1.gb', game_wrapper=True, window_type="null") + pyboy = PyBoy('tetris_1.1.gb', window_type="null") pyboy.set_emulation_speed(0) - tetris = pyboy.game_wrapper() + tetris = pyboy.game_wrapper tetris.start_game() pyboy._rendering(False) pyboy.send_input(WindowEvent.SCREEN_RECORDING_TOGGLE) From 0e3b8cd5ccc4453c6fb14dae88a8a06acba44793 Mon Sep 17 00:00:00 2001 From: Mads Ynddal <5528170+Baekalfen@users.noreply.github.com> Date: Thu, 14 Dec 2023 21:15:46 +0100 Subject: [PATCH 33/65] Remove OpenAI Gym --- pyboy/openai_gym.py | 174 ------------------ pyboy/pyboy.py | 33 ---- requirements_tests.txt | 1 - tests/test_game_wrapper_mario.py | 62 ------- tests/test_mario_rl.py | 1 - tests/test_openai_gym.py | 297 ------------------------------- 6 files changed, 568 deletions(-) delete mode 100644 pyboy/openai_gym.py delete mode 100644 tests/test_openai_gym.py diff --git a/pyboy/openai_gym.py b/pyboy/openai_gym.py deleted file mode 100644 index d6e405851..000000000 --- a/pyboy/openai_gym.py +++ /dev/null @@ -1,174 +0,0 @@ -# -# License: See LICENSE.md file -# GitHub: https://github.com/Baekalfen/PyBoy -# - -import numpy as np - -from .api.constants import TILES -from .utils import WindowEvent - -try: - from gym import Env - from gym.spaces import Box, Discrete, MultiDiscrete - 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)) != "": - 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.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.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(1, self.observation_type == "raw") - 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(1, self.observation_type == "raw") - - 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) diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index 3e164e36b..a8f516b48 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -12,8 +12,6 @@ from pyboy.api.screen import Screen from pyboy.api.tilemap import TileMap from pyboy.logging import get_logger -from pyboy.openai_gym import PyBoyGymEnv -from pyboy.openai_gym import enabled as gym_enabled from pyboy.plugins.manager import PluginManager from pyboy.utils import IntIOWrapper, WindowEvent @@ -368,37 +366,6 @@ def stop(self, save=True): # Scripts and bot methods # - def openai_gym(self, observation_type="tiles", action_type="press", simultaneous_actions=False, **kwargs): - """ - For Reinforcement learning, it is often easier to use the standard gym environment. This method will provide one. - This function requires PyBoy to implement a Game Wrapper for the loaded ROM. You can find the supported games in pyboy.plugins. - Additional kwargs are passed to the start_game method of the game_wrapper. - - Args: - observation_type (str): Define what the agent will be able to see: - * `"raw"`: Gives the raw pixels color - * `"tiles"`: Gives the id of the sprites in 8x8 pixel zones of the game_area defined by the game_wrapper. - * `"compressed"`: Gives a more detailled but heavier representation than `"minimal"`. - * `"minimal"`: Gives a minimal representation defined by the game_wrapper (recommended). - - action_type (str): Define how the agent will interact with button inputs - * `"press"`: The agent will only press inputs for 1 frame an then release it. - * `"toggle"`: The agent will toggle inputs, first time it press and second time it release. - * `"all"`: The agent have access to all inputs, press and release are separated. - - simultaneous_actions (bool): Allow to inject multiple input at once. This dramatically increases the action_space: \\(n \\rightarrow 2^n\\) - - Returns - ------- - `pyboy.openai_gym.PyBoyGymEnv`: - A Gym environment based on the `Pyboy` object. - """ - if gym_enabled: - return PyBoyGymEnv(self, observation_type, action_type, simultaneous_actions, **kwargs) - else: - logger.error("%s: Missing dependency \"gym\". ", __name__) - return None - def button(self, input): """ Send input to PyBoy in the form of "a", "b", "start", "select", "left", "right", "up" and "down". diff --git a/requirements_tests.txt b/requirements_tests.txt index de3f0f27d..4ab6cf953 100644 --- a/requirements_tests.txt +++ b/requirements_tests.txt @@ -5,7 +5,6 @@ pytest-xdist pytest-lazy-fixtures pyopengl PyOpenGL_accelerate -gym; platform_python_implementation == 'CPython' filelock cryptography GitPython diff --git a/tests/test_game_wrapper_mario.py b/tests/test_game_wrapper_mario.py index b8cfaee7b..1e2765363 100644 --- a/tests/test_game_wrapper_mario.py +++ b/tests/test_game_wrapper_mario.py @@ -64,65 +64,3 @@ def test_mario_game_over(supermarioland_rom): if mario.game_over(): break pyboy.stop() - - -@pytest.mark.skipif(is_pypy, reason="This requires gym, which doesn't work on this platform") -class TestOpenAIGym: - def test_observation_type_compressed(self, supermarioland_rom): - pyboy = PyBoy(supermarioland_rom, window_type="null") - pyboy.set_emulation_speed(0) - - env = pyboy.openai_gym(observation_type="compressed") - if env is None: - raise Exception("'env' is None. Did you remember to install 'gym'?") - observation = env.reset() - - expected_observation = np.zeros_like(observation) - expected_observation[-4:-2, 4:6] = 1 # Mario - expected_observation[-2:, :] = 10 # Ground - expected_observation[-4:-2, 1:3] = 14 # Pipe - expected_observation[9, 5] = 13 # ? Block - - print(observation) - print(expected_observation) - assert np.all(observation == expected_observation) - - def test_observation_type_minimal(self, supermarioland_rom): - pyboy = PyBoy(supermarioland_rom, window_type="null") - pyboy.set_emulation_speed(0) - - env = pyboy.openai_gym(observation_type="minimal") - if env is None: - raise Exception("'env' is None. Did you remember to install 'gym'?") - observation = env.reset() - - expected_observation = np.zeros_like(observation) - expected_observation[-4:-2, 4:6] = 1 # Mario - expected_observation[-2:, :] = 3 # Ground - expected_observation[-4:-2, 1:3] = 3 # Pipe - expected_observation[9, 5] = 3 # ? Block - - print(observation) - print(expected_observation) - assert np.all(observation == expected_observation) - - def test_start_level(self, supermarioland_rom): - pyboy = PyBoy(supermarioland_rom, window_type="null") - pyboy.set_emulation_speed(0) - - starting_level = (2, 1) - env = pyboy.openai_gym(observation_type="minimal", action_type="toggle", world_level=starting_level) - if env is None: - raise Exception("'env' is None. Did you remember to install 'gym'?") - observation = env.reset() - - print(env.game_wrapper.world, starting_level) - assert env.game_wrapper.world == starting_level - - env.game_wrapper.set_lives_left(0) - env.step(4) # PRESS LEFT - - for _ in range(200): - env.step(0) - assert env.game_wrapper.time_left == 399 - assert env.game_wrapper.world == starting_level diff --git a/tests/test_mario_rl.py b/tests/test_mario_rl.py index cd06a30ef..c985fa2b5 100644 --- a/tests/test_mario_rl.py +++ b/tests/test_mario_rl.py @@ -52,7 +52,6 @@ # Variables ### episodes = 40000 -# gym variables documentation: https://docs.pyboy.dk/openai_gym.html#pyboy.openai_gym.PyBoyGymEnv observation_types = ["raw", "tiles", "compressed", "minimal"] observation_type = observation_types[1] action_types = ["press", "toggle", "all"] diff --git a/tests/test_openai_gym.py b/tests/test_openai_gym.py deleted file mode 100644 index 2a2da5c35..000000000 --- a/tests/test_openai_gym.py +++ /dev/null @@ -1,297 +0,0 @@ -# -# License: See LICENSE.md file -# GitHub: https://github.com/Baekalfen/PyBoy -# - -import os -import platform -import sys - -import numpy as np -import pytest - -from pyboy import PyBoy, WindowEvent -from pyboy.api.constants import COLS, ROWS - -py_version = platform.python_version()[:3] -is_pypy = platform.python_implementation() == "PyPy" - - -@pytest.fixture -def pyboy(tetris_rom): - pyboy = PyBoy(tetris_rom, window_type="null", disable_input=True) - pyboy.set_emulation_speed(0) - return pyboy - - -@pytest.fixture -def id0_block(): - return np.array((1, 1, 2, 2)) - - -@pytest.fixture -def id1_block(): - return np.array((3, 4, 4, 5)) - - -@pytest.fixture -def tiles_id(): - return {"BLANK": 47, "Z": 130, "DEADBLOCK": 135} - - -@pytest.mark.skipif(is_pypy, reason="This requires gym, which doesn't work on this platform") -class TestOpenAIGym: - def test_raw(self, pyboy): - env = pyboy.openai_gym(observation_type="raw", action_type="press") - observation = env.reset() - assert observation.shape == (ROWS, COLS, 4) - assert observation.dtype == np.uint8 - - observation, _, _, _ = env.step(0) - assert observation.shape == (ROWS, COLS, 4) - assert observation.dtype == np.uint8 - - def test_tiles(self, pyboy, tiles_id, id0_block, id1_block): - env = pyboy.openai_gym(observation_type="tiles") - tetris = pyboy.game_wrapper - tetris.set_tetromino("Z") - observation = env.reset() - - # Build the expected first observation - game_area_shape = pyboy.game_wrapper.shape[::-1] - expected_observation = tiles_id["BLANK"] * np.ones(game_area_shape, dtype=np.uint16) - expected_observation[id0_block, id1_block] = tiles_id["Z"] - print(observation, expected_observation) - assert np.all(observation == expected_observation) - - expected_observation[id0_block, id1_block] = tiles_id["BLANK"] - - action = 2 # DOWN - observation, _, _, _ = env.step(action) # Press DOWN - observation, _, _, _ = env.step(action) # Press DOWN - - # Build the expected second observation - expected_observation[id0_block + 1, id1_block] = tiles_id["Z"] - print(observation, expected_observation) - assert np.all(observation == expected_observation) - - def test_compressed(self, pyboy, tiles_id, id0_block, id1_block): - env = pyboy.openai_gym(observation_type="compressed") - tetris = pyboy.game_wrapper - tetris.set_tetromino("Z") - observation = env.reset() - - # Build the expected first observation - game_area_shape = pyboy.game_wrapper.shape[::-1] - expected_observation = np.zeros(game_area_shape, dtype=np.uint16) - expected_observation[id0_block, id1_block] = 2 - print(observation, expected_observation) - assert np.all(observation == expected_observation) - - expected_observation[id0_block, id1_block] = 0 - - action = 2 # DOWN - observation, _, _, _ = env.step(action) # Press DOWN - observation, _, _, _ = env.step(action) # Press DOWN - - # Build the expected second observation - expected_observation[id0_block + 1, id1_block] = 2 - print(observation, expected_observation) - assert np.all(observation == expected_observation) - - def test_minimal(self, pyboy, tiles_id, id0_block, id1_block): - env = pyboy.openai_gym(observation_type="minimal") - tetris = pyboy.game_wrapper - tetris.set_tetromino("Z") - observation = env.reset() - - # Build the expected first observation - game_area_shape = pyboy.game_wrapper.shape[::-1] - expected_observation = np.zeros(game_area_shape, dtype=np.uint16) - expected_observation[id0_block, id1_block] = 1 - print(observation, expected_observation) - assert np.all(observation == expected_observation) - - expected_observation[id0_block, id1_block] = 0 - - action = 2 # DOWN - observation, _, _, _ = env.step(action) # Press DOWN - observation, _, _, _ = env.step(action) # Press DOWN - - # Build the expected second observation - expected_observation[id0_block + 1, id1_block] = 1 - print(observation, expected_observation) - assert np.all(observation == expected_observation) - - def test_press(self, pyboy, tiles_id, id0_block, id1_block): - env = pyboy.openai_gym(observation_type="tiles", action_type="press") - tetris = pyboy.game_wrapper - tetris.set_tetromino("Z") - assert env.action_space.n == 9 - - env.reset() - action = 3 # RIGHT - observation, _, _, _ = env.step(action) # Press RIGHT - observation, _, _, _ = env.step(0) # Press NOTHING - - game_area_shape = pyboy.game_wrapper.shape[::-1] - expected_observation = tiles_id["BLANK"] * np.ones(game_area_shape, dtype=np.uint16) - expected_observation[id0_block, id1_block + 1] = tiles_id["Z"] - print(observation, expected_observation) - assert np.all(observation == expected_observation) - - action = 0 # NOTHING - for _ in range(25): - observation, _, _, _ = env.step(action) # Press NOTHING - print(observation, expected_observation) - assert np.all(observation == expected_observation) - - def test_toggle(self, pyboy, tiles_id, id0_block, id1_block): - env = pyboy.openai_gym(observation_type="tiles", action_type="toggle") - tetris = pyboy.game_wrapper - tetris.set_tetromino("Z") - assert env.action_space.n == 9 - - env.reset() - action = 3 # RIGHT - observation, _, _, _ = env.step(action) # Press RIGHT - observation, _, _, _ = env.step(0) # Press NOTHING - - game_area_shape = pyboy.game_wrapper.shape[::-1] - expected_observation = tiles_id["BLANK"] * np.ones(game_area_shape, dtype=np.uint16) - expected_observation[id0_block, id1_block + 1] = tiles_id["Z"] - print(observation, expected_observation) - assert np.all(observation == expected_observation) - - expected_observation[id0_block, id1_block + 1] = tiles_id["BLANK"] - - action = 0 # NOTHING - for _ in range(25): - observation, _, _, _ = env.step(action) # Press NOTHING - print(observation, expected_observation) - expected_observation[id0_block, id1_block + 2] = tiles_id["Z"] - assert np.all(observation == expected_observation) - - def test_all(self, pyboy): - env = pyboy.openai_gym(observation_type="tiles", action_type="all") - assert env.action_space.n == 17 - - def test_tetris(self, pyboy): - tetris = pyboy.game_wrapper - tetris.set_tetromino("I") - tetris.start_game() - - assert tetris.score == 0 - assert tetris.lines == 0 - - for n in range(3): - pyboy.button("right") - pyboy.tick(2, True) - - pyboy.button_press("down") - while tetris.score == 0: - pyboy.tick(1, True) - pyboy.tick(2, True) - pyboy.button_release("down") - - for n in range(3): - pyboy.button("left") - pyboy.tick(2, True) - - tetris.set_tetromino("O") - pyboy.button_press("down") - while tetris.score == 16: - pyboy.tick(1, True) - pyboy.tick(2, True) - pyboy.button_release("down") - - pyboy.tick(1, True) - pyboy.button_press("down") - while tetris.score == 32: - pyboy.tick(1, True) - pyboy.tick(2, True) - pyboy.button_release("down") - - while tetris.score == 47: - pyboy.tick(1, True) - - pyboy.tick(2, True) - assert tetris.score == 87 - assert tetris.lines == 1 - - while not tetris.game_over(): - pyboy.button("down") - pyboy.tick(2, True) - - pyboy.stop(save=False) - - def test_reward(self, pyboy): - tetris = pyboy.game_wrapper - tetris.set_tetromino("I") - - env = pyboy.openai_gym(action_type="all") - env.reset() - - for n in range(3): - _, reward, _, _ = env.step(WindowEvent.PRESS_ARROW_RIGHT) - assert reward == 0 - _, reward, _, _ = env.step(WindowEvent.RELEASE_ARROW_RIGHT) - assert reward == 0 - - _, reward, _, _ = env.step(WindowEvent.PRESS_ARROW_DOWN) - while tetris.score == 0: - assert reward == 0 - _, reward, _, _ = env.step(0) - assert reward == 16 - - env.step(0) - env.step(0) - _, reward, _, _ = env.step(WindowEvent.RELEASE_ARROW_DOWN) - assert reward == 0 - - for n in range(3): - _, reward, _, _ = env.step(WindowEvent.PRESS_ARROW_LEFT) - assert reward == 0 - _, reward, _, _ = env.step(WindowEvent.RELEASE_ARROW_LEFT) - assert reward == 0 - - tetris.set_tetromino("O") - env.step(WindowEvent.PRESS_ARROW_DOWN) - while tetris.score == 16: - assert reward == 0 - _, reward, _, _ = env.step(0) - assert reward == 16 - - _, reward, _, _ = env.step(0) - assert reward == 0 - - env.step(0) - _, reward, _, _ = env.step(WindowEvent.RELEASE_ARROW_DOWN) - assert reward == 0 - - env.step(0) - env.step(WindowEvent.PRESS_ARROW_DOWN) - while tetris.score == 32: - assert reward == 0 - _, reward, _, _ = env.step(0) - assert reward == 15 - - env.step(0) - env.step(0) - _, reward, _, _ = env.step(WindowEvent.RELEASE_ARROW_DOWN) - - while tetris.score == 47: - assert reward == 0 - _, reward, _, _ = env.step(0) - assert reward == 40 - - env.step(0) - env.step(0) - assert tetris.score == 87 - assert tetris.lines == 1 - - while not tetris.game_over(): - env.step(WindowEvent.PRESS_ARROW_DOWN) - env.step(WindowEvent.RELEASE_ARROW_DOWN) - - env.close() From 4606675f30fb3bb36bab7ec0c7697551ffeb6636 Mon Sep 17 00:00:00 2001 From: NicoleFaye <41876584+NicoleFaye@users.noreply.github.com> Date: Sun, 17 Dec 2023 21:00:48 +0100 Subject: [PATCH 34/65] Add MemoryScanner --- pyboy/__init__.py | 4 +- pyboy/api/memory_scanner.pxd | 11 +++ pyboy/api/memory_scanner.py | 154 +++++++++++++++++++++++++++++++++++ pyboy/pyboy.pxd | 2 + pyboy/pyboy.py | 20 +++++ pyboy/utils.pxd | 9 ++ pyboy/utils.py | 52 ++++++++++++ 7 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 pyboy/api/memory_scanner.pxd create mode 100644 pyboy/api/memory_scanner.py diff --git a/pyboy/__init__.py b/pyboy/__init__.py index afa09db57..376745ae3 100644 --- a/pyboy/__init__.py +++ b/pyboy/__init__.py @@ -9,7 +9,7 @@ "pyboy": False, "utils": False, } -__all__ = ["PyBoy", "WindowEvent"] +__all__ = ["PyBoy", "WindowEvent", "dec_to_bcd", "bcd_to_dec"] from .pyboy import PyBoy -from .utils import WindowEvent +from .utils import WindowEvent, bcd_to_dec, dec_to_bcd diff --git a/pyboy/api/memory_scanner.pxd b/pyboy/api/memory_scanner.pxd new file mode 100644 index 000000000..551cba12f --- /dev/null +++ b/pyboy/api/memory_scanner.pxd @@ -0,0 +1,11 @@ +from libc.stdint cimport uint64_t + + +cdef class MemoryScanner: + cdef object pyboy + cdef dict _memory_cache + cdef int _memory_cache_byte_width + + cpdef list rescan_memory(self, object new_value=*, dynamic_comparison_type=*, byteorder=*) + cpdef list scan_memory(self, object target_value=*, int start_addr=*, int end_addr=*, standard_comparison_type=*, value_type=*, int byte_width=*, byteorder=*) noexcept + cpdef bint _check_value(self, uint64_t, uint64_t, int)noexcept diff --git a/pyboy/api/memory_scanner.py b/pyboy/api/memory_scanner.py new file mode 100644 index 000000000..69b05e49b --- /dev/null +++ b/pyboy/api/memory_scanner.py @@ -0,0 +1,154 @@ +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). + + 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. + + 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") diff --git a/pyboy/pyboy.pxd b/pyboy/pyboy.pxd index 95ef75fb7..bee92de61 100644 --- a/pyboy/pyboy.pxd +++ b/pyboy/pyboy.pxd @@ -8,6 +8,7 @@ cimport cython from libc cimport time from libc.stdint cimport int64_t, uint64_t +from pyboy.api.memory_scanner cimport MemoryScanner from pyboy.api.screen cimport Screen from pyboy.api.tilemap cimport TileMap from pyboy.core.mb cimport Motherboard @@ -53,6 +54,7 @@ cdef class PyBoy: cdef readonly TileMap tilemap_background cdef readonly TileMap tilemap_window cdef readonly object game_wrapper + cdef readonly MemoryScanner memory_scanner cdef bint limit_emulationspeed cdef int emulationspeed, target_emulationspeed, save_target_emulationspeed diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index a8f516b48..dd2c6e182 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -9,6 +9,7 @@ import os import time +from pyboy.api.memory_scanner import MemoryScanner from pyboy.api.screen import Screen from pyboy.api.tilemap import TileMap from pyboy.logging import get_logger @@ -134,6 +135,25 @@ def __init__( ``` """ + 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: + ``` + >>> 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 + >>> current_score = 8 # You write the new score in game + >>> 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 diff --git a/pyboy/utils.pxd b/pyboy/utils.pxd index f88f1b365..2c7448581 100644 --- a/pyboy/utils.pxd +++ b/pyboy/utils.pxd @@ -47,3 +47,12 @@ cdef class WindowEventMouse(WindowEvent): cdef readonly int mouse_scroll_x cdef readonly int mouse_scroll_y cdef readonly int mouse_button + + +############################################################## +# Memory Scanning +# + +cpdef uint64_t dec_to_bcd(uint64_t,int byte_width=*,byteorder=*) noexcept +@cython.locals(decimal_value=uint64_t, multiplier=uint64_t) +cpdef uint64_t bcd_to_dec(uint64_t,int byte_width=*,byteorder=*) noexcept diff --git a/pyboy/utils.py b/pyboy/utils.py index a333a1ec9..8d38f742f 100644 --- a/pyboy/utils.py +++ b/pyboy/utils.py @@ -2,6 +2,7 @@ # License: See LICENSE.md file # GitHub: https://github.com/Baekalfen/PyBoy # +from enum import Enum STATE_VERSION = 9 @@ -278,3 +279,54 @@ def __init__( self.mouse_scroll_x = mouse_scroll_x self.mouse_scroll_y = mouse_scroll_y self.mouse_button = mouse_button + + +############################################################## +# Memory Scanning +# + + +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. + + 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. + + 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 From 3e7484a2869fc225ca556b143f6e8965c3ef7983 Mon Sep 17 00:00:00 2001 From: NicoleFaye <41876584+NicoleFaye@users.noreply.github.com> Date: Sun, 17 Dec 2023 21:04:15 +0100 Subject: [PATCH 35/65] Add CGB palette and bank number support for api/sprite.py --- pyboy/api/sprite.pxd | 3 ++- pyboy/api/sprite.py | 19 ++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/pyboy/api/sprite.pxd b/pyboy/api/sprite.pxd index bfbba208c..85ed8af29 100644 --- a/pyboy/api/sprite.pxd +++ b/pyboy/api/sprite.pxd @@ -19,7 +19,8 @@ cdef class Sprite: cdef readonly bint attr_obj_bg_priority cdef readonly bint attr_y_flip cdef readonly bint attr_x_flip - cdef readonly bint attr_palette_number + cdef readonly int attr_palette_number + cdef readonly bint attr_cgb_bank_number cdef readonly tuple shape cdef readonly list tiles cdef readonly bint on_screen diff --git a/pyboy/api/sprite.py b/pyboy/api/sprite.py index 99eb0dfd8..6847e7a0f 100644 --- a/pyboy/api/sprite.py +++ b/pyboy/api/sprite.py @@ -116,7 +116,18 @@ def __init__(self, mb, sprite_index): 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)](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). @@ -127,6 +138,12 @@ def __init__(self, mb, sprite_index): 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._get_sprite_height() else 8 self.shape = (8, sprite_height) From e4db84ceb624fca877b19b886228d4fbb345d289 Mon Sep 17 00:00:00 2001 From: NicoleFaye <41876584+NicoleFaye@users.noreply.github.com> Date: Sun, 17 Dec 2023 21:05:04 +0100 Subject: [PATCH 36/65] Added .vscode to gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a47a7e175..c8b3996d9 100644 --- a/.gitignore +++ b/.gitignore @@ -96,4 +96,5 @@ test.replay /mooneye/ /blargg/ /GB Tests/ -/SameSuite/ \ No newline at end of file +/SameSuite/ +.vscode/ From 23cf042208f473b59f0b41db3758dbb9b4611a92 Mon Sep 17 00:00:00 2001 From: NicoleFaye <41876584+NicoleFaye@users.noreply.github.com> Date: Sun, 17 Dec 2023 22:01:57 +0100 Subject: [PATCH 37/65] Add MemoryScanner and bcd tests --- tests/test_memoryscanner.py | 126 ++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 tests/test_memoryscanner.py diff --git a/tests/test_memoryscanner.py b/tests/test_memoryscanner.py new file mode 100644 index 000000000..330ded236 --- /dev/null +++ b/tests/test_memoryscanner.py @@ -0,0 +1,126 @@ +from pyboy import PyBoy, bcd_to_dec, dec_to_bcd +from pyboy.api.memory_scanner import DynamicComparisonType + + +def test_bcd_to_dec_single_byte(): + assert bcd_to_dec(0b00110000) == 30 + assert bcd_to_dec(0b00110001) == 31 + + +def test_dec_to_bcd_single_byte(): + assert dec_to_bcd(30) == 0b00110000 + assert dec_to_bcd(32) == 0b00110010 + + +def test_bcd_to_dec_multi_byte(): + assert bcd_to_dec(0b0011010000110010, byte_width=2) == 3432 + assert bcd_to_dec(0b0011010000110010, byte_width=2, byteorder="big") == 3234 + + +def test_dec_to_bcd_multi_byte(): + assert dec_to_bcd(3034, byte_width=2) == 0b0011000000110100 + + +def test_bcd_to_dec_complex(): + assert bcd_to_dec(0b011101100101010010001001, byte_width=3) == 765489 + + +def test_dec_to_bcd_complex(): + assert dec_to_bcd(765489, byte_width=3) == 0b011101100101010010001001 + + +def test_bcd_to_dec_complex2(): + assert bcd_to_dec(0b10000000000000000000000000000000001000000, byte_width=6, byteorder="little") == 10000000040 + + +def test_memoryscanner_basic(default_rom): + pyboy = PyBoy(default_rom, window_type="null", game_wrapper=True) + pyboy.set_emulation_speed(0) + addresses = pyboy.memory_scanner.scan_memory() + assert len(addresses) == 0x10000 + addresses = pyboy.memory_scanner.scan_memory(start_addr=0xC000, end_addr=0xDFFF) + assert len(addresses) == 0xDFFF - 0xC000 + 1 + addresses = pyboy.memory_scanner.rescan_memory(None, DynamicComparisonType.INCREASED) + assert len(addresses) == 0 + + +def test_memoryscanner_boundary(default_rom): + pyboy = PyBoy(default_rom, window_type="null", game_wrapper=True) + pyboy.set_emulation_speed(0) + + # Byte width of 1 + pyboy.memory[0xC000:0xD000] = 0 + addresses = pyboy.memory_scanner.scan_memory(0, start_addr=0xC000, end_addr=0xC0FF, byte_width=1) + assert len(addresses) == 0x100 # We scan all two-byte addresses from 0, 1, 2, ..., n (including n) + + pyboy.memory[0xC0FF] = 1 + addresses = pyboy.memory_scanner.scan_memory(0, start_addr=0xC000, end_addr=0xC0FF, byte_width=1) + assert len(addresses) == 0x100 - 1 # Where one value is now not 0 + + # Byte width of 2 + pyboy.memory[0xC000:0xD000] = 0 + addresses = pyboy.memory_scanner.scan_memory(0, start_addr=0xC000, end_addr=0xC0FF, byte_width=2) + assert len( + addresses + ) == 0x100 - 1 # We scan all two-byte addresses from 0, 1, 2, ..., n-1 (excluding n, as we can't go to n+1 for the second byte) + + pyboy.memory[0xC0FF] = 1 + addresses = pyboy.memory_scanner.scan_memory(0, start_addr=0xC000, end_addr=0xC0FF, byte_width=2) + assert len(addresses) == 0x100 - 1 - 1 # Where one value is now not 0, and we cannot get n+1 + + +SCORE_100 = 0xD072 + + +def test_memoryscanner_absolute(kirby_rom): + pyboy = PyBoy(kirby_rom, window_type="null", game_wrapper=True) + pyboy.set_emulation_speed(0) + kirby = pyboy.game_wrapper + kirby.start_game() + + # Find score address + pyboy.tick(1, True) + pyboy.button_press("right") + + pyboy.tick(175, True) + assert pyboy.memory[SCORE_100] == 4 + addresses = pyboy.memory_scanner.scan_memory(4, start_addr=0xC000, end_addr=0xDFFF) + assert SCORE_100 in addresses + values = [pyboy.memory[x] for x in addresses] + assert all([x == 4 for x in values]) + + pyboy.tick(175, True) + assert pyboy.memory[SCORE_100] == 8 + addresses = pyboy.memory_scanner.rescan_memory(8, DynamicComparisonType.MATCH) + values = [pyboy.memory[x] for x in addresses] + assert all([x == 8 for x in values]) + + # Actual score address for third digit + assert SCORE_100 in addresses + assert pyboy.memory[SCORE_100] == 8 + + +def test_memoryscanner_relative(kirby_rom): + pyboy = PyBoy(kirby_rom, window_type="null", game_wrapper=True) + pyboy.set_emulation_speed(0) + kirby = pyboy.game_wrapper + kirby.start_game() + + # Find score address + pyboy.tick(1, True) + score = 0 + addresses = pyboy.memory_scanner.scan_memory(score, start_addr=0xC000, end_addr=0xDFFF) + assert len(addresses) > 1000, "We expected more values to be 0" + pyboy.button_press("right") + + pyboy.tick(175, True) + addresses1 = pyboy.memory_scanner.rescan_memory(None, DynamicComparisonType.INCREASED) + values1 = [pyboy.memory[x] for x in addresses1] + + pyboy.tick(175, True) + addresses2 = pyboy.memory_scanner.rescan_memory(None, DynamicComparisonType.INCREASED) + values2 = [pyboy.memory[x] for x in addresses2] + + # Actual score address for third digit + assert SCORE_100 in addresses2 + assert pyboy.memory[SCORE_100] == 8 From 7a513096aa2ccda507beefa8dbfecae8f37cde4b Mon Sep 17 00:00:00 2001 From: Mads Ynddal <5528170+Baekalfen@users.noreply.github.com> Date: Thu, 4 Jan 2024 19:40:21 +0100 Subject: [PATCH 38/65] Fix Kirby game wrapper score calculation --- pyboy/plugins/game_wrapper_kirby_dream_land.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyboy/plugins/game_wrapper_kirby_dream_land.py b/pyboy/plugins/game_wrapper_kirby_dream_land.py index e98f2069d..d730c3c62 100644 --- a/pyboy/plugins/game_wrapper_kirby_dream_land.py +++ b/pyboy/plugins/game_wrapper_kirby_dream_land.py @@ -49,9 +49,9 @@ def post_tick(self): self._sprite_cache_invalid = True self.score = 0 - score_digits = 5 + score_digits = 4 for n in range(score_digits): - self.score += self.pyboy.memory[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 From dda136349b60bb6f69648ec150d95cd7aa6e17c9 Mon Sep 17 00:00:00 2001 From: Mads Ynddal <5528170+Baekalfen@users.noreply.github.com> Date: Thu, 4 Jan 2024 19:39:22 +0100 Subject: [PATCH 39/65] Change pyboy.cartridge_title() to property --- extras/examples/gamewrapper_kirby.py | 2 +- extras/examples/gamewrapper_mario.py | 2 +- extras/examples/gamewrapper_tetris.py | 2 +- pyboy/plugins/base_plugin.py | 2 +- pyboy/plugins/game_wrapper_pokemon_gen1.py | 2 +- pyboy/plugins/screen_recorder.py | 2 +- pyboy/plugins/screenshot_recorder.py | 2 +- pyboy/pyboy.pxd | 1 + pyboy/pyboy.py | 23 +++++++++++----------- tests/test_game_wrapper_mario.py | 4 ++-- tests/test_replay.py | 2 +- tests/test_states.py | 2 +- tests/test_tetris_ai.py | 2 +- 13 files changed, 24 insertions(+), 24 deletions(-) diff --git a/extras/examples/gamewrapper_kirby.py b/extras/examples/gamewrapper_kirby.py index 41aa5ab26..e9557b1c1 100644 --- a/extras/examples/gamewrapper_kirby.py +++ b/extras/examples/gamewrapper_kirby.py @@ -22,7 +22,7 @@ quiet = "--quiet" in sys.argv pyboy = PyBoy(filename, window_type="null" if quiet else "SDL2", window_scale=3, debug=not quiet, game_wrapper=True) pyboy.set_emulation_speed(0) -assert pyboy.cartridge_title() == "KIRBY DREAM LA" +assert pyboy.cartridge_title == "KIRBY DREAM LA" kirby = pyboy.game_wrapper kirby.start_game() diff --git a/extras/examples/gamewrapper_mario.py b/extras/examples/gamewrapper_mario.py index 5cd678e8d..5003c9836 100644 --- a/extras/examples/gamewrapper_mario.py +++ b/extras/examples/gamewrapper_mario.py @@ -22,7 +22,7 @@ quiet = "--quiet" in sys.argv pyboy = PyBoy(filename, window_type="null" if quiet else "SDL2", window_scale=3, debug=not quiet, game_wrapper=True) pyboy.set_emulation_speed(0) -assert pyboy.cartridge_title() == "SUPER MARIOLAN" +assert pyboy.cartridge_title == "SUPER MARIOLAN" mario = pyboy.game_wrapper mario.start_game() diff --git a/extras/examples/gamewrapper_tetris.py b/extras/examples/gamewrapper_tetris.py index 7d807eadd..ab593f543 100644 --- a/extras/examples/gamewrapper_tetris.py +++ b/extras/examples/gamewrapper_tetris.py @@ -22,7 +22,7 @@ quiet = "--quiet" in sys.argv pyboy = PyBoy(filename, window_type="null" if quiet else "SDL2", window_scale=3, debug=not quiet, game_wrapper=True) pyboy.set_emulation_speed(0) -assert pyboy.cartridge_title() == "TETRIS" +assert pyboy.cartridge_title == "TETRIS" tetris = pyboy.game_wrapper tetris.start_game(timer_div=0x00) # The timer_div works like a random seed in Tetris diff --git a/pyboy/plugins/base_plugin.py b/pyboy/plugins/base_plugin.py index 663ff7688..86c634246 100644 --- a/pyboy/plugins/base_plugin.py +++ b/pyboy/plugins/base_plugin.py @@ -119,7 +119,7 @@ def __init__(self, *args, game_area_section=(0, 0, 32, 32), game_area_wrap_aroun self._cached_game_area_tiles = memoryview(self._cached_game_area_tiles_raw).cast("I", shape=(width, height)) def enabled(self): - return self.cartridge_title is None or self.pyboy.cartridge_title() == self.cartridge_title + return self.cartridge_title is None or self.pyboy.cartridge_title == self.cartridge_title def post_tick(self): raise NotImplementedError("post_tick not implemented in game wrapper") diff --git a/pyboy/plugins/game_wrapper_pokemon_gen1.py b/pyboy/plugins/game_wrapper_pokemon_gen1.py index 12fa4cd76..d2717082f 100644 --- a/pyboy/plugins/game_wrapper_pokemon_gen1.py +++ b/pyboy/plugins/game_wrapper_pokemon_gen1.py @@ -32,7 +32,7 @@ def __init__(self, *args, **kwargs): self.sprite_offset = 0x1000 def enabled(self): - return (self.pyboy.cartridge_title() == "POKEMON RED") or (self.pyboy.cartridge_title() == "POKEMON BLUE") + return (self.pyboy.cartridge_title == "POKEMON RED") or (self.pyboy.cartridge_title == "POKEMON BLUE") def post_tick(self): self._tile_cache_invalid = True diff --git a/pyboy/plugins/screen_recorder.py b/pyboy/plugins/screen_recorder.py index 8f38b5a14..68fd6b41b 100644 --- a/pyboy/plugins/screen_recorder.py +++ b/pyboy/plugins/screen_recorder.py @@ -55,7 +55,7 @@ def save(self, path=None, fps=60): directory = os.path.join(os.path.curdir, "recordings") if not os.path.exists(directory): os.makedirs(directory, mode=0o755) - path = os.path.join(directory, time.strftime(f"{self.pyboy.cartridge_title()}-%Y.%m.%d-%H.%M.%S.gif")) + path = os.path.join(directory, time.strftime(f"{self.pyboy.cartridge_title}-%Y.%m.%d-%H.%M.%S.gif")) if len(self.frames) > 0: self.frames[0].save( diff --git a/pyboy/plugins/screenshot_recorder.py b/pyboy/plugins/screenshot_recorder.py index b72cd4517..2cb37fd9d 100644 --- a/pyboy/plugins/screenshot_recorder.py +++ b/pyboy/plugins/screenshot_recorder.py @@ -35,7 +35,7 @@ def save(self, path=None): directory = os.path.join(os.path.curdir, "screenshots") if not os.path.exists(directory): os.makedirs(directory, mode=0o755) - path = os.path.join(directory, time.strftime(f"{self.pyboy.cartridge_title()}-%Y.%m.%d-%H.%M.%S.png")) + path = os.path.join(directory, time.strftime(f"{self.pyboy.cartridge_title}-%Y.%m.%d-%H.%M.%S.png")) self.pyboy.screen.image.save(path) diff --git a/pyboy/pyboy.pxd b/pyboy/pyboy.pxd index bee92de61..247abbc60 100644 --- a/pyboy/pyboy.pxd +++ b/pyboy/pyboy.pxd @@ -55,6 +55,7 @@ cdef class PyBoy: cdef readonly TileMap tilemap_window cdef readonly object game_wrapper cdef readonly MemoryScanner memory_scanner + cdef readonly str cartridge_title cdef bint limit_emulationspeed cdef int emulationspeed, target_emulationspeed, save_target_emulationspeed diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index dd2c6e182..c3b0e22a4 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -180,6 +180,17 @@ def __init__( 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. + + Returns + ------- + str : + Game title + """ + self._plugin_manager = PluginManager(self, self.mb, kwargs) """ Returns @@ -607,18 +618,6 @@ def set_emulation_speed(self, target_speed): logger.warning("The emulation speed might not be accurate when speed-target is higher than 5") self.target_emulationspeed = target_speed - def cartridge_title(self): - """ - Get the title stored on the currently loaded cartridge ROM. The title is all upper-case ASCII and may - have been truncated to 11 characters. - - Returns - ------- - str : - Game title - """ - return self.mb.cartridge.gamename - def __rendering(self, value): """ Disable or enable rendering diff --git a/tests/test_game_wrapper_mario.py b/tests/test_game_wrapper_mario.py index 1e2765363..238628cb2 100644 --- a/tests/test_game_wrapper_mario.py +++ b/tests/test_game_wrapper_mario.py @@ -19,7 +19,7 @@ def test_mario_basics(supermarioland_rom): pyboy = PyBoy(supermarioland_rom, window_type="null") pyboy.set_emulation_speed(0) - assert pyboy.cartridge_title() == "SUPER MARIOLAN" + assert pyboy.cartridge_title == "SUPER MARIOLAN" mario = pyboy.game_wrapper mario.start_game(world_level=(1, 1)) @@ -35,7 +35,7 @@ def test_mario_basics(supermarioland_rom): def test_mario_advanced(supermarioland_rom): pyboy = PyBoy(supermarioland_rom, window_type="null") pyboy.set_emulation_speed(0) - assert pyboy.cartridge_title() == "SUPER MARIOLAN" + assert pyboy.cartridge_title == "SUPER MARIOLAN" mario = pyboy.game_wrapper mario.start_game(world_level=(3, 2)) diff --git a/tests/test_replay.py b/tests/test_replay.py index 0bbdf158a..e3eb78ef6 100644 --- a/tests/test_replay.py +++ b/tests/test_replay.py @@ -142,7 +142,7 @@ def replay( recording ^= True if gif_destination: - move_gif(pyboy.cartridge_title(), gif_destination) + move_gif(pyboy.cartridge_title, gif_destination) if gif_hash is not None and not overwrite and sys.platform == "darwin": verify_file_hash(gif_destination, gif_hash) diff --git a/tests/test_states.py b/tests/test_states.py index 6d4042ac5..d7ebd9742 100644 --- a/tests/test_states.py +++ b/tests/test_states.py @@ -12,7 +12,7 @@ def test_load_save_consistency(tetris_rom): pyboy = PyBoy(tetris_rom, window_type="null") - assert pyboy.cartridge_title() == "TETRIS" + assert pyboy.cartridge_title == "TETRIS" pyboy.set_emulation_speed(0) pyboy.memory[NEXT_TETROMINO_ADDR] diff --git a/tests/test_tetris_ai.py b/tests/test_tetris_ai.py index 57252de9c..8ce64afee 100644 --- a/tests/test_tetris_ai.py +++ b/tests/test_tetris_ai.py @@ -131,7 +131,7 @@ def eval_network(epoch, child_index, child_model, record_to): directory = os.path.join(os.path.curdir, "recordings") if not os.path.exists(directory): os.makedirs(directory, mode=0o755) - path = os.path.join(directory, time.strftime(f"{pyboy.cartridge_title()}-%Y.%m.%d-%H.%M.%S.gif")) + path = os.path.join(directory, time.strftime(f"{pyboy.cartridge_title}-%Y.%m.%d-%H.%M.%S.gif")) frames[0].save( path, From 73060842ac544a9ce5363f58ba775d57c19c1f34 Mon Sep 17 00:00:00 2001 From: Mads Ynddal <5528170+Baekalfen@users.noreply.github.com> Date: Fri, 27 Oct 2023 23:03:00 +0200 Subject: [PATCH 40/65] Refactor game_area code for v2.0.0 --- pyboy/plugins/__init__.py | 16 +-- pyboy/plugins/base_plugin.pxd | 12 +- pyboy/plugins/base_plugin.py | 47 +++---- .../plugins/game_wrapper_kirby_dream_land.py | 2 +- pyboy/plugins/game_wrapper_pokemon_gen1.py | 6 +- .../plugins/game_wrapper_super_mario_land.py | 21 ++-- pyboy/plugins/game_wrapper_tetris.py | 25 ++-- pyboy/plugins/manager.pxd | 7 +- pyboy/plugins/manager.py | 12 +- pyboy/pyboy.py | 116 +++++++++++++++++- pyboy/utils.py | 1 + tests/conftest.py | 5 + tests/test_examples.py | 4 +- tests/test_game_wrapper.py | 54 +++++++- 14 files changed, 252 insertions(+), 76 deletions(-) diff --git a/pyboy/plugins/__init__.py b/pyboy/plugins/__init__.py index b8afa8093..bc4e61598 100644 --- a/pyboy/plugins/__init__.py +++ b/pyboy/plugins/__init__.py @@ -8,18 +8,18 @@ __pdoc__ = { # docs exclude - "window_open_gl": False, - "window_sdl2": False, - "auto_pause": False, "disable_input": False, "rewind": False, - "manager_gen": False, + "window_sdl2": False, + "screenshot_recorder": False, + "debug_prompt": False, + "screen_recorder": False, "debug": False, + "manager": False, "record_replay": False, + "manager_gen": False, + "window_open_gl": False, + "auto_pause": False, "window_null": False, - "manager": False, - "debug_prompt": False, - "screenshot_recorder": False, - "screen_recorder": False, # docs exclude end } diff --git a/pyboy/plugins/base_plugin.pxd b/pyboy/plugins/base_plugin.pxd index 40d4fcdc7..6f555480c 100644 --- a/pyboy/plugins/base_plugin.pxd +++ b/pyboy/plugins/base_plugin.pxd @@ -4,6 +4,7 @@ # cimport cython +cimport numpy as cnp from cpython.array cimport array from libc.stdint cimport uint8_t, uint16_t, uint32_t @@ -48,17 +49,18 @@ cdef class PyBoyGameWrapper(PyBoyPlugin): cdef object tilemap_window cdef bint tilemap_use_background cdef uint16_t sprite_offset + cdef object mapping cdef bint _tile_cache_invalid cdef array _cached_game_area_tiles_raw - cdef uint32_t[:, :] _cached_game_area_tiles + cdef object _cached_game_area_tiles @cython.locals(xx=int, yy=int, width=int, height=int, SCX=int, SCY=int, _x=int, _y=int) - cdef uint32_t[:, :] _game_area_tiles(self) noexcept + cdef cnp.ndarray[cnp.uint32_t, ndim=2] _game_area_tiles(self) noexcept - cdef bint game_area_wrap_around + cdef bint game_area_follow_scxy cdef tuple game_area_section - @cython.locals(tiles_matrix=uint32_t[:, :], sprites=list, xx=int, yy=int, width=int, height=int, _x=int, _y=int) - cpdef uint32_t[:, :] game_area(self) noexcept + @cython.locals(tiles_matrix=cnp.ndarray, sprites=list, xx=int, yy=int, width=int, height=int, _x=int, _y=int) + cpdef cnp.ndarray[cnp.uint32_t, ndim=2] game_area(self) noexcept cdef bint _sprite_cache_invalid cdef list _cached_sprites_on_screen diff --git a/pyboy/plugins/base_plugin.py b/pyboy/plugins/base_plugin.py index 86c634246..b6a049127 100644 --- a/pyboy/plugins/base_plugin.py +++ b/pyboy/plugins/base_plugin.py @@ -98,18 +98,20 @@ class PyBoyGameWrapper(PyBoyPlugin): cartridge_title = None argv = [("--game-wrapper", {"action": "store_true", "help": "Enable game wrapper for the current game"})] - def __init__(self, *args, game_area_section=(0, 0, 32, 32), game_area_wrap_around=False, **kwargs): + def __init__(self, *args, game_area_section=(0, 0, 32, 32), game_area_follow_scxy=False, **kwargs): super().__init__(*args, **kwargs) - self.tilemap_background = self.pyboy.tilemap_background - self.tilemap_window = self.pyboy.tilemap_window + if not cythonmode: + self.tilemap_background = self.pyboy.tilemap_background + self.tilemap_window = self.pyboy.tilemap_window self.tilemap_use_background = True + self.mapping = np.asarray([x for x in range(768)], dtype=np.uint32) self.sprite_offset = 0 self.game_has_started = False self._tile_cache_invalid = True self._sprite_cache_invalid = True self.game_area_section = game_area_section - self.game_area_wrap_around = game_area_wrap_around + self.game_area_follow_scxy = game_area_follow_scxy width = self.game_area_section[2] - self.game_area_section[0] height = self.game_area_section[3] - self.game_area_section[1] self._cached_game_area_tiles_raw = array("B", [0xFF] * (width*height*4)) @@ -118,11 +120,16 @@ def __init__(self, *args, game_area_section=(0, 0, 32, 32), game_area_wrap_aroun self._cached_game_area_tiles = memoryview(self._cached_game_area_tiles_raw).cast("I", shape=(width, height)) + def __cinit__(self, pyboy, mb, pyboy_argv, *args, **kwargs): + self.tilemap_background = self.pyboy.tilemap_background + self.tilemap_window = self.pyboy.tilemap_window + def enabled(self): return self.cartridge_title is None or self.pyboy.cartridge_title == self.cartridge_title def post_tick(self): - raise NotImplementedError("post_tick not implemented in game wrapper") + self._tile_cache_invalid = True + self._sprite_cache_invalid = True def _set_timer_div(self, timer_div): if timer_div is None: @@ -186,7 +193,7 @@ def _game_area_tiles(self): height = self.game_area_section[3] scanline_parameters = self.pyboy.screen.tilemap_position_list - if self.game_area_wrap_around: + if self.game_area_follow_scxy: self._cached_game_area_tiles = np.ndarray(shape=(height, width), dtype=np.uint32) for y in range(height): SCX = scanline_parameters[(yy+y) * 8][0] // 8 @@ -222,7 +229,7 @@ def game_area(self): 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] @@ -232,29 +239,13 @@ def game_area(self): _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 - def _game_area_np(self, observation_type="tiles"): - if observation_type == "tiles": - return np.asarray(self.game_area(), dtype=np.uint16) - elif observation_type == "compressed": - try: - return self.tiles_compressed[np.asarray(self.game_area(), dtype=np.uint16)] - except AttributeError: - raise AttributeError( - f"Game wrapper miss the attribute tiles_compressed for observation_type : {observation_type}" - ) - elif observation_type == "minimal": - try: - return self.tiles_minimal[np.asarray(self.game_area(), dtype=np.uint16)] - except AttributeError: - raise AttributeError( - f"Game wrapper miss the attribute tiles_minimal for observation_type : {observation_type}" - ) - else: - raise ValueError(f"Invalid observation_type : {observation_type}") + def game_area_mapping(self, mapping, sprite_offest): + self.mapping = np.asarray(mapping, dtype=np.uint32) + self.sprite_offset = sprite_offest def _sum_number_on_screen(self, x, y, length, blank_tile_identifier, tile_identifier_offset): number = 0 diff --git a/pyboy/plugins/game_wrapper_kirby_dream_land.py b/pyboy/plugins/game_wrapper_kirby_dream_land.py index d730c3c62..ae28a4f41 100644 --- a/pyboy/plugins/game_wrapper_kirby_dream_land.py +++ b/pyboy/plugins/game_wrapper_kirby_dream_land.py @@ -42,7 +42,7 @@ def __init__(self, *args, **kwargs): .. 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 diff --git a/pyboy/plugins/game_wrapper_pokemon_gen1.py b/pyboy/plugins/game_wrapper_pokemon_gen1.py index d2717082f..b116bf8d9 100644 --- a/pyboy/plugins/game_wrapper_pokemon_gen1.py +++ b/pyboy/plugins/game_wrapper_pokemon_gen1.py @@ -28,8 +28,8 @@ class GameWrapperPokemonGen1(PyBoyGameWrapper): def __init__(self, *args, **kwargs): self.shape = (20, 18) - super().__init__(*args, game_area_section=(0, 0) + self.shape, game_area_wrap_around=True, **kwargs) - self.sprite_offset = 0x1000 + super().__init__(*args, game_area_section=(0, 0) + self.shape, game_area_follow_scxy=True, **kwargs) + self.sprite_offset = 0 def enabled(self): return (self.pyboy.cartridge_title == "POKEMON RED") or (self.pyboy.cartridge_title == "POKEMON BLUE") @@ -44,7 +44,7 @@ def post_tick(self): self.use_background(WY != 0) def _get_screen_background_tilemap(self): - ### SIMILAR TO CURRENT pyboy.game_wrapper._game_area_np(), BUT ONLY FOR BACKGROUND TILEMAP, SO NPC ARE SKIPPED + ### SIMILAR TO CURRENT pyboy.game_wrapper.game_area(), BUT ONLY FOR BACKGROUND TILEMAP, SO NPC ARE SKIPPED bsm = self.pyboy.botsupport_manager() ((scx, scy), (wx, wy)) = bsm.screen().tilemap_position() tilemap = np.array(bsm.tilemap_background[:, :]) diff --git a/pyboy/plugins/game_wrapper_super_mario_land.py b/pyboy/plugins/game_wrapper_super_mario_land.py index 14398a737..75c5ba53b 100644 --- a/pyboy/plugins/game_wrapper_super_mario_land.py +++ b/pyboy/plugins/game_wrapper_super_mario_land.py @@ -60,7 +60,7 @@ spike = [237] TILES = 384 -tiles_minimal = np.zeros(TILES, dtype=np.uint8) +mapping_minimal = np.zeros(TILES, dtype=np.uint8) minimal_list = [ base_scripts + plane + submarine, coin + mushroom + heart + star + lever, @@ -70,9 +70,9 @@ ] for i, tile_list in enumerate(minimal_list): for tile in tile_list: - tiles_minimal[tile] = i + 1 + mapping_minimal[tile] = i + 1 -tiles_compressed = np.zeros(TILES, dtype=np.uint8) +mapping_compressed = np.zeros(TILES, dtype=np.uint8) compressed_list = [ base_scripts, plane, submarine, shoots, coin, mushroom, heart, star, lever, neutral_blocks, moving_blocks, pushable_blokcs, question_block, pipes, goomba, koopa, plant, moth, flying_moth, sphinx, big_sphinx, fist, bill, @@ -80,7 +80,7 @@ ] for i, tile_list in enumerate(compressed_list): for tile in tile_list: - tiles_compressed[tile] = i + 1 + mapping_compressed[tile] = i + 1 np_in_mario_tiles = np.vectorize(lambda x: x in base_scripts) @@ -106,9 +106,14 @@ class GameWrapperSuperMarioLand(PyBoyGameWrapper): 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""" @@ -133,7 +138,7 @@ def __init__(self, *args, **kwargs): 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 diff --git a/pyboy/plugins/game_wrapper_tetris.py b/pyboy/plugins/game_wrapper_tetris.py index cd792d9b3..eb0b73954 100644 --- a/pyboy/plugins/game_wrapper_tetris.py +++ b/pyboy/plugins/game_wrapper_tetris.py @@ -36,17 +36,17 @@ 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): @@ -56,9 +56,14 @@ class GameWrapperTetris(PyBoyGameWrapper): 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""" @@ -81,7 +86,7 @@ def __init__(self, *args, **kwargs): self._cached_game_area_tiles_raw = array("B", [0xFF] * (ROWS*COLS*4)) self._cached_game_area_tiles = memoryview(self._cached_game_area_tiles_raw).cast("I", shape=(ROWS, COLS)) - super().__init__(*args, game_area_section=(2, 0) + self.shape, game_area_wrap_around=True, **kwargs) + super().__init__(*args, game_area_section=(2, 0) + self.shape, game_area_follow_scxy=False, **kwargs) def _game_area_tiles(self): if self._tile_cache_invalid: @@ -280,7 +285,7 @@ def __repr__(self): "\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()) ] ) ) diff --git a/pyboy/plugins/manager.pxd b/pyboy/plugins/manager.pxd index 8095f230d..335d8e8fd 100644 --- a/pyboy/plugins/manager.pxd +++ b/pyboy/plugins/manager.pxd @@ -6,11 +6,6 @@ cimport cython from pyboy.plugins.base_plugin cimport PyBoyGameWrapper - -from pyboy.logging.logging cimport Logger -from pyboy.plugins.auto_pause cimport AutoPause - -# isort:skip # imports from pyboy.plugins.window_sdl2 cimport WindowSDL2 from pyboy.plugins.window_open_gl cimport WindowOpenGL @@ -33,6 +28,8 @@ from pyboy.plugins.game_wrapper_pokemon_gen1 cimport GameWrapperPokemonGen1 cdef class PluginManager: cdef object pyboy + cdef public PyBoyGameWrapper generic_game_wrapper + cdef bint generic_game_wrapper_enabled # plugin_cdef cdef public WindowSDL2 window_sdl2 cdef public WindowOpenGL window_open_gl diff --git a/pyboy/plugins/manager.py b/pyboy/plugins/manager.py index b2af42a8a..7034e5439 100644 --- a/pyboy/plugins/manager.py +++ b/pyboy/plugins/manager.py @@ -49,6 +49,8 @@ class PluginManager: def __init__(self, pyboy, mb, pyboy_argv): self.pyboy = pyboy + self.generic_game_wrapper = PyBoyGameWrapper(pyboy, mb, pyboy_argv) + self.generic_game_wrapper_enabled = False # plugins_enabled self.window_sdl2 = WindowSDL2(pyboy, mb, pyboy_argv) self.window_sdl2_enabled = self.window_sdl2.enabled() @@ -89,7 +91,8 @@ def gamewrapper(self): if self.game_wrapper_kirby_dream_land_enabled: return self.game_wrapper_kirby_dream_land if self.game_wrapper_pokemon_gen1_enabled: return self.game_wrapper_pokemon_gen1 # gamewrapper end - return None + self.generic_game_wrapper_enabled = True + return self.generic_game_wrapper def handle_events(self, events): # foreach windows events = [].handle_events(events) @@ -126,6 +129,8 @@ def handle_events(self, events): if self.game_wrapper_pokemon_gen1_enabled: events = self.game_wrapper_pokemon_gen1.handle_events(events) # foreach end + if self.generic_game_wrapper_enabled: + events = self.generic_game_wrapper.handle_events(events) return events def post_tick(self): @@ -153,6 +158,8 @@ def post_tick(self): if self.game_wrapper_pokemon_gen1_enabled: self.game_wrapper_pokemon_gen1.post_tick() # foreach end + if self.generic_game_wrapper_enabled: + self.generic_game_wrapper.post_tick() self._post_tick_windows() @@ -273,7 +280,8 @@ def stop(self): if self.game_wrapper_pokemon_gen1_enabled: self.game_wrapper_pokemon_gen1.stop() # foreach end - pass + if self.generic_game_wrapper_enabled: + self.generic_game_wrapper.stop() def handle_breakpoint(self): if self.debug_prompt_enabled: diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index c3b0e22a4..9c04fc034 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -9,6 +9,8 @@ import os import time +import numpy as np + from pyboy.api.memory_scanner import MemoryScanner from pyboy.api.screen import Screen from pyboy.api.tilemap import TileMap @@ -129,7 +131,7 @@ def __init__( Provides a `pyboy.PyBoyMemoryView` object for reading and writing the memory space of the Game Boy. Example: - ``` + ```python >>> values = pyboy.memory[0x0000:0x10000] >>> pyboy.memory[0xC000:0xC0010] = 0 ``` @@ -143,7 +145,7 @@ def __init__( _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): @@ -581,8 +583,114 @@ def load_state(self, file_like_object): self.mb.load_state(IntIOWrapper(file_like_object)) + 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. + + 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 + """ + + if mapping is None: + mapping = [x for x in range(768)] + + assert isinstance(sprite_offset, int) + assert isinstance(mapping, (np.ndarray, list)) + assert len(mapping) == 384 or len(mapping) == 768 + + self.game_wrapper.game_area_mapping(mapping, sprite_offset) + def game_area(self): - raise Exception("game_area not implemented") + """ + 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: + + ```text + 0 1 2 3 4 5 6 7 8 9 + ____________________________________________ + 0 | 47 47 47 47 47 47 47 47 47 47 + 1 | 47 47 47 47 47 47 47 47 47 47 + 2 | 47 47 47 47 47 47 47 132 132 132 + 3 | 47 47 47 47 47 47 47 132 47 47 + 4 | 47 47 47 47 47 47 47 47 47 47 + 5 | 47 47 47 47 47 47 47 47 47 47 + 6 | 47 47 47 47 47 47 47 47 47 47 + 7 | 47 47 47 47 47 47 47 47 47 47 + 8 | 47 47 47 47 47 47 47 47 47 47 + 9 | 47 47 47 47 47 47 47 47 47 47 + 10 | 47 47 47 47 47 47 47 47 47 47 + 11 | 47 47 47 47 47 47 47 47 47 47 + 12 | 47 47 47 47 47 47 47 47 47 47 + 13 | 47 47 47 47 47 47 47 47 47 47 + 14 | 47 47 47 47 47 47 47 47 47 47 + 15 | 47 47 47 47 47 47 47 47 47 47 + 16 | 47 47 47 47 47 47 47 47 47 47 + 17 | 47 47 47 47 47 47 138 139 139 143 + ``` + + 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 + ------- + memoryview: + Simplified 2-dimensional memoryview of the screen + """ + + return self.game_wrapper.game_area() def _serial(self): """ @@ -714,7 +822,7 @@ def get_sprite_by_tile_identifier(self, tile_identifiers, on_screen=True): `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], []] ``` diff --git a/pyboy/utils.py b/pyboy/utils.py index 8d38f742f..4d0f85aa5 100644 --- a/pyboy/utils.py +++ b/pyboy/utils.py @@ -152,6 +152,7 @@ class WindowEvent: It can be used as follows: + ```python >>> from pyboy import PyBoy, WindowEvent >>> pyboy = PyBoy('file.rom') >>> pyboy.send_input(WindowEvent.PAUSE) diff --git a/tests/conftest.py b/tests/conftest.py index beb61bd0c..52ac27666 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,6 +12,11 @@ from pathlib import Path from zipfile import ZipFile +import numpy as np + +np.set_printoptions(threshold=2**32) +np.set_printoptions(linewidth=np.inf) + import git import pytest from cryptography.fernet import Fernet diff --git a/tests/test_examples.py b/tests/test_examples.py index 7fcd72fad..8a82d46a5 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -24,6 +24,8 @@ def test_record_replay(gamewrapper, rom): base = Path(f"{script_path}/../extras/examples/") assert rom is not None - return_code = subprocess.Popen([sys.executable, str(base / gamewrapper), str(Path(rom)), "--quiet"]).wait() + p = subprocess.Popen([sys.executable, str(base / gamewrapper), str(Path(rom)), "--quiet"]) + return_code = p.wait() if return_code != 0: + print(p.communicate()) sys.exit(return_code) diff --git a/tests/test_game_wrapper.py b/tests/test_game_wrapper.py index ff75eb9be..2decbbed9 100644 --- a/tests/test_game_wrapper.py +++ b/tests/test_game_wrapper.py @@ -22,5 +22,57 @@ def test_game_wrapper_basics(default_rom): generic_wrapper = pyboy.game_wrapper assert generic_wrapper is not None - # pyboy.game_area() + pyboy.game_area() + pyboy.stop() + + +def test_game_wrapper_mapping(default_rom): + pyboy = PyBoy(default_rom, window_type="null", debug=True) + pyboy.set_emulation_speed(0) + assert np.all(pyboy.game_area() == 256) + + generic_wrapper = pyboy.game_wrapper + assert generic_wrapper is not None + pyboy.tick(5, True) + assert np.all( + pyboy.game_area()[8:11, + 8:13] == np.array([ + [1, 0, 1, 0, 0], + [2, 3, 4, 5, 3], + [0, 6, 0, 0, 6], + ], dtype=np.uint32) + ) + + # List-based mapping, don't call tick + mapping = [x for x in range(384)] # 1:1 mapping + mapping[0] = 10 + mapping[1] = 10 + mapping[2] = 10 + mapping[3] = 10 + pyboy.game_area_mapping(mapping, 1000) + assert np.all( + pyboy.game_area()[8:11, 8:13] == np.array([ + [10, 10, 10, 10, 10], + [10, 10, 4, 5, 10], + [10, 6, 10, 10, 6], + ], + dtype=np.uint32) + ) + + # Array-based mapping, don't call tick + mapping = np.asarray(mapping) + mapping[0] = 1 # Map tile identifier 0 -> 1 + mapping[1] = 1 # Map tile identifier 1 -> 1 + mapping[2] = 1 # Map tile identifier 2 -> 1 + mapping[3] = 1 # Map tile identifier 3 -> 1 + pyboy.game_area_mapping(mapping, 1000) + assert np.all( + pyboy.game_area()[8:11, + 8:13] == np.array([ + [1, 1, 1, 1, 1], + [1, 1, 4, 5, 1], + [1, 6, 1, 1, 6], + ], dtype=np.uint32) + ) + pyboy.stop() From f822017de34a2889d17b7d52fba5fd29c4409e4f Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Thu, 18 Jan 2024 21:29:45 +0100 Subject: [PATCH 41/65] Remove fitness scores from game wrappers --- extras/examples/gamewrapper_mario.py | 9 +++------ extras/examples/gamewrapper_tetris.py | 2 -- pyboy/plugins/game_wrapper_kirby_dream_land.pxd | 1 - pyboy/plugins/game_wrapper_kirby_dream_land.py | 12 +----------- pyboy/plugins/game_wrapper_super_mario_land.pxd | 2 -- pyboy/plugins/game_wrapper_super_mario_land.py | 17 +---------------- pyboy/plugins/game_wrapper_tetris.pxd | 1 - pyboy/plugins/game_wrapper_tetris.py | 12 +----------- tests/test_game_wrapper_mario.py | 2 -- 9 files changed, 6 insertions(+), 52 deletions(-) diff --git a/extras/examples/gamewrapper_mario.py b/extras/examples/gamewrapper_mario.py index 5003c9836..c3dcf0fac 100644 --- a/extras/examples/gamewrapper_mario.py +++ b/extras/examples/gamewrapper_mario.py @@ -31,20 +31,17 @@ assert mario.lives_left == 2 assert mario.time_left == 400 assert mario.world == (1, 1) -assert mario.fitness == 0 # A built-in fitness score for AI development -last_fitness = 0 +last_time = mario.time_left print(mario) pyboy.send_input(WindowEvent.PRESS_ARROW_RIGHT) for _ in range(1000): - assert mario.fitness >= last_fitness - last_fitness = mario.fitness + assert mario.time_left <= mario.time_left + last_time = mario.time_left pyboy.tick(1, True) if mario.lives_left == 1: - assert last_fitness == 27700 - assert mario.fitness == 17700 # Loosing a live, means 10.000 points in this fitness scoring print(mario) break else: diff --git a/extras/examples/gamewrapper_tetris.py b/extras/examples/gamewrapper_tetris.py index ab593f543..6a905fcf9 100644 --- a/extras/examples/gamewrapper_tetris.py +++ b/extras/examples/gamewrapper_tetris.py @@ -32,7 +32,6 @@ assert tetris.score == 0 assert tetris.level == 0 assert tetris.lines == 0 -assert tetris.fitness == 0 # A built-in fitness score for AI development # Checking that a reset on the same `timer_div` results in the same Tetromino tetris.reset_game(timer_div=0x00) @@ -63,7 +62,6 @@ assert tetris.score == 0 assert tetris.level == 0 assert tetris.lines == 0 -assert tetris.fitness == 0 # A built-in fitness score for AI development # Assert there is something on the bottom of the game area assert any(filter(lambda x: x != blank_tile, game_area[-1, :])) diff --git a/pyboy/plugins/game_wrapper_kirby_dream_land.pxd b/pyboy/plugins/game_wrapper_kirby_dream_land.pxd index ef48d9586..3109af115 100644 --- a/pyboy/plugins/game_wrapper_kirby_dream_land.pxd +++ b/pyboy/plugins/game_wrapper_kirby_dream_land.pxd @@ -18,5 +18,4 @@ cdef class GameWrapperKirbyDreamLand(PyBoyGameWrapper): cdef readonly int score cdef readonly int health cdef readonly int lives_left - cdef readonly int fitness cdef readonly int _game_over \ No newline at end of file diff --git a/pyboy/plugins/game_wrapper_kirby_dream_land.py b/pyboy/plugins/game_wrapper_kirby_dream_land.py index ae28a4f41..1baa4faf2 100644 --- a/pyboy/plugins/game_wrapper_kirby_dream_land.py +++ b/pyboy/plugins/game_wrapper_kirby_dream_land.py @@ -18,7 +18,7 @@ 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. """ @@ -35,13 +35,7 @@ def __init__(self, *args, **kwargs): """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_follow_scxy=True, **kwargs) def post_tick(self): @@ -62,9 +56,6 @@ def post_tick(self): self.lives_left = self.pyboy.memory[0xD089] - 1 - if self.game_has_started: - self.fitness = self.score * self.health * self.lives_left - 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 @@ -160,7 +151,6 @@ def __repr__(self): 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" + diff --git a/pyboy/plugins/game_wrapper_super_mario_land.pxd b/pyboy/plugins/game_wrapper_super_mario_land.pxd index 12681072b..07b243d3f 100644 --- a/pyboy/plugins/game_wrapper_super_mario_land.pxd +++ b/pyboy/plugins/game_wrapper_super_mario_land.pxd @@ -21,8 +21,6 @@ cdef class GameWrapperSuperMarioLand(PyBoyGameWrapper): cdef readonly int score cdef readonly int time_left cdef readonly int level_progress - cdef readonly int _level_progress_max - cdef readonly int fitness cpdef void start_game(self, timer_div=*, world_level=*, unlock_level_select=*) noexcept cpdef void reset_game(self, timer_div=*) noexcept diff --git a/pyboy/plugins/game_wrapper_super_mario_land.py b/pyboy/plugins/game_wrapper_super_mario_land.py index 75c5ba53b..ca6843af6 100644 --- a/pyboy/plugins/game_wrapper_super_mario_land.py +++ b/pyboy/plugins/game_wrapper_super_mario_land.py @@ -98,8 +98,7 @@ def _bcm_to_dec(value): 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.__ @@ -129,14 +128,6 @@ def __init__(self, *args, **kwargs): """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_follow_scxy=True, **kwargs) @@ -157,11 +148,6 @@ def post_tick(self): 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. @@ -320,7 +306,6 @@ def __repr__(self): 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" + diff --git a/pyboy/plugins/game_wrapper_tetris.pxd b/pyboy/plugins/game_wrapper_tetris.pxd index 0209da08f..30316efd7 100644 --- a/pyboy/plugins/game_wrapper_tetris.pxd +++ b/pyboy/plugins/game_wrapper_tetris.pxd @@ -18,7 +18,6 @@ cdef class GameWrapperTetris(PyBoyGameWrapper): cdef readonly int score cdef readonly int level cdef readonly int lines - cdef readonly int fitness cpdef void set_tetromino(self, str) noexcept cpdef str next_tetromino(self) noexcept diff --git a/pyboy/plugins/game_wrapper_tetris.py b/pyboy/plugins/game_wrapper_tetris.py index eb0b73954..646b058ec 100644 --- a/pyboy/plugins/game_wrapper_tetris.py +++ b/pyboy/plugins/game_wrapper_tetris.py @@ -51,7 +51,7 @@ 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. """ @@ -73,13 +73,7 @@ def __init__(self, *args, **kwargs): """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 @@ -103,9 +97,6 @@ def post_tick(self): 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 @@ -274,7 +265,6 @@ def __repr__(self): 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" + diff --git a/tests/test_game_wrapper_mario.py b/tests/test_game_wrapper_mario.py index 238628cb2..3803e9183 100644 --- a/tests/test_game_wrapper_mario.py +++ b/tests/test_game_wrapper_mario.py @@ -28,7 +28,6 @@ def test_mario_basics(supermarioland_rom): assert mario.lives_left == 2 assert mario.time_left == 400 assert mario.world == (1, 1) - assert mario.fitness == 0 # A built-in fitness score for AI development pyboy.stop() @@ -47,7 +46,6 @@ def test_mario_advanced(supermarioland_rom): assert mario.lives_left == lives assert mario.time_left == 400 assert mario.world == (3, 2) - assert mario.fitness == 10000*lives + 6510 # A built-in fitness score for AI development pyboy.stop() From 0cdd7fd608b02f49bbe534f19ac7f66194863364 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Fri, 19 Jan 2024 20:23:22 +0100 Subject: [PATCH 42/65] Fix debug in DMG-CGB mode --- pyboy/core/mb.pxd | 2 +- pyboy/core/mb.py | 1 + pyboy/plugins/debug.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pyboy/core/mb.pxd b/pyboy/core/mb.pxd index 43ab9ffdf..6f07c626a 100644 --- a/pyboy/core/mb.pxd +++ b/pyboy/core/mb.pxd @@ -44,7 +44,7 @@ cdef class Motherboard: cdef HDMA hdma cdef uint8_t key1 cdef bint double_speed - cdef public bint cgb + cdef readonly bint cgb, cartridge_cgb cdef list breakpoints_list cdef bint breakpoint_singlestep diff --git a/pyboy/core/mb.py b/pyboy/core/mb.py index 81057b02d..cae4eb0d8 100644 --- a/pyboy/core/mb.py +++ b/pyboy/core/mb.py @@ -67,6 +67,7 @@ def __init__( self.key1 = 0 self.double_speed = False self.cgb = cgb + self.cartridge_cgb = self.cartridge.cgb if self.cgb: self.hdma = HDMA() diff --git a/pyboy/plugins/debug.py b/pyboy/plugins/debug.py index a428e2d77..2bd65cfaa 100644 --- a/pyboy/plugins/debug.py +++ b/pyboy/plugins/debug.py @@ -82,7 +82,7 @@ def __init__(self, pyboy, mb, pyboy_argv): if not self.enabled(): return - self.cgb = mb.cgb + self.cgb = mb.cartridge_cgb self.sdl2_event_pump = self.pyboy_argv.get("window_type") != "SDL2" if self.sdl2_event_pump: From f813e8856572f52a93a3b11519edfbe8e16c8347 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Fri, 19 Jan 2024 20:25:41 +0100 Subject: [PATCH 43/65] Fix call to logger.warn --- pyboy/plugins/debug.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyboy/plugins/debug.py b/pyboy/plugins/debug.py index 2bd65cfaa..10656425a 100644 --- a/pyboy/plugins/debug.py +++ b/pyboy/plugins/debug.py @@ -728,11 +728,11 @@ def draw_text(self, x, y, text): self.dst.y = y for i, c in enumerate(text): if not 0 <= c < 256: - logger.warn(f"Invalid character {c} in {bytes(text).decode('cp437')}") # This may error too... + logger.warning(f"Invalid character {c} in {bytes(text).decode('cp437')}") # This may error too... c = 0 self.src.y = 16 * c if self.dst.x > self.width - 8: - logger.warn(f"Text overrun while printing {bytes(text).decode('cp437')}") + logger.warning(f"Text overrun while printing {bytes(text).decode('cp437')}") break sdl2.SDL_RenderCopy(self._sdlrenderer, self.font_texture, self.src, self.dst) self.dst.x += 8 From 3645653dbdb4c9d0db95afd7a8c1b9c2bd471173 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Thu, 18 Jan 2024 22:42:42 +0100 Subject: [PATCH 44/65] Move screen buffer metadata to attributes buffer, output real RGBA data --- pyboy/api/screen.py | 13 +++++--- pyboy/api/tile.py | 6 ++-- pyboy/core/lcd.pxd | 5 ++- pyboy/core/lcd.py | 56 ++++++++++++++++++++------------ pyboy/plugins/debug.pxd | 2 +- pyboy/plugins/debug.py | 18 ++++++---- pyboy/plugins/screen_recorder.py | 2 +- pyboy/utils.py | 2 +- tests/test_acid_cgb.py | 5 +-- tests/test_acid_dmg.py | 5 +-- tests/test_basics.py | 4 +-- tests/test_external_api.py | 28 +++++++++------- tests/test_magen.py | 5 +-- tests/test_mooneye.py | 7 ++-- tests/test_replay.py | 2 +- tests/test_rtc3test.py | 5 +-- tests/test_samesuite.py | 5 +-- tests/test_shonumi.py | 5 +-- tests/test_which.py | 5 +-- tests/test_whichboot.py | 5 +-- 20 files changed, 114 insertions(+), 71 deletions(-) diff --git a/pyboy/api/screen.py b/pyboy/api/screen.py index 7bd9d7800..982b52f8c 100644 --- a/pyboy/api/screen.py +++ b/pyboy/api/screen.py @@ -61,11 +61,14 @@ def __init__(self, mb): Returns ------- str: - Color format of the raw screen buffer. E.g. 'RGBX'. + Color format of the raw screen buffer. E.g. 'RGBA'. """ self.image = None """ - Generates a PIL Image from the screen buffer. The screen buffer is internally row-major, but PIL hides this. + 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, @@ -90,8 +93,10 @@ def __init__(self, mb): dtype=np.uint8, ).reshape(ROWS, COLS, 4) """ - Provides the screen data in NumPy format. The format is given by `pyboy.api.screen.Screen.raw_buffer_format`. - The screen buffer is row-major. + 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 ------- diff --git a/pyboy/api/tile.py b/pyboy/api/tile.py index 7fb98dacc..81a2703d1 100644 --- a/pyboy/api/tile.py +++ b/pyboy/api/tile.py @@ -97,7 +97,7 @@ def __init__(self, mb, identifier): Returns ------- str: - Color format of the raw screen buffer. E.g. 'RGBX'. + Color format of the raw screen buffer. E.g. 'RGBA'. """ def image(self): @@ -116,9 +116,9 @@ def image(self): return None if cythonmode: - return Image.fromarray(self._image_data().base, mode="RGBX") + return Image.fromarray(self._image_data().base, mode=self.raw_buffer_format) else: - return Image.frombytes("RGBX", (8, 8), self._image_data()) + return Image.frombytes(self.raw_buffer_format, (8, 8), self._image_data()) def image_ndarray(self): """ diff --git a/pyboy/core/lcd.pxd b/pyboy/core/lcd.pxd index 0d1ec33d2..35b8b4155 100644 --- a/pyboy/core/lcd.pxd +++ b/pyboy/core/lcd.pxd @@ -119,9 +119,11 @@ cdef class Renderer: cdef bint cgb cdef array _screenbuffer_raw + cdef array _screenbuffer_attributes_raw cdef object _screenbuffer_ptr cdef array _tilecache0_raw, _spritecache0_raw, _spritecache1_raw cdef uint32_t[:,:] _screenbuffer + cdef uint8_t[:,:] _screenbuffer_attributes cdef uint32_t[:,:] _tilecache0, _spritecache0, _spritecache1 cdef int[10] sprites_to_render @@ -150,6 +152,7 @@ cdef class Renderer: yy=int, tilecache=uint32_t[:,:], bg_priority_apply=uint32_t, + col0=uint8_t, ) cdef void scanline(self, LCD, int) noexcept nogil @@ -175,7 +178,7 @@ cdef class Renderer: pixel=uint32_t, bgmappriority=bint, ) - cdef void scanline_sprites(self, LCD, int, uint32_t[:,:], bint) noexcept nogil + cdef void scanline_sprites(self, LCD, int, uint32_t[:,:], uint8_t[:,:], bint) noexcept nogil cdef void sort_sprites(self, int) noexcept nogil cdef inline uint8_t color_code(self, uint8_t, uint8_t, uint8_t) noexcept nogil diff --git a/pyboy/core/lcd.py b/pyboy/core/lcd.py index d97b16169..581f044bf 100644 --- a/pyboy/core/lcd.py +++ b/pyboy/core/lcd.py @@ -23,10 +23,11 @@ def rgb_to_bgr(color): + a = 0xFF r = (color >> 16) & 0xFF g = (color >> 8) & 0xFF b = color & 0xFF - return (b << 16) | (g << 8) | r + return (a << 24) | (b << 16) | (g << 8) | r class LCD: @@ -166,7 +167,9 @@ def tick(self, cycles): self.clock_target += 206 * multiplier self.renderer.scanline(self, self.LY) - self.renderer.scanline_sprites(self, self.LY, self.renderer._screenbuffer, False) + self.renderer.scanline_sprites( + self, self.LY, self.renderer._screenbuffer, self.renderer._screenbuffer_attributes, False + ) if self.LY < 143: self.next_stat_mode = 2 else: @@ -305,10 +308,7 @@ def get(self): return self.value def getcolor(self, i): - if i==0: - return self.palette_mem_rgb[self.lookup[0]] | COL0_FLAG - else: - return self.palette_mem_rgb[self.lookup[i]] + return self.palette_mem_rgb[self.lookup[i]] class STATRegister: @@ -371,14 +371,14 @@ def _get_sprite_height(self): return self.sprite_height -COL0_FLAG = 0b01 << 24 -BG_PRIORITY_FLAG = 0b10 << 24 +COL0_FLAG = 0b01 +BG_PRIORITY_FLAG = 0b10 class Renderer: def __init__(self, cgb): self.cgb = cgb - self.color_format = "RGBX" + self.color_format = "RGBA" self.buffer_dims = (ROWS, COLS) @@ -387,6 +387,7 @@ def __init__(self, cgb): # Init buffers as white self._screenbuffer_raw = array("B", [0x00] * (ROWS*COLS*4)) + self._screenbuffer_attributes_raw = array("B", [0x00] * (ROWS*COLS)) self._tilecache0_raw = array("B", [0x00] * (TILES*8*8*4)) self._spritecache0_raw = array("B", [0x00] * (TILES*8*8*4)) self._spritecache1_raw = array("B", [0x00] * (TILES*8*8*4)) @@ -398,6 +399,7 @@ def __init__(self, cgb): self.clear_cache() self._screenbuffer = memoryview(self._screenbuffer_raw).cast("I", shape=(ROWS, COLS)) + self._screenbuffer_attributes = memoryview(self._screenbuffer_attributes_raw).cast("B", shape=(ROWS, COLS)) self._tilecache0 = memoryview(self._tilecache0_raw).cast("I", shape=(TILES * 8, 8)) # OBP0 palette self._spritecache0 = memoryview(self._spritecache0_raw).cast("I", shape=(TILES * 8, 8)) @@ -470,6 +472,7 @@ def scanline(self, lcd, y): yy = (8*wt + (7 - (self.ly_window) % 8)) if vertflip else (8*wt + (self.ly_window) % 8) pixel = lcd.bcpd.getcolor(palette, tilecache[yy, xx]) + col0 = (tilecache[yy, xx] == 0) & 1 if bg_priority: # We hide extra rendering information in the lower 8 bits (A) of the 32-bit RGBA format bg_priority_apply = BG_PRIORITY_FLAG @@ -478,8 +481,14 @@ def scanline(self, lcd, y): xx = (x-wx) % 8 yy = 8*wt + (self.ly_window) % 8 pixel = lcd.BGP.getcolor(self._tilecache0[yy, xx]) - - self._screenbuffer[y, x] = pixel | bg_priority_apply + col0 = (self._tilecache0[yy, xx] == 0) & 1 + + self._screenbuffer[y, x] = pixel + # COL0_FLAG is 1 + self._screenbuffer_attributes[y, x] = bg_priority_apply | col0 + # self._screenbuffer_attributes[y, x] = bg_priority_apply + # if col0: + # self._screenbuffer_attributes[y, x] = self._screenbuffer_attributes[y, x] | col0 # background_enable doesn't exist for CGB. It works as master priority instead elif (not self.cgb and lcd._LCDC.background_enable) or self.cgb: tile_addr = background_offset + (y+by) // 8 * 32 % 0x400 + (x+bx) // 8 % 32 @@ -506,6 +515,7 @@ def scanline(self, lcd, y): yy = (8*bt + (7 - (y+by) % 8)) if vertflip else (8*bt + (y+by) % 8) pixel = lcd.bcpd.getcolor(palette, tilecache[yy, xx]) + col0 = (tilecache[yy, xx] == 0) & 1 if bg_priority: # We hide extra rendering information in the lower 8 bits (A) of the 32-bit RGBA format bg_priority_apply = BG_PRIORITY_FLAG @@ -514,11 +524,14 @@ def scanline(self, lcd, y): xx = (x+offset) % 8 yy = 8*bt + (y+by) % 8 pixel = lcd.BGP.getcolor(self._tilecache0[yy, xx]) + col0 = (self._tilecache0[yy, xx] == 0) & 1 - self._screenbuffer[y, x] = pixel | bg_priority_apply + self._screenbuffer[y, x] = pixel + self._screenbuffer_attributes[y, x] = bg_priority_apply | col0 else: # If background is disabled, it becomes white self._screenbuffer[y, x] = lcd.BGP.getcolor(0) + self._screenbuffer_attributes[y, x] = 0 if y == 143: # Reset at the end of a frame. We set it to -1, so it will be 0 after the first increment @@ -541,7 +554,7 @@ def sort_sprites(self, sprite_count): # Insert the key into its correct position in the sorted portion self.sprites_to_render[j + 1] = key - def scanline_sprites(self, lcd, ly, buffer, ignore_priority): + def scanline_sprites(self, lcd, ly, buffer, buffer_attributes, ignore_priority): if not lcd._LCDC.sprite_enable or lcd.disable_renderer: return @@ -621,14 +634,14 @@ def scanline_sprites(self, lcd, ly, buffer, ignore_priority): if 0 <= x < COLS and not color_code == 0: # If pixel is not transparent if self.cgb: pixel = lcd.ocpd.getcolor(palette, color_code) - bgmappriority = buffer[ly, x] & BG_PRIORITY_FLAG + bgmappriority = buffer_attributes[ly, x] & BG_PRIORITY_FLAG if lcd._LCDC.cgb_master_priority: # If 0, sprites are always on top, if 1 follow priorities if bgmappriority: # If 0, use spritepriority, if 1 take priority - if buffer[ly, x] & COL0_FLAG: + if buffer_attributes[ly, x] & COL0_FLAG: buffer[ly, x] = pixel elif spritepriority: # If 1, sprite is behind bg/window. Color 0 of window/bg is transparent - if buffer[ly, x] & COL0_FLAG: + if buffer_attributes[ly, x] & COL0_FLAG: buffer[ly, x] = pixel else: buffer[ly, x] = pixel @@ -642,7 +655,7 @@ def scanline_sprites(self, lcd, ly, buffer, ignore_priority): pixel = lcd.OBP0.getcolor(color_code) if spritepriority: # If 1, sprite is behind bg/window. Color 0 of window/bg is transparent - if buffer[ly, x] & COL0_FLAG: # if BG pixel is transparent + if buffer_attributes[ly, x] & COL0_FLAG: # if BG pixel is transparent buffer[ly, x] = pixel else: buffer[ly, x] = pixel @@ -748,6 +761,7 @@ def blank_screen(self, lcd): for y in range(ROWS): for x in range(COLS): self._screenbuffer[y, x] = lcd.BGP.getcolor(0) + self._screenbuffer_attributes[y, x] = 0 def save_state(self, f): for y in range(ROWS): @@ -761,6 +775,7 @@ def save_state(self, f): for y in range(ROWS): for x in range(COLS): f.write_32bit(self._screenbuffer[y, x]) + f.write(self._screenbuffer_attributes[y, x]) def load_state(self, f, state_version): if state_version >= 2: @@ -777,6 +792,8 @@ def load_state(self, f, state_version): for y in range(ROWS): for x in range(COLS): self._screenbuffer[y, x] = f.read_32bit() + if state_version >= 10: + self._screenbuffer_attributes[y, x] = f.read() self.clear_cache() @@ -971,13 +988,12 @@ def __init__(self, i_reg): self.palette_mem_rgb[n + m] = self.cgb_to_rgb(c[m], m) def cgb_to_rgb(self, cgb_color, index): + alpha = 0xFF red = (cgb_color & 0x1F) << 3 green = ((cgb_color >> 5) & 0x1F) << 3 blue = ((cgb_color >> 10) & 0x1F) << 3 # NOTE: Actually BGR, not RGB - rgb_color = ((blue << 16) | (green << 8) | red) - if index % 4 == 0: - rgb_color |= COL0_FLAG + rgb_color = ((alpha << 24) | (blue << 16) | (green << 8) | red) return rgb_color def set(self, val): diff --git a/pyboy/plugins/debug.pxd b/pyboy/plugins/debug.pxd index a719e5e79..3c1d24b38 100644 --- a/pyboy/plugins/debug.pxd +++ b/pyboy/plugins/debug.pxd @@ -55,8 +55,8 @@ cdef class BaseDebugWindow(PyBoyWindowPlugin): cdef object _window cdef object _sdlrenderer cdef object _sdltexturebuffer - cdef array buf cdef uint32_t[:,:] buf0 + cdef uint8_t[:,:] buf0_attributes cdef object buf_p @cython.locals(y=int, x=int, _y=int, _x=int) diff --git a/pyboy/plugins/debug.py b/pyboy/plugins/debug.py index 10656425a..7e1e6370f 100644 --- a/pyboy/plugins/debug.py +++ b/pyboy/plugins/debug.py @@ -219,11 +219,14 @@ def enabled(self): return False -def make_buffer(w, h): - buf = array("B", [0x55] * (w*h*4)) - buf0 = memoryview(buf).cast("I", shape=(h, w)) +def make_buffer(w, h, depth=4): + buf = array("B", [0x55] * (w*h*depth)) + if depth == 4: + buf0 = memoryview(buf).cast("I", shape=(h, w)) + else: + buf0 = memoryview(buf).cast("B", shape=(h, w)) buf_p = c_void_p(buf.buffer_info()[0]) - return buf, buf0, buf_p + return buf0, buf_p class BaseDebugWindow(PyBoyWindowPlugin): @@ -241,7 +244,8 @@ def __init__(self, pyboy, mb, pyboy_argv, *, scale, title, width, height, pos_x, ) self.window_id = sdl2.SDL_GetWindowID(self._window) - self.buf, self.buf0, self.buf_p = make_buffer(width, height) + self.buf0, self.buf_p = make_buffer(width, height) + self.buf0_attributes, _ = make_buffer(width, height, 1) self._sdlrenderer = sdl2.SDL_CreateRenderer(self._window, -1, sdl2.SDL_RENDERER_ACCELERATED) sdl2.SDL_RenderSetLogicalSize(self._sdlrenderer, width, height) @@ -614,7 +618,7 @@ def post_tick(self): self.buf0[y, x] = SPRITE_BACKGROUND for ly in range(144): - self.mb.lcd.renderer.scanline_sprites(self.mb.lcd, ly, self.buf0, True) + self.mb.lcd.renderer.scanline_sprites(self.mb.lcd, ly, self.buf0, self.buf0_attributes, True) self.draw_overlay() BaseDebugWindow.post_tick(self) @@ -656,7 +660,7 @@ def __init__(self, *args, **kwargs): font_blob = "".join(line.strip() for line in font_lines[font_lines.index("BASE64DATA:\n") + 1:]) font_bytes = zlib.decompress(b64decode(font_blob.encode())) - self.fbuf, self.fbuf0, self.fbuf_p = make_buffer(8, 16 * 256) + self.fbuf0, self.fbuf_p = make_buffer(8, 16 * 256) for y, b in enumerate(font_bytes): for x in range(8): self.fbuf0[y, x] = 0xFFFFFFFF if ((0x80 >> x) & b) else 0x00000000 diff --git a/pyboy/plugins/screen_recorder.py b/pyboy/plugins/screen_recorder.py index 68fd6b41b..36a95758a 100644 --- a/pyboy/plugins/screen_recorder.py +++ b/pyboy/plugins/screen_recorder.py @@ -42,7 +42,7 @@ def handle_events(self, events): def post_tick(self): # Plugin: Screen Recorder if self.recording: - self.add_frame(self.pyboy.screen.image.convert(mode="RGBA")) + self.add_frame(self.pyboy.screen.image.copy()) def add_frame(self, frame): # Pillow makes artifacts in the output, if we use 'RGB', which is PyBoy's default format diff --git a/pyboy/utils.py b/pyboy/utils.py index 4d0f85aa5..3f0232e05 100644 --- a/pyboy/utils.py +++ b/pyboy/utils.py @@ -4,7 +4,7 @@ # from enum import Enum -STATE_VERSION = 9 +STATE_VERSION = 10 ############################################################## # Buffer classes diff --git a/tests/test_acid_cgb.py b/tests/test_acid_cgb.py index 5fb9388b9..9c0278c7f 100644 --- a/tests/test_acid_cgb.py +++ b/tests/test_acid_cgb.py @@ -26,8 +26,9 @@ def test_cgb_acid(cgb_acid_file): png_path.parents[0].mkdir(parents=True, exist_ok=True) image.save(png_path) else: - old_image = PIL.Image.open(png_path) - diff = PIL.ImageChops.difference(image.convert(mode="RGB"), old_image) + # Converting to RGB as ImageChops.difference cannot handle Alpha: https://github.com/python-pillow/Pillow/issues/4849 + old_image = PIL.Image.open(png_path).convert("RGB") + diff = PIL.ImageChops.difference(image.convert("RGB"), old_image) if diff.getbbox() and not os.environ.get("TEST_CI"): image.show() old_image.show() diff --git a/tests/test_acid_dmg.py b/tests/test_acid_dmg.py index dd31d1a8c..43fe0d8d5 100644 --- a/tests/test_acid_dmg.py +++ b/tests/test_acid_dmg.py @@ -27,8 +27,9 @@ def test_dmg_acid(cgb, dmg_acid_file): png_path.parents[0].mkdir(parents=True, exist_ok=True) image.save(png_path) else: - old_image = PIL.Image.open(png_path) - diff = PIL.ImageChops.difference(image.convert(mode="RGB"), old_image) + # Converting to RGB as ImageChops.difference cannot handle Alpha: https://github.com/python-pillow/Pillow/issues/4849 + old_image = PIL.Image.open(png_path).convert("RGB") + diff = PIL.ImageChops.difference(image.convert("RGB"), old_image) if diff.getbbox() and not os.environ.get("TEST_CI"): image.show() old_image.show() diff --git a/tests/test_basics.py b/tests/test_basics.py index 4412843fa..e79338b91 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -218,8 +218,8 @@ def test_all_modes(cgb, _bootrom, frames, rom, any_rom_cgb, boot_cgb_rom): png_buf.write(b"".join([(x ^ 0b10011101).to_bytes(1, sys.byteorder) for x in data])) png_buf.seek(0) - old_image = PIL.Image.open(png_buf) - diff = PIL.ImageChops.difference(image.convert(mode="RGB"), old_image) + old_image = PIL.Image.open(png_buf).convert("RGB") + diff = PIL.ImageChops.difference(image.convert("RGB"), old_image) if diff.getbbox() and not os.environ.get("TEST_CI"): image.show() old_image.show() diff --git a/tests/test_external_api.py b/tests/test_external_api.py index f392b8c16..60fd6b01a 100644 --- a/tests/test_external_api.py +++ b/tests/test_external_api.py @@ -19,7 +19,7 @@ from .conftest import BOOTROM_FRAMES_UNTIL_LOGO NDARRAY_COLOR_DEPTH = 4 -NDARRAY_COLOR_FORMAT = "RGBX" +NDARRAY_COLOR_FORMAT = "RGBA" def test_misc(default_rom): @@ -141,7 +141,7 @@ def test_tiles_cgb(any_rom_cgb): def test_screen_buffer_and_image(tetris_rom, boot_rom): - cformat = "RGBX" + cformat = "RGBA" boot_logo_hash_predigested = b"_M\x0e\xd9\xe2\xdb\\o]\x83U\x93\xebZm\x1e\xaaFR/Q\xa52\x1c{8\xe7g\x95\xbcIz" pyboy = PyBoy(tetris_rom, window_type="null", bootrom_file=boot_rom) @@ -166,10 +166,10 @@ def test_screen_buffer_and_image(tetris_rom, boot_rom): b"\xa4\x0eR&\xda9\xfcg\xf7\x0f|\xba}\x08\xb6$" ) boot_logo_png_hash = hashlib.sha256() - image = pyboy.screen.image + image = pyboy.screen.image.convert("RGB") assert isinstance(image, PIL.Image.Image) image_data = io.BytesIO() - image.convert(mode="RGB").save(image_data, format="BMP") + image.save(image_data, format="BMP") boot_logo_png_hash.update(image_data.getvalue()) assert boot_logo_png_hash.digest() == boot_logo_png_hash_predigested @@ -183,26 +183,32 @@ def test_screen_buffer_and_image(tetris_rom, boot_rom): # ) == (b"\r\t\x87\x131\xe8\x06\x82\xcaO=\n\x1e\xa2K$" # b"\xd6\x8e\x91R( H7\xd8a*B+\xc7\x1f\x19") - # Check PIL image is reference for performance + ## Check PIL image is reference for performance + ## Converting to RGB as ImageChops.difference cannot handle Alpha: https://github.com/python-pillow/Pillow/issues/4849 + + # Initial, direct reference and copy are the same pyboy.tick(1, True) new_image1 = pyboy.screen.image _new_image1 = new_image1.copy() - diff = ImageChops.difference(new_image1, _new_image1) + diff = ImageChops.difference(new_image1.convert("RGB"), _new_image1.convert("RGB")) assert not diff.getbbox() + # Changing reference, and it now differs from copy nd_image = pyboy.screen.ndarray - nd_image[:, :] = 0 - diff = ImageChops.difference(new_image1, _new_image1) + nd_image[:, :, :] = 0 + diff = ImageChops.difference(new_image1.convert("RGB"), _new_image1.convert("RGB")) assert diff.getbbox() + # Old reference lives after tick, and equals new reference pyboy.tick(1, True) new_image2 = pyboy.screen.image - diff = ImageChops.difference(new_image1, new_image2) + diff = ImageChops.difference(new_image1.convert("RGB"), new_image2.convert("RGB")) assert not diff.getbbox() + # Changing reference, and it now differs from copy new_image3 = new_image1.copy() - nd_image[:, :] = 0xFF - diff = ImageChops.difference(new_image1, new_image3) + nd_image[:, :, :] = 0xFF + diff = ImageChops.difference(new_image1.convert("RGB"), new_image3.convert("RGB")) assert diff.getbbox() pyboy.stop(save=False) diff --git a/tests/test_magen.py b/tests/test_magen.py index ee4650f28..0e5c9ffbe 100644 --- a/tests/test_magen.py +++ b/tests/test_magen.py @@ -26,8 +26,9 @@ def test_magen_test(magen_test_file): png_path.parents[0].mkdir(parents=True, exist_ok=True) image.save(png_path) else: - old_image = PIL.Image.open(png_path) - diff = PIL.ImageChops.difference(image.convert(mode="RGB"), old_image) + # Converting to RGB as ImageChops.difference cannot handle Alpha: https://github.com/python-pillow/Pillow/issues/4849 + old_image = PIL.Image.open(png_path).convert("RGB") + diff = PIL.ImageChops.difference(image.convert("RGB"), old_image) if diff.getbbox() and not os.environ.get("TEST_CI"): image.show() old_image.show() diff --git a/tests/test_mooneye.py b/tests/test_mooneye.py index 363f28e94..f177eefde 100644 --- a/tests/test_mooneye.py +++ b/tests/test_mooneye.py @@ -175,14 +175,15 @@ def test_mooneye(clean, rom, mooneye_dir, default_rom): png_path.parents[0].mkdir(parents=True, exist_ok=True) image.save(png_path) else: - old_image = PIL.Image.open(png_path) + # Converting to RGB as ImageChops.difference cannot handle Alpha: https://github.com/python-pillow/Pillow/issues/4849 + old_image = PIL.Image.open(png_path).convert("RGB") if "acceptance" in rom: # The registers are too volatile to depend on. We crop the top out, and only match the assertions. diff = PIL.ImageChops.difference( - image.crop((0, 72, 160, 144)).convert(mode="RGB"), old_image.crop((0, 72, 160, 144)) + image.convert("RGB").crop((0, 72, 160, 144)), old_image.crop((0, 72, 160, 144)) ) else: - diff = PIL.ImageChops.difference(image.convert(mode="RGB"), old_image) + diff = PIL.ImageChops.difference(image.convert("RGB"), old_image) if diff.getbbox() and not os.environ.get("TEST_CI"): image.show() diff --git a/tests/test_replay.py b/tests/test_replay.py index e3eb78ef6..52de72d04 100644 --- a/tests/test_replay.py +++ b/tests/test_replay.py @@ -34,7 +34,7 @@ def verify_screen_image_np(pyboy, saved_array): from PIL import Image original = Image.frombytes("RGB", (160, 144), np.frombuffer(saved_array, dtype=np.uint8).reshape(144, 160, 3)) original.show() - new = pyboy.screen.image + new = pyboy.screen.image.convert("RGB") new.show() import PIL.ImageChops PIL.ImageChops.difference(original, new).show() diff --git a/tests/test_rtc3test.py b/tests/test_rtc3test.py index 8bc8d5fb4..bdd7e86ab 100644 --- a/tests/test_rtc3test.py +++ b/tests/test_rtc3test.py @@ -42,8 +42,9 @@ def test_rtc3test(subtest, rtc3test_file): png_path.parents[0].mkdir(parents=True, exist_ok=True) image.save(png_path) else: - old_image = PIL.Image.open(png_path) - diff = PIL.ImageChops.difference(image.convert(mode="RGB"), old_image) + # Converting to RGB as ImageChops.difference cannot handle Alpha: https://github.com/python-pillow/Pillow/issues/4849 + old_image = PIL.Image.open(png_path).convert("RGB") + diff = PIL.ImageChops.difference(image.convert("RGB"), old_image) if diff.getbbox() and not os.environ.get("TEST_CI"): image.show() old_image.show() diff --git a/tests/test_samesuite.py b/tests/test_samesuite.py index 35815ddea..3718ade95 100644 --- a/tests/test_samesuite.py +++ b/tests/test_samesuite.py @@ -151,8 +151,9 @@ def test_samesuite(clean, gb_type, rom, samesuite_dir, boot_cgb_rom, boot_rom, d png_path.parents[0].mkdir(parents=True, exist_ok=True) image.save(png_path) else: - old_image = PIL.Image.open(png_path) - diff = PIL.ImageChops.difference(image.convert(mode="RGB"), old_image) + # Converting to RGB as ImageChops.difference cannot handle Alpha: https://github.com/python-pillow/Pillow/issues/4849 + old_image = PIL.Image.open(png_path).convert("RGB") + diff = PIL.ImageChops.difference(image.convert("RGB"), old_image) if diff.getbbox() and not os.environ.get("TEST_CI"): image.show() diff --git a/tests/test_shonumi.py b/tests/test_shonumi.py index b63d82680..e6b019116 100644 --- a/tests/test_shonumi.py +++ b/tests/test_shonumi.py @@ -32,9 +32,10 @@ def test_shonumi(rom, shonumi_dir): png_path.parents[0].mkdir(parents=True, exist_ok=True) image = pyboy.screen.image - old_image = PIL.Image.open(png_path) + # Converting to RGB as ImageChops.difference cannot handle Alpha: https://github.com/python-pillow/Pillow/issues/4849 + old_image = PIL.Image.open(png_path).convert("RGB") old_image = old_image.resize(image.size, resample=PIL.Image.Dither.NONE) - diff = PIL.ImageChops.difference(image.convert(mode="RGB"), old_image) + diff = PIL.ImageChops.difference(image.convert("RGB"), old_image) if diff.getbbox() and not os.environ.get("TEST_CI"): image.show() diff --git a/tests/test_which.py b/tests/test_which.py index daa7eee60..901d5c9a1 100644 --- a/tests/test_which.py +++ b/tests/test_which.py @@ -27,8 +27,9 @@ def test_which(cgb, which_file): png_path.parents[0].mkdir(parents=True, exist_ok=True) image.save(png_path) else: - old_image = PIL.Image.open(png_path) - diff = PIL.ImageChops.difference(image.convert(mode="RGB"), old_image) + # Converting to RGB as ImageChops.difference cannot handle Alpha: https://github.com/python-pillow/Pillow/issues/4849 + old_image = PIL.Image.open(png_path).convert("RGB") + diff = PIL.ImageChops.difference(image.convert("RGB"), old_image) if diff.getbbox() and not os.environ.get("TEST_CI"): image.show() old_image.show() diff --git a/tests/test_whichboot.py b/tests/test_whichboot.py index ded627915..1cb7acb54 100644 --- a/tests/test_whichboot.py +++ b/tests/test_whichboot.py @@ -27,8 +27,9 @@ def test_which(cgb, whichboot_file): png_path.parents[0].mkdir(parents=True, exist_ok=True) image.save(png_path) else: - old_image = PIL.Image.open(png_path) - diff = PIL.ImageChops.difference(image.convert(mode="RGB"), old_image) + # Converting to RGB as ImageChops.difference cannot handle Alpha: https://github.com/python-pillow/Pillow/issues/4849 + old_image = PIL.Image.open(png_path).convert("RGB") + diff = PIL.ImageChops.difference(image.convert("RGB"), old_image) if diff.getbbox() and not os.environ.get("TEST_CI"): image.show() old_image.show() From 01a5320669be5a27edb1d95d6c19e5cdbad89c8c Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Sun, 11 Feb 2024 23:00:31 +0100 Subject: [PATCH 45/65] Fix API for tile --- pyboy/api/tile.pxd | 2 +- pyboy/api/tile.py | 5 ++--- tests/test_external_api.py | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/pyboy/api/tile.pxd b/pyboy/api/tile.pxd index a5f3bb420..4394963da 100644 --- a/pyboy/api/tile.pxd +++ b/pyboy/api/tile.pxd @@ -22,7 +22,7 @@ cdef class Tile: cdef readonly int data_address cdef readonly tuple shape cpdef object image(self) noexcept - cpdef object image_ndarray(self) noexcept + cpdef object ndarray(self) noexcept cdef uint32_t[:,:] data # TODO: Add to locals instead @cython.locals(byte1=uint8_t, byte2=uint8_t, colorcode=uint32_t) diff --git a/pyboy/api/tile.py b/pyboy/api/tile.py index 81a2703d1..a19266992 100644 --- a/pyboy/api/tile.py +++ b/pyboy/api/tile.py @@ -120,7 +120,7 @@ def image(self): else: return Image.frombytes(self.raw_buffer_format, (8, 8), self._image_data()) - def image_ndarray(self): + def ndarray(self): """ 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 @@ -160,8 +160,7 @@ def _image_data(self): for x in range(8): colorcode = utils.color_code(byte1, byte2, 7 - x) - alpha_mask = 0x00FFFFFF - self.data[k // 2][x] = self.mb.lcd.BGP.getcolor(colorcode) & alpha_mask + self.data[k // 2][x] = self.mb.lcd.BGP.getcolor(colorcode) return self.data def __eq__(self, other): diff --git a/tests/test_external_api.py b/tests/test_external_api.py index 60fd6b01a..8179bcace 100644 --- a/tests/test_external_api.py +++ b/tests/test_external_api.py @@ -65,14 +65,14 @@ def test_tiles(default_rom): tile = pyboy.get_tile(1) image = tile.image() assert isinstance(image, PIL.Image.Image) - ndarray = tile.image_ndarray() + ndarray = tile.ndarray() assert isinstance(ndarray, np.ndarray) assert ndarray.shape == (8, 8, NDARRAY_COLOR_DEPTH) assert ndarray.dtype == np.uint8 # data = tile.image_data() # assert data.shape == (8, 8) - assert [[x for x in y] for y in ndarray.view(dtype=np.uint32).reshape(8, 8) + assert [[x & 0xFFFFFF for x in y] for y in ndarray.view(dtype=np.uint32).reshape(8, 8) ] == [[0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF], [0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF], [0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF], @@ -104,7 +104,7 @@ def test_tiles_cgb(any_rom_cgb): tile = pyboy.get_tile(1) image = tile.image() assert isinstance(image, PIL.Image.Image) - ndarray = tile.image_ndarray() + ndarray = tile.ndarray() assert isinstance(ndarray, np.ndarray) assert ndarray.shape == (8, 8, NDARRAY_COLOR_DEPTH) assert ndarray.dtype == np.uint8 From 07df75c3fd798344c9942518f7b43bd8a9156f49 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Wed, 7 Feb 2024 19:44:42 +0100 Subject: [PATCH 46/65] Remove PyBoy.get_input --- pyboy/pyboy.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index 9c04fc034..c436651ed 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -506,29 +506,6 @@ def send_input(self, event): """ self.events.append(WindowEvent(event)) - 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. - - 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` - """ - return [x for x in self.old_events if x not in ignore] - def save_state(self, file_like_object): """ Saves the complete state of the emulator. It can be called at any time, and enable you to revert any progress in From 06c4aba04c1fd2e1e0960bc1cb8b4c70edabd7ae Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Wed, 7 Feb 2024 19:44:42 +0100 Subject: [PATCH 47/65] Improved documentation on PyBoy API --- .github/workflows/pr-test.yml | 6 + pyboy/__init__.py | 8 +- pyboy/api/constants.py | 42 +- pyboy/api/memory_scanner.py | 30 +- pyboy/api/screen.py | 103 ++++- pyboy/api/tile.py | 34 +- pyboy/api/tilemap.py | 24 +- pyboy/conftest.py | 36 ++ pyboy/plugins/base_plugin.py | 57 ++- .../plugins/game_wrapper_kirby_dream_land.py | 19 +- pyboy/plugins/game_wrapper_pokemon_gen1.py | 18 +- .../plugins/game_wrapper_super_mario_land.py | 272 +++++++++-- pyboy/plugins/game_wrapper_tetris.py | 26 +- pyboy/pyboy.py | 429 +++++++++++++++--- pyboy/utils.py | 25 +- 15 files changed, 926 insertions(+), 203 deletions(-) create mode 100644 pyboy/conftest.py diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index 45ebc2020..386d5bd4f 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -30,6 +30,9 @@ jobs: pip install --upgrade pip pip install --upgrade wheel setuptools pip install --prefer-binary -r requirements.txt + - name: Doctest + run: | + python -m pytest pyboy/ --doctest-modules - name: Build PyBoy run: | python setup.py build_ext -j $(getconf _NPROCESSORS_ONLN) --inplace @@ -98,6 +101,9 @@ jobs: pypy3 -m ensurepip pypy3 -m pip install --upgrade pip pypy3 -m pip install wheel + - name: Doctest + run: | + pypy3 -m pytest pyboy/ --doctest-modules - name: Run PyTest env: PYTEST_SECRETS_KEY: ${{ secrets.PYTEST_SECRETS_KEY }} diff --git a/pyboy/__init__.py b/pyboy/__init__.py index 376745ae3..95360b99a 100644 --- a/pyboy/__init__.py +++ b/pyboy/__init__.py @@ -8,8 +8,10 @@ "logging": False, "pyboy": False, "utils": False, + "conftest": False, } -__all__ = ["PyBoy", "WindowEvent", "dec_to_bcd", "bcd_to_dec"] -from .pyboy import PyBoy -from .utils import WindowEvent, bcd_to_dec, dec_to_bcd +__all__ = ["PyBoy", "PyBoyMemoryView", "WindowEvent", "dec_to_bcd", "bcd_to_dec"] + +from .pyboy import PyBoy, PyBoyMemoryView +from .utils import WindowEvent, bcd_to_dec, dec_to_bcd \ No newline at end of file diff --git a/pyboy/api/constants.py b/pyboy/api/constants.py index 7d4dd06d4..42cc02b0d 100644 --- a/pyboy/api/constants.py +++ b/pyboy/api/constants.py @@ -7,14 +7,54 @@ """ VRAM_OFFSET = 0x8000 +""" +Start address of VRAM +""" LCDC_OFFSET = 0xFF40 +""" +LCDC Register +""" OAM_OFFSET = 0xFE00 +""" +Start address of Object-Attribute-Memory (OAM) +""" LOW_TILEMAP = 0x1800 + VRAM_OFFSET +""" +Start address of lower tilemap +""" HIGH_TILEMAP = 0x1C00 + VRAM_OFFSET +""" +Start address of high tilemap +""" LOW_TILEDATA = VRAM_OFFSET +""" +Start address of lower tile data +""" LOW_TILEDATA_NTILES = 0x100 +""" +Number of tiles in lower tile data +""" HIGH_TILEDATA = 0x800 + VRAM_OFFSET +""" +Start address of high tile data +""" TILES = 384 +""" +Number of tiles supported on Game Boy DMG (non-color) +""" TILES_CGB = 768 +""" +Number of tiles supported on Game Boy Color +""" SPRITES = 40 -ROWS, COLS = 144, 160 +""" +Number of sprites supported +""" +ROWS = 144 +""" +Rows (horizontal lines) on the screen +""" +COLS = 160 +""" +Columns (vertical lines) on the screen +""" diff --git a/pyboy/api/memory_scanner.py b/pyboy/api/memory_scanner.py index 69b05e49b..4711685e1 100644 --- a/pyboy/api/memory_scanner.py +++ b/pyboy/api/memory_scanner.py @@ -30,12 +30,6 @@ class ScanMode(Enum): 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 @@ -53,6 +47,14 @@ def scan_memory( """ 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. @@ -86,6 +88,22 @@ def rescan_memory( """ 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. diff --git a/pyboy/api/screen.py b/pyboy/api/screen.py index 982b52f8c..527346677 100644 --- a/pyboy/api/screen.py +++ b/pyboy/api/screen.py @@ -27,7 +27,7 @@ class 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.tilemap_background`, `pyboy.tilemap_window`, and `pyboy.sprite` instead. + It's much more efficient to use `pyboy.PyBoy.tilemap_background`, `pyboy.PyBoy.tilemap_window`, and `pyboy.PyBoy.get_sprite` instead. """ def __init__(self, mb): self.mb = mb @@ -40,6 +40,17 @@ def __init__(self, mb): Use this, only if you need to bypass the overhead of `Screen.image` or `Screen.ndarray`. + Example: + ```python + >>> import numpy as np + >>> rows, cols = pyboy.screen.raw_buffer_dims + >>> ndarray = np.frombuffer( + ... pyboy.screen.raw_buffer, + ... dtype=np.uint8, + ... ).reshape(rows, cols, 4) # Just an example, use pyboy.screen.ndarray instead + + ``` + Returns ------- bytes: @@ -49,6 +60,13 @@ def __init__(self, mb): """ Returns the dimensions of the raw screen buffer. The screen buffer is row-major. + Example: + ```python + >>> pyboy.screen.raw_buffer_dims + (144, 160) + + ``` + Returns ------- tuple: @@ -58,6 +76,20 @@ def __init__(self, mb): """ Returns the color format of the raw screen buffer. **This format is subject to change.** + Example: + ```python + >>> from PIL import Image + >>> pyboy.screen.raw_buffer_format + 'RGBA' + >>> image = Image.frombuffer( + ... pyboy.screen.raw_buffer_format, + ... pyboy.screen.raw_buffer_dims[::-1], + ... pyboy.screen.raw_buffer, + ... ) # Just an example, use pyboy.screen.image instead + >>> image.save('frame.png') + + ``` + Returns ------- str: @@ -68,12 +100,19 @@ def __init__(self, mb): 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. + Example: + ```python + >>> image = pyboy.screen.image + >>> type(image) + + >>> image.save('frame.png') + + ``` + Returns ------- PIL.Image: @@ -98,10 +137,33 @@ def __init__(self, mb): The format is given by `pyboy.api.screen.Screen.raw_buffer_format`. The screen buffer is row-major. + Example: + ```python + >>> pyboy.screen.ndarray.shape + (144, 160, 4) + >>> # Display "P" on screen from the PyBoy bootrom + >>> pyboy.screen.ndarray[66:80,64:72,0] + array([[255, 255, 255, 255, 255, 255, 255, 255], + [255, 0, 0, 0, 0, 0, 255, 255], + [255, 0, 0, 0, 0, 0, 0, 255], + [255, 0, 0, 255, 255, 0, 0, 255], + [255, 0, 0, 255, 255, 0, 0, 255], + [255, 0, 0, 255, 255, 0, 0, 255], + [255, 0, 0, 0, 0, 0, 0, 255], + [255, 0, 0, 0, 0, 0, 255, 255], + [255, 0, 0, 255, 255, 255, 255, 255], + [255, 0, 0, 255, 255, 255, 255, 255], + [255, 0, 0, 255, 255, 255, 255, 255], + [255, 0, 0, 255, 255, 255, 255, 255], + [255, 0, 0, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255]], dtype=uint8) + + ``` + Returns ------- numpy.ndarray: - Screendata in `ndarray` of bytes with shape (144, 160, 3) + Screendata in `ndarray` of bytes with shape (144, 160, 4) """ @property @@ -109,9 +171,29 @@ def tilemap_position_list(self): """ This function provides the screen (SCX, SCY) and window (WX. WY) position for each horizontal line in the screen buffer. These parameters are often used for visual effects, and some games will reset the registers at - the end of each call to `pyboy.PyBoy.tick()`. For such games, `Screen.tilemap_position` becomes useless. - - See `Screen.tilemap_position` for more information. + the end of each call to `pyboy.PyBoy.tick()`. + + See `Screen.get_tilemap_position` for more information. + + Example: + ```python + >>> pyboy.tick(25) + True + >>> swoosh = pyboy.screen.tilemap_position_list[67:78] + >>> print(*swoosh, sep=newline) # Just to pretty-print it + [0, 0, -7, 0] + [1, 0, -7, 0] + [2, 0, -7, 0] + [2, 0, -7, 0] + [3, 0, -7, 0] + [3, 0, -7, 0] + [3, 0, -7, 0] + [3, 0, -7, 0] + [2, 0, -7, 0] + [1, 0, -7, 0] + [0, 0, -7, 0] + + ``` Returns ------- @@ -137,6 +219,13 @@ def get_tilemap_position(self): For more details, see "7.4 Viewport" in the [report](https://github.com/Baekalfen/PyBoy/raw/master/extras/PyBoy.pdf), or the Pan Docs under [LCD Position and Scrolling](https://gbdev.io/pandocs/Scrolling.html). + Example: + ```python + >>> pyboy.screen.get_tilemap_position() + ((0, 0), (-7, 0)) + + ``` + Returns ------- tuple: diff --git a/pyboy/api/tile.py b/pyboy/api/tile.py index a19266992..779e63534 100644 --- a/pyboy/api/tile.py +++ b/pyboy/api/tile.py @@ -32,7 +32,7 @@ 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.tile`, `pyboy.api.sprite.Sprite` and `pyboy.api.tilemap.TileMap`, when refering to graphics. + `pyboy.PyBoy.get_tile`, `pyboy.api.sprite.Sprite` and `pyboy.api.tilemap.TileMap`, when refering to graphics. This class is not meant to be instantiated by developers reading this documentation, but it will be created internally and returned by `pyboy.api.sprite.Sprite.tiles` and @@ -106,6 +106,13 @@ def image(self): Be aware, that the graphics for this tile can change between each call to `pyboy.PyBoy.tick`. + Example: + ```python + >>> tile = pyboy.get_tile(1) + >>> tile.image().save('tile_1.png') + + ``` + Returns ------- PIL.Image : @@ -128,6 +135,31 @@ def ndarray(self): Be aware, that the graphics for this tile can change between each call to `pyboy.PyBoy.tick`. + Example: + ```python + >>> tile1 = pyboy.get_tile(1) + >>> tile1.ndarray()[:,:,0] # Upper part of "P" + array([[255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255], + [255, 0, 0, 0, 0, 0, 255, 255], + [255, 0, 0, 0, 0, 0, 0, 255], + [255, 0, 0, 255, 255, 0, 0, 255], + [255, 0, 0, 255, 255, 0, 0, 255], + [255, 0, 0, 255, 255, 0, 0, 255]], dtype=uint8) + >>> tile2 = pyboy.get_tile(2) + >>> tile2.ndarray()[:,:,0] # Lower part of "P" + array([[255, 0, 0, 0, 0, 0, 0, 255], + [255, 0, 0, 0, 0, 0, 255, 255], + [255, 0, 0, 255, 255, 255, 255, 255], + [255, 0, 0, 255, 255, 255, 255, 255], + [255, 0, 0, 255, 255, 255, 255, 255], + [255, 0, 0, 255, 255, 255, 255, 255], + [255, 0, 0, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255]], dtype=uint8) + + ``` + Returns ------- numpy.ndarray : diff --git a/pyboy/api/tilemap.py b/pyboy/api/tilemap.py index 510c82778..25045f58a 100644 --- a/pyboy/api/tilemap.py +++ b/pyboy/api/tilemap.py @@ -28,17 +28,13 @@ def __init__(self, pyboy, mb, select): 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 @@ -88,9 +84,9 @@ def search_for_identifiers(self, identifiers): 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 diff --git a/pyboy/conftest.py b/pyboy/conftest.py new file mode 100644 index 000000000..6e3128d16 --- /dev/null +++ b/pyboy/conftest.py @@ -0,0 +1,36 @@ +# +# License: See LICENSE.md file +# GitHub: https://github.com/Baekalfen/PyBoy +# + +import os +import shutil +from pathlib import Path +from unittest import mock + +import pytest + +from . import PyBoy + + +@pytest.fixture(scope="session") +def default_rom(): + return str(Path("pyboy/default_rom.gb")) + + +@pytest.fixture(autouse=True) +def doctest_fixtures(doctest_namespace, default_rom): + # We mock get_sprite_by_tile_identifier as default_rom doesn't use sprites + with mock.patch("pyboy.PyBoy.get_sprite_by_tile_identifier", return_value=[[0, 2, 4], []]): + pyboy = PyBoy(default_rom, window_type="null", symbols_file="extras/default_rom/default_rom.sym") + pyboy.set_emulation_speed(0) + pyboy.tick(10) # Just a few to get the logo up + doctest_namespace["pyboy"] = pyboy + + try: + os.remove("file.rom") + except: + pass + shutil.copyfile(default_rom, "file.rom") + yield None + os.remove("file.rom") diff --git a/pyboy/plugins/base_plugin.py b/pyboy/plugins/base_plugin.py index b6a049127..13c87e27e 100644 --- a/pyboy/plugins/base_plugin.py +++ b/pyboy/plugins/base_plugin.py @@ -98,6 +98,10 @@ class PyBoyGameWrapper(PyBoyPlugin): cartridge_title = None argv = [("--game-wrapper", {"action": "store_true", "help": "Enable game wrapper for the current game"})] + mapping_one_to_one = np.arange(384 * 2, dtype=np.uint8) + """ + Example mapping of 1:1 + """ def __init__(self, *args, game_area_section=(0, 0, 32, 32), game_area_follow_scxy=False, **kwargs): super().__init__(*args, **kwargs) if not cythonmode: @@ -110,16 +114,23 @@ def __init__(self, *args, game_area_section=(0, 0, 32, 32), game_area_follow_scx self._tile_cache_invalid = True self._sprite_cache_invalid = True - self.game_area_section = game_area_section - self.game_area_follow_scxy = game_area_follow_scxy - width = self.game_area_section[2] - self.game_area_section[0] - height = self.game_area_section[3] - self.game_area_section[1] + self.shape = None + """ + The shape of the game area. This can be modified with `pyboy.PyBoy.game_area_dimensions`. + + Example: + ```python + >>> pyboy.game_wrapper.shape + (32, 32) + ``` + """ + self._set_dimensions(*game_area_section, game_area_follow_scxy) + width, height = self.shape self._cached_game_area_tiles_raw = array("B", [0xFF] * (width*height*4)) + self._cached_game_area_tiles = memoryview(self._cached_game_area_tiles_raw).cast("I", shape=(width, height)) self.saved_state = io.BytesIO() - self._cached_game_area_tiles = memoryview(self._cached_game_area_tiles_raw).cast("I", shape=(width, height)) - def __cinit__(self, pyboy, mb, pyboy_argv, *args, **kwargs): self.tilemap_background = self.pyboy.tilemap_background self.tilemap_window = self.pyboy.tilemap_window @@ -247,9 +258,43 @@ def game_area_mapping(self, mapping, sprite_offest): self.mapping = np.asarray(mapping, dtype=np.uint32) self.sprite_offset = sprite_offest + def _set_dimensions(self, x, y, width, height, follow_scrolling=True): + self.shape = (width, height) + self.game_area_section = (x, y, width, height) + self.game_area_follow_scxy = follow_scrolling + def _sum_number_on_screen(self, x, y, length, blank_tile_identifier, tile_identifier_offset): number = 0 for i, x in enumerate(self.tilemap_background[x:x + length, y]): if x != blank_tile_identifier: number += (x+tile_identifier_offset) * (10**(length - 1 - i)) return number + + def __repr__(self): + adjust = 4 + # yapf: disable + + sprites = "\n".join([str(s) for s in self._sprites_on_screen()]) + + tiles_header = ( + " "*4 + "".join([f"{i: >4}" for i in range(self.shape[0])]) + "\n" + + "_"*(adjust*self.shape[0]+4) + ) + + tiles = "\n".join( + [ + (f"{i: <3}|" + "".join([str(tile).rjust(adjust) for tile in line])).strip() + for i, line in enumerate(self.game_area()) + ] + ) + + return ( + "Sprites on screen:\n" + + sprites + + "\n" + + "Tiles on screen:\n" + + tiles_header + + "\n" + + tiles + ) + # yapf: enable diff --git a/pyboy/plugins/game_wrapper_kirby_dream_land.py b/pyboy/plugins/game_wrapper_kirby_dream_land.py index 1baa4faf2..1920dbfa8 100644 --- a/pyboy/plugins/game_wrapper_kirby_dream_land.py +++ b/pyboy/plugins/game_wrapper_kirby_dream_land.py @@ -25,8 +25,6 @@ class GameWrapperKirbyDreamLand(PyBoyGameWrapper): cartridge_title = "KIRBY DREAM LA" def __init__(self, *args, **kwargs): - self.shape = (20, 16) - """The shape of the game area""" self.score = 0 """The score provided by the game""" self.health = 0 @@ -36,7 +34,7 @@ def __init__(self, *args, **kwargs): self._game_over = False """The game over state""" - super().__init__(*args, game_area_section=(0, 0) + self.shape, game_area_follow_scxy=True, **kwargs) + super().__init__(*args, game_area_section=(0, 0, 20, 16), game_area_follow_scxy=True, **kwargs) def post_tick(self): self._tile_cache_invalid = True @@ -144,25 +142,12 @@ def game_over(self): return self._game_over def __repr__(self): - adjust = 4 # yapf: disable return ( f"Kirby Dream Land:\n" + f"Score: {self.score}\n" + f"Health: {self.health}\n" + f"Lives left: {self.lives_left}\n" + - "Sprites on screen:\n" + - "\n".join([str(s) for s in self._sprites_on_screen()]) + - "\n" + - "Tiles on screen:\n" + - " "*5 + "".join([f"{i: <4}" for i in range(10)]) + "\n" + - "_"*(adjust*20+4) + - "\n" + - "\n".join( - [ - f"{i: <3}| " + "".join([str(tile).ljust(adjust) for tile in line]) - for i, line in enumerate(self.game_area()) - ] - ) + super().__repr__() ) # yapf: enable diff --git a/pyboy/plugins/game_wrapper_pokemon_gen1.py b/pyboy/plugins/game_wrapper_pokemon_gen1.py index b116bf8d9..4b47e4717 100644 --- a/pyboy/plugins/game_wrapper_pokemon_gen1.py +++ b/pyboy/plugins/game_wrapper_pokemon_gen1.py @@ -27,8 +27,7 @@ class GameWrapperPokemonGen1(PyBoyGameWrapper): cartridge_title = None def __init__(self, *args, **kwargs): - self.shape = (20, 18) - super().__init__(*args, game_area_section=(0, 0) + self.shape, game_area_follow_scxy=True, **kwargs) + super().__init__(*args, game_area_section=(0, 0, 20, 18), game_area_follow_scxy=True, **kwargs) self.sprite_offset = 0 def enabled(self): @@ -81,22 +80,9 @@ def game_area_collision(self): return game_area def __repr__(self): - adjust = 4 # yapf: disable return ( f"Pokemon Gen 1:\n" + - "Sprites on screen:\n" + - "\n".join([str(s) for s in self._sprites_on_screen()]) + - "\n" + - "Tiles on screen:\n" + - " "*5 + "".join([f"{i: <4}" for i in range(10)]) + "\n" + - "_"*(adjust*20+4) + - "\n" + - "\n".join( - [ - f"{i: <3}| " + "".join([str(tile).ljust(adjust) for tile in line]) - for i, line in enumerate(self.game_area()) - ] - ) + super().__repr__() ) # yapf: enable diff --git a/pyboy/plugins/game_wrapper_super_mario_land.py b/pyboy/plugins/game_wrapper_super_mario_land.py index ca6843af6..2b12c376c 100644 --- a/pyboy/plugins/game_wrapper_super_mario_land.py +++ b/pyboy/plugins/game_wrapper_super_mario_land.py @@ -100,36 +100,184 @@ class GameWrapperSuperMarioLand(PyBoyGameWrapper): """ 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. + + ![Super Mario Land](supermarioland.png) + ```python + >>> pyboy = PyBoy(supermarioland_rom) + >>> pyboy.game_wrapper.start_game() + >>> pyboy.tick(1, True) + True + >>> pyboy.screen.image.resize((320,288)).save('docs/plugins/supermarioland.png') # The exact screenshot shown above + >>> pyboy.game_wrapper + Super Mario Land: World 1-1 + Coins: 0 + lives_left: 2 + Score: 0 + Time left: 400 + Level progress: 251 + Sprites on screen: + Sprite [3]: Position: (35, 112), Shape: (8, 8), Tiles: (Tile: 0), On screen: True + Sprite [4]: Position: (43, 112), Shape: (8, 8), Tiles: (Tile: 1), On screen: True + Sprite [5]: Position: (35, 120), Shape: (8, 8), Tiles: (Tile: 16), On screen: True + Sprite [6]: Position: (43, 120), Shape: (8, 8), Tiles: (Tile: 17), On screen: True + Tiles on screen: + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + ____________________________________________________________________________________ + 0 | 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 + 1 | 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 + 2 | 300 300 300 300 300 300 300 300 300 300 300 300 321 322 321 322 323 300 300 300 + 3 | 300 300 300 300 300 300 300 300 300 300 300 324 325 326 325 326 327 300 300 300 + 4 | 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 + 5 | 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 + 6 | 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 + 7 | 300 300 300 300 300 300 300 300 310 350 300 300 300 300 300 300 300 300 300 300 + 8 | 300 300 300 300 300 300 300 310 300 300 350 300 300 300 300 300 300 300 300 300 + 9 | 300 300 300 300 300 129 310 300 300 300 300 350 300 300 300 300 300 300 300 300 + 10 | 300 300 300 300 300 310 300 300 300 300 300 300 350 300 300 300 300 300 300 300 + 11 | 300 300 310 350 310 300 300 300 300 306 307 300 300 350 300 300 300 300 300 300 + 12 | 300 368 369 300 0 1 300 306 307 305 300 300 300 300 350 300 300 300 300 300 + 13 | 310 370 371 300 16 17 300 305 300 305 300 300 300 300 300 350 300 300 300 300 + 14 | 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 + 15 | 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 + ``` + """ cartridge_title = "SUPER MARIOLAN" mapping_compressed = mapping_compressed """ Compressed mapping for `pyboy.PyBoy.game_area_mapping` + + Example using `mapping_compressed`, Mario is `1`. He is standing on the ground which is `10`: + ```python + >>> pyboy = PyBoy(supermarioland_rom) + >>> pyboy.game_wrapper.start_game() + >>> pyboy.game_wrapper.game_area_mapping(pyboy.game_wrapper.mapping_compressed, 0) + >>> pyboy.game_wrapper.game_area() + 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, 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], + [ 0, 0, 0, 0, 0, 13, 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, 14, 14, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 14, 14, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10], + [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10]], dtype=uint32) + ``` """ mapping_minimal = mapping_minimal """ Minimal mapping for `pyboy.PyBoy.game_area_mapping` + + Example using `mapping_minimal`, Mario is `1`. He is standing on the ground which is `3`: + ```python + >>> pyboy = PyBoy(supermarioland_rom) + >>> pyboy.game_wrapper.start_game() + >>> pyboy.game_wrapper.game_area_mapping(pyboy.game_wrapper.mapping_minimal, 0) + >>> pyboy.game_wrapper.game_area() + 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, 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], + [0, 0, 0, 0, 0, 3, 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, 3, 3, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 3, 3, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3], + [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]], dtype=uint32) + ``` """ def __init__(self, *args, **kwargs): - self.shape = (20, 16) - """The shape of the game area""" self.world = (0, 0) - """Provides the current "world" Mario is in, as a tuple of as two integers (world, level).""" + """ + Provides the current "world" Mario is in, as a tuple of as two integers (world, level). + + You can force a level change with `set_world_level`. + + Example: + ```python + >>> pyboy = PyBoy(supermarioland_rom) + >>> pyboy.game_wrapper.start_game() + >>> pyboy.game_wrapper.world + (1, 1) + ``` + + """ self.coins = 0 - """The number of collected coins.""" + """ + The number of collected coins. + + Example: + ```python + >>> pyboy = PyBoy(supermarioland_rom) + >>> pyboy.game_wrapper.start_game() + >>> pyboy.game_wrapper.coins + 0 + ``` + """ self.lives_left = 0 - """The number of lives Mario has left""" + """ + The number of lives Mario has left + + Example: + ```python + >>> pyboy = PyBoy(supermarioland_rom) + >>> pyboy.game_wrapper.start_game() + >>> pyboy.game_wrapper.lives_left + 2 + ``` + """ self.score = 0 - """The score provided by the game""" + """ + The score provided by the game + + Example: + ```python + >>> pyboy = PyBoy(supermarioland_rom) + >>> pyboy.game_wrapper.start_game() + >>> pyboy.game_wrapper.score + 0 + ``` + """ self.time_left = 0 - """The number of seconds left to finish the level""" + """ + The number of seconds left to finish the level + + Example: + ```python + >>> pyboy = PyBoy(supermarioland_rom) + >>> pyboy.game_wrapper.start_game() + >>> pyboy.game_wrapper.time_left + 400 + ``` + """ self.level_progress = 0 - """An integer of the current "global" X position in this level. Can be used for AI scoring.""" + """ + An integer of the current "global" X position in this level. Can be used for AI scoring. + + Example: + ```python + >>> pyboy = PyBoy(supermarioland_rom) + >>> pyboy.game_wrapper.start_game() + >>> pyboy.game_wrapper.level_progress + 251 + ``` + """ - super().__init__(*args, game_area_section=(0, 2) + self.shape, game_area_follow_scxy=True, **kwargs) + super().__init__(*args, game_area_section=(0, 2, 20, 16), game_area_follow_scxy=True, **kwargs) def post_tick(self): self._tile_cache_invalid = True @@ -154,6 +302,19 @@ def set_lives_left(self, amount): This should only be called when the game has started. + Example: + ```python + >>> pyboy = PyBoy(supermarioland_rom) + >>> pyboy.game_wrapper.start_game() + >>> pyboy.game_wrapper.lives_left + 2 + >>> pyboy.game_wrapper.set_lives_left(10) + >>> pyboy.tick(1, False) + True + >>> pyboy.game_wrapper.lives_left + 10 + ``` + Args: amount (int): The wanted number of lives """ @@ -173,6 +334,15 @@ def set_world_level(self, world, level): """ Patches the handler for pressing start in the menu. It hardcodes a world and level to always "continue" from. + Example: + ```python + >>> pyboy = PyBoy(supermarioland_rom) + >>> pyboy.game_wrapper.set_world_level(3, 2) + >>> pyboy.game_wrapper.start_game() + >>> pyboy.game_wrapper.world + (3, 2) + ``` + Args: world (int): The world to select a level from, 0-3 level (int): The level to start from, 0-2 @@ -203,6 +373,14 @@ def start_game(self, timer_div=None, world_level=None, unlock_level_select=False If you're not using the game wrapper for unattended use, you can unlock the level selector for the main menu. Enabling the selector, will make this function return before entering the game. + Example: + ```python + >>> pyboy = PyBoy(supermarioland_rom) + >>> pyboy.game_wrapper.start_game(world_level=(4,1)) + >>> pyboy.game_wrapper.world + (4, 1) + ``` + Kwargs: timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. world_level (tuple): (world, level) to start the game from @@ -242,10 +420,17 @@ def start_game(self, timer_div=None, world_level=None, unlock_level_select=False def reset_game(self, timer_div=None): """ - After calling `start_game`, use this method to reset Mario to the beginning of world 1-1. + After calling `start_game`, use this method to reset Mario to the beginning of `start_game`. - If you want to reset to later parts of the game -- for example world 1-2 or 3-1 -- use the methods - `pyboy.PyBoy.save_state` and `pyboy.PyBoy.load_state`. + If you want to reset to other worlds or levels of the game -- for example world 1-2 or 3-1 -- reset the entire + emulator and provide the `world_level` on `start_game`. + + Example: + ```python + >>> pyboy = PyBoy(supermarioland_rom) + >>> pyboy.game_wrapper.start_game() + >>> pyboy.game_wrapper.reset_game() + ``` Kwargs: timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. @@ -262,26 +447,28 @@ def game_area(self): In Super Mario Land, this is almost the entire screen, expect for the top part showing the score, lives left and so on. These values can be found in the variables of this class. - In this example, Mario is `0`, `1`, `16` and `17`. He is standing on the ground which is `352` and `353`: - ```text - 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 - ____________________________________________________________________________________ - 0 | 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 - 1 | 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 - 2 | 300 300 300 300 300 300 300 300 300 300 300 300 321 322 321 322 323 300 300 300 - 3 | 300 300 300 300 300 300 300 300 300 300 300 324 325 326 325 326 327 300 300 300 - 4 | 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 - 5 | 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 - 6 | 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 - 7 | 300 300 300 300 300 300 300 300 310 350 300 300 300 300 300 300 300 300 300 300 - 8 | 300 300 300 300 300 300 300 310 300 300 350 300 300 300 300 300 300 300 300 300 - 9 | 300 300 300 300 300 129 310 300 300 300 300 350 300 300 300 300 300 300 300 300 - 10 | 300 300 300 300 300 310 300 300 300 300 300 300 350 300 300 300 300 300 300 300 - 11 | 300 300 310 350 310 300 300 300 300 306 307 300 300 350 300 300 300 300 300 300 - 12 | 300 368 369 300 0 1 300 306 307 305 300 300 300 300 350 300 300 300 300 300 - 13 | 310 370 371 300 16 17 300 305 300 305 300 300 300 300 300 350 300 300 300 300 - 14 | 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 - 15 | 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 + In this example using `mapping_minimal`, Mario is `1`. He is standing on the ground which is `3`: + ```python + >>> pyboy = PyBoy(supermarioland_rom) + >>> pyboy.game_wrapper.start_game() + >>> pyboy.game_wrapper.game_area_mapping(pyboy.game_wrapper.mapping_minimal, 0) + >>> pyboy.game_wrapper.game_area() + 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, 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], + [0, 0, 0, 0, 0, 3, 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, 3, 3, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 3, 3, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3], + [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]], dtype=uint32) ``` Returns @@ -297,27 +484,14 @@ def game_over(self): return self.pyboy.memory[0xC0A4] == 0x39 def __repr__(self): - adjust = 4 # yapf: disable return ( - f"Super Mario Land: World {'-'.join([str(i) for i in self.world])}\n" + + f"Super Mario Land: World {'-'.join([str(i) for i in self.world])}\n" f"Coins: {self.coins}\n" + f"lives_left: {self.lives_left}\n" + f"Score: {self.score}\n" + f"Time left: {self.time_left}\n" + f"Level progress: {self.level_progress}\n" + - "Sprites on screen:\n" + - "\n".join([str(s) for s in self._sprites_on_screen()]) + - "\n" + - "Tiles on screen:\n" + - " "*5 + "".join([f"{i: <4}" for i in range(20)]) + "\n" + - "_"*(adjust*20+4) + - "\n" + - "\n".join( - [ - f"{i: <3}| " + "".join([str(tile).ljust(adjust) for tile in line]) - for i, line in enumerate(self.game_area()) - ] - ) + super().__repr__() ) # yapf: enable diff --git a/pyboy/plugins/game_wrapper_tetris.py b/pyboy/plugins/game_wrapper_tetris.py index 646b058ec..ba5776fcc 100644 --- a/pyboy/plugins/game_wrapper_tetris.py +++ b/pyboy/plugins/game_wrapper_tetris.py @@ -65,8 +65,6 @@ class GameWrapperTetris(PyBoyGameWrapper): Minimal mapping for `pyboy.PyBoy.game_area_mapping` """ def __init__(self, *args, **kwargs): - self.shape = (10, 18) - """The shape of the game area""" self.score = 0 """The score provided by the game""" self.level = 0 @@ -74,13 +72,13 @@ def __init__(self, *args, **kwargs): self.lines = 0 """The number of cleared lines""" - super().__init__(*args, **kwargs) + # 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)) + # ROWS, COLS = self.shape + # self._cached_game_area_tiles_raw = array("B", [0xFF] * (ROWS*COLS*4)) + # self._cached_game_area_tiles = memoryview(self._cached_game_area_tiles_raw).cast("I", shape=(ROWS, COLS)) - super().__init__(*args, game_area_section=(2, 0) + self.shape, game_area_follow_scxy=False, **kwargs) + super().__init__(*args, game_area_section=(2, 0, 10, 18), game_area_follow_scxy=False, **kwargs) def _game_area_tiles(self): if self._tile_cache_invalid: @@ -265,18 +263,6 @@ def __repr__(self): f"Score: {self.score}\n" + f"Level: {self.level}\n" + f"Lines: {self.lines}\n" + - "Sprites on screen:\n" + - "\n".join([str(s) for s in self._sprites_on_screen()]) + - "\n" + - "Tiles on screen:\n" + - " "*5 + "".join([f"{i: <4}" for i in range(10)]) + "\n" + - "_"*(adjust*10+4) + - "\n" + - "\n".join( - [ - f"{i: <3}| " + "".join([str(tile).ljust(adjust) for tile in line]) - for i, line in enumerate(self.game_area()) - ] - ) + super().__repr__() ) # yapf: enable diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index c436651ed..235d4664b 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -51,26 +51,39 @@ def __init__( 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.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 "bots", which interact with the game. Only the `gamerom_file` argument is required. + Example: + ```python + >>> pyboy = PyBoy('game_rom.gb') + >>> for _ in range(60): # Use 'while True:' for infinite + ... pyboy.tick() + True... + >>> pyboy.stop() + + ``` + Args: gamerom_file (str): Filepath to a game-ROM for Game Boy or Game Boy Color. Kwargs: - bootrom_file (str): Filepath to a boot-ROM to use. If unsure, specify `None`. - disable_renderer (bool): Can be used to optimize performance, by internally disable rendering of the screen. - color_palette (tuple): Specify the color palette to use for rendering. - cgb_color_palette (list of tuple): Specify the color palette to use for rendering in CGB-mode for non-color games. + * symbols_file (str): Filepath to a .sym file to use. If unsure, specify `None`. + + * bootrom_file (str): Filepath to a boot-ROM to use. If unsure, specify `None`. + + * sound (bool): Enable sound emulation and output - Other keyword arguments may exist for plugins that are not listed here. They can be viewed with the - `parser_arguments()` method in the pyboy.plugins.manager module, or by running pyboy --help in the terminal. + * sound_emulated (bool): Enable sound emulation without any output. Used for compatibility. + + * color_palette (tuple): Specify the color palette to use for rendering. + + * cgb_color_palette (list of tuple): Specify the color palette to use for rendering in CGB-mode for non-color games. + + Other keyword arguments may exist for plugins that are not listed here. They can be viewed by running `pyboy --help` in the terminal. """ self.initialized = False @@ -119,7 +132,17 @@ def __init__( 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.api.screen.Screen.tilemap_position` for more information. + `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' + + ``` Returns ------- @@ -130,11 +153,16 @@ def __init__( """ Provides a `pyboy.PyBoyMemoryView` object for reading and writing the memory space of the Game Boy. + For a more comprehensive description, see the `pyboy.PyBoyMemoryView` class. + Example: ```python - >>> values = pyboy.memory[0x0000:0x10000] - >>> pyboy.memory[0xC000:0xC0010] = 0 + >>> pyboy.memory[0x0000:0x0010] # Read 16 bytes from ROM bank 0 + [49, 254, 255, 33, 0, 128, 175, 34, 124, 254, 160, 32, 249, 6, 48, 33] + >>> pyboy.memory[1, 0x2000] = 12 # Override address 0x2000 from ROM bank 1 with the value 12 + >>> pyboy.memory[0xC000] = 1 # Write to address 0xC000 with value 1 ``` + """ self.memory_scanner = MemoryScanner(self) @@ -148,11 +176,16 @@ def __init__( ```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 + ... 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 + [] + ``` """ @@ -163,6 +196,17 @@ def __init__( 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`: @@ -176,6 +220,17 @@ def __init__( 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`: @@ -187,6 +242,13 @@ def __init__( 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 : @@ -206,10 +268,17 @@ def __init__( 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. + If a game-specific wrapper is not found, a generic wrapper will be returned. To get more information, find the wrapper for your game in `pyboy.plugins`. + Example: + ```python + >>> pyboy.game_wrapper.start_game() + >>> pyboy.game_wrapper.reset_game() + + ``` + Returns ------- `pyboy.plugins.base_plugin.PyBoyGameWrapper`: @@ -267,19 +336,39 @@ def _tick(self, render): def tick(self, count=1, render=True): """ - Progresses the emulator ahead by one frame. + Progresses the emulator ahead by `count` frame(s). - 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 + 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. - _Open an issue on GitHub if you need finer control, and we will take a look at it._ + 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 this tick. For AI training, it's adviced to use - this sparingly, as it will reduce performance substantially. While setting `render` to `False`, you can still - access the `PyBoy.game_area` to get a simpler representation of 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. - If the screen was rendered, use `pyboy.api.screen.Screen` to get NumPy buffer or a raw memory buffer. + For AI training, it's adviced to use as high a count as practical, as it will otherwise reduce performance + substantially. While setting `render` to `False`, you can still access the `PyBoy.game_area` to get a simpler + representation of the game. + + If `render` was enabled, use `pyboy.api.screen.Screen` to get a NumPy buffer or raw memory buffer. + + Example: + ```python + >>> pyboy.tick() # Progress 1 frame with rendering + True + >>> pyboy.tick(1) # Progress 1 frame with rendering + True + >>> pyboy.tick(60, False) # Progress 60 frames *without* rendering + True + >>> pyboy.tick(60, True) # Progress 60 frames and render *only the last frame* + True + >>> for _ in range(60): # Progress 60 frames and render every frame + ... if not pyboy.tick(1, True): + ... break + >>> + ``` Args: count (int): Number of ticks to process @@ -383,6 +472,13 @@ def stop(self, save=True): """ 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. @@ -405,6 +501,16 @@ def button(self, input): The button will automatically be released at the following call to `PyBoy.tick`. + Example: + ```python + >>> pyboy.button('a') # Press button 'a' and release after `pyboy.tick()` + >>> pyboy.tick() # Button 'a' pressed + True + >>> pyboy.tick() # Button 'a' released + True + + ``` + Args: input (str): button to press """ @@ -442,6 +548,19 @@ def button_press(self, input): The button will remain press until explicitly released with `PyBoy.button_release` or `PyBoy.send_input`. + 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: input (str): button to press """ @@ -472,6 +591,19 @@ def button_release(self, input): This will release a button after a call to `PyBoy.button_press` or `PyBoy.send_input`. + 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: input (str): button to release """ @@ -497,9 +629,24 @@ def button_release(self, 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.WindowEvent` for which events to send. + + Consider using `PyBoy.button` instead for easier access. + + Example: + ```python + >>> from pyboy 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 send. + ``` Args: event (pyboy.WindowEvent): The event to send @@ -514,13 +661,19 @@ def save_state(self, file_like_object): 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. @@ -543,10 +696,12 @@ def load_state(self, file_like_object): 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. @@ -567,7 +722,11 @@ def game_area_dimensions(self, x, y, width, height, follow_scrolling=True): Example: ```python - >>> pyboy.game_area_dimensions(0, 0, 10, 18, False) + >>> pyboy.game_wrapper.shape + (32, 32) + >>> pyboy.game_area_dimensions(2, 2, 10, 18, False) + >>> pyboy.game_wrapper.shape + (10, 18) ``` Args: @@ -577,8 +736,7 @@ def game_area_dimensions(self, x, y, width, height, follow_scrolling=True): 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 + self.game_wrapper._set_dimensions(x, y, width, height, follow_scrolling=True) def game_area_collision(self): """ @@ -586,6 +744,23 @@ def game_area_collision(self): The output will be unique for each game wrapper. + Example: + ```python + >>> # This example show nothing, but a supported game will + >>> pyboy.game_area_collision() + array([[0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint32) + + ``` + Returns ------- memoryview: @@ -605,12 +780,14 @@ def game_area_mapping(self, mapping, sprite_offset=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) + >>> pyboy.game_area_mapping(pyboy.game_wrapper.mapping_one_to_one, 0) + ``` Args: @@ -634,27 +811,28 @@ def game_area(self): The layout will vary from game to game. Below is an example from Tetris: - ```text - 0 1 2 3 4 5 6 7 8 9 - ____________________________________________ - 0 | 47 47 47 47 47 47 47 47 47 47 - 1 | 47 47 47 47 47 47 47 47 47 47 - 2 | 47 47 47 47 47 47 47 132 132 132 - 3 | 47 47 47 47 47 47 47 132 47 47 - 4 | 47 47 47 47 47 47 47 47 47 47 - 5 | 47 47 47 47 47 47 47 47 47 47 - 6 | 47 47 47 47 47 47 47 47 47 47 - 7 | 47 47 47 47 47 47 47 47 47 47 - 8 | 47 47 47 47 47 47 47 47 47 47 - 9 | 47 47 47 47 47 47 47 47 47 47 - 10 | 47 47 47 47 47 47 47 47 47 47 - 11 | 47 47 47 47 47 47 47 47 47 47 - 12 | 47 47 47 47 47 47 47 47 47 47 - 13 | 47 47 47 47 47 47 47 47 47 47 - 14 | 47 47 47 47 47 47 47 47 47 47 - 15 | 47 47 47 47 47 47 47 47 47 47 - 16 | 47 47 47 47 47 47 47 47 47 47 - 17 | 47 47 47 47 47 47 138 139 139 143 + 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 @@ -691,6 +869,15 @@ def set_emulation_speed(self, target_speed): Some window types do not implement a frame-limiter, and will always run at full speed. + Example: + ```python + >>> pyboy.tick() # Delays 16.67ms + True + >>> pyboy.set_emulation_speed(0) # Disable limit + >>> pyboy.tick() # As fast as possible + True + ``` + Args: target_speed (int): Target emulation speed as multiplier of real-time. """ @@ -783,6 +970,17 @@ def get_sprite(self, sprite_index): 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 @@ -796,12 +994,13 @@ 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. + `pyboy.PyBoy.get_sprite` function to get a `pyboy.api.sprite.Sprite` object. Example: ```python >>> print(pyboy.get_sprite_by_tile_identifier([43, 123])) [[0, 2, 4], []] + ``` Meaning, that tile identifier `43` is found at the sprite indexes: 0, 2, and 4, while tile identifier @@ -836,6 +1035,16 @@ def get_tile(self, 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`: @@ -845,6 +1054,106 @@ def get_tile(self, identifier): class PyBoyMemoryView: + """ + This class cannot be used directly, but is accessed through `PyBoy.memory`. + + This class serves four purposes: Reading memory (ROM/RAM), writing memory (ROM/RAM), overriding memory (ROM/RAM) and special registers. + + See the [Pan Docs: Memory Map](https://gbdev.io/pandocs/Memory_Map.html) for a great overview of the memory space. + + Memory can be accessed as individual bytes (`pyboy.memory[0x00]`) or as slices (`pyboy.memory[0x00:0x10]`). And if + applicable, a specific ROM/RAM bank can be defined before the address (`pyboy.memory[0, 0x00]` or `pyboy.memory[0, 0x00:0x10]`). + + The boot ROM is accessed using the special "-1" ROM bank. + + The find addresses of interest, either search online for something like: "[game title] RAM map", or find them yourself + using `PyBoy.memory_scanner`. + + **Read:** + + If you're developing a bot or AI with this API, you're most likely going to be using read the most. This is how you + would efficiently read the score, time, coins, positions etc. in a game's memory. + + ```python + >>> pyboy.memory[0x0000] # Read one byte at address 0x0000 + 49 + >>> pyboy.memory[0x0000:0x0010] # Read 16 bytes from 0x0000 to 0x0010 (excluding 0x0010) + [49, 254, 255, 33, 0, 128, 175, 34, 124, 254, 160, 32, 249, 6, 48, 33] + >>> pyboy.memory[-1, 0x0000:0x0010] # Read 16 bytes from 0x0000 to 0x0010 (excluding 0x0010) from the boot ROM + [49, 254, 255, 33, 0, 128, 175, 34, 124, 254, 160, 32, 249, 6, 48, 33] + >>> pyboy.memory[0, 0x0000:0x0010] # Read 16 bytes from 0x0000 to 0x0010 (excluding 0x0010) from ROM bank 0 + [64, 65, 66, 67, 68, 69, 70, 65, 65, 65, 71, 65, 65, 65, 72, 73] + >>> pyboy.memory[2, 0xA000] # Read from external RAM on cartridge (if any) from bank 2 at address 0xA000 + 0 + ``` + + **Write:** + + Writing to Game Boy memory can be complicated because of the limited address space. There's a lot of memory that + isn't directly accessible, and can be hidden through "memory banking". This means that the same address range + (for example 0x4000 to 0x8000) can change depending on what state the game is in. + + If you want to change an address in the ROM, then look at override below. Issuing writes to the ROM area actually + sends commands to the [Memory Bank Controller (MBC)](https://gbdev.io/pandocs/MBCs.html#mbcs) on the cartridge. + + A write is done by assigning to the `PyBoy.memory` object. It's recommended to define the bank to avoid mistakes + (`pyboy.memory[2, 0xA000]=1`). Without defining the bank, PyBoy will pick the current bank for the given address if + needed (`pyboy.memory[0xA000]=1`). + + At this point, all reads will return a new list of the values in the given range. The slices will not reference back to the PyBoy memory. This feature might come in the future. + + ```python + >>> pyboy.memory[0xC000] = 123 # Write to WRAM at address 0xC000 + >>> pyboy.memory[0xC000:0xC00A] = [0,1,2,3,4,5,6,7,8,9] # Write to WRAM from address 0xC000 to 0xC00A + >>> pyboy.memory[0xC010:0xC01A] = 0 # Write to WRAM from address 0xC010 to 0xC01A + >>> pyboy.memory[0x1000] = 123 # Not writing 123 at address 0x1000! This sends a command to the cartridge's MBC. + >>> pyboy.memory[2, 0xA000] = 123 # Write to external RAM on cartridge (if any) for bank 2 at address 0xA000 + >>> # Game Boy Color (CGB) only: + >>> pyboy_cgb.memory[1, 0x8000] = 25 # Write to VRAM bank 1 at address 0xD000 when in CGB mode + >>> pyboy_cgb.memory[6, 0xD000] = 25 # Write to WRAM bank 6 at address 0xD000 when in CGB mode + ``` + + **Override:** + + Override data at a given memory address of the Game Boy's ROM. + + This can be used to reprogram a game ROM to change its behavior. + + This will not let your override RAM or a special register. This will let you override data in the ROM at any given bank. + This is the memory allocated at 0x0000 to 0x8000, where 0x4000 to 0x8000 can be changed from the MBC. + + _NOTE_: Any changes here are not saved or loaded to game states! Use this function with caution and reapply + any overrides when reloading the ROM. + + To override, it's required to provide the ROM-bank you're changing. Otherwise, it'll be considered a regular 'write' as described above. + + ```python + >>> pyboy.memory[0, 0x0010] = 10 # Override ROM-bank 0 at address 0x0010 + >>> pyboy.memory[0, 0x0010:0x001A] = [0,1,2,3,4,5,6,7,8,9] # Override ROM-bank 0 at address 0x0010 to 0x001A + >>> pyboy.memory[-1, 0x0010] = 10 # Override boot ROM at address 0x0010 + >>> pyboy.memory[1, 0x6000] = 12 # Override ROM-bank 1 at address 0x6000 + >>> pyboy.memory[0x1000] = 12 # This will not override, as there is no ROM bank assigned! + ``` + + **Special Registers:** + + The Game Boy has a range of memory addresses known as [hardware registers](https://gbdev.io/pandocs/Hardware_Reg_List.html). These control parts of the hardware like LCD, + Timer, DMA, serial and so on. Even though they might appear as regular RAM addresses, reading/writing these addresses + often results in special side-effects. + + The [DIV (0xFF04) register](https://gbdev.io/pandocs/Timer_and_Divider_Registers.html#ff04--div-divider-register) for example provides a number that increments 16 thousand times each second. This can be + used as a source of randomness in games. If you read the value, you'll get a pseudo-random number. But if you write + *any* value to the register, it'll reset to zero. + + ```python + >>> pyboy.memory[0xFF04] # DIV register + 163 + >>> pyboy.memory[0xFF04] = 123 # Trying to write to it will always reset it to zero + >>> pyboy.memory[0xFF04] + 0 + ``` + + """ def __init__(self, mb): self.mb = mb diff --git a/pyboy/utils.py b/pyboy/utils.py index 3f0232e05..55948d4df 100644 --- a/pyboy/utils.py +++ b/pyboy/utils.py @@ -153,10 +153,9 @@ class WindowEvent: It can be used as follows: ```python - >>> from pyboy import PyBoy, WindowEvent - >>> pyboy = PyBoy('file.rom') + >>> from pyboy import WindowEvent >>> pyboy.send_input(WindowEvent.PAUSE) - >>> + ``` Just for button presses, it might be easier to use: `pyboy.PyBoy.button`, @@ -296,6 +295,16 @@ def dec_to_bcd(value, byte_width=1, byteorder="little"): 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 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. """ @@ -318,6 +327,16 @@ def bcd_to_dec(value, byte_width=1, byteorder="little"): 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 import bcd_to_dec + >>> bcd_to_dec(0b00110000) + 30 + >>> bcd_to_dec(0b00110010) + 32 + + ``` + Returns: int: The decimal equivalent of the BCD value. """ From c2f0a2d60eb16944d888f822ed2c0880a9d67478 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Wed, 7 Feb 2024 19:44:42 +0100 Subject: [PATCH 48/65] Add default_rom and symbols to make build --- .gitignore | 10 ++--- Makefile | 4 ++ extras/bootrom/Makefile | 2 +- extras/bootrom/bootrom_cgb.sym | 20 +++++++++ extras/bootrom/bootrom_dmg.sym | 20 +++++++++ extras/default_rom/Makefile | 13 ++++-- extras/default_rom/default_rom.asm | 13 +++++- extras/default_rom/default_rom.sym | 13 ++++++ extras/default_rom/default_rom_cgb.asm | 4 ++ extras/default_rom/default_rom_cgb.sym | 13 ++++++ extras/default_rom/hardware.inc | 54 ++++++++++++------------- pyboy/default_rom.gb | Bin 32768 -> 32768 bytes pyboy/default_rom_cgb.gb | Bin 0 -> 32768 bytes tests/test_basics.py | 2 +- tests/test_replay.py | 3 ++ 15 files changed, 132 insertions(+), 39 deletions(-) create mode 100644 extras/bootrom/bootrom_cgb.sym create mode 100644 extras/bootrom/bootrom_dmg.sym create mode 100644 extras/default_rom/default_rom.sym create mode 100644 extras/default_rom/default_rom_cgb.asm create mode 100644 extras/default_rom/default_rom_cgb.sym create mode 100644 pyboy/default_rom_cgb.gb diff --git a/.gitignore b/.gitignore index c8b3996d9..99e575250 100644 --- a/.gitignore +++ b/.gitignore @@ -82,14 +82,14 @@ PyBoy-RL/ # Bootrom extras/bootrom/bootrom*.map extras/bootrom/bootrom*.obj -extras/bootrom/bootrom*.sym extras/bootrom/bootrom*.gb extras/bootrom/logo.asm -extras/default_rom/default_rom.map -extras/default_rom/default_rom.obj -extras/default_rom/default_rom.sym -!default_rom.gb +extras/default_rom/default_rom*.map +extras/default_rom/default_rom*.obj +!default_rom*.gb +extras/default_rom/default_rom.gb +extras/default_rom/default_rom_cgb.gb test.replay diff --git a/Makefile b/Makefile index b99c0c1a9..b51a8bf0a 100644 --- a/Makefile +++ b/Makefile @@ -28,10 +28,14 @@ codecov: clean build: @echo "Building..." + cd ${ROOT_DIR}/extras/default_rom && $(MAKE) + cd ${ROOT_DIR}/extras/bootrom && $(MAKE) CFLAGS=$(CFLAGS) ${PY} setup.py build_ext -j $(shell getconf _NPROCESSORS_ONLN) --inplace clean: @echo "Cleaning..." + cd ${ROOT_DIR}/extras/default_rom && $(MAKE) clean + cd ${ROOT_DIR}/extras/bootrom && $(MAKE) clean rm -rf PyBoy.egg-info rm -rf build rm -rf dist diff --git a/extras/bootrom/Makefile b/extras/bootrom/Makefile index 771bc7c83..34386896a 100644 --- a/extras/bootrom/Makefile +++ b/extras/bootrom/Makefile @@ -20,4 +20,4 @@ build: head -c 256 bootrom_cgb.gb > ${ROOT_DIR}/pyboy/core/bootrom_cgb.bin clean: - rm logo.asm bootrom_*.map bootrom_*.sym bootrom_*.obj bootrom_*.gb + rm -f logo.asm bootrom_*.map bootrom_*.obj bootrom_*.gb diff --git a/extras/bootrom/bootrom_cgb.sym b/extras/bootrom/bootrom_cgb.sym new file mode 100644 index 000000000..f1cf16d3a --- /dev/null +++ b/extras/bootrom/bootrom_cgb.sym @@ -0,0 +1,20 @@ +; File generated by rgblink +00:0000 main +00:0006 main.erase +00:0015 main.memcpy +00:002c main.four_range +00:0059 main.wait_vblank +00:006c main.no_effect +00:0071 main.play_sound +00:007e main.adj_sound +00:0087 main.wave_table +00:0097 main.effect +00:00a5 main.exit_vblank +00:00bb main.logo +00:00bb main.P1 +00:00c3 main.P2 +00:00cb main.Y1 +00:00d3 main.B2 +00:00db main.O +00:00e3 main.Y2 +00:00fc exit diff --git a/extras/bootrom/bootrom_dmg.sym b/extras/bootrom/bootrom_dmg.sym new file mode 100644 index 000000000..f1cf16d3a --- /dev/null +++ b/extras/bootrom/bootrom_dmg.sym @@ -0,0 +1,20 @@ +; File generated by rgblink +00:0000 main +00:0006 main.erase +00:0015 main.memcpy +00:002c main.four_range +00:0059 main.wait_vblank +00:006c main.no_effect +00:0071 main.play_sound +00:007e main.adj_sound +00:0087 main.wave_table +00:0097 main.effect +00:00a5 main.exit_vblank +00:00bb main.logo +00:00bb main.P1 +00:00c3 main.P2 +00:00cb main.Y1 +00:00d3 main.B2 +00:00db main.O +00:00e3 main.Y2 +00:00fc exit diff --git a/extras/default_rom/Makefile b/extras/default_rom/Makefile index ab602723e..99c5a208a 100644 --- a/extras/default_rom/Makefile +++ b/extras/default_rom/Makefile @@ -7,8 +7,15 @@ ROOT_DIR := $(shell git rev-parse --show-toplevel) build: rgbasm -o default_rom.obj default_rom.asm - rgblink -m default_rom.map -n default_rom.sym -o ${ROOT_DIR}/pyboy/default_rom.gb default_rom.obj - rgbfix -p0 -f hg ${ROOT_DIR}/pyboy/default_rom.gb + rgblink -m default_rom.map -n default_rom.sym -o default_rom.gb default_rom.obj + rgbfix -p0 -f hg default_rom.gb + cp default_rom.gb ${ROOT_DIR}/pyboy/default_rom.gb + + rgbasm -o default_rom_cgb.obj default_rom_cgb.asm + rgblink -m default_rom_cgb.map -n default_rom_cgb.sym -o default_rom_cgb.gb default_rom_cgb.obj + rgbfix -p0 -f hg default_rom_cgb.gb + cp default_rom_cgb.gb ${ROOT_DIR}/pyboy/default_rom_cgb.gb clean: - rm default_rom.map default_rom.sym default_rom.obj + rm -f default_rom.map default_rom.obj + rm -f default_rom_cgb.map default_rom_cgb.obj diff --git a/extras/default_rom/default_rom.asm b/extras/default_rom/default_rom.asm index 078fa7218..84fa8c7bc 100644 --- a/extras/default_rom/default_rom.asm +++ b/extras/default_rom/default_rom.asm @@ -6,7 +6,7 @@ EntryPoint: jp Main SECTION "Title", ROM0[$134] - db "NO-ROM" + db "DEFAULT-ROM" SECTION "Tileset", ROM0 Tileset: @@ -17,6 +17,15 @@ Tilemap: db $40, $41, $42, $43, $44, $45, $46, $41, $41, $41, $47, $41, $41, $41 db $48, $49, $4A, $4B, $4C, $4D, $4E, $49, $4F, $50, $51, $41, $41, $41 +SECTION "CartridgeType", ROM0[$147] + db $11 + +SECTION "CartridgeROMCount", ROM0[$148] + db $00 + +SECTION "CartridgeRAMCount", ROM0[$149] + db $05 + SECTION "Main", ROM0[$150] Main: nop @@ -38,7 +47,7 @@ Main: ld [hl], 144 inc hl ld [hl], 43 - + ld bc, $8400 ld de, $8700 ld hl, Tileset diff --git a/extras/default_rom/default_rom.sym b/extras/default_rom/default_rom.sym new file mode 100644 index 000000000..c2540a1e0 --- /dev/null +++ b/extras/default_rom/default_rom.sym @@ -0,0 +1,13 @@ +; File generated by rgblink +00:0000 Tilemap +00:0100 EntryPoint +00:0150 Main +00:0155 Main.waitVBlank +00:015c Main.setup +00:0174 Main.readTileset +00:0187 Main.readTilemap +00:01a0 Main.clearOAM +00:01ab Main.loop +00:01bb Main.sync +00:01c2 Main.move +00:01cd Tileset diff --git a/extras/default_rom/default_rom_cgb.asm b/extras/default_rom/default_rom_cgb.asm new file mode 100644 index 000000000..f552c3526 --- /dev/null +++ b/extras/default_rom/default_rom_cgb.asm @@ -0,0 +1,4 @@ +INCLUDE "default_rom.asm" + +SECTION "CGB Flag", ROM0[$143] + db $80 diff --git a/extras/default_rom/default_rom_cgb.sym b/extras/default_rom/default_rom_cgb.sym new file mode 100644 index 000000000..c2540a1e0 --- /dev/null +++ b/extras/default_rom/default_rom_cgb.sym @@ -0,0 +1,13 @@ +; File generated by rgblink +00:0000 Tilemap +00:0100 EntryPoint +00:0150 Main +00:0155 Main.waitVBlank +00:015c Main.setup +00:0174 Main.readTileset +00:0187 Main.readTilemap +00:01a0 Main.clearOAM +00:01ab Main.loop +00:01bb Main.sync +00:01c2 Main.move +00:01cd Tileset diff --git a/extras/default_rom/hardware.inc b/extras/default_rom/hardware.inc index 139fd86dd..26b5929f1 100644 --- a/extras/default_rom/hardware.inc +++ b/extras/default_rom/hardware.inc @@ -1,29 +1,29 @@ ; hardware.inc -rP1 EQU $FF00 -rSB EQU $FF01 -rSC EQU $FF02 -rDIV EQU $FF04 -rTIMA EQU $FF05 -rTMA EQU $FF06 -rTAC EQU $FF07 -rIF EQU $FF0F -rLCDC EQU $FF40 -rSTAT EQU $FF41 -rSCY EQU $FF42 -rSCX EQU $FF43 -rLY EQU $FF44 -rLYC EQU $FF45 -rDMA EQU $FF46 -rBGP EQU $FF47 -rOBP0 EQU $FF48 -rOBP1 EQU $FF49 -rWY EQU $FF4A -rWX EQU $FF4B -rIE EQU $FFFF +DEF rP1 EQU $FF00 +DEF rSB EQU $FF01 +DEF rSC EQU $FF02 +DEF rDIV EQU $FF04 +DEF rTIMA EQU $FF05 +DEF rTMA EQU $FF06 +DEF rTAC EQU $FF07 +DEF rIF EQU $FF0F +DEF rLCDC EQU $FF40 +DEF rSTAT EQU $FF41 +DEF rSCY EQU $FF42 +DEF rSCX EQU $FF43 +DEF rLY EQU $FF44 +DEF rLYC EQU $FF45 +DEF rDMA EQU $FF46 +DEF rBGP EQU $FF47 +DEF rOBP0 EQU $FF48 +DEF rOBP1 EQU $FF49 +DEF rWY EQU $FF4A +DEF rWX EQU $FF4B +DEF rIE EQU $FFFF -_VRAM EQU $8000 -_SCRN0 EQU $9800 -_SCRN1 EQU $9C00 -_RAM EQU $C000 -_HRAM EQU $FF80 -_OAMRAM EQU $FE00 +DEF _VRAM EQU $8000 +DEF _SCRN0 EQU $9800 +DEF _SCRN1 EQU $9C00 +DEF _RAM EQU $C000 +DEF _HRAM EQU $FF80 +DEF _OAMRAM EQU $FE00 diff --git a/pyboy/default_rom.gb b/pyboy/default_rom.gb index 3d13f23e8027899d3e910c3fe29729af638dfd82..d6608f46207a42dc53d3f628293aed7a3c2f54c7 100644 GIT binary patch delta 91 zcmZo@U}|V!+Q7m%QPDug#nsI*)F(tY$lsR%3IrKgfvk<)dXobfl^D-XE@re)`sMZi qUzEkq4=n%wpE|@S@!^BZzX=MzPM+P(sOa_I^Z|>+z0GGBjq3pGe<8O3 delta 122 zcmZo@U}|V!+Q7nSCP*Ui^VbdX_noLH%&gy`Iyrz*iSf+jVnz$a4_^PGEPj1p`S<_S jK}Ly>A6))TQ22H7>@G${um7eGSS0RmKFera$H)KxxOO4a diff --git a/pyboy/default_rom_cgb.gb b/pyboy/default_rom_cgb.gb new file mode 100644 index 0000000000000000000000000000000000000000..6d22b2f2f0c24cbf1fd2fa72feeec976fc33e83a GIT binary patch literal 32768 zcmeIuy-UMD7{~D^zHUOD92?ve-v&#D5Utji)|aa2B7%cpH*pe03=V>WOaB1>2xr|a z1ZwHOAWkhfNI)#uA@Qz9P~4pT{vek=ce#76pV{o(d~RVeo6Rn%XE|S3SzTMt7dJLl zCI2Cn4ePIeQ+w^KZjWsh*Yws)^`xX%jZ-({@?P1q!dz5#+EsftuC;1!b|KD`hcly= zT&3jNu3PmTYox3D^xkRNB9`2y25!yN*@M#x28-uGdjC9{a9(YR)IvsvFQPF0(JsW% zuJal8MDf$3H5BSD>2|gLV5tt%FFixg>Iy1{ot&tc`uEtc($|SusZ^Rxv&Ymy&}fu8 zG4-U%u}T=4dPYUhd7i2DXwLIhj#R!+b6wxpmG8T*$aiP`y-1@G1m@dP>EIx_xt`H0 zB6ljflZlAuCBOAs_cgnd^F@+=dS2hk^OF8QZ>x6_1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ k0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILK;W0a7xtQq=l}o! literal 0 HcmV?d00001 diff --git a/tests/test_basics.py b/tests/test_basics.py index e79338b91..e3524e550 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -51,7 +51,7 @@ def test_record_replay(boot_rom, default_rom): os.remove(default_rom + ".replay") - assert digest == b"\xc0\xfe\x0f\xaa\x1b0YY\x1a\x174\x8c\xad\xeaDZ\x1dQ\xa8\xa2\x9fA\xaap\x15(\xc9\xd9#\xd4]{", \ + assert digest == (b'r\x80\x19)\x1a\x88\r\xcc\xb9\xab\xa3\xda\xb1&i\xc8"\xc2\xfb\x8a\x01\x9b\xa81@\x92V=5\x92\\5'), \ "The replay did not result in the expected output" diff --git a/tests/test_replay.py b/tests/test_replay.py index 52de72d04..1430f4ef5 100644 --- a/tests/test_replay.py +++ b/tests/test_replay.py @@ -81,6 +81,9 @@ def replay( ): with open(replay, "rb") as f: recorded_input, b64_romhash, b64_state = json.loads(zlib.decompress(f.read()).decode("ascii")) + if ROM.endswith("default_rom.gb"): + # Hotfixing replay as compiler has changed + b64_romhash = "0sJieqcWfVjl4LAHLZFzcU3hRoIfQ6n0UQLd7RkZh/k=" verify_file_hash(ROM, b64_romhash) state_data = io.BytesIO(base64.b64decode(b64_state.encode("utf8"))) if b64_state is not None else None From a36d2ec80dbc06c8f0852a00e81cae3fd100adb8 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Wed, 7 Feb 2024 19:44:42 +0100 Subject: [PATCH 49/65] Add symbols support for hook_(de)register --- pyboy/plugins/debug_prompt.py | 49 +++++------------ pyboy/pyboy.pxd | 2 + pyboy/pyboy.py | 100 ++++++++++++++++++++++++++++++---- tests/test_breakpoints.py | 64 ++++++++++++++++++++++ 4 files changed, 171 insertions(+), 44 deletions(-) diff --git a/pyboy/plugins/debug_prompt.py b/pyboy/plugins/debug_prompt.py index bf6a016c1..675516359 100644 --- a/pyboy/plugins/debug_prompt.py +++ b/pyboy/plugins/debug_prompt.py @@ -14,7 +14,16 @@ class DebugPrompt(PyBoyPlugin): - argv = [("--breakpoints", {"type": str, "help": "Add breakpoints on start-up (internal use)"})] + argv = [ + ("--breakpoints", { + "type": str, + "help": "Add breakpoints on start-up (internal use)" + }), + ("--symbols-file", { + "type": str, + "help": "Path for a symbols file for ROM (internal use)" + }), + ] def __init__(self, pyboy, mb, pyboy_argv): super().__init__(pyboy, mb, pyboy_argv) @@ -22,35 +31,7 @@ def __init__(self, pyboy, mb, pyboy_argv): if not self.enabled(): return - self.rom_symbols = {} - if pyboy_argv.get("ROM"): - gamerom_file_no_ext, rom_ext = os.path.splitext(pyboy_argv.get("ROM")) - for sym_ext in [".sym", rom_ext + ".sym"]: - sym_path = gamerom_file_no_ext + sym_ext - if os.path.isfile(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()) + self.rom_symbols = pyboy._load_symbols() for _b in (self.pyboy_argv.get("breakpoints") or "").split(","): b = _b.strip() @@ -75,10 +56,10 @@ def parse_bank_addr_sym_label(self, command): addr = int(addr, 16) return bank, addr else: - for bank, addresses in self.rom_symbols.items(): - for addr, label in addresses.items(): - if label == command: - return bank, addr + try: + self.pyboy._lookup_symbol(command) + except ValueError: + pass return None, None def handle_breakpoint(self): diff --git a/pyboy/pyboy.pxd b/pyboy/pyboy.pxd index 247abbc60..f4fffe735 100644 --- a/pyboy/pyboy.pxd +++ b/pyboy/pyboy.pxd @@ -81,6 +81,8 @@ cdef class PyBoy: cdef void _post_tick(self) noexcept cdef dict _hooks + cdef object symbols_file + cdef dict rom_symbols cpdef bint _handle_hooks(self) cpdef int hook_register(self, uint16_t, uint16_t, object, object) except -1 cpdef int hook_deregister(self, uint16_t, uint16_t) except -1 diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index 235d4664b..af15b272b 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -7,6 +7,7 @@ """ import os +import re import time import numpy as np @@ -39,6 +40,7 @@ def __init__( self, gamerom_file, *, + symbols_file=None, bootrom_file=None, sound=False, sound_emulated=False, @@ -96,6 +98,13 @@ def __init__( 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 @@ -899,6 +908,43 @@ def __rendering(self, 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. @@ -910,18 +956,39 @@ def hook_register(self, bank, addr, callback, context): ```python >>> context = "Hello from hook" >>> def my_callback(context): - print(context) - >>> pyboy.hook_register(0, 0x2000, my_callback, context) - >>> pyboy.tick(60) + ... 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(..., symbols_file="game_rom.gb.sym")`. + + Then provide `None` for `bank` and the symbol for `addr` to trigger the automatic lookup. + + Example: + ```python + >>> # Continued example above + >>> pyboy.hook_register(None, "Main.move", lambda x: print(x), "Hello from hook2") + >>> pyboy.tick(80) + Hello from hook2 + True + ``` Args: - bank (int): ROM or RAM bank - addr (int): Address in the Game Boy's address space + 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.") @@ -937,15 +1004,28 @@ def hook_deregister(self, bank, addr): ```python >>> context = "Hello from hook" >>> def my_callback(context): - print(context) - >>> hook_index = pyboy.hook_register(0, 0x2000, my_callback, context) - >>> pyboy.hook_deregister(hook_index) + ... 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): ROM or RAM bank - addr (int): Address in the Game Boy's address space + 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") diff --git a/tests/test_breakpoints.py b/tests/test_breakpoints.py index 06914d8f0..4c58544b8 100644 --- a/tests/test_breakpoints.py +++ b/tests/test_breakpoints.py @@ -5,6 +5,7 @@ import io import platform +import shutil from unittest.mock import Mock import pytest @@ -72,6 +73,69 @@ def _inner_callback(context): assert len(_context) == 1 +def test_register_hook_print(default_rom, capfd): + pyboy = PyBoy(default_rom, window_type="null") + pyboy.set_emulation_speed(0) + + def _inner_callback(context): + print(context) + + bank = -1 # "ROM bank number" for bootrom + addr = 0xFC # Address of last instruction. Expected to execute once. + pyboy.hook_register(bank, addr, _inner_callback, "Hello!") + for _ in range(120): + pyboy.tick() + + out, err = capfd.readouterr() + assert out == "Hello!\n" + + +def test_symbols_none(default_rom): + pyboy = PyBoy(default_rom, window_type="null") + pyboy.set_emulation_speed(0) + + with pytest.raises(ValueError): + pyboy._lookup_symbol("Main.waitVBlank") + + +def test_symbols_auto_locate(default_rom): + new_path = "extras/default_rom/default_rom.gb" + shutil.copyfile(default_rom, new_path) + pyboy = PyBoy(new_path, window_type="null") + pyboy.set_emulation_speed(0) + + _bank, _addr = pyboy._lookup_symbol("Main.waitVBlank") + assert _bank is not None + assert _addr is not None + + +def test_symbols_path_locate(default_rom): + pyboy = PyBoy(default_rom, window_type="null", symbols_file="extras/default_rom/default_rom.sym") + pyboy.set_emulation_speed(0) + + _bank, _addr = pyboy._lookup_symbol("Main.waitVBlank") + assert _bank is not None + assert _addr is not None + + +def test_register_hook_label(default_rom): + pyboy = PyBoy(default_rom, window_type="null", symbols_file="extras/default_rom/default_rom.sym") + pyboy.set_emulation_speed(0) + + def _inner_callback(context): + context.append(1) + + _context = [] + + bank = None # Use None to look up symbol + symbol = "Main.move" # Address of last instruction. Expected to execute once. + pyboy.hook_register(bank, symbol, _inner_callback, _context) + for _ in range(120): + pyboy.tick() + + assert len(_context) == 31 + + def test_register_hook_context2(default_rom): pyboy = PyBoy(default_rom, window_type="null") pyboy.set_emulation_speed(0) From 75ea5a6b25fb9b63bf221117d2a44810d26713b0 Mon Sep 17 00:00:00 2001 From: NicoleFaye <41876584+NicoleFaye@users.noreply.github.com> Date: Thu, 22 Feb 2024 21:39:44 -0800 Subject: [PATCH 50/65] initialize hooks for use in wrapper --- pyboy/pyboy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index af15b272b..70c0351bf 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -264,6 +264,8 @@ def __init__( Game title """ + self._hooks = {} + self._plugin_manager = PluginManager(self, self.mb, kwargs) """ Returns @@ -294,7 +296,6 @@ def __init__( A game-specific wrapper object. """ - self._hooks = {} self.initialized = True def _tick(self, render): From 056e7204e17f5b51ab9afcfa46b1fa4594ffbf24 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Sat, 10 Feb 2024 17:03:59 +0100 Subject: [PATCH 51/65] Improved slicing in tilemap --- pyboy/api/tilemap.py | 53 +++++++++++++++++++++++++++++++++----------- tests/test_basics.py | 3 +++ 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/pyboy/api/tilemap.py b/pyboy/api/tilemap.py index 25045f58a..635b8e51f 100644 --- a/pyboy/api/tilemap.py +++ b/pyboy/api/tilemap.py @@ -216,19 +216,46 @@ def use_tile_objects(self, switch): """ 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 x == slice(None): - x = slice(0, 32, 1) + if addr.stop is None: + stop = 32 + else: + stop = addr.stop - if y == slice(None): - y = slice(0, 32, 1) + if step < 0: + raise ValueError("Reversed ranges are unsupported") + elif start > stop: + raise ValueError("Invalid range") + return start, stop, step - 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) + 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) + + if y_slice: + y = self._fix_slice(y) + else: + assert isinstance(y, int) if self._use_tile_objects: tile_fun = self.tile @@ -236,10 +263,10 @@ def __getitem__(self, xy): 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) diff --git a/tests/test_basics.py b/tests/test_basics.py index e3524e550..47d6f66ef 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -107,6 +107,9 @@ def test_tilemaps(kirby_rom): assert bck_tilemap.map_offset != wdw_tilemap.map_offset assert bck_tilemap[0, 0] == 256 + assert bck_tilemap[30:, 29:] == [[254, 254], [256, 256], [256, 256]] + # assert bck_tilemap[30::-1, 29::-1] == [[256, 256], [256, 256], [254, 254]] # TODO: Not supported + assert bck_tilemap[30:32, 30:32] == [[256, 256], [256, 256]] assert bck_tilemap[:5, 0] == [256, 256, 256, 256, 170] assert bck_tilemap[:20, :10] == [ [256, 256, 256, 256, 170, 176, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256], From 80e126266a6320b55a82fe2be00d606c899fd60e Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Sat, 10 Feb 2024 22:18:35 +0100 Subject: [PATCH 52/65] Implement custom doctest runner --- .github/workflows/pr-test.yml | 17 ++- Makefile | 9 +- pyboy/conftest.py | 277 ++++++++++++++++++++++++++++++++-- setup.py | 2 +- 4 files changed, 283 insertions(+), 22 deletions(-) diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index 386d5bd4f..79aaa1220 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -30,9 +30,11 @@ jobs: pip install --upgrade pip pip install --upgrade wheel setuptools pip install --prefer-binary -r requirements.txt + pip install --prefer-binary -r requirements_tests.txt - name: Doctest + if: ${{ !contains(matrix.os, 'windows') }} run: | - python -m pytest pyboy/ --doctest-modules + python -m pytest pyboy/ -n auto - name: Build PyBoy run: | python setup.py build_ext -j $(getconf _NPROCESSORS_ONLN) --inplace @@ -47,9 +49,7 @@ jobs: TEST_CI: 1 TEST_NO_UI: 1 run: | - pip install --upgrade pip - pip install --prefer-binary -r requirements_tests.txt - python -m pytest tests/ -n2 -v + python -m pytest tests/ -n auto -v - name: Build and upload wheel if: ${{ github.event_name == 'release' && github.event.action == 'published' && !github.event.release.prerelease && !contains(matrix.os, 'ubuntu') }} run: | @@ -101,18 +101,19 @@ jobs: pypy3 -m ensurepip pypy3 -m pip install --upgrade pip pypy3 -m pip install wheel + pypy3 -m pip install --prefer-binary -r requirements.txt + pypy3 -m pip install --prefer-binary -r requirements_tests.txt - name: Doctest + if: ${{ !contains(matrix.os, 'windows') }} run: | - pypy3 -m pytest pyboy/ --doctest-modules + pypy3 -m pytest pyboy/ -n auto - name: Run PyTest env: PYTEST_SECRETS_KEY: ${{ secrets.PYTEST_SECRETS_KEY }} TEST_CI: 1 TEST_NO_UI: 1 run: | - pip install --upgrade pip - pypy3 -m pip install --prefer-binary -r requirements_tests.txt - pypy3 -m pytest tests/ -n2 -v + pypy3 -m pytest tests/ -n auto -v test_manylinux: name: ManyLinux - Build, Test and Deploy diff --git a/Makefile b/Makefile index b51a8bf0a..c69c4882e 100644 --- a/Makefile +++ b/Makefile @@ -64,13 +64,16 @@ uninstall: ${PY} -m pip uninstall pyboy test: export DEBUG=1 -test: clean build test_cython test_pypy +test: clean test_pypy test_cpython_doctest build test_cython + +test_cpython_doctest: + ${PY} -m pytest pyboy/ -n auto -v test_cython: - ${PY} -m pytest tests/ -n4 -v + ${PY} -m pytest tests/ -n auto -v test_pypy: - ${PYPY} -m pytest tests/ -n4 -v + ${PYPY} -m pytest tests/ pyboy/ -n auto -v test_all: test diff --git a/pyboy/conftest.py b/pyboy/conftest.py index 6e3128d16..497830fa3 100644 --- a/pyboy/conftest.py +++ b/pyboy/conftest.py @@ -3,34 +3,291 @@ # GitHub: https://github.com/Baekalfen/PyBoy # +import doctest +import hashlib +import io import os -import shutil +import time +import urllib.request from pathlib import Path from unittest import mock +from zipfile import ZipFile +import numpy as np import pytest +from cryptography.fernet import Fernet +from filelock import FileLock from . import PyBoy +np.set_printoptions(threshold=2**32) +np.set_printoptions(linewidth=np.inf) + +default_rom_path = "test_roms/secrets/" + +extra_test_rom_dir = Path("test_roms/") +os.makedirs(extra_test_rom_dir, exist_ok=True) + + +def url_open(url): + # https://stackoverflow.com/questions/62684468/pythons-requests-triggers-cloudflares-security-while-urllib-does-not + headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/77.0"} + for _ in range(5): + try: + request = urllib.request.Request(url, headers=headers) + return urllib.request.urlopen(request).read() + except urllib.error.HTTPError as ex: + print("HTTPError in url_open", ex) + time.sleep(3) + + +def locate_roms(path=default_rom_path): + if not os.path.isdir(path): + print(f"locate_roms: No directory found: {path}") + return {} + + gb_files = map( + lambda x: path + x, + filter( + lambda x: x.lower().endswith(".gb") or x.lower().endswith(".gbc") or x.endswith(".bin"), os.listdir(path) + ) + ) + + entries = {} + for rom in gb_files: + with open(rom, "rb") as f: + m = hashlib.sha256() + m.update(f.read()) + entries[rom] = m.digest() + + return entries + + +rom_entries = None + + +def locate_sha256(digest): + global rom_entries + if rom_entries is None: + rom_entries = locate_roms() + digest_bytes = bytes.fromhex(digest.decode("ASCII")) + return next(filter(lambda kv: kv[1] == digest_bytes, rom_entries.items()), [None])[0] + + +@pytest.fixture(scope="session") +def secrets(): + path = extra_test_rom_dir / Path("secrets") + with FileLock(path.with_suffix(".lock")) as lock: + if not os.path.isdir(path): + if not os.environ.get("PYTEST_SECRETS_KEY"): + pytest.skip("Cannot access secrets") + fernet = Fernet(os.environ["PYTEST_SECRETS_KEY"].encode()) + + test_data = url_open("https://pyboy.dk/mirror/test_data.encrypted") + data = io.BytesIO() + data.write(fernet.decrypt(test_data)) + + with ZipFile(data, "r") as _zip: + _zip.extractall(path) + return str(path) + @pytest.fixture(scope="session") def default_rom(): return str(Path("pyboy/default_rom.gb")) +@pytest.fixture(scope="session") +def default_rom_cgb(): + return str(Path("pyboy/default_rom_cgb.gb")) + + +@pytest.fixture(scope="session") +def supermarioland_rom(secrets): + return locate_sha256(b"470d6c45c9bcf7f0397d00c1ae6de727c63dd471049c8eedbefdc540ceea80b4") + + +tetris_game_area = np.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=np.uint32) + + @pytest.fixture(autouse=True) -def doctest_fixtures(doctest_namespace, default_rom): +def doctest_fixtures(doctest_namespace, default_rom, default_rom_cgb, supermarioland_rom): + pyboy = PyBoy(default_rom, window_type="null", symbols_file="extras/default_rom/default_rom.sym") + pyboy_cgb = PyBoy(default_rom_cgb, window_type="null", symbols_file="extras/default_rom/default_rom_cgb.sym") + + def mock_PyBoy(filename, *args, **kwargs): + if not os.path.isfile(filename): + filename = default_rom + kwargs.pop("window_type", None) + return PyBoy(filename, *args, window_type="null", **kwargs) + # We mock get_sprite_by_tile_identifier as default_rom doesn't use sprites - with mock.patch("pyboy.PyBoy.get_sprite_by_tile_identifier", return_value=[[0, 2, 4], []]): - pyboy = PyBoy(default_rom, window_type="null", symbols_file="extras/default_rom/default_rom.sym") + with mock.patch("pyboy.PyBoy.get_sprite_by_tile_identifier", return_value=[[0, 2, 4], []]), \ + mock.patch("pyboy.PyBoy.game_area_collision", return_value=np.zeros(shape=(10,9), dtype=np.uint32)), \ + mock.patch("pyboy.PyBoy.game_area", return_value=tetris_game_area), \ + mock.patch("pyboy.PyBoy.load_state", return_value=None), \ + mock.patch("pyboy.PyBoy.stop", return_value=None), \ + mock.patch("PIL.Image.Image.show", return_value=None): + pyboy.set_emulation_speed(0) pyboy.tick(10) # Just a few to get the logo up doctest_namespace["pyboy"] = pyboy + doctest_namespace["pyboy_cgb"] = pyboy_cgb + doctest_namespace["PyBoy"] = mock_PyBoy + doctest_namespace["newline"] = "\n" + doctest_namespace["supermarioland_rom"] = supermarioland_rom - try: - os.remove("file.rom") - except: - pass - shutil.copyfile(default_rom, "file.rom") yield None - os.remove("file.rom") + + +# Code taken from PyTest is licensed with the following: +# The MIT License (MIT) + +# Copyright (c) 2004 Holger Krekel and others + +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is furnished to do +# so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +# NOTE: Taken from Pytest +class PytestDoctestRunner(doctest.DebugRunner): + """Runner to collect failures. + + Note that the out variable in this case is a list instead of a + stdout-like object. + """ + def __init__( + self, + checker=None, + verbose=None, + optionflags: int = 0, + continue_on_failure: bool = True, + ) -> None: + super().__init__(checker=checker, verbose=verbose, optionflags=optionflags) + self.continue_on_failure = continue_on_failure + + def report_failure( + self, + out, + test: "doctest.DocTest", + example: "doctest.Example", + got: str, + ) -> None: + failure = doctest.DocTestFailure(test, example, got) + if self.continue_on_failure: + out.append(failure) + else: + raise failure + + def report_unexpected_exception( + self, + out, + test: "doctest.DocTest", + example: "doctest.Example", + exc_info, + ) -> None: + # if isinstance(exc_info[1], OutcomeException): + # raise exc_info[1] + # if isinstance(exc_info[1], bdb.BdbQuit): + # outcomes.exit("Quitting debugger") + failure = doctest.UnexpectedException(test, example, exc_info) + if self.continue_on_failure: + out.append(failure) + else: + raise failure + + +# NOTE: Taken from Pytest +class DoctestTextfile(pytest.Module): + obj = None + + def collect(self): + import doctest + + # Inspired by doctest.testfile; ideally we would use it directly, + # but it doesn't support passing a custom checker. + encoding = self.config.getini("doctest_encoding") + text = self.path.read_text(encoding) + filename = str(self.path) + name = self.path.name + globs = {"__name__": "__main__"} + + optionflags = doctest.ELLIPSIS + + runner = PytestDoctestRunner( + verbose=False, + optionflags=optionflags, + checker=doctest.OutputChecker(), + continue_on_failure=False, + ) + + parser = doctest.DocTestParser() + examples = parser.get_examples(text, name) + + if not examples: + return + + last = examples[0].lineno + grouped_examples = [[]] + for (lineno, example), i in zip([(e.lineno, e) for e in examples], range(len(examples))): + # Fix parsing error when example ends + example.want = example.want.split("```\n")[0] # Stop parsing, if the docstring ends + + if lineno - i == last: + grouped_examples[-1].append(example) + else: + grouped_examples.append([example]) + last = lineno - i + last += example.source.count("\n") - 1 # Handle multi-line definitions + last += example.want.count("\n") # Handle multi-line definitions + + # TODO: Better naming + for test in [ + doctest.DocTest(x, globs, f"{name}_{i}", filename, 0, text) for i, x in enumerate(grouped_examples) + ]: + if test.examples: + yield pytest.DoctestItem.from_parent(self, name=test.name, runner=runner, dtest=test) + + +# NOTE: Taken from Pytest +def pytest_collect_file(file_path, parent): + config = parent.config + if file_path.suffix == ".py": + txt = DoctestTextfile.from_parent(parent, path=file_path) + return txt + return None diff --git a/setup.py b/setup.py index eed0370ed..67516aaf3 100644 --- a/setup.py +++ b/setup.py @@ -88,7 +88,7 @@ def initialize_options(self): def prep_pxd_py_files(): - ignore_py_files = ["__main__.py", "manager_gen.py", "opcodes_gen.py"] + ignore_py_files = ["__main__.py", "manager_gen.py", "opcodes_gen.py", "conftest.py"] # Cython doesn't trigger a recompile on .py files, where only the .pxd file has changed. So we fix this here. # We also yield the py_files that have a .pxd file, as we feed these into the cythonize call. for root, dirs, files in os.walk(ROOT_DIR): From ec4250c4818e40369726d7244d47865a14105ae0 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Sun, 11 Feb 2024 09:14:00 +0100 Subject: [PATCH 53/65] Move WindowEvent, bcd_to_dec and dec_to_bcd to utils --- extras/examples/gamewrapper_kirby.py | 3 ++- extras/examples/gamewrapper_mario.py | 3 ++- extras/examples/gamewrapper_tetris.py | 3 ++- pyboy/__init__.py | 4 +--- pyboy/pyboy.py | 4 ++-- pyboy/utils.py | 17 +++++------------ tests/test_basics.py | 3 ++- tests/test_external_api.py | 4 ++-- tests/test_game_wrapper.py | 3 ++- tests/test_game_wrapper_mario.py | 3 ++- tests/test_memoryscanner.py | 3 ++- tests/test_pokemon_rl.py | 2 +- tests/test_replay.py | 3 ++- tests/test_rtc3test.py | 2 +- tests/test_states.py | 2 +- tests/test_tetris_ai.py | 3 ++- 16 files changed, 31 insertions(+), 31 deletions(-) diff --git a/extras/examples/gamewrapper_kirby.py b/extras/examples/gamewrapper_kirby.py index e9557b1c1..701ed3d57 100644 --- a/extras/examples/gamewrapper_kirby.py +++ b/extras/examples/gamewrapper_kirby.py @@ -10,7 +10,8 @@ file_path = os.path.dirname(os.path.realpath(__file__)) sys.path.insert(0, file_path + "/../..") -from pyboy import PyBoy, WindowEvent # isort:skip +from pyboy import PyBoy # isort:skip +from pyboy.utils import WindowEvent # isort:skip # Check if the ROM is given through argv if len(sys.argv) > 1: diff --git a/extras/examples/gamewrapper_mario.py b/extras/examples/gamewrapper_mario.py index c3dcf0fac..530a14307 100644 --- a/extras/examples/gamewrapper_mario.py +++ b/extras/examples/gamewrapper_mario.py @@ -10,7 +10,8 @@ file_path = os.path.dirname(os.path.realpath(__file__)) sys.path.insert(0, file_path + "/../..") -from pyboy import PyBoy, WindowEvent # isort:skip +from pyboy import PyBoy # isort:skip +from pyboy.utils import WindowEvent # isort:skip # Check if the ROM is given through argv if len(sys.argv) > 1: diff --git a/extras/examples/gamewrapper_tetris.py b/extras/examples/gamewrapper_tetris.py index 6a905fcf9..bc1ede6ed 100644 --- a/extras/examples/gamewrapper_tetris.py +++ b/extras/examples/gamewrapper_tetris.py @@ -10,7 +10,8 @@ file_path = os.path.dirname(os.path.realpath(__file__)) sys.path.insert(0, file_path + "/../..") -from pyboy import PyBoy, WindowEvent # isort:skip +from pyboy import PyBoy # isort:skip +from pyboy.utils import WindowEvent # isort:skip # Check if the ROM is given through argv if len(sys.argv) > 1: diff --git a/pyboy/__init__.py b/pyboy/__init__.py index 95360b99a..76db24215 100644 --- a/pyboy/__init__.py +++ b/pyboy/__init__.py @@ -7,11 +7,9 @@ "core": False, "logging": False, "pyboy": False, - "utils": False, "conftest": False, } -__all__ = ["PyBoy", "PyBoyMemoryView", "WindowEvent", "dec_to_bcd", "bcd_to_dec"] +__all__ = ["PyBoy", "PyBoyMemoryView"] from .pyboy import PyBoy, PyBoyMemoryView -from .utils import WindowEvent, bcd_to_dec, dec_to_bcd \ No newline at end of file diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index 70c0351bf..6b680ad14 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -640,13 +640,13 @@ def button_release(self, input): def send_input(self, event): """ Send a single input to control the emulator. This is both Game Boy buttons and emulator controls. See - `pyboy.WindowEvent` for which events to send. + `pyboy.utils.WindowEvent` for which events to send. Consider using `PyBoy.button` instead for easier access. Example: ```python - >>> from pyboy import WindowEvent + >>> 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 diff --git a/pyboy/utils.py b/pyboy/utils.py index 55948d4df..ab8b2ff57 100644 --- a/pyboy/utils.py +++ b/pyboy/utils.py @@ -2,7 +2,8 @@ # License: See LICENSE.md file # GitHub: https://github.com/Baekalfen/PyBoy # -from enum import Enum + +__all__ = ["WindowEvent", "dec_to_bcd", "bcd_to_dec"] STATE_VERSION = 10 @@ -133,14 +134,6 @@ def color_code(byte1, byte2, offset): return (((byte2 >> (offset)) & 0b1) << 1) + ((byte1 >> (offset)) & 0b1) -def flatten_list(l): - flat_list = [] - for sublist in l: - for item in sublist: - flat_list.append(item) - return flat_list - - ############################################################## # Window Events # Temporarily placed here to not be exposed on public API @@ -153,7 +146,7 @@ class WindowEvent: It can be used as follows: ```python - >>> from pyboy import WindowEvent + >>> from pyboy.utils import WindowEvent >>> pyboy.send_input(WindowEvent.PAUSE) ``` @@ -297,7 +290,7 @@ def dec_to_bcd(value, byte_width=1, byteorder="little"): Example: ```python - >>> from pyboy import dec_to_bcd + >>> from pyboy.utils import dec_to_bcd >>> f"{dec_to_bcd(30):08b}" '00110000' >>> f"{dec_to_bcd(32):08b}" @@ -329,7 +322,7 @@ def bcd_to_dec(value, byte_width=1, byteorder="little"): Example: ```python - >>> from pyboy import bcd_to_dec + >>> from pyboy.utils import bcd_to_dec >>> bcd_to_dec(0b00110000) 30 >>> bcd_to_dec(0b00110010) diff --git a/tests/test_basics.py b/tests/test_basics.py index 47d6f66ef..c2b36def6 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -15,9 +15,10 @@ import pytest from pytest_lazy_fixtures import lf -from pyboy import PyBoy, WindowEvent +from pyboy import PyBoy from pyboy import __main__ as main from pyboy.api.tile import Tile +from pyboy.utils import WindowEvent is_pypy = platform.python_implementation() == "PyPy" diff --git a/tests/test_external_api.py b/tests/test_external_api.py index 8179bcace..7af3459a4 100644 --- a/tests/test_external_api.py +++ b/tests/test_external_api.py @@ -12,9 +12,9 @@ import pytest from PIL import ImageChops -from pyboy import PyBoy, WindowEvent +from pyboy import PyBoy from pyboy.api.tile import Tile -from pyboy.utils import IntIOWrapper +from pyboy.utils import IntIOWrapper, WindowEvent from .conftest import BOOTROM_FRAMES_UNTIL_LOGO diff --git a/tests/test_game_wrapper.py b/tests/test_game_wrapper.py index 2decbbed9..0eb2e21b1 100644 --- a/tests/test_game_wrapper.py +++ b/tests/test_game_wrapper.py @@ -10,7 +10,8 @@ import numpy as np import pytest -from pyboy import PyBoy, WindowEvent +from pyboy import PyBoy +from pyboy.utils import WindowEvent py_version = platform.python_version()[:3] is_pypy = platform.python_implementation() == "PyPy" diff --git a/tests/test_game_wrapper_mario.py b/tests/test_game_wrapper_mario.py index 3803e9183..c90bffb88 100644 --- a/tests/test_game_wrapper_mario.py +++ b/tests/test_game_wrapper_mario.py @@ -10,7 +10,8 @@ import numpy as np import pytest -from pyboy import PyBoy, WindowEvent +from pyboy import PyBoy +from pyboy.utils import WindowEvent py_version = platform.python_version()[:3] is_pypy = platform.python_implementation() == "PyPy" diff --git a/tests/test_memoryscanner.py b/tests/test_memoryscanner.py index 330ded236..4795905f0 100644 --- a/tests/test_memoryscanner.py +++ b/tests/test_memoryscanner.py @@ -1,5 +1,6 @@ -from pyboy import PyBoy, bcd_to_dec, dec_to_bcd +from pyboy import PyBoy from pyboy.api.memory_scanner import DynamicComparisonType +from pyboy.utils import bcd_to_dec, dec_to_bcd def test_bcd_to_dec_single_byte(): diff --git a/tests/test_pokemon_rl.py b/tests/test_pokemon_rl.py index e87c9c87a..2d3f5bff3 100644 --- a/tests/test_pokemon_rl.py +++ b/tests/test_pokemon_rl.py @@ -43,7 +43,7 @@ from stable_baselines3.common.callbacks import CheckpointCallback from argparse_pokemon import * -from pyboy import WindowEvent +from pyboy.utils import WindowEvent RedGymEnv.add_video_frame = lambda x: None diff --git a/tests/test_replay.py b/tests/test_replay.py index 1430f4ef5..5dc558f5a 100644 --- a/tests/test_replay.py +++ b/tests/test_replay.py @@ -16,7 +16,8 @@ import numpy as np import pytest -from pyboy import PyBoy, WindowEvent, utils +from pyboy import PyBoy +from pyboy.utils import WindowEvent event_filter = [ WindowEvent.PRESS_SPEED_UP, diff --git a/tests/test_rtc3test.py b/tests/test_rtc3test.py index bdd7e86ab..e07efc080 100644 --- a/tests/test_rtc3test.py +++ b/tests/test_rtc3test.py @@ -9,7 +9,7 @@ import PIL import pytest -from pyboy import PyBoy, WindowEvent +from pyboy import PyBoy OVERWRITE_PNGS = False diff --git a/tests/test_states.py b/tests/test_states.py index d7ebd9742..cdd8e2141 100644 --- a/tests/test_states.py +++ b/tests/test_states.py @@ -5,7 +5,7 @@ import io import platform -from pyboy import PyBoy, WindowEvent +from pyboy import PyBoy NEXT_TETROMINO_ADDR = 0xC213 diff --git a/tests/test_tetris_ai.py b/tests/test_tetris_ai.py index 8ce64afee..565f51b0f 100644 --- a/tests/test_tetris_ai.py +++ b/tests/test_tetris_ai.py @@ -23,7 +23,8 @@ from core.gen_algo import get_score, Population, Network from core.utils import check_needed_turn, do_action, drop_down, \ do_sideway, do_turn, check_needed_dirs, feature_names -from pyboy import PyBoy, WindowEvent +from pyboy import PyBoy +from pyboy.utils import WindowEvent from multiprocessing import Pool, cpu_count logger = utils.getLogger("tetris") From fb0d6df95f8b5d8f63b77ad0f3be6f7a03a02474 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Sun, 11 Feb 2024 09:21:54 +0100 Subject: [PATCH 54/65] Generic game wrapper enables start_game and reset_game --- pyboy/plugins/base_plugin.py | 3 +++ pyboy/plugins/game_wrapper_kirby_dream_land.py | 2 -- pyboy/plugins/game_wrapper_super_mario_land.py | 2 +- pyboy/plugins/game_wrapper_tetris.py | 2 -- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pyboy/plugins/base_plugin.py b/pyboy/plugins/base_plugin.py index 13c87e27e..e59c457f4 100644 --- a/pyboy/plugins/base_plugin.py +++ b/pyboy/plugins/base_plugin.py @@ -164,6 +164,9 @@ def start_game(self, timer_div=None): if not self.pyboy.frame_count == 0: 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) def reset_game(self, timer_div=None): """ diff --git a/pyboy/plugins/game_wrapper_kirby_dream_land.py b/pyboy/plugins/game_wrapper_kirby_dream_land.py index 1920dbfa8..60c51f326 100644 --- a/pyboy/plugins/game_wrapper_kirby_dream_land.py +++ b/pyboy/plugins/game_wrapper_kirby_dream_land.py @@ -88,8 +88,6 @@ def start_game(self, timer_div=None): # Wait for transition to finish (exit level intro screen, enter game) self.pyboy.tick(60, False) - self.game_has_started = True - self.saved_state.seek(0) self.pyboy.save_state(self.saved_state) diff --git a/pyboy/plugins/game_wrapper_super_mario_land.py b/pyboy/plugins/game_wrapper_super_mario_land.py index 2b12c376c..543510619 100644 --- a/pyboy/plugins/game_wrapper_super_mario_land.py +++ b/pyboy/plugins/game_wrapper_super_mario_land.py @@ -410,7 +410,7 @@ def start_game(self, timer_div=None, world_level=None, unlock_level_select=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) diff --git a/pyboy/plugins/game_wrapper_tetris.py b/pyboy/plugins/game_wrapper_tetris.py index ba5776fcc..bcb404ac5 100644 --- a/pyboy/plugins/game_wrapper_tetris.py +++ b/pyboy/plugins/game_wrapper_tetris.py @@ -123,8 +123,6 @@ def start_game(self, timer_div=None): 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): From 8294b80644d1058a7f02325a1044d70986ecd455 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Fri, 16 Feb 2024 23:11:38 +0100 Subject: [PATCH 55/65] Fix bug with PIL.Image.save() removing buffer reference --- pyboy/api/screen.py | 12 +++++++----- pyboy/pyboy.py | 5 +++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/pyboy/api/screen.py b/pyboy/api/screen.py index 527346677..ce956c086 100644 --- a/pyboy/api/screen.py +++ b/pyboy/api/screen.py @@ -120,12 +120,8 @@ def __init__(self, mb): """ if not Image: logger.warning("Cannot generate screen image. Missing dependency \"Pillow\".") - 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._set_image() self.ndarray = np.frombuffer( self.mb.lcd.renderer._screenbuffer_raw, @@ -166,6 +162,12 @@ def __init__(self, mb): Screendata in `ndarray` of bytes with shape (144, 160, 4) """ + def _set_image(self): + self.image = Image.frombuffer( + self.mb.lcd.renderer.color_format, self.mb.lcd.renderer.buffer_dims[::-1], + self.mb.lcd.renderer._screenbuffer_raw + ) + @property def tilemap_position_list(self): """ diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index 6b680ad14..33133d814 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -450,6 +450,11 @@ def _unpause(self): self._update_window_title() def _post_tick(self): + # Fix buggy PIL. They will copy our image buffer and destroy the + # reference on some user operations like .save(). + if not self.screen.image.readonly: + self.screen._set_image() + if self.frame_count % 60 == 0: self._update_window_title() self._plugin_manager.post_tick() From 6e6ce7e410497ae02327e97e2dcd1f426ef845e6 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Sun, 25 Feb 2024 00:19:13 +0100 Subject: [PATCH 56/65] Add delay option to pyboy.button --- pyboy/pyboy.pxd | 3 +-- pyboy/pyboy.py | 38 ++++++++++++++++++++++++-------------- tests/test_external_api.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 16 deletions(-) diff --git a/pyboy/pyboy.pxd b/pyboy/pyboy.pxd index f4fffe735..399d02606 100644 --- a/pyboy/pyboy.pxd +++ b/pyboy/pyboy.pxd @@ -42,8 +42,7 @@ cdef class PyBoy: cdef double avg_tick cdef double avg_post - cdef list old_events - cdef list events + cdef readonly list events cdef list queued_input cdef bint quitting cdef bint stopped diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index 33133d814..5fab9b8bc 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -6,6 +6,7 @@ The core module of the emulator """ +import heapq import os import re import time @@ -128,7 +129,6 @@ def __init__( self.paused = False self.events = [] self.queued_input = [] - self.old_events = [] self.quitting = False self.stopped = False self.window_title = "PyBoy" @@ -461,9 +461,10 @@ def _post_tick(self): 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.queued_input - self.queued_input = [] + self.events = [] + while self.queued_input and self.frame_count == self.queued_input[0][0]: + _, _event = heapq.heappop(self.queued_input) + self.events.append(WindowEvent(_event)) def _update_window_title(self): avg_emu = self.avg_pre + self.avg_tick + self.avg_post @@ -510,7 +511,7 @@ def stop(self, save=True): # Scripts and bot methods # - def button(self, input): + def button(self, input, delay=1): """ Send input to PyBoy in the form of "a", "b", "start", "select", "left", "right", "up" and "down". @@ -523,37 +524,46 @@ def button(self, input): True >>> pyboy.tick() # Button 'a' released True - + >>> pyboy.button('a', 3) # Press button 'a' and release after 3 `pyboy.tick()` + >>> pyboy.tick() # Button 'a' pressed + True + >>> pyboy.tick() # Button 'a' still pressed + True + >>> pyboy.tick() # Button 'a' still pressed + True + >>> pyboy.tick() # Button 'a' released + True ``` Args: input (str): button to press + delay (int, optional): Number of frames to delay the release. Defaults to 1 """ input = input.lower() if input == "left": self.send_input(WindowEvent.PRESS_ARROW_LEFT) - self.queued_input.append(WindowEvent(WindowEvent.RELEASE_ARROW_LEFT)) + heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_ARROW_LEFT)) elif input == "right": self.send_input(WindowEvent.PRESS_ARROW_RIGHT) - self.queued_input.append(WindowEvent(WindowEvent.RELEASE_ARROW_RIGHT)) + heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_ARROW_RIGHT)) elif input == "up": self.send_input(WindowEvent.PRESS_ARROW_UP) - self.queued_input.append(WindowEvent(WindowEvent.RELEASE_ARROW_UP)) + heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_ARROW_UP)) elif input == "down": self.send_input(WindowEvent.PRESS_ARROW_DOWN) - self.queued_input.append(WindowEvent(WindowEvent.RELEASE_ARROW_DOWN)) + heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_ARROW_DOWN)) elif input == "a": self.send_input(WindowEvent.PRESS_BUTTON_A) - self.queued_input.append(WindowEvent(WindowEvent.RELEASE_BUTTON_A)) + heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_BUTTON_A)) elif input == "b": self.send_input(WindowEvent.PRESS_BUTTON_B) - self.queued_input.append(WindowEvent(WindowEvent.RELEASE_BUTTON_B)) + heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_BUTTON_B)) elif input == "start": self.send_input(WindowEvent.PRESS_BUTTON_START) - self.queued_input.append(WindowEvent(WindowEvent.RELEASE_BUTTON_START)) + heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_BUTTON_START)) elif input == "select": self.send_input(WindowEvent.PRESS_BUTTON_SELECT) - self.queued_input.append(WindowEvent(WindowEvent.RELEASE_BUTTON_SELECT)) + heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_BUTTON_SELECT)) else: raise Exception("Unrecognized input:", input) diff --git a/tests/test_external_api.py b/tests/test_external_api.py index 7af3459a4..fda367ec7 100644 --- a/tests/test_external_api.py +++ b/tests/test_external_api.py @@ -466,6 +466,35 @@ def test_tilemap_position_list(supermarioland_rom): pyboy.stop(save=False) +def test_button(default_rom): + pyboy = PyBoy(default_rom, window_type="null") + pyboy.set_emulation_speed(0) + + assert len(pyboy.events) == 0 # Nothing injected yet + pyboy.button("start") + assert len(pyboy.events) == 1 # Button press immediately + assert pyboy.events[0].event == WindowEvent.PRESS_BUTTON_START + pyboy.tick(1, False) + assert len(pyboy.events) == 1 # Button release delayed + assert pyboy.events[0].event == WindowEvent.RELEASE_BUTTON_START + pyboy.tick(1, False) + assert len(pyboy.events) == 0 # No input + + assert len(pyboy.events) == 0 # Nothing injected yet + pyboy.button("start", 3) + assert len(pyboy.events) == 1 # Button press immediately + assert pyboy.events[0].event == WindowEvent.PRESS_BUTTON_START + pyboy.tick(1, False) + assert len(pyboy.events) == 0 # No input + pyboy.tick(1, False) + assert len(pyboy.events) == 0 # No input + pyboy.tick(1, False) + assert len(pyboy.events) == 1 # Button release delayed + assert pyboy.events[0].event == WindowEvent.RELEASE_BUTTON_START + pyboy.tick(1, False) + assert len(pyboy.events) == 0 # No input + + def test_get_set_override(default_rom): pyboy = PyBoy(default_rom, window_type="null") pyboy.set_emulation_speed(0) From 356ee9890d389f3d538e11aa6b699c5cbf892103 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Thu, 14 Mar 2024 21:18:28 +0100 Subject: [PATCH 57/65] Add symbol_lookup method --- pyboy/pyboy.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index 5fab9b8bc..b01c1e377 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -961,6 +961,31 @@ def _lookup_symbol(self, symbol): return bank, addr raise ValueError("Symbol not found: %s" % symbol) + def symbol_lookup(self, symbol): + """ + Look up a specific symbol from provided symbols file. + + This can be useful in combination with `PyBoy.memory` or even `PyBoy.hook_register`. + + See `PyBoy.hook_register` for how to load symbol into PyBoy. + + Example: + ```python + >>> # Continued example above + >>> bank, addr = pyboy.symbol_lookup("Tileset") + >>> pyboy.memory[bank, addr] + 0 + >>> pyboy.memory[bank, addr:addr+10] + [0, 0, 0, 0, 0, 0, 102, 102, 102, 102] + + ``` + Returns + ------- + (int, int): + ROM/RAM bank, address + """ + return self._lookup_symbol(symbol) + def hook_register(self, bank, addr, callback, context): """ Adds a hook into a specific bank and memory address. From 6e0e105787571b2390e596f0e4f5dfc760ac78c1 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Sat, 16 Mar 2024 20:44:40 +0100 Subject: [PATCH 58/65] Verify and fix PyBoy kwargs --- extras/examples/gamewrapper_kirby.py | 2 +- extras/examples/gamewrapper_mario.py | 2 +- extras/examples/gamewrapper_tetris.py | 2 +- pyboy/__main__.py | 14 ++++--- pyboy/plugins/base_plugin.py | 1 - pyboy/plugins/debug_prompt.py | 11 +----- .../plugins/game_wrapper_kirby_dream_land.py | 4 +- .../plugins/game_wrapper_super_mario_land.py | 8 ++-- pyboy/plugins/game_wrapper_tetris.py | 4 +- pyboy/pyboy.pxd | 1 - pyboy/pyboy.py | 38 ++++++++++++++----- tests/test_basics.py | 4 +- tests/test_mario_rl.py | 2 +- tests/test_memoryscanner.py | 8 ++-- tests/test_replay.py | 1 - tests/test_samesuite.py | 4 +- 16 files changed, 57 insertions(+), 49 deletions(-) diff --git a/extras/examples/gamewrapper_kirby.py b/extras/examples/gamewrapper_kirby.py index 701ed3d57..d03a5bf41 100644 --- a/extras/examples/gamewrapper_kirby.py +++ b/extras/examples/gamewrapper_kirby.py @@ -21,7 +21,7 @@ exit(1) quiet = "--quiet" in sys.argv -pyboy = PyBoy(filename, window_type="null" if quiet else "SDL2", window_scale=3, debug=not quiet, game_wrapper=True) +pyboy = PyBoy(filename, window_type="null" if quiet else "SDL2", scale=3, debug=not quiet) pyboy.set_emulation_speed(0) assert pyboy.cartridge_title == "KIRBY DREAM LA" diff --git a/extras/examples/gamewrapper_mario.py b/extras/examples/gamewrapper_mario.py index 530a14307..20917283e 100644 --- a/extras/examples/gamewrapper_mario.py +++ b/extras/examples/gamewrapper_mario.py @@ -21,7 +21,7 @@ exit(1) quiet = "--quiet" in sys.argv -pyboy = PyBoy(filename, window_type="null" if quiet else "SDL2", window_scale=3, debug=not quiet, game_wrapper=True) +pyboy = PyBoy(filename, window_type="null" if quiet else "SDL2", scale=3, debug=not quiet) pyboy.set_emulation_speed(0) assert pyboy.cartridge_title == "SUPER MARIOLAN" diff --git a/extras/examples/gamewrapper_tetris.py b/extras/examples/gamewrapper_tetris.py index bc1ede6ed..5761a5af2 100644 --- a/extras/examples/gamewrapper_tetris.py +++ b/extras/examples/gamewrapper_tetris.py @@ -21,7 +21,7 @@ exit(1) quiet = "--quiet" in sys.argv -pyboy = PyBoy(filename, window_type="null" if quiet else "SDL2", window_scale=3, debug=not quiet, game_wrapper=True) +pyboy = PyBoy(filename, window_type="null" if quiet else "SDL2", scale=3, debug=not quiet) pyboy.set_emulation_speed(0) assert pyboy.cartridge_title == "TETRIS" diff --git a/pyboy/__main__.py b/pyboy/__main__.py index 1bd2a86ad..539fe03c2 100644 --- a/pyboy/__main__.py +++ b/pyboy/__main__.py @@ -5,11 +5,11 @@ # import argparse +import copy import os import pyboy from pyboy import PyBoy, core, utils -from pyboy.logging import log_level from pyboy.plugins.manager import parser_arguments from pyboy.pyboy import defaults @@ -42,11 +42,10 @@ def valid_file_path(path): epilog="Warning: Features marked with (internal use) might be subject to change.", ) parser.add_argument("ROM", type=valid_file_path, help="Path to a Game Boy compatible ROM file") -parser.add_argument("-b", "--bootrom", type=valid_file_path, help="Path to a boot-ROM file") -parser.add_argument("--randomize-ram", action="store_true", help="Randomize Game Boy RAM on startup") +parser.add_argument("-b", "--bootrom", dest="bootrom_file", type=valid_file_path, help="Path to a boot-ROM file") parser.add_argument( "--log-level", - default="INFO", + default=defaults["log_level"], type=str, choices=["ERROR", "WARNING", "INFO", "DEBUG", "DISABLE"], help="Set logging level" @@ -107,7 +106,6 @@ def valid_file_path(path): def main(): argv = parser.parse_args() - log_level(argv.log_level) print( """ @@ -148,7 +146,11 @@ def main(): ) # Start PyBoy and run loop - pyboy = PyBoy(argv.ROM, **vars(argv)) + kwargs = copy.deepcopy(vars(argv)) + kwargs.pop("ROM", None) + kwargs.pop("loadstate", None) + kwargs.pop("no_renderer", None) + pyboy = PyBoy(argv.ROM, **kwargs) if argv.loadstate is not None: if argv.loadstate == INTERNAL_LOADSTATE: diff --git a/pyboy/plugins/base_plugin.py b/pyboy/plugins/base_plugin.py index e59c457f4..589867ff1 100644 --- a/pyboy/plugins/base_plugin.py +++ b/pyboy/plugins/base_plugin.py @@ -96,7 +96,6 @@ class PyBoyGameWrapper(PyBoyPlugin): """ cartridge_title = None - argv = [("--game-wrapper", {"action": "store_true", "help": "Enable game wrapper for the current game"})] mapping_one_to_one = np.arange(384 * 2, dtype=np.uint8) """ diff --git a/pyboy/plugins/debug_prompt.py b/pyboy/plugins/debug_prompt.py index 675516359..d4f34087a 100644 --- a/pyboy/plugins/debug_prompt.py +++ b/pyboy/plugins/debug_prompt.py @@ -14,16 +14,7 @@ class DebugPrompt(PyBoyPlugin): - argv = [ - ("--breakpoints", { - "type": str, - "help": "Add breakpoints on start-up (internal use)" - }), - ("--symbols-file", { - "type": str, - "help": "Path for a symbols file for ROM (internal use)" - }), - ] + argv = [("--breakpoints", {"type": str, "help": "Add breakpoints on start-up (internal use)"})] def __init__(self, pyboy, mb, pyboy_argv): super().__init__(pyboy, mb, pyboy_argv) diff --git a/pyboy/plugins/game_wrapper_kirby_dream_land.py b/pyboy/plugins/game_wrapper_kirby_dream_land.py index 60c51f326..be8c4acc2 100644 --- a/pyboy/plugins/game_wrapper_kirby_dream_land.py +++ b/pyboy/plugins/game_wrapper_kirby_dream_land.py @@ -63,7 +63,7 @@ def start_game(self, timer_div=None): instantly. Kwargs: - timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. + * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. """ PyBoyGameWrapper.start_game(self, timer_div=timer_div) @@ -98,7 +98,7 @@ def reset_game(self, timer_div=None): After calling `start_game`, you can call this method at any time to reset the game. Kwargs: - timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. + * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. """ PyBoyGameWrapper.reset_game(self, timer_div=timer_div) diff --git a/pyboy/plugins/game_wrapper_super_mario_land.py b/pyboy/plugins/game_wrapper_super_mario_land.py index 543510619..9be910210 100644 --- a/pyboy/plugins/game_wrapper_super_mario_land.py +++ b/pyboy/plugins/game_wrapper_super_mario_land.py @@ -382,9 +382,9 @@ def start_game(self, timer_div=None, world_level=None, unlock_level_select=False ``` Kwargs: - timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. - world_level (tuple): (world, level) to start the game from - unlock_level_select (bool): Unlock level selector menu + * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. + * world_level (tuple): (world, level) to start the game from + * unlock_level_select (bool): Unlock level selector menu """ PyBoyGameWrapper.start_game(self, timer_div=timer_div) @@ -433,7 +433,7 @@ def reset_game(self, timer_div=None): ``` Kwargs: - timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. + * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. """ PyBoyGameWrapper.reset_game(self, timer_div=timer_div) diff --git a/pyboy/plugins/game_wrapper_tetris.py b/pyboy/plugins/game_wrapper_tetris.py index bcb404ac5..e8d3c4a24 100644 --- a/pyboy/plugins/game_wrapper_tetris.py +++ b/pyboy/plugins/game_wrapper_tetris.py @@ -104,7 +104,7 @@ def start_game(self, timer_div=None): instantly. Kwargs: - timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. + * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. """ # We don't supply the timer_div arg here, as it won't have the desired effect PyBoyGameWrapper.start_game(self) @@ -130,7 +130,7 @@ def reset_game(self, timer_div=None): After calling `start_game`, you can call this method at any time to reset the game. Kwargs: - timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. + * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. """ PyBoyGameWrapper.reset_game(self, timer_div=timer_div) diff --git a/pyboy/pyboy.pxd b/pyboy/pyboy.pxd index 399d02606..7ddd45313 100644 --- a/pyboy/pyboy.pxd +++ b/pyboy/pyboy.pxd @@ -59,7 +59,6 @@ cdef class PyBoy: cdef bint limit_emulationspeed cdef int emulationspeed, target_emulationspeed, save_target_emulationspeed cdef bint record_input - cdef bint disable_input cdef str record_input_file cdef list recorded_input cdef list external_input diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index b01c1e377..8ef006a15 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -16,8 +16,8 @@ from pyboy.api.memory_scanner import MemoryScanner from pyboy.api.screen import Screen from pyboy.api.tilemap import TileMap -from pyboy.logging import get_logger -from pyboy.plugins.manager import PluginManager +from pyboy.logging import get_logger, log_level +from pyboy.plugins.manager import PluginManager, parser_arguments from pyboy.utils import IntIOWrapper, WindowEvent from .api import Sprite, Tile, constants @@ -33,6 +33,7 @@ (0xFFFFFF, 0xFF8484, 0x943A3A, 0x000000)), "scale": 3, "window_type": "SDL2", + "log_level": "ERROR", } @@ -41,12 +42,13 @@ def __init__( self, gamerom_file, *, + window_type=defaults["window_type"], + scale=defaults["scale"], symbols_file=None, bootrom_file=None, sound=False, sound_emulated=False, cgb=None, - randomize=False, **kwargs ): """ @@ -74,16 +76,14 @@ def __init__( gamerom_file (str): Filepath to a game-ROM for Game Boy or Game Boy Color. Kwargs: + * window_type (str): "SDL2", "OpenGL", or "null" + * scale (int): Window scale factor. Doesn't apply to API. * symbols_file (str): Filepath to a .sym file to use. If unsure, specify `None`. - * bootrom_file (str): Filepath to a boot-ROM to use. If unsure, specify `None`. - - * sound (bool): Enable sound emulation and output - + * sound (bool): Enable sound emulation and output. * sound_emulated (bool): Enable sound emulation without any output. Used for compatibility. - + * cgb (bool): Forcing Game Boy Color mode. * color_palette (tuple): Specify the color palette to use for rendering. - * cgb_color_palette (list of tuple): Specify the color palette to use for rendering in CGB-mode for non-color games. Other keyword arguments may exist for plugins that are not listed here. They can be viewed by running `pyboy --help` in the terminal. @@ -91,10 +91,16 @@ def __init__( self.initialized = False + kwargs["window_type"] = window_type + kwargs["scale"] = scale + randomize = kwargs.pop("randomize", False) # Undocumented feature + for k, v in defaults.items(): if k not in kwargs: kwargs[k] = kwargs.get(k, defaults[k]) + log_level(kwargs.pop("log_level")) + if not os.path.isfile(gamerom_file): raise FileNotFoundError(f"ROM file {gamerom_file} was not found!") self.gamerom_file = gamerom_file @@ -108,7 +114,7 @@ def __init__( self.mb = Motherboard( gamerom_file, - bootrom_file or kwargs.get("bootrom"), # Our current way to provide cli arguments is broken + bootrom_file, kwargs["color_palette"], kwargs["cgb_color_palette"], sound, @@ -117,6 +123,18 @@ def __init__( randomize=randomize, ) + # Validate all kwargs + plugin_manager_keywords = [] + for x in parser_arguments(): + if not x: + continue + plugin_manager_keywords.extend(z.strip("-").replace("-", "_") for y in x for z in y[:-1]) + + for k, v in kwargs.items(): + if k not in defaults and k not in plugin_manager_keywords: + logger.error("Unknown keyword argument: %s", k) + raise KeyError("Unknown keyword argument: %s", k) + # Performance measures self.avg_pre = 0 self.avg_tick = 0 diff --git a/tests/test_basics.py b/tests/test_basics.py index c2b36def6..4b25f3dc3 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -69,11 +69,11 @@ def test_argv_parser(*args): for k, v in { "ROM": file_that_exists, "autopause": False, - "bootrom": None, + "bootrom_file": None, "debug": False, "loadstate": None, "no_input": False, - "log_level": "INFO", + "log_level": "ERROR", "record_input": False, "rewind": False, "scale": 3, diff --git a/tests/test_mario_rl.py b/tests/test_mario_rl.py index c985fa2b5..4ee9b7d7d 100644 --- a/tests/test_mario_rl.py +++ b/tests/test_mario_rl.py @@ -84,7 +84,7 @@ ### # Load emulator ### -pyboy = PyBoy(game, window_type="null", window_scale=3, debug=False) +pyboy = PyBoy(game, window_type="null", scale=3, debug=False) ### # Load enviroment diff --git a/tests/test_memoryscanner.py b/tests/test_memoryscanner.py index 4795905f0..4e8cfc5a8 100644 --- a/tests/test_memoryscanner.py +++ b/tests/test_memoryscanner.py @@ -35,7 +35,7 @@ def test_bcd_to_dec_complex2(): def test_memoryscanner_basic(default_rom): - pyboy = PyBoy(default_rom, window_type="null", game_wrapper=True) + pyboy = PyBoy(default_rom, window_type="null") pyboy.set_emulation_speed(0) addresses = pyboy.memory_scanner.scan_memory() assert len(addresses) == 0x10000 @@ -46,7 +46,7 @@ def test_memoryscanner_basic(default_rom): def test_memoryscanner_boundary(default_rom): - pyboy = PyBoy(default_rom, window_type="null", game_wrapper=True) + pyboy = PyBoy(default_rom, window_type="null") pyboy.set_emulation_speed(0) # Byte width of 1 @@ -74,7 +74,7 @@ def test_memoryscanner_boundary(default_rom): def test_memoryscanner_absolute(kirby_rom): - pyboy = PyBoy(kirby_rom, window_type="null", game_wrapper=True) + pyboy = PyBoy(kirby_rom, window_type="null") pyboy.set_emulation_speed(0) kirby = pyboy.game_wrapper kirby.start_game() @@ -102,7 +102,7 @@ def test_memoryscanner_absolute(kirby_rom): def test_memoryscanner_relative(kirby_rom): - pyboy = PyBoy(kirby_rom, window_type="null", game_wrapper=True) + pyboy = PyBoy(kirby_rom, window_type="null") pyboy.set_emulation_speed(0) kirby = pyboy.game_wrapper kirby.start_game() diff --git a/tests/test_replay.py b/tests/test_replay.py index 5dc558f5a..ba9683cc6 100644 --- a/tests/test_replay.py +++ b/tests/test_replay.py @@ -93,7 +93,6 @@ def replay( ROM, window_type=window, bootrom_file=bootrom_file, - disable_input=True, rewind=rewind, randomize=randomize, cgb=cgb, diff --git a/tests/test_samesuite.py b/tests/test_samesuite.py index 3718ade95..42d5c133a 100644 --- a/tests/test_samesuite.py +++ b/tests/test_samesuite.py @@ -116,7 +116,7 @@ def test_samesuite(clean, gb_type, rom, samesuite_dir, boot_cgb_rom, boot_rom, d default_rom, window_type="null", cgb=gb_type == "cgb", - bootrom=boot_cgb_rom if gb_type == "cgb" else boot_rom, + bootrom_file=boot_cgb_rom if gb_type == "cgb" else boot_rom, sound_emulated=True, ) pyboy.set_emulation_speed(0) @@ -129,7 +129,7 @@ def test_samesuite(clean, gb_type, rom, samesuite_dir, boot_cgb_rom, boot_rom, d samesuite_dir + rom, window_type="null", cgb=gb_type == "cgb", - bootrom=boot_cgb_rom if gb_type == "cgb" else boot_rom, + bootrom_file=boot_cgb_rom if gb_type == "cgb" else boot_rom, sound_emulated=True, ) pyboy.set_emulation_speed(0) From 24f012904fb100187143a2210c7974addd675b96 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Sat, 16 Mar 2024 23:47:19 +0100 Subject: [PATCH 59/65] Remove "_file" and "_type" postfixes from PyBoy kwargs --- extras/examples/gamewrapper_kirby.py | 2 +- extras/examples/gamewrapper_mario.py | 2 +- extras/examples/gamewrapper_tetris.py | 2 +- pyboy/__main__.py | 5 +-- pyboy/conftest.py | 8 ++-- pyboy/core/mb.py | 4 +- pyboy/plugins/debug.py | 2 +- pyboy/plugins/record_replay.py | 4 +- pyboy/plugins/window_null.py | 4 +- pyboy/plugins/window_open_gl.py | 3 +- pyboy/plugins/window_sdl2.py | 2 +- pyboy/pyboy.pxd | 2 +- pyboy/pyboy.py | 63 +++++++++++++++++---------- tests/test_acid_cgb.py | 2 +- tests/test_acid_dmg.py | 2 +- tests/test_basics.py | 16 +++---- tests/test_blargg.py | 2 +- tests/test_breakpoints.py | 26 +++++------ tests/test_external_api.py | 20 ++++----- tests/test_game_wrapper.py | 4 +- tests/test_game_wrapper_mario.py | 6 +-- tests/test_magen.py | 2 +- tests/test_mario_rl.py | 2 +- tests/test_memoryscanner.py | 8 ++-- tests/test_memoryview.py | 2 +- tests/test_mooneye.py | 4 +- tests/test_replay.py | 4 +- tests/test_rtc3test.py | 2 +- tests/test_samesuite.py | 8 ++-- tests/test_shonumi.py | 2 +- tests/test_states.py | 2 +- tests/test_tetris_ai.py | 2 +- tests/test_which.py | 2 +- tests/test_whichboot.py | 2 +- 34 files changed, 119 insertions(+), 104 deletions(-) diff --git a/extras/examples/gamewrapper_kirby.py b/extras/examples/gamewrapper_kirby.py index d03a5bf41..7758d12db 100644 --- a/extras/examples/gamewrapper_kirby.py +++ b/extras/examples/gamewrapper_kirby.py @@ -21,7 +21,7 @@ exit(1) quiet = "--quiet" in sys.argv -pyboy = PyBoy(filename, window_type="null" if quiet else "SDL2", scale=3, debug=not quiet) +pyboy = PyBoy(filename, window="null" if quiet else "SDL2", scale=3, debug=not quiet) pyboy.set_emulation_speed(0) assert pyboy.cartridge_title == "KIRBY DREAM LA" diff --git a/extras/examples/gamewrapper_mario.py b/extras/examples/gamewrapper_mario.py index 20917283e..93e7b572f 100644 --- a/extras/examples/gamewrapper_mario.py +++ b/extras/examples/gamewrapper_mario.py @@ -21,7 +21,7 @@ exit(1) quiet = "--quiet" in sys.argv -pyboy = PyBoy(filename, window_type="null" if quiet else "SDL2", scale=3, debug=not quiet) +pyboy = PyBoy(filename, window="null" if quiet else "SDL2", scale=3, debug=not quiet) pyboy.set_emulation_speed(0) assert pyboy.cartridge_title == "SUPER MARIOLAN" diff --git a/extras/examples/gamewrapper_tetris.py b/extras/examples/gamewrapper_tetris.py index 5761a5af2..f0cb1a4e3 100644 --- a/extras/examples/gamewrapper_tetris.py +++ b/extras/examples/gamewrapper_tetris.py @@ -21,7 +21,7 @@ exit(1) quiet = "--quiet" in sys.argv -pyboy = PyBoy(filename, window_type="null" if quiet else "SDL2", scale=3, debug=not quiet) +pyboy = PyBoy(filename, window="null" if quiet else "SDL2", scale=3, debug=not quiet) pyboy.set_emulation_speed(0) assert pyboy.cartridge_title == "TETRIS" diff --git a/pyboy/__main__.py b/pyboy/__main__.py index 539fe03c2..9fe431e04 100644 --- a/pyboy/__main__.py +++ b/pyboy/__main__.py @@ -42,7 +42,7 @@ def valid_file_path(path): epilog="Warning: Features marked with (internal use) might be subject to change.", ) parser.add_argument("ROM", type=valid_file_path, help="Path to a Game Boy compatible ROM file") -parser.add_argument("-b", "--bootrom", dest="bootrom_file", type=valid_file_path, help="Path to a boot-ROM file") +parser.add_argument("-b", "--bootrom", dest="bootrom", type=valid_file_path, help="Path to a boot-ROM file") parser.add_argument( "--log-level", default=defaults["log_level"], @@ -78,9 +78,8 @@ def valid_file_path(path): ) parser.add_argument( "-w", - "--window-type", "--window", - default=defaults["window_type"], + default=defaults["window"], type=str, choices=["SDL2", "OpenGL", "null"], help="Specify window-type to use" diff --git a/pyboy/conftest.py b/pyboy/conftest.py index 497830fa3..9dd53ed85 100644 --- a/pyboy/conftest.py +++ b/pyboy/conftest.py @@ -132,14 +132,14 @@ def supermarioland_rom(secrets): @pytest.fixture(autouse=True) def doctest_fixtures(doctest_namespace, default_rom, default_rom_cgb, supermarioland_rom): - pyboy = PyBoy(default_rom, window_type="null", symbols_file="extras/default_rom/default_rom.sym") - pyboy_cgb = PyBoy(default_rom_cgb, window_type="null", symbols_file="extras/default_rom/default_rom_cgb.sym") + pyboy = PyBoy(default_rom, window="null", symbols="extras/default_rom/default_rom.sym") + pyboy_cgb = PyBoy(default_rom_cgb, window="null", symbols="extras/default_rom/default_rom_cgb.sym") def mock_PyBoy(filename, *args, **kwargs): if not os.path.isfile(filename): filename = default_rom - kwargs.pop("window_type", None) - return PyBoy(filename, *args, window_type="null", **kwargs) + kwargs.pop("window", None) + return PyBoy(filename, *args, window="null", **kwargs) # We mock get_sprite_by_tile_identifier as default_rom doesn't use sprites with mock.patch("pyboy.PyBoy.get_sprite_by_tile_identifier", return_value=[[0, 2, 4], []]), \ diff --git a/pyboy/core/mb.py b/pyboy/core/mb.py index cae4eb0d8..799b057b3 100644 --- a/pyboy/core/mb.py +++ b/pyboy/core/mb.py @@ -19,7 +19,7 @@ class Motherboard: def __init__( self, - gamerom_file, + gamerom, bootrom_file, color_palette, cgb_color_palette, @@ -31,7 +31,7 @@ def __init__( if bootrom_file is not None: logger.info("Boot-ROM file provided") - self.cartridge = cartridge.load_cartridge(gamerom_file) + self.cartridge = cartridge.load_cartridge(gamerom) logger.debug("Cartridge started:\n%s", str(self.cartridge)) if cgb is None: cgb = self.cartridge.cgb diff --git a/pyboy/plugins/debug.py b/pyboy/plugins/debug.py index 7e1e6370f..012bbbca6 100644 --- a/pyboy/plugins/debug.py +++ b/pyboy/plugins/debug.py @@ -84,7 +84,7 @@ def __init__(self, pyboy, mb, pyboy_argv): self.cgb = mb.cartridge_cgb - self.sdl2_event_pump = self.pyboy_argv.get("window_type") != "SDL2" + self.sdl2_event_pump = self.pyboy_argv.get("window") != "SDL2" if self.sdl2_event_pump: sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO) diff --git a/pyboy/plugins/record_replay.py b/pyboy/plugins/record_replay.py index 6cd525d89..aebb8226f 100644 --- a/pyboy/plugins/record_replay.py +++ b/pyboy/plugins/record_replay.py @@ -46,9 +46,9 @@ def handle_events(self, events): def stop(self): save_replay( - self.pyboy.gamerom_file, + self.pyboy.gamerom, self.pyboy_argv.get("loadstate"), - self.pyboy.gamerom_file + ".replay", + self.pyboy.gamerom + ".replay", self.recorded_input, ) diff --git a/pyboy/plugins/window_null.py b/pyboy/plugins/window_null.py index 6e53d4c7a..2257db97d 100644 --- a/pyboy/plugins/window_null.py +++ b/pyboy/plugins/window_null.py @@ -17,13 +17,13 @@ def __init__(self, pyboy, mb, pyboy_argv): if not self.enabled(): return - if pyboy_argv.get("window_type") in ["headless", "dummy"]: + if pyboy_argv.get("window") in ["headless", "dummy"]: logger.error( 'Deprecated use of "headless" or "dummy" window. Change to "null" window instead. https://github.com/Baekalfen/PyBoy/wiki/Migrating-from-v1.x.x-to-v2.0.0' ) def enabled(self): - return self.pyboy_argv.get("window_type") in ["null", "headless", "dummy"] + return self.pyboy_argv.get("window") in ["null", "headless", "dummy"] def set_title(self, title): logger.debug(title) diff --git a/pyboy/plugins/window_open_gl.py b/pyboy/plugins/window_open_gl.py index 63a2e1324..7374c3b40 100644 --- a/pyboy/plugins/window_open_gl.py +++ b/pyboy/plugins/window_open_gl.py @@ -4,6 +4,7 @@ # import time + import numpy as np import pyboy @@ -149,7 +150,7 @@ def frame_limiter(self, speed): return True def enabled(self): - if self.pyboy_argv.get("window_type") == "OpenGL": + if self.pyboy_argv.get("window") == "OpenGL": if opengl_enabled: if bool(OpenGL.GLUT.freeglut.glutMainLoopEvent): return True diff --git a/pyboy/plugins/window_sdl2.py b/pyboy/plugins/window_sdl2.py index 2e7b950f8..7919a45af 100644 --- a/pyboy/plugins/window_sdl2.py +++ b/pyboy/plugins/window_sdl2.py @@ -196,7 +196,7 @@ def post_tick(self): sdl2.SDL_RenderClear(self._sdlrenderer) def enabled(self): - if self.pyboy_argv.get("window_type") in ("SDL2", None): + if self.pyboy_argv.get("window") in ("SDL2", None): if not sdl2: logger.error("Failed to import sdl2, needed for sdl2 window") return False # Disable, or raise exception? diff --git a/pyboy/pyboy.pxd b/pyboy/pyboy.pxd index 7ddd45313..a41c93462 100644 --- a/pyboy/pyboy.pxd +++ b/pyboy/pyboy.pxd @@ -35,7 +35,7 @@ cdef class PyBoy: cdef Motherboard mb cdef readonly PluginManager _plugin_manager cdef readonly uint64_t frame_count - cdef readonly str gamerom_file + cdef readonly str gamerom cdef readonly bint paused cdef double avg_pre diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index 8ef006a15..d64d23ed3 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -32,7 +32,7 @@ "cgb_color_palette": ((0xFFFFFF, 0x7BFF31, 0x0063C5, 0x000000), (0xFFFFFF, 0xFF8484, 0x943A3A, 0x000000), (0xFFFFFF, 0xFF8484, 0x943A3A, 0x000000)), "scale": 3, - "window_type": "SDL2", + "window": "SDL2", "log_level": "ERROR", } @@ -40,12 +40,12 @@ class PyBoy: def __init__( self, - gamerom_file, + gamerom, *, - window_type=defaults["window_type"], + window=defaults["window"], scale=defaults["scale"], - symbols_file=None, - bootrom_file=None, + symbols=None, + bootrom=None, sound=False, sound_emulated=False, cgb=None, @@ -60,7 +60,7 @@ def __init__( GitHub, if other methods are needed for your projects. Take a look at the files in `examples/` for a crude "bots", which interact with the game. - Only the `gamerom_file` argument is required. + Only the `gamerom` argument is required. Example: ```python @@ -73,13 +73,13 @@ def __init__( ``` Args: - gamerom_file (str): Filepath to a game-ROM for Game Boy or Game Boy Color. + gamerom (str): Filepath to a game-ROM for Game Boy or Game Boy Color. Kwargs: - * window_type (str): "SDL2", "OpenGL", or "null" + * window (str): "SDL2", "OpenGL", or "null" * scale (int): Window scale factor. Doesn't apply to API. - * symbols_file (str): Filepath to a .sym file to use. If unsure, specify `None`. - * bootrom_file (str): Filepath to a boot-ROM to use. If unsure, specify `None`. + * symbols (str): Filepath to a .sym file to use. If unsure, specify `None`. + * bootrom (str): Filepath to a boot-ROM to use. If unsure, specify `None`. * sound (bool): Enable sound emulation and output. * sound_emulated (bool): Enable sound emulation without any output. Used for compatibility. * cgb (bool): Forcing Game Boy Color mode. @@ -91,7 +91,22 @@ def __init__( self.initialized = False - kwargs["window_type"] = window_type + if "bootrom_file" in kwargs: + logger.error( + "Deprecated use of 'bootrom_file'. Use 'bootrom' keyword argument instead. https://github.com/Baekalfen/PyBoy/wiki/Migrating-from-v1.x.x-to-v2.0.0" + ) + bootrom = kwargs.pop("bootrom_file") + + if "window_type" in kwargs: + logger.error( + "Deprecated use of 'window_type'. Use 'window' keyword argument instead. https://github.com/Baekalfen/PyBoy/wiki/Migrating-from-v1.x.x-to-v2.0.0" + ) + window = kwargs.pop("window_type") + + if window not in ["SDL2", "OpenGL", "null", "headless", "dummy"]: + raise KeyError(f'Unknown window type: {window}. Use "SDL2", "OpenGL", or "null"') + + kwargs["window"] = window kwargs["scale"] = scale randomize = kwargs.pop("randomize", False) # Undocumented feature @@ -101,20 +116,20 @@ def __init__( log_level(kwargs.pop("log_level")) - if not os.path.isfile(gamerom_file): - raise FileNotFoundError(f"ROM file {gamerom_file} was not found!") - self.gamerom_file = gamerom_file + if not os.path.isfile(gamerom): + raise FileNotFoundError(f"ROM file {gamerom} was not found!") + self.gamerom = gamerom 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 + if symbols is not None: + if not os.path.isfile(symbols): + raise FileNotFoundError(f"Symbols file {symbols} was not found!") + self.symbols_file = symbols self._load_symbols() self.mb = Motherboard( - gamerom_file, - bootrom_file, + gamerom, + bootrom, kwargs["color_palette"], kwargs["cgb_color_palette"], sound, @@ -133,7 +148,7 @@ def __init__( for k, v in kwargs.items(): if k not in defaults and k not in plugin_manager_keywords: logger.error("Unknown keyword argument: %s", k) - raise KeyError("Unknown keyword argument: %s", k) + raise KeyError(f"Unknown keyword argument: {k}") # Performance measures self.avg_pre = 0 @@ -425,10 +440,10 @@ def _handle_events(self, events): self.target_emulationspeed = int(bool(self.target_emulationspeed) ^ True) logger.debug("Speed limit: %d", self.target_emulationspeed) elif event == WindowEvent.STATE_SAVE: - with open(self.gamerom_file + ".state", "wb") as f: + with open(self.gamerom + ".state", "wb") as f: self.mb.save_state(IntIOWrapper(f)) elif event == WindowEvent.STATE_LOAD: - state_path = self.gamerom_file + ".state" + state_path = self.gamerom + ".state" if not os.path.isfile(state_path): logger.error("State file not found: %s", state_path) continue @@ -943,7 +958,7 @@ 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) + gamerom_file_no_ext, rom_ext = os.path.splitext(self.gamerom) for sym_path in [self.symbols_file, gamerom_file_no_ext + ".sym", gamerom_file_no_ext + rom_ext + ".sym"]: if sym_path and os.path.isfile(sym_path): logger.info("Loading symbol file: %s", sym_path) diff --git a/tests/test_acid_cgb.py b/tests/test_acid_cgb.py index 9c0278c7f..41f21c1c8 100644 --- a/tests/test_acid_cgb.py +++ b/tests/test_acid_cgb.py @@ -15,7 +15,7 @@ # https://github.com/mattcurrie/cgb-acid2 def test_cgb_acid(cgb_acid_file): - pyboy = PyBoy(cgb_acid_file, window_type="null") + pyboy = PyBoy(cgb_acid_file, window="null") pyboy.set_emulation_speed(0) pyboy.tick(59, True) pyboy.tick(25, True) diff --git a/tests/test_acid_dmg.py b/tests/test_acid_dmg.py index 43fe0d8d5..adf942af1 100644 --- a/tests/test_acid_dmg.py +++ b/tests/test_acid_dmg.py @@ -16,7 +16,7 @@ @pytest.mark.parametrize("cgb", [False, True]) def test_dmg_acid(cgb, dmg_acid_file): - pyboy = PyBoy(dmg_acid_file, window_type="null", cgb=cgb) + pyboy = PyBoy(dmg_acid_file, window="null", cgb=cgb) pyboy.set_emulation_speed(0) pyboy.tick(59, True) pyboy.tick(25, True) diff --git a/tests/test_basics.py b/tests/test_basics.py index 4b25f3dc3..64c68a173 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -24,7 +24,7 @@ def test_record_replay(boot_rom, default_rom): - pyboy = PyBoy(default_rom, window_type="null", bootrom_file=boot_rom, record_input=True) + pyboy = PyBoy(default_rom, window="null", bootrom=boot_rom, record_input=True) pyboy.set_emulation_speed(0) pyboy.tick(1, True) pyboy.button_press("down") @@ -69,7 +69,7 @@ def test_argv_parser(*args): for k, v in { "ROM": file_that_exists, "autopause": False, - "bootrom_file": None, + "bootrom": None, "debug": False, "loadstate": None, "no_input": False, @@ -77,7 +77,7 @@ def test_argv_parser(*args): "record_input": False, "rewind": False, "scale": 3, - "window_type": "SDL2" + "window": "SDL2" }.items(): assert empty[k] == v @@ -97,7 +97,7 @@ def test_argv_parser(*args): def test_tilemaps(kirby_rom): - pyboy = PyBoy(kirby_rom, window_type="null") + pyboy = PyBoy(kirby_rom, window="null") pyboy.set_emulation_speed(0) pyboy.tick(120, False) @@ -156,7 +156,7 @@ def test_tilemaps(kirby_rom): def test_randomize_ram(default_rom): - pyboy = PyBoy(default_rom, window_type="null", randomize=False) + pyboy = PyBoy(default_rom, window="null", randomize=False) # RAM banks should all be 0 by default assert not any(pyboy.memory[0x8000:0xA000]), "VRAM not zeroed" assert not any(pyboy.memory[0xC000:0xE000]), "Internal RAM 0 not zeroed" @@ -166,7 +166,7 @@ def test_randomize_ram(default_rom): assert not any(pyboy.memory[0xFF80:0xFFFF]), "Internal RAM 1 not zeroed" pyboy.stop(save=False) - pyboy = PyBoy(default_rom, window_type="null", randomize=True) + pyboy = PyBoy(default_rom, window="null", randomize=True) # RAM banks should have at least one nonzero value now assert any(pyboy.memory[0x8000:0xA000]), "VRAM not randomized" assert any(pyboy.memory[0xC000:0xE000]), "Internal RAM 0 not randomized" @@ -178,7 +178,7 @@ def test_randomize_ram(default_rom): def test_not_cgb(pokemon_crystal_rom): - pyboy = PyBoy(pokemon_crystal_rom, window_type="null", cgb=False) + pyboy = PyBoy(pokemon_crystal_rom, window="null", cgb=False) pyboy.set_emulation_speed(0) pyboy.tick(60 * 7, False) @@ -202,7 +202,7 @@ def test_all_modes(cgb, _bootrom, frames, rom, any_rom_cgb, boot_cgb_rom): if cgb == None and _bootrom == boot_cgb_rom and rom != any_rom_cgb: pytest.skip("Invalid combination") - pyboy = PyBoy(rom, window_type="null", bootrom_file=_bootrom, cgb=cgb) + pyboy = PyBoy(rom, window="null", bootrom=_bootrom, cgb=cgb) pyboy.set_emulation_speed(0) pyboy.tick(frames, True) diff --git a/tests/test_blargg.py b/tests/test_blargg.py index 02dfa8d9d..eac9731a7 100644 --- a/tests/test_blargg.py +++ b/tests/test_blargg.py @@ -21,7 +21,7 @@ def run_rom(rom): - pyboy = PyBoy(str(rom), window_type="null", cgb="cgb" in rom, sound_emulated=True) + pyboy = PyBoy(str(rom), window="null", cgb="cgb" in rom, sound_emulated=True) pyboy.set_emulation_speed(0) t = time.time() result = "" diff --git a/tests/test_breakpoints.py b/tests/test_breakpoints.py index 4c58544b8..54a6b4869 100644 --- a/tests/test_breakpoints.py +++ b/tests/test_breakpoints.py @@ -16,7 +16,7 @@ def test_debugprompt(default_rom, monkeypatch): - pyboy = PyBoy(default_rom, window_type="null", breakpoints="0:0100,-1:0", debug=False) + pyboy = PyBoy(default_rom, window="null", breakpoints="0:0100,-1:0", debug=False) pyboy.set_emulation_speed(0) # Break at 0, step once, continue, break at 100, continue @@ -30,7 +30,7 @@ def test_debugprompt(default_rom, monkeypatch): @pytest.mark.parametrize("commands", ["n\nc\n", "n\nn\nc\n", "c\nc\n", "n\nn\nn\nn\nn\nn\nc\n"]) def test_debugprompt2(default_rom, monkeypatch, commands): - pyboy = PyBoy(default_rom, window_type="null", breakpoints="-1:0,-1:3", debug=False) + pyboy = PyBoy(default_rom, window="null", breakpoints="-1:0,-1:3", debug=False) pyboy.set_emulation_speed(0) monkeypatch.setattr("sys.stdin", io.StringIO(commands)) @@ -42,7 +42,7 @@ def test_debugprompt2(default_rom, monkeypatch, commands): def test_register_hooks(default_rom): - pyboy = PyBoy(default_rom, window_type="null") + pyboy = PyBoy(default_rom, window="null") pyboy.set_emulation_speed(0) mock = Mock() @@ -56,7 +56,7 @@ def test_register_hooks(default_rom): def test_register_hook_context(default_rom): - pyboy = PyBoy(default_rom, window_type="null") + pyboy = PyBoy(default_rom, window="null") pyboy.set_emulation_speed(0) def _inner_callback(context): @@ -74,7 +74,7 @@ def _inner_callback(context): def test_register_hook_print(default_rom, capfd): - pyboy = PyBoy(default_rom, window_type="null") + pyboy = PyBoy(default_rom, window="null") pyboy.set_emulation_speed(0) def _inner_callback(context): @@ -91,7 +91,7 @@ def _inner_callback(context): def test_symbols_none(default_rom): - pyboy = PyBoy(default_rom, window_type="null") + pyboy = PyBoy(default_rom, window="null") pyboy.set_emulation_speed(0) with pytest.raises(ValueError): @@ -101,7 +101,7 @@ def test_symbols_none(default_rom): def test_symbols_auto_locate(default_rom): new_path = "extras/default_rom/default_rom.gb" shutil.copyfile(default_rom, new_path) - pyboy = PyBoy(new_path, window_type="null") + pyboy = PyBoy(new_path, window="null") pyboy.set_emulation_speed(0) _bank, _addr = pyboy._lookup_symbol("Main.waitVBlank") @@ -110,7 +110,7 @@ def test_symbols_auto_locate(default_rom): def test_symbols_path_locate(default_rom): - pyboy = PyBoy(default_rom, window_type="null", symbols_file="extras/default_rom/default_rom.sym") + pyboy = PyBoy(default_rom, window="null", symbols="extras/default_rom/default_rom.sym") pyboy.set_emulation_speed(0) _bank, _addr = pyboy._lookup_symbol("Main.waitVBlank") @@ -119,7 +119,7 @@ def test_symbols_path_locate(default_rom): def test_register_hook_label(default_rom): - pyboy = PyBoy(default_rom, window_type="null", symbols_file="extras/default_rom/default_rom.sym") + pyboy = PyBoy(default_rom, window="null", symbols="extras/default_rom/default_rom.sym") pyboy.set_emulation_speed(0) def _inner_callback(context): @@ -137,7 +137,7 @@ def _inner_callback(context): def test_register_hook_context2(default_rom): - pyboy = PyBoy(default_rom, window_type="null") + pyboy = PyBoy(default_rom, window="null") pyboy.set_emulation_speed(0) def _inner_callback(context): @@ -156,7 +156,7 @@ def _inner_callback(context): def test_register_hooks_double(default_rom): - pyboy = PyBoy(default_rom, window_type="null") + pyboy = PyBoy(default_rom, window="null") pyboy.set_emulation_speed(0) mock = Mock() @@ -166,7 +166,7 @@ def test_register_hooks_double(default_rom): def test_deregister_hooks(default_rom): - pyboy = PyBoy(default_rom, window_type="null") + pyboy = PyBoy(default_rom, window="null") pyboy.set_emulation_speed(0) mock = Mock() @@ -183,7 +183,7 @@ def test_deregister_hooks(default_rom): def test_deregister_hooks2(default_rom): - pyboy = PyBoy(default_rom, window_type="null") + pyboy = PyBoy(default_rom, window="null") pyboy.set_emulation_speed(0) mock = Mock() diff --git a/tests/test_external_api.py b/tests/test_external_api.py index fda367ec7..dedfe6f9d 100644 --- a/tests/test_external_api.py +++ b/tests/test_external_api.py @@ -23,14 +23,14 @@ def test_misc(default_rom): - pyboy = PyBoy(default_rom, window_type="null") + pyboy = PyBoy(default_rom, window="null") pyboy.set_emulation_speed(0) pyboy.tick(1, False) pyboy.stop(save=False) def test_faulty_state(default_rom): - pyboy = PyBoy(default_rom, window_type="null") + pyboy = PyBoy(default_rom, window="null") a = IntIOWrapper(io.BytesIO(b"abcd")) a.seek(0) @@ -53,7 +53,7 @@ def test_faulty_state(default_rom): def test_tiles(default_rom): - pyboy = PyBoy(default_rom, window_type="null") + pyboy = PyBoy(default_rom, window="null") pyboy.set_emulation_speed(0) pyboy.tick(BOOTROM_FRAMES_UNTIL_LOGO, False) @@ -94,7 +94,7 @@ def test_tiles(default_rom): def test_tiles_cgb(any_rom_cgb): - pyboy = PyBoy(any_rom_cgb, window_type="null") + pyboy = PyBoy(any_rom_cgb, window="null") pyboy.set_emulation_speed(0) pyboy.tick(BOOTROM_FRAMES_UNTIL_LOGO, False) @@ -133,7 +133,7 @@ def test_tiles_cgb(any_rom_cgb): def test_tiles_cgb(any_rom_cgb): - pyboy = PyBoy(any_rom_cgb, window_type="null") + pyboy = PyBoy(any_rom_cgb, window="null") pyboy.set_emulation_speed(0) pyboy.tick(BOOTROM_FRAMES_UNTIL_LOGO, False) @@ -144,7 +144,7 @@ def test_screen_buffer_and_image(tetris_rom, boot_rom): cformat = "RGBA" boot_logo_hash_predigested = b"_M\x0e\xd9\xe2\xdb\\o]\x83U\x93\xebZm\x1e\xaaFR/Q\xa52\x1c{8\xe7g\x95\xbcIz" - pyboy = PyBoy(tetris_rom, window_type="null", bootrom_file=boot_rom) + pyboy = PyBoy(tetris_rom, window="null", bootrom=boot_rom) pyboy.set_emulation_speed(0) pyboy.tick(275, True) # Iterate to boot logo @@ -217,7 +217,7 @@ def test_screen_buffer_and_image(tetris_rom, boot_rom): def test_tetris(tetris_rom): NEXT_TETROMINO = 0xC213 - pyboy = PyBoy(tetris_rom, bootrom_file="pyboy_fast", window_type="null") + pyboy = PyBoy(tetris_rom, bootrom="pyboy_fast", window="null") pyboy.set_emulation_speed(0) tetris = pyboy.game_wrapper tetris.set_tetromino("T") @@ -433,7 +433,7 @@ def test_tetris(tetris_rom): def test_tilemap_position_list(supermarioland_rom): - pyboy = PyBoy(supermarioland_rom, window_type="null") + pyboy = PyBoy(supermarioland_rom, window="null") pyboy.set_emulation_speed(0) pyboy.tick(100, False) @@ -467,7 +467,7 @@ def test_tilemap_position_list(supermarioland_rom): def test_button(default_rom): - pyboy = PyBoy(default_rom, window_type="null") + pyboy = PyBoy(default_rom, window="null") pyboy.set_emulation_speed(0) assert len(pyboy.events) == 0 # Nothing injected yet @@ -496,7 +496,7 @@ def test_button(default_rom): def test_get_set_override(default_rom): - pyboy = PyBoy(default_rom, window_type="null") + pyboy = PyBoy(default_rom, window="null") pyboy.set_emulation_speed(0) pyboy.tick(1, False) diff --git a/tests/test_game_wrapper.py b/tests/test_game_wrapper.py index 0eb2e21b1..9c38b5e2f 100644 --- a/tests/test_game_wrapper.py +++ b/tests/test_game_wrapper.py @@ -18,7 +18,7 @@ def test_game_wrapper_basics(default_rom): - pyboy = PyBoy(default_rom, window_type="null") + pyboy = PyBoy(default_rom, window="null") pyboy.set_emulation_speed(0) generic_wrapper = pyboy.game_wrapper @@ -28,7 +28,7 @@ def test_game_wrapper_basics(default_rom): def test_game_wrapper_mapping(default_rom): - pyboy = PyBoy(default_rom, window_type="null", debug=True) + pyboy = PyBoy(default_rom, window="null", debug=True) pyboy.set_emulation_speed(0) assert np.all(pyboy.game_area() == 256) diff --git a/tests/test_game_wrapper_mario.py b/tests/test_game_wrapper_mario.py index c90bffb88..9d835759b 100644 --- a/tests/test_game_wrapper_mario.py +++ b/tests/test_game_wrapper_mario.py @@ -18,7 +18,7 @@ def test_mario_basics(supermarioland_rom): - pyboy = PyBoy(supermarioland_rom, window_type="null") + pyboy = PyBoy(supermarioland_rom, window="null") pyboy.set_emulation_speed(0) assert pyboy.cartridge_title == "SUPER MARIOLAN" @@ -33,7 +33,7 @@ def test_mario_basics(supermarioland_rom): def test_mario_advanced(supermarioland_rom): - pyboy = PyBoy(supermarioland_rom, window_type="null") + pyboy = PyBoy(supermarioland_rom, window="null") pyboy.set_emulation_speed(0) assert pyboy.cartridge_title == "SUPER MARIOLAN" @@ -51,7 +51,7 @@ def test_mario_advanced(supermarioland_rom): def test_mario_game_over(supermarioland_rom): - pyboy = PyBoy(supermarioland_rom, window_type="null") + pyboy = PyBoy(supermarioland_rom, window="null") pyboy.set_emulation_speed(0) mario = pyboy.game_wrapper diff --git a/tests/test_magen.py b/tests/test_magen.py index 0e5c9ffbe..e3999bd9f 100644 --- a/tests/test_magen.py +++ b/tests/test_magen.py @@ -15,7 +15,7 @@ # https://github.com/alloncm/MagenTests def test_magen_test(magen_test_file): - pyboy = PyBoy(magen_test_file, window_type="null") + pyboy = PyBoy(magen_test_file, window="null") pyboy.set_emulation_speed(0) pyboy.tick(59, True) pyboy.tick(25, True) diff --git a/tests/test_mario_rl.py b/tests/test_mario_rl.py index 4ee9b7d7d..cf4384275 100644 --- a/tests/test_mario_rl.py +++ b/tests/test_mario_rl.py @@ -84,7 +84,7 @@ ### # Load emulator ### -pyboy = PyBoy(game, window_type="null", scale=3, debug=False) +pyboy = PyBoy(game, window="null", scale=3, debug=False) ### # Load enviroment diff --git a/tests/test_memoryscanner.py b/tests/test_memoryscanner.py index 4e8cfc5a8..5dd542c73 100644 --- a/tests/test_memoryscanner.py +++ b/tests/test_memoryscanner.py @@ -35,7 +35,7 @@ def test_bcd_to_dec_complex2(): def test_memoryscanner_basic(default_rom): - pyboy = PyBoy(default_rom, window_type="null") + pyboy = PyBoy(default_rom, window="null") pyboy.set_emulation_speed(0) addresses = pyboy.memory_scanner.scan_memory() assert len(addresses) == 0x10000 @@ -46,7 +46,7 @@ def test_memoryscanner_basic(default_rom): def test_memoryscanner_boundary(default_rom): - pyboy = PyBoy(default_rom, window_type="null") + pyboy = PyBoy(default_rom, window="null") pyboy.set_emulation_speed(0) # Byte width of 1 @@ -74,7 +74,7 @@ def test_memoryscanner_boundary(default_rom): def test_memoryscanner_absolute(kirby_rom): - pyboy = PyBoy(kirby_rom, window_type="null") + pyboy = PyBoy(kirby_rom, window="null") pyboy.set_emulation_speed(0) kirby = pyboy.game_wrapper kirby.start_game() @@ -102,7 +102,7 @@ def test_memoryscanner_absolute(kirby_rom): def test_memoryscanner_relative(kirby_rom): - pyboy = PyBoy(kirby_rom, window_type="null") + pyboy = PyBoy(kirby_rom, window="null") pyboy.set_emulation_speed(0) kirby = pyboy.game_wrapper kirby.start_game() diff --git a/tests/test_memoryview.py b/tests/test_memoryview.py index 7a5889523..88fc52c42 100644 --- a/tests/test_memoryview.py +++ b/tests/test_memoryview.py @@ -9,7 +9,7 @@ def test_memoryview(default_rom, boot_rom): - p = PyBoy(default_rom, bootrom_file=boot_rom) + p = PyBoy(default_rom, bootrom=boot_rom) with open(default_rom, "rb") as f: rom_bytes = [ord(f.read(1)) for x in range(16)] diff --git a/tests/test_mooneye.py b/tests/test_mooneye.py index f177eefde..c5bf9b705 100644 --- a/tests/test_mooneye.py +++ b/tests/test_mooneye.py @@ -152,14 +152,14 @@ def test_mooneye(clean, rom, mooneye_dir, default_rom): if saved_state is None: # HACK: We load any rom and load it until the last frame in the boot rom. # Then we save it, so we won't need to redo it. - pyboy = PyBoy(default_rom, window_type="null", cgb=False, sound_emulated=True) + pyboy = PyBoy(default_rom, window="null", cgb=False, sound_emulated=True) pyboy.set_emulation_speed(0) saved_state = io.BytesIO() pyboy.tick(59, True) pyboy.save_state(saved_state) pyboy.stop(save=False) - pyboy = PyBoy(mooneye_dir + rom, window_type="null", cgb=False) + pyboy = PyBoy(mooneye_dir + rom, window="null", cgb=False) pyboy.set_emulation_speed(0) saved_state.seek(0) if clean: diff --git a/tests/test_replay.py b/tests/test_replay.py index ba9683cc6..d855def23 100644 --- a/tests/test_replay.py +++ b/tests/test_replay.py @@ -91,8 +91,8 @@ def replay( pyboy = PyBoy( ROM, - window_type=window, - bootrom_file=bootrom_file, + window=window, + bootrom=bootrom_file, rewind=rewind, randomize=randomize, cgb=cgb, diff --git a/tests/test_rtc3test.py b/tests/test_rtc3test.py index e07efc080..3118d6536 100644 --- a/tests/test_rtc3test.py +++ b/tests/test_rtc3test.py @@ -18,7 +18,7 @@ @pytest.mark.skip("RTC is too unstable") @pytest.mark.parametrize("subtest", [0, 1, 2]) def test_rtc3test(subtest, rtc3test_file): - pyboy = PyBoy(rtc3test_file, window_type="null") + pyboy = PyBoy(rtc3test_file, window="null") pyboy.set_emulation_speed(0) pyboy.tick(59, True) pyboy.tick(25, True) diff --git a/tests/test_samesuite.py b/tests/test_samesuite.py index 42d5c133a..16e0e5bba 100644 --- a/tests/test_samesuite.py +++ b/tests/test_samesuite.py @@ -114,9 +114,9 @@ def test_samesuite(clean, gb_type, rom, samesuite_dir, boot_cgb_rom, boot_rom, d # Then we save it, so we won't need to redo it. pyboy = PyBoy( default_rom, - window_type="null", + window="null", cgb=gb_type == "cgb", - bootrom_file=boot_cgb_rom if gb_type == "cgb" else boot_rom, + bootrom=boot_cgb_rom if gb_type == "cgb" else boot_rom, sound_emulated=True, ) pyboy.set_emulation_speed(0) @@ -127,9 +127,9 @@ def test_samesuite(clean, gb_type, rom, samesuite_dir, boot_cgb_rom, boot_rom, d pyboy = PyBoy( samesuite_dir + rom, - window_type="null", + window="null", cgb=gb_type == "cgb", - bootrom_file=boot_cgb_rom if gb_type == "cgb" else boot_rom, + bootrom=boot_cgb_rom if gb_type == "cgb" else boot_rom, sound_emulated=True, ) pyboy.set_emulation_speed(0) diff --git a/tests/test_shonumi.py b/tests/test_shonumi.py index e6b019116..01adfe01b 100644 --- a/tests/test_shonumi.py +++ b/tests/test_shonumi.py @@ -19,7 +19,7 @@ "sprite_suite.gb", ]) def test_shonumi(rom, shonumi_dir): - pyboy = PyBoy(shonumi_dir + rom, window_type="null", color_palette=(0xFFFFFF, 0x999999, 0x606060, 0x000000)) + pyboy = PyBoy(shonumi_dir + rom, window="null", color_palette=(0xFFFFFF, 0x999999, 0x606060, 0x000000)) pyboy.set_emulation_speed(0) # sprite_suite.gb diff --git a/tests/test_states.py b/tests/test_states.py index cdd8e2141..dff4a02ac 100644 --- a/tests/test_states.py +++ b/tests/test_states.py @@ -11,7 +11,7 @@ def test_load_save_consistency(tetris_rom): - pyboy = PyBoy(tetris_rom, window_type="null") + pyboy = PyBoy(tetris_rom, window="null") assert pyboy.cartridge_title == "TETRIS" pyboy.set_emulation_speed(0) pyboy.memory[NEXT_TETROMINO_ADDR] diff --git a/tests/test_tetris_ai.py b/tests/test_tetris_ai.py index 565f51b0f..392363ca7 100644 --- a/tests/test_tetris_ai.py +++ b/tests/test_tetris_ai.py @@ -51,7 +51,7 @@ def eval_network(epoch, child_index, child_model, record_to): - pyboy = PyBoy('tetris_1.1.gb', window_type="null") + pyboy = PyBoy('tetris_1.1.gb', window="null") pyboy.set_emulation_speed(0) tetris = pyboy.game_wrapper tetris.start_game() diff --git a/tests/test_which.py b/tests/test_which.py index 901d5c9a1..afa67f46e 100644 --- a/tests/test_which.py +++ b/tests/test_which.py @@ -16,7 +16,7 @@ @pytest.mark.parametrize("cgb", [False, True]) def test_which(cgb, which_file): - pyboy = PyBoy(which_file, window_type="null", cgb=cgb) + pyboy = PyBoy(which_file, window="null", cgb=cgb) pyboy.set_emulation_speed(0) pyboy.tick(59, True) pyboy.tick(25, True) diff --git a/tests/test_whichboot.py b/tests/test_whichboot.py index 1cb7acb54..34d5bdb19 100644 --- a/tests/test_whichboot.py +++ b/tests/test_whichboot.py @@ -16,7 +16,7 @@ @pytest.mark.parametrize("cgb", [False, True]) def test_which(cgb, whichboot_file): - pyboy = PyBoy(whichboot_file, window_type="null", cgb=cgb) + pyboy = PyBoy(whichboot_file, window="null", cgb=cgb) pyboy.set_emulation_speed(0) pyboy.tick(59, True) pyboy.tick(25, True) From 0077ffb1cdb321fe4e901a5ae835cbc06d186f0f Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Sun, 17 Mar 2024 10:33:14 +0100 Subject: [PATCH 60/65] Add tests for kwarg log_level --- tests/test_basics.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_basics.py b/tests/test_basics.py index 64c68a173..757ed4dca 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -23,6 +23,30 @@ is_pypy = platform.python_implementation() == "PyPy" +def test_log_level_none(default_rom, capsys): + pyboy = PyBoy(default_rom, window="null") + captured = capsys.readouterr() + assert captured.out == "" + + +def test_log_level_default(default_rom, capsys): + pyboy = PyBoy(default_rom, window="dummy") + captured = capsys.readouterr() + assert "pyboy.plugins.window_null ERROR" in captured.out + + +def test_log_level_default(default_rom, capsys): + pyboy = PyBoy(default_rom, window="dummy", log_level="ERROR") + captured = capsys.readouterr() + assert "pyboy.plugins.window_null ERROR" in captured.out + + +def test_log_level_critical(default_rom, capsys): + pyboy = PyBoy(default_rom, window="dummy", log_level="CRITICAL") + captured = capsys.readouterr() + assert captured.out == "" + + def test_record_replay(boot_rom, default_rom): pyboy = PyBoy(default_rom, window="null", bootrom=boot_rom, record_input=True) pyboy.set_emulation_speed(0) From 0831ff8542dfa50bd818bc42f033a196204423f0 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Sun, 17 Mar 2024 10:57:29 +0100 Subject: [PATCH 61/65] Add support for multiple symbols on same bank/address --- extras/default_rom/default_rom.asm | 1 + extras/default_rom/default_rom.sym | 1 + extras/default_rom/default_rom_cgb.sym | 1 + pyboy/pyboy.py | 9 ++++++--- tests/test_breakpoints.py | 13 +++++++++++++ 5 files changed, 22 insertions(+), 3 deletions(-) diff --git a/extras/default_rom/default_rom.asm b/extras/default_rom/default_rom.asm index 84fa8c7bc..568bf2cca 100644 --- a/extras/default_rom/default_rom.asm +++ b/extras/default_rom/default_rom.asm @@ -14,6 +14,7 @@ INCBIN "default_rom.2bpp" SECTION "Tilemap", ROM0 Tilemap: +Tilemap2: ; Used to test when two symbols point to the same address db $40, $41, $42, $43, $44, $45, $46, $41, $41, $41, $47, $41, $41, $41 db $48, $49, $4A, $4B, $4C, $4D, $4E, $49, $4F, $50, $51, $41, $41, $41 diff --git a/extras/default_rom/default_rom.sym b/extras/default_rom/default_rom.sym index c2540a1e0..ea801cfcc 100644 --- a/extras/default_rom/default_rom.sym +++ b/extras/default_rom/default_rom.sym @@ -1,5 +1,6 @@ ; File generated by rgblink 00:0000 Tilemap +00:0000 Tilemap2 00:0100 EntryPoint 00:0150 Main 00:0155 Main.waitVBlank diff --git a/extras/default_rom/default_rom_cgb.sym b/extras/default_rom/default_rom_cgb.sym index c2540a1e0..ea801cfcc 100644 --- a/extras/default_rom/default_rom_cgb.sym +++ b/extras/default_rom/default_rom_cgb.sym @@ -1,5 +1,6 @@ ; File generated by rgblink 00:0000 Tilemap +00:0000 Tilemap2 00:0100 EntryPoint 00:0150 Main 00:0155 Main.waitVBlank diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index d64d23ed3..8f906e813 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -982,15 +982,18 @@ def _load_symbols(self): if not bank in self.rom_symbols: self.rom_symbols[bank] = {} - self.rom_symbols[bank][addr] = sym_label + if not addr in self.rom_symbols[bank]: + self.rom_symbols[bank][addr] = [] + + self.rom_symbols[bank][addr].append(sym_label) except ValueError as ex: logger.warning("Skipping .sym line: %s", line.strip()) return self.rom_symbols def _lookup_symbol(self, symbol): for bank, addresses in self.rom_symbols.items(): - for addr, label in addresses.items(): - if label == symbol: + for addr, labels in addresses.items(): + if symbol in labels: return bank, addr raise ValueError("Symbol not found: %s" % symbol) diff --git a/tests/test_breakpoints.py b/tests/test_breakpoints.py index 54a6b4869..c704f6570 100644 --- a/tests/test_breakpoints.py +++ b/tests/test_breakpoints.py @@ -109,6 +109,19 @@ def test_symbols_auto_locate(default_rom): assert _addr is not None +def test_symbols_auto_locate_double_symbol(default_rom): + pyboy = PyBoy(default_rom, window="null", symbols="extras/default_rom/default_rom.sym") + pyboy.set_emulation_speed(0) + + _bank, _addr = pyboy.symbol_lookup("Tilemap") + assert _bank is not None + assert _addr is not None + + _bank2, _addr2 = pyboy.symbol_lookup("Tilemap2") + assert _bank == _bank2 + assert _addr == _addr2 + + def test_symbols_path_locate(default_rom): pyboy = PyBoy(default_rom, window="null", symbols="extras/default_rom/default_rom.sym") pyboy.set_emulation_speed(0) From a2463f2f4b1ff2bbdcfa61f391a51fa36c580184 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Sun, 17 Mar 2024 11:20:10 +0100 Subject: [PATCH 62/65] Allow specifying bank for pyboy.memory in DMG mode when it's 0 --- pyboy/pyboy.pxd | 2 +- pyboy/pyboy.py | 13 ++++++++----- tests/test_breakpoints.py | 17 +++++++++++++++++ 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/pyboy/pyboy.pxd b/pyboy/pyboy.pxd index a41c93462..9ad5ec637 100644 --- a/pyboy/pyboy.pxd +++ b/pyboy/pyboy.pxd @@ -80,7 +80,7 @@ cdef class PyBoy: cdef dict _hooks cdef object symbols_file - cdef dict rom_symbols + cdef public dict rom_symbols cpdef bint _handle_hooks(self) cpdef int hook_register(self, uint16_t, uint16_t, object, object) except -1 cpdef int hook_deregister(self, uint16_t, uint16_t) except -1 diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index 8f906e813..4d1c7f6f6 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -1007,7 +1007,10 @@ def symbol_lookup(self, symbol): Example: ```python - >>> # Continued example above + >>> # Directly + >>> pyboy.memory[pyboy.symbol_lookup("Tileset")] + 0 + >>> # By bank and address >>> bank, addr = pyboy.symbol_lookup("Tileset") >>> pyboy.memory[bank, addr] 0 @@ -1378,7 +1381,7 @@ def __getitem(self, start, stop, step, bank, is_single, is_bank): start -= 0x8000 stop -= 0x8000 # CGB VRAM Banks - assert self.mb.cgb, "Selecting bank of VRAM is only supported for CGB mode" + assert self.mb.cgb or (bank == 0), "Selecting bank of VRAM is only supported for CGB mode" assert stop < 0x2000, "Out of bounds for reading VRAM bank" assert bank <= 1, "VRAM Bank out of range" @@ -1418,7 +1421,7 @@ def __getitem(self, start, stop, step, bank, is_single, is_bank): start -= 0x1000 stop -= 0x1000 # CGB VRAM banks - assert self.mb.cgb, "Selecting bank of WRAM is only supported for CGB mode" + assert self.mb.cgb or (bank == 0), "Selecting bank of WRAM is only supported for CGB mode" assert stop < 0x1000, "Out of bounds for reading VRAM bank" assert bank <= 7, "WRAM Bank out of range" if not is_single: @@ -1516,7 +1519,7 @@ def __setitem(self, start, stop, step, v, bank, is_single, is_bank): start -= 0x8000 stop -= 0x8000 # CGB VRAM Banks - assert self.mb.cgb, "Selecting bank of VRAM is only supported for CGB mode" + assert self.mb.cgb or (bank == 0), "Selecting bank of VRAM is only supported for CGB mode" assert stop < 0x2000, "Out of bounds for reading VRAM bank" assert bank <= 1, "VRAM Bank out of range" @@ -1571,7 +1574,7 @@ def __setitem(self, start, stop, step, v, bank, is_single, is_bank): start -= 0x1000 stop -= 0x1000 # CGB VRAM banks - assert self.mb.cgb, "Selecting bank of WRAM is only supported for CGB mode" + assert self.mb.cgb or (bank == 0), "Selecting bank of WRAM is only supported for CGB mode" assert stop < 0x1000, "Out of bounds for reading VRAM bank" assert bank <= 7, "WRAM Bank out of range" if not is_single: diff --git a/tests/test_breakpoints.py b/tests/test_breakpoints.py index c704f6570..9de40a2e8 100644 --- a/tests/test_breakpoints.py +++ b/tests/test_breakpoints.py @@ -122,6 +122,23 @@ def test_symbols_auto_locate_double_symbol(default_rom): assert _addr == _addr2 +def test_symbols_bank0_wram(default_rom): + pyboy = PyBoy(default_rom, window="null", symbols="extras/default_rom/default_rom.sym") + pyboy.set_emulation_speed(0) + + pyboy.rom_symbols[0][0xC000] = ["test1"] + pyboy.rom_symbols[0][0xD000] = ["test2"] + + _bank, _addr = pyboy.symbol_lookup("test1") + assert _bank == 0 and _addr == 0xC000 + + _bank, _addr = pyboy.symbol_lookup("test2") + assert _bank == 0 and _addr == 0xD000 + + pyboy.memory[pyboy.symbol_lookup("test1")] + pyboy.memory[pyboy.symbol_lookup("test2")] + + def test_symbols_path_locate(default_rom): pyboy = PyBoy(default_rom, window="null", symbols="extras/default_rom/default_rom.sym") pyboy.set_emulation_speed(0) From bcf764131a54fb2323ca84aeca8b4f3bcbb2e401 Mon Sep 17 00:00:00 2001 From: Mads Ynddal <5528170+Baekalfen@users.noreply.github.com> Date: Sat, 9 Dec 2023 19:20:30 +0100 Subject: [PATCH 63/65] Updated README for v2.0.0 --- README.md | 187 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 130 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index c6247ce14..da15b1dfc 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,7 @@ __If you have any questions, or just want to chat, [join us on Discord](https://discord.gg/Zrf2nyH).__ - -It is highly recommended to read the [report](https://github.com/Baekalfen/PyBoy/raw/master/extras/PyBoy.pdf) to get a light introduction to Game Boy emulation. _But do be aware, that the Python implementation has changed a lot_. The report is relevant, eventhough you want to contribute to another emulator, or create your own. - -If you've read the report and want more explicit details, have a look at the [Pan Docs](http://bgb.bircd.org/pandocs.htm). - -__If you are looking to make a bot or AI__, you can find all the external components in the [PyBoy Documentation](https://baekalfen.github.io/PyBoy/index.html). There is also a short example on our Wiki page [Scripts, AI and Bots](https://github.com/Baekalfen/PyBoy/wiki/Scripts,-AI-and-Bots) as well as in the [examples directory](https://github.com/Baekalfen/PyBoy/tree/master/extras/examples). If more features are needed, or if you find a bug, don't hesitate to make an issue here on GitHub, or post on our [Discord channel](https://discord.gg/Zrf2nyH). - +[![Discord](https://img.shields.io/discord/584149554132418570?style=for-the-badge&logo=Discord&label=PyBoy)](https://discord.gg/Zrf2nyH) + + + + + + + - Rewind any game
+ Play the classics
- Beat world records
+ Create your own AI
- Train with Reinforcement Learning
+ Beat world records with AI
- + - + - + +Getting Started +=============== +The instructions are simple: -Installation -============ -The instructions are simple if you already have a functioning Python environment on your machine. +```sh +$ pip install pyboy +``` - 1. Install PyBoy using __`pip install pyboy`__ (add __` --user`__ if your system asks) - 2. If your system isn't supported by [pysdl2-dll](https://pypi.org/project/pysdl2-dll/), you'll need to install SDL2 from your package manager. +For details, see [installation instructions](https://github.com/Baekalfen/PyBoy/wiki/Installation). -If you need more details, or if you need to compile from source, check out the detailed [installation instructions](https://github.com/Baekalfen/PyBoy/wiki/Installation). We support: macOS, Raspberry Pi (Raspbian), Linux (Ubuntu), and Windows 10. +Now you're ready! Either use PyBoy directly from the terminal +```sh +$ pyboy game_rom.gb +``` -Now you're ready! Either use PyBoy directly from the terminal __`$ pyboy file.rom`__ or use it in your Python scripts: +Or use it in your Python scripts: ```python from pyboy import PyBoy -pyboy = PyBoy('ROMs/gamerom.gb') -while pyboy.tick(1, True): +pyboy = PyBoy('game_rom.gb') +while pyboy.tick(): pass pyboy.stop() ``` @@ -68,60 +81,120 @@ pyboy.stop() + +The API +======= + +If you are looking to make a bot or AI, then these resources are a good place to start: + * [PyBoy API Documentation](https://baekalfen.github.io/PyBoy/index.html) + * [Wiki Pages](https://github.com/Baekalfen/PyBoy/wiki/) + * [Using PyBoy with Gym](https://github.com/Baekalfen/PyBoy/wiki/Using-PyBoy-with-Gym) + * [Example: Kirby](https://github.com/Baekalfen/PyBoy/wiki/Example-Kirby) + * [Example: Tetris](https://github.com/Baekalfen/PyBoy/wiki/Example-Tetris) + * [Example: Super Mario Land](https://github.com/Baekalfen/PyBoy/wiki/Example-Super-Mario-Land) + * [Code Examples](https://github.com/Baekalfen/PyBoy/tree/master/examples) + * [Discord](https://discord.gg/Zrf2nyH) + + When the emulator is running, you can easily access [PyBoy's API](https://baekalfen.github.io/PyBoy/index.html): ```python +pyboy.set_emulation_speed(0) # No speed limit pyboy.button('down') pyboy.button('a') -some_value = pyboy.memory[0xC345] - -pyboy.send_input(WindowEvent.PRESS_ARROW_DOWN) -pyboy.tick() # Process one frame to let the game register the input -pyboy.send_input(WindowEvent.RELEASE_ARROW_DOWN) +pyboy.tick() # Process at least one frame to let the game register the input +value_of_interest = pyboy.memory[0xC345] -pil_image = pyboy.screen_image() -pyboy.tick(1, True) +pil_image = pyboy.screen.image pil_image.save('screenshot.png') ``` -The Wiki shows how to interface with PyBoy from your own project: [Wiki](https://github.com/Baekalfen/PyBoy/wiki). +The [Wiki](https://github.com/Baekalfen/PyBoy/wiki) shows how to interface with PyBoy from your own project. +Performance +=========== -Contributors -============ +Performance is a priority for PyBoy, to make your AI training and scripts as fast as possible. + +The easiest way to improve your performance, is to skip rendering of unnecessary frames. If you know your +character takes X frames to move, or the game doesn't take input every frame, you can skip those to potentially triple +your performance. All game logic etc. will still process. + +Here is a simple comparison of rendering every frame, rendering every 15th frame, and not rendering any frames (higher is better). See [`pyboy.tick`](https://docs.pyboy.dk/#pyboy.PyBoy.tick) for how it works. Your performance will depend on the game. + + + + + + + + + + + + + + + + + + + +
+ Full rendering + + Frame-skip 15 + + No rendering +
+ x124 realtime + + x344 realtime + + x395 realtime +
+ +```python +for _ in range(target): + pyboy.tick() -Thanks to all the people who have contributed to the project! +``` + + +```python +for _ in range(target//15): + pyboy.tick(15, True) -Original Developers -------------------- +``` + - * Asger Anders Lund Hansen - [AsgerLundHansen](https://github.com/AsgerLundHansen) - * Mads Ynddal - [baekalfen](https://github.com/Baekalfen) - * Troels Ynddal - [troelsy](https://github.com/troelsy) +```python +pyboy.tick(target) -GitHub Collaborators --------------------- +``` +
- * Kristian Sims - [krs013](https://github.com/krs013) +The Game Boy was originally running at 60 frames per second, so a speed-up of 100x realtime is 6,000 frames per +second. And trivially from the table above, simulating 395 hours of gameplay can be done in 1 hour with no rendering. -Student Projects ----------------- +It's also recommended to be running multiple instances of PyBoy in parallel. On an 8-core machine, you could potentially +do 3160 hours of gameplay in 1 hour. - * __Rewind Time:__ Jacob Olsen - [JacobO1](https://github.com/JacobO1) - * __Link Cable:__ Jonas Flach-Jensen - [thejomas](https://github.com/thejomas) - * __Game Boy Color:__ Christian Marslev and Jonas Grønborg - [CKuke](https://github.com/CKuke) and [kaff3](https://github.com/kaff3) +Contributing +============ +Any contribution is appreciated. The currently known problems are tracked in [the Issues tab](https://github.com/Baekalfen/PyBoy/issues). Feel free to take a swing at any one of them. If you have something original in mind, come and [discuss it on on Discord](https://discord.gg/Zrf2nyH). -Contribute -========== -Any contribution is appreciated. The currently known problems are tracked in the Issues tab. Feel free to take a swing at any one of them. +[![Discord](https://img.shields.io/discord/584149554132418570?style=for-the-badge&logo=Discord&label=PyBoy)](https://discord.gg/Zrf2nyH) For the more major features, there are the following that you can give a try. They are also described in more detail in the [project list in the Wiki](https://github.com/Baekalfen/PyBoy/wiki/Student-Projects): +* Hacking games * Link Cable -* _(Experimental)_ AI - use the `botsupport` or game wrappers to train a neural network -* _(Experimental)_ Game Wrappers - make wrappers for popular games +* Debugger (VSCode, GDB, terminal or otherwise) +* AI - [use the `api`](https://baekalfen.github.io/PyBoy/index.html) or game wrappers to train a neural network +* Game Wrappers - make wrappers for popular games If you want to implement something which is not on the list, feel free to do so anyway. If you want to merge it into our repo, then just send a pull request and we will have a look at it. From 1ed7460bdce85228a87792b1e4c044c92f5293a2 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Sun, 11 Feb 2024 09:46:52 +0100 Subject: [PATCH 64/65] Update docs --- docs/api/constants.html | 192 + docs/api/index.html | 117 + docs/api/memory_scanner.html | 739 ++++ docs/api/screen.html | 783 ++++ docs/{botsupport => api}/sprite.html | 153 +- docs/{botsupport => api}/tile.html | 425 ++- docs/{botsupport => api}/tilemap.html | 344 +- docs/botsupport/index.html | 480 --- docs/botsupport/screen.html | 579 --- docs/index.html | 3390 ++++++++++++----- docs/openai_gym.html | 596 --- docs/plugins/base_plugin.html | 302 +- .../game_wrapper_kirby_dream_land.html | 188 +- docs/plugins/game_wrapper_pokemon_gen1.html | 91 +- .../game_wrapper_super_mario_land.html | 1084 ++++-- docs/plugins/game_wrapper_tetris.html | 269 +- docs/plugins/index.html | 20 +- docs/utils.html | 889 +++++ 18 files changed, 6988 insertions(+), 3653 deletions(-) create mode 100644 docs/api/constants.html create mode 100644 docs/api/index.html create mode 100644 docs/api/memory_scanner.html create mode 100644 docs/api/screen.html rename docs/{botsupport => api}/sprite.html (77%) rename docs/{botsupport => api}/tile.html (51%) rename docs/{botsupport => api}/tilemap.html (74%) delete mode 100644 docs/botsupport/index.html delete mode 100644 docs/botsupport/screen.html delete mode 100644 docs/openai_gym.html create mode 100644 docs/utils.html diff --git a/docs/api/constants.html b/docs/api/constants.html new file mode 100644 index 000000000..d4b592a0f --- /dev/null +++ b/docs/api/constants.html @@ -0,0 +1,192 @@ + + + + + + +pyboy.api.constants API documentation + + + + + + + + + + +
+
+
+

Module pyboy.api.constants

+
+
+

Memory constants used internally to calculate tile and tile map addresses.

+
+ +Expand source code + +
#
+# License: See LICENSE.md file
+# GitHub: https://github.com/Baekalfen/PyBoy
+#
+"""
+Memory constants used internally to calculate tile and tile map addresses.
+"""
+
+VRAM_OFFSET = 0x8000
+"""
+Start address of VRAM
+"""
+LCDC_OFFSET = 0xFF40
+"""
+LCDC Register
+"""
+OAM_OFFSET = 0xFE00
+"""
+Start address of Object-Attribute-Memory (OAM)
+"""
+LOW_TILEMAP = 0x1800 + VRAM_OFFSET
+"""
+Start address of lower tilemap
+"""
+HIGH_TILEMAP = 0x1C00 + VRAM_OFFSET
+"""
+Start address of high tilemap
+"""
+LOW_TILEDATA = VRAM_OFFSET
+"""
+Start address of lower tile data
+"""
+LOW_TILEDATA_NTILES = 0x100
+"""
+Number of tiles in lower tile data
+"""
+HIGH_TILEDATA = 0x800 + VRAM_OFFSET
+"""
+Start address of high tile data
+"""
+TILES = 384
+"""
+Number of tiles supported on Game Boy DMG (non-color)
+"""
+TILES_CGB = 768
+"""
+Number of tiles supported on Game Boy Color
+"""
+SPRITES = 40
+"""
+Number of sprites supported
+"""
+ROWS = 144
+"""
+Rows (horizontal lines) on the screen
+"""
+COLS = 160
+"""
+Columns (vertical lines) on the screen
+"""
+
+
+
+
+
+

Global variables

+
+
var VRAM_OFFSET
+
+

Start address of VRAM

+
+
var LCDC_OFFSET
+
+

LCDC Register

+
+
var OAM_OFFSET
+
+

Start address of Object-Attribute-Memory (OAM)

+
+
var LOW_TILEMAP
+
+

Start address of lower tilemap

+
+
var HIGH_TILEMAP
+
+

Start address of high tilemap

+
+
var LOW_TILEDATA
+
+

Start address of lower tile data

+
+
var LOW_TILEDATA_NTILES
+
+

Number of tiles in lower tile data

+
+
var HIGH_TILEDATA
+
+

Start address of high tile data

+
+
var TILES
+
+

Number of tiles supported on Game Boy DMG (non-color)

+
+
var TILES_CGB
+
+

Number of tiles supported on Game Boy Color

+
+
var SPRITES
+
+

Number of sprites supported

+
+
var ROWS
+
+

Rows (horizontal lines) on the screen

+
+
var COLS
+
+

Columns (vertical lines) on the screen

+
+
+
+
+
+
+
+
+ +
+ + + + + \ No newline at end of file diff --git a/docs/api/index.html b/docs/api/index.html new file mode 100644 index 000000000..69d4af7ea --- /dev/null +++ b/docs/api/index.html @@ -0,0 +1,117 @@ + + + + + + +pyboy.api API documentation + + + + + + + + + + +
+
+
+

Module pyboy.api

+
+
+

Tools to help interfacing with the Game Boy hardware

+
+ +Expand source code + +
#
+# License: See LICENSE.md file
+# GitHub: https://github.com/Baekalfen/PyBoy
+#
+"""
+Tools to help interfacing with the Game Boy hardware
+"""
+
+from . import constants
+from .screen import Screen
+from .sprite import Sprite
+from .tile import Tile
+from .tilemap import TileMap
+
+# __pdoc__ = {
+#     "constants": False,
+#     "manager": False,
+# }
+# __all__ = ["API"]
+
+
+
+

Sub-modules

+
+
pyboy.api.constants
+
+

Memory constants used internally to calculate tile and tile map addresses.

+
+
pyboy.api.memory_scanner
+
+
+
+
pyboy.api.screen
+
+

This class gives access to the frame buffer and other screen parameters of PyBoy.

+
+
pyboy.api.sprite
+
+

This class presents an interface to the sprites held in the OAM data on the Game Boy.

+
+
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 …

+
+
pyboy.api.tilemap
+
+

The Game Boy has two tile maps, which defines what is rendered on the screen.

+
+
+
+
+
+
+
+
+
+
+ +
+ + + + + \ No newline at end of file diff --git a/docs/api/memory_scanner.html b/docs/api/memory_scanner.html new file mode 100644 index 000000000..4152de771 --- /dev/null +++ b/docs/api/memory_scanner.html @@ -0,0 +1,739 @@ + + + + + + +pyboy.api.memory_scanner API documentation + + + + + + + + + + +
+
+
+

Module pyboy.api.memory_scanner

+
+
+
+ +Expand source code + +
from enum import Enum
+
+from pyboy.utils import bcd_to_dec
+
+
+class StandardComparisonType(Enum):
+    """Enumeration for defining types of comparisons that do not require a previous value."""
+    EXACT = 1
+    LESS_THAN = 2
+    GREATER_THAN = 3
+    LESS_THAN_OR_EQUAL = 4
+    GREATER_THAN_OR_EQUAL = 5
+
+
+class DynamicComparisonType(Enum):
+    """Enumeration for defining types of comparisons that require a previous value."""
+    UNCHANGED = 1
+    CHANGED = 2
+    INCREASED = 3
+    DECREASED = 4
+    MATCH = 5
+
+
+class ScanMode(Enum):
+    """Enumeration for defining scanning modes."""
+    INT = 1
+    BCD = 2
+
+
+class MemoryScanner():
+    """A class for scanning memory within a given range."""
+    def __init__(self, pyboy):
+        self.pyboy = pyboy
+        self._memory_cache = {}
+        self._memory_cache_byte_width = 1
+
+    def scan_memory(
+        self,
+        target_value=None,
+        start_addr=0x0000,
+        end_addr=0xFFFF,
+        standard_comparison_type=StandardComparisonType.EXACT,
+        value_type=ScanMode.INT,
+        byte_width=1,
+        byteorder="little"
+    ):
+        """
+        This function scans a specified range of memory for a target value from the `start_addr` to the `end_addr` (both included).
+
+        Example:
+        ```python
+        >>> current_score = 4 # You write current score in game
+        >>> pyboy.memory_scanner.scan_memory(current_score, start_addr=0xC000, end_addr=0xDFFF)
+        []
+
+        ```
+
+        Args:
+            start_addr (int): The starting address for the scan.
+            end_addr (int): The ending address for the scan.
+            target_value (int or None): The value to search for. If None, any value is considered a match.
+            standard_comparison_type (StandardComparisonType): The type of comparison to use.
+            value_type (ValueType): The type of value (INT or BCD) to consider.
+            byte_width (int): The number of bytes to consider for each value.
+            byteorder (str): The endian type to use. This is only used for 16-bit values and higher. See [int.from_bytes](https://docs.python.org/3/library/stdtypes.html#int.from_bytes) for more details.
+
+        Returns:
+            list of int: A list of addresses where the target value is found.
+        """
+        self._memory_cache = {}
+        self._memory_cache_byte_width = byte_width
+        for addr in range(start_addr, end_addr - (byte_width-1) + 1): # Adjust the loop to prevent reading past end_addr
+            # Read multiple bytes based on byte_width and byteorder
+            value_bytes = self.pyboy.memory[addr:addr + byte_width]
+            value = int.from_bytes(value_bytes, byteorder)
+
+            if value_type == ScanMode.BCD:
+                value = bcd_to_dec(value, byte_width, byteorder)
+
+            if target_value is None or self._check_value(value, target_value, standard_comparison_type.value):
+                self._memory_cache[addr] = value
+
+        return list(self._memory_cache.keys())
+
+    def rescan_memory(
+        self, new_value=None, dynamic_comparison_type=DynamicComparisonType.UNCHANGED, byteorder="little"
+    ):
+        """
+        Rescans the memory and updates the memory cache based on a dynamic comparison type.
+
+        Example:
+        ```python
+        >>> current_score = 4 # You write current score in game
+        >>> pyboy.memory_scanner.scan_memory(current_score, start_addr=0xC000, end_addr=0xDFFF)
+        []
+        >>> for _ in range(175):
+        ...     pyboy.tick(1, True) # Progress the game to change score
+        True...
+        >>> current_score = 8 # You write the new score in game
+        >>> from pyboy.api.memory_scanner import DynamicComparisonType
+        >>> addresses = pyboy.memory_scanner.rescan_memory(current_score, DynamicComparisonType.MATCH)
+        >>> print(addresses) # If repeated enough, only one address will remain
+        []
+
+        ```
+
+        Args:
+            new_value (int, optional): The new value for comparison. If not provided, the current value in memory is used.
+            dynamic_comparison_type (DynamicComparisonType): The type of comparison to use. Defaults to UNCHANGED.
+
+        Returns:
+            list of int: A list of addresses remaining in the memory cache after the rescan.
+        """
+        for addr, value in self._memory_cache.copy().items():
+            current_value = int.from_bytes(
+                self.pyboy.memory[addr:addr + self._memory_cache_byte_width], byteorder=byteorder
+            )
+            if (dynamic_comparison_type == DynamicComparisonType.UNCHANGED):
+                if value != current_value:
+                    self._memory_cache.pop(addr)
+                else:
+                    self._memory_cache[addr] = current_value
+            elif (dynamic_comparison_type == DynamicComparisonType.CHANGED):
+                if value == current_value:
+                    self._memory_cache.pop(addr)
+                else:
+                    self._memory_cache[addr] = current_value
+            elif (dynamic_comparison_type == DynamicComparisonType.INCREASED):
+                if value >= current_value:
+                    self._memory_cache.pop(addr)
+                else:
+                    self._memory_cache[addr] = current_value
+            elif (dynamic_comparison_type == DynamicComparisonType.DECREASED):
+                if value <= current_value:
+                    self._memory_cache.pop(addr)
+                else:
+                    self._memory_cache[addr] = current_value
+            elif (dynamic_comparison_type == DynamicComparisonType.MATCH):
+                if new_value == None:
+                    raise ValueError("new_value must be specified when using DynamicComparisonType.MATCH")
+                if current_value != new_value:
+                    self._memory_cache.pop(addr)
+                else:
+                    self._memory_cache[addr] = current_value
+            else:
+                raise ValueError("Invalid comparison type")
+        return list(self._memory_cache.keys())
+
+    def _check_value(self, value, target_value, standard_comparison_type):
+        """
+        Compares a value with the target value based on the specified compare type.
+
+        Args:
+            value (int): The value to compare.
+            target_value (int or None): The target value to compare against.
+            standard_comparison_type (StandardComparisonType): The type of comparison to use.
+
+        Returns:
+            bool: True if the comparison condition is met, False otherwise.
+        """
+        if standard_comparison_type == StandardComparisonType.EXACT.value:
+            return value == target_value
+        elif standard_comparison_type == StandardComparisonType.LESS_THAN.value:
+            return value < target_value
+        elif standard_comparison_type == StandardComparisonType.GREATER_THAN.value:
+            return value > target_value
+        elif standard_comparison_type == StandardComparisonType.LESS_THAN_OR_EQUAL.value:
+            return value <= target_value
+        elif standard_comparison_type == StandardComparisonType.GREATER_THAN_OR_EQUAL.value:
+            return value >= target_value
+        else:
+            raise ValueError("Invalid comparison type")
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class StandardComparisonType +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
+
+

Enumeration for defining types of comparisons that do not require a previous value.

+
+ +Expand source code + +
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
+
+

Ancestors

+
    +
  • enum.Enum
  • +
+

Class variables

+
+
var EXACT
+
+
+
+
var LESS_THAN
+
+
+
+
var GREATER_THAN
+
+
+
+
var LESS_THAN_OR_EQUAL
+
+
+
+
var GREATER_THAN_OR_EQUAL
+
+
+
+
+
+
+class DynamicComparisonType +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
+
+

Enumeration for defining types of comparisons that require a previous value.

+
+ +Expand source code + +
class DynamicComparisonType(Enum):
+    """Enumeration for defining types of comparisons that require a previous value."""
+    UNCHANGED = 1
+    CHANGED = 2
+    INCREASED = 3
+    DECREASED = 4
+    MATCH = 5
+
+

Ancestors

+
    +
  • enum.Enum
  • +
+

Class variables

+
+
var UNCHANGED
+
+
+
+
var CHANGED
+
+
+
+
var INCREASED
+
+
+
+
var DECREASED
+
+
+
+
var MATCH
+
+
+
+
+
+
+class ScanMode +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
+
+

Enumeration for defining scanning modes.

+
+ +Expand source code + +
class ScanMode(Enum):
+    """Enumeration for defining scanning modes."""
+    INT = 1
+    BCD = 2
+
+

Ancestors

+
    +
  • enum.Enum
  • +
+

Class variables

+
+
var INT
+
+
+
+
var BCD
+
+
+
+
+
+
+class MemoryScanner +(pyboy) +
+
+

A class for scanning memory within a given range.

+
+ +Expand source code + +
class MemoryScanner():
+    """A class for scanning memory within a given range."""
+    def __init__(self, pyboy):
+        self.pyboy = pyboy
+        self._memory_cache = {}
+        self._memory_cache_byte_width = 1
+
+    def scan_memory(
+        self,
+        target_value=None,
+        start_addr=0x0000,
+        end_addr=0xFFFF,
+        standard_comparison_type=StandardComparisonType.EXACT,
+        value_type=ScanMode.INT,
+        byte_width=1,
+        byteorder="little"
+    ):
+        """
+        This function scans a specified range of memory for a target value from the `start_addr` to the `end_addr` (both included).
+
+        Example:
+        ```python
+        >>> current_score = 4 # You write current score in game
+        >>> pyboy.memory_scanner.scan_memory(current_score, start_addr=0xC000, end_addr=0xDFFF)
+        []
+
+        ```
+
+        Args:
+            start_addr (int): The starting address for the scan.
+            end_addr (int): The ending address for the scan.
+            target_value (int or None): The value to search for. If None, any value is considered a match.
+            standard_comparison_type (StandardComparisonType): The type of comparison to use.
+            value_type (ValueType): The type of value (INT or BCD) to consider.
+            byte_width (int): The number of bytes to consider for each value.
+            byteorder (str): The endian type to use. This is only used for 16-bit values and higher. See [int.from_bytes](https://docs.python.org/3/library/stdtypes.html#int.from_bytes) for more details.
+
+        Returns:
+            list of int: A list of addresses where the target value is found.
+        """
+        self._memory_cache = {}
+        self._memory_cache_byte_width = byte_width
+        for addr in range(start_addr, end_addr - (byte_width-1) + 1): # Adjust the loop to prevent reading past end_addr
+            # Read multiple bytes based on byte_width and byteorder
+            value_bytes = self.pyboy.memory[addr:addr + byte_width]
+            value = int.from_bytes(value_bytes, byteorder)
+
+            if value_type == ScanMode.BCD:
+                value = bcd_to_dec(value, byte_width, byteorder)
+
+            if target_value is None or self._check_value(value, target_value, standard_comparison_type.value):
+                self._memory_cache[addr] = value
+
+        return list(self._memory_cache.keys())
+
+    def rescan_memory(
+        self, new_value=None, dynamic_comparison_type=DynamicComparisonType.UNCHANGED, byteorder="little"
+    ):
+        """
+        Rescans the memory and updates the memory cache based on a dynamic comparison type.
+
+        Example:
+        ```python
+        >>> current_score = 4 # You write current score in game
+        >>> pyboy.memory_scanner.scan_memory(current_score, start_addr=0xC000, end_addr=0xDFFF)
+        []
+        >>> for _ in range(175):
+        ...     pyboy.tick(1, True) # Progress the game to change score
+        True...
+        >>> current_score = 8 # You write the new score in game
+        >>> from pyboy.api.memory_scanner import DynamicComparisonType
+        >>> addresses = pyboy.memory_scanner.rescan_memory(current_score, DynamicComparisonType.MATCH)
+        >>> print(addresses) # If repeated enough, only one address will remain
+        []
+
+        ```
+
+        Args:
+            new_value (int, optional): The new value for comparison. If not provided, the current value in memory is used.
+            dynamic_comparison_type (DynamicComparisonType): The type of comparison to use. Defaults to UNCHANGED.
+
+        Returns:
+            list of int: A list of addresses remaining in the memory cache after the rescan.
+        """
+        for addr, value in self._memory_cache.copy().items():
+            current_value = int.from_bytes(
+                self.pyboy.memory[addr:addr + self._memory_cache_byte_width], byteorder=byteorder
+            )
+            if (dynamic_comparison_type == DynamicComparisonType.UNCHANGED):
+                if value != current_value:
+                    self._memory_cache.pop(addr)
+                else:
+                    self._memory_cache[addr] = current_value
+            elif (dynamic_comparison_type == DynamicComparisonType.CHANGED):
+                if value == current_value:
+                    self._memory_cache.pop(addr)
+                else:
+                    self._memory_cache[addr] = current_value
+            elif (dynamic_comparison_type == DynamicComparisonType.INCREASED):
+                if value >= current_value:
+                    self._memory_cache.pop(addr)
+                else:
+                    self._memory_cache[addr] = current_value
+            elif (dynamic_comparison_type == DynamicComparisonType.DECREASED):
+                if value <= current_value:
+                    self._memory_cache.pop(addr)
+                else:
+                    self._memory_cache[addr] = current_value
+            elif (dynamic_comparison_type == DynamicComparisonType.MATCH):
+                if new_value == None:
+                    raise ValueError("new_value must be specified when using DynamicComparisonType.MATCH")
+                if current_value != new_value:
+                    self._memory_cache.pop(addr)
+                else:
+                    self._memory_cache[addr] = current_value
+            else:
+                raise ValueError("Invalid comparison type")
+        return list(self._memory_cache.keys())
+
+    def _check_value(self, value, target_value, standard_comparison_type):
+        """
+        Compares a value with the target value based on the specified compare type.
+
+        Args:
+            value (int): The value to compare.
+            target_value (int or None): The target value to compare against.
+            standard_comparison_type (StandardComparisonType): The type of comparison to use.
+
+        Returns:
+            bool: True if the comparison condition is met, False otherwise.
+        """
+        if standard_comparison_type == StandardComparisonType.EXACT.value:
+            return value == target_value
+        elif standard_comparison_type == StandardComparisonType.LESS_THAN.value:
+            return value < target_value
+        elif standard_comparison_type == StandardComparisonType.GREATER_THAN.value:
+            return value > target_value
+        elif standard_comparison_type == StandardComparisonType.LESS_THAN_OR_EQUAL.value:
+            return value <= target_value
+        elif standard_comparison_type == StandardComparisonType.GREATER_THAN_OR_EQUAL.value:
+            return value >= target_value
+        else:
+            raise ValueError("Invalid comparison type")
+
+

Methods

+
+
+def scan_memory(self, target_value=None, start_addr=0, end_addr=65535, 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:

+
>>> 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 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())
+
+
+
+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:

+
>>> 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.
+
+
+ +Expand source code + +
def rescan_memory(
+    self, new_value=None, dynamic_comparison_type=DynamicComparisonType.UNCHANGED, byteorder="little"
+):
+    """
+    Rescans the memory and updates the memory cache based on a dynamic comparison type.
+
+    Example:
+    ```python
+    >>> current_score = 4 # You write current score in game
+    >>> pyboy.memory_scanner.scan_memory(current_score, start_addr=0xC000, end_addr=0xDFFF)
+    []
+    >>> for _ in range(175):
+    ...     pyboy.tick(1, True) # Progress the game to change score
+    True...
+    >>> current_score = 8 # You write the new score in game
+    >>> from pyboy.api.memory_scanner import DynamicComparisonType
+    >>> addresses = pyboy.memory_scanner.rescan_memory(current_score, DynamicComparisonType.MATCH)
+    >>> print(addresses) # If repeated enough, only one address will remain
+    []
+
+    ```
+
+    Args:
+        new_value (int, optional): The new value for comparison. If not provided, the current value in memory is used.
+        dynamic_comparison_type (DynamicComparisonType): The type of comparison to use. Defaults to UNCHANGED.
+
+    Returns:
+        list of int: A list of addresses remaining in the memory cache after the rescan.
+    """
+    for addr, value in self._memory_cache.copy().items():
+        current_value = int.from_bytes(
+            self.pyboy.memory[addr:addr + self._memory_cache_byte_width], byteorder=byteorder
+        )
+        if (dynamic_comparison_type == DynamicComparisonType.UNCHANGED):
+            if value != current_value:
+                self._memory_cache.pop(addr)
+            else:
+                self._memory_cache[addr] = current_value
+        elif (dynamic_comparison_type == DynamicComparisonType.CHANGED):
+            if value == current_value:
+                self._memory_cache.pop(addr)
+            else:
+                self._memory_cache[addr] = current_value
+        elif (dynamic_comparison_type == DynamicComparisonType.INCREASED):
+            if value >= current_value:
+                self._memory_cache.pop(addr)
+            else:
+                self._memory_cache[addr] = current_value
+        elif (dynamic_comparison_type == DynamicComparisonType.DECREASED):
+            if value <= current_value:
+                self._memory_cache.pop(addr)
+            else:
+                self._memory_cache[addr] = current_value
+        elif (dynamic_comparison_type == DynamicComparisonType.MATCH):
+            if new_value == None:
+                raise ValueError("new_value must be specified when using DynamicComparisonType.MATCH")
+            if current_value != new_value:
+                self._memory_cache.pop(addr)
+            else:
+                self._memory_cache[addr] = current_value
+        else:
+            raise ValueError("Invalid comparison type")
+    return list(self._memory_cache.keys())
+
+
+
+
+
+
+
+ +
+ + + + + \ No newline at end of file diff --git a/docs/api/screen.html b/docs/api/screen.html new file mode 100644 index 000000000..bd8701056 --- /dev/null +++ b/docs/api/screen.html @@ -0,0 +1,783 @@ + + + + + + +pyboy.api.screen API documentation + + + + + + + + + + +
+
+
+

Module pyboy.api.screen

+
+
+

This class gives access to the frame buffer and other screen parameters of PyBoy.

+
+ +Expand source code + +
#
+# License: See LICENSE.md file
+# GitHub: https://github.com/Baekalfen/PyBoy
+#
+"""
+This class gives access to the frame buffer and other screen parameters of PyBoy.
+"""
+
+import numpy as np
+
+from pyboy import utils
+from pyboy.logging import get_logger
+
+from .constants import COLS, ROWS
+
+logger = get_logger(__name__)
+
+try:
+    from PIL import Image
+except ImportError:
+    Image = None
+
+
+class Screen:
+    """
+    As part of the emulation, we generate a screen buffer in 32-bit RGBA format. This class has several helper methods
+    to make it possible to read this buffer out.
+
+    If you're making an AI or bot, it's highly recommended to _not_ use this class for detecting objects on the screen.
+    It's much more efficient to use `pyboy.PyBoy.tilemap_background`, `pyboy.PyBoy.tilemap_window`, and `pyboy.PyBoy.get_sprite` instead.
+    """
+    def __init__(self, mb):
+        self.mb = mb
+
+        self.raw_buffer = self.mb.lcd.renderer._screenbuffer
+        """
+        Provides a raw, unfiltered `bytes` object with the data from the screen. Check
+        `Screen.raw_buffer_format` to see which dataformat is used. **The returned type and dataformat are
+        subject to change.** The screen buffer is row-major.
+
+        Use this, only if you need to bypass the overhead of `Screen.image` or `Screen.ndarray`.
+
+        Example:
+        ```python
+        >>> import numpy as np
+        >>> rows, cols = pyboy.screen.raw_buffer_dims
+        >>> ndarray = np.frombuffer(
+        ...     pyboy.screen.raw_buffer,
+        ...     dtype=np.uint8,
+        ... ).reshape(rows, cols, 4) # Just an example, use pyboy.screen.ndarray instead
+
+        ```
+
+        Returns
+        -------
+        bytes:
+            92160 bytes of screen data in a `bytes` object.
+        """
+        self.raw_buffer_dims = self.mb.lcd.renderer.buffer_dims
+        """
+        Returns the dimensions of the raw screen buffer. The screen buffer is row-major.
+
+        Example:
+        ```python
+        >>> pyboy.screen.raw_buffer_dims
+        (144, 160)
+
+        ```
+
+        Returns
+        -------
+        tuple:
+            A two-tuple of the buffer dimensions. E.g. (144, 160).
+        """
+        self.raw_buffer_format = self.mb.lcd.renderer.color_format
+        """
+        Returns the color format of the raw screen buffer. **This format is subject to change.**
+
+        Example:
+        ```python
+        >>> from PIL import Image
+        >>> pyboy.screen.raw_buffer_format
+        'RGBA'
+        >>> image = Image.frombuffer(
+        ...    pyboy.screen.raw_buffer_format,
+        ...    pyboy.screen.raw_buffer_dims[::-1],
+        ...    pyboy.screen.raw_buffer,
+        ... ) # Just an example, use pyboy.screen.image instead
+        >>> image.save('frame.png')
+
+        ```
+
+        Returns
+        -------
+        str:
+            Color format of the raw screen buffer. E.g. 'RGBA'.
+        """
+        self.image = None
+        """
+        Reference to a PIL Image from the screen buffer. **Remember to copy, resize or convert this object** if you
+        intend to store it. The backing buffer will update, but it will be the same `PIL.Image` object.
+
+        Convenient for screen captures, but might be a bottleneck, if you use it to train a neural network. In which
+        case, read up on the `pyboy.api` features, [Pan Docs](https://gbdev.io/pandocs/) on tiles/sprites,
+        and join our Discord channel for more help.
+
+        Example:
+        ```python
+        >>> image = pyboy.screen.image
+        >>> type(image)
+        <class 'PIL.Image.Image'>
+        >>> image.save('frame.png')
+
+        ```
+
+        Returns
+        -------
+        PIL.Image:
+            RGB image of (160, 144) pixels
+        """
+        if not Image:
+            logger.warning("Cannot generate screen image. Missing dependency \"Pillow\".")
+        else:
+            self._set_image()
+
+        self.ndarray = np.frombuffer(
+            self.mb.lcd.renderer._screenbuffer_raw,
+            dtype=np.uint8,
+        ).reshape(ROWS, COLS, 4)
+        """
+        References the screen data in NumPy format. **Remember to copy this object** if you intend to store it.
+        The backing buffer will update, but it will be the same `ndarray` object.
+
+        The format is given by `pyboy.api.screen.Screen.raw_buffer_format`. The screen buffer is row-major.
+
+        Example:
+        ```python
+        >>> pyboy.screen.ndarray.shape
+        (144, 160, 4)
+        >>> # Display "P" on screen from the PyBoy bootrom
+        >>> pyboy.screen.ndarray[66:80,64:72,0]
+        array([[255, 255, 255, 255, 255, 255, 255, 255],
+               [255,   0,   0,   0,   0,   0, 255, 255],
+               [255,   0,   0,   0,   0,   0,   0, 255],
+               [255,   0,   0, 255, 255,   0,   0, 255],
+               [255,   0,   0, 255, 255,   0,   0, 255],
+               [255,   0,   0, 255, 255,   0,   0, 255],
+               [255,   0,   0,   0,   0,   0,   0, 255],
+               [255,   0,   0,   0,   0,   0, 255, 255],
+               [255,   0,   0, 255, 255, 255, 255, 255],
+               [255,   0,   0, 255, 255, 255, 255, 255],
+               [255,   0,   0, 255, 255, 255, 255, 255],
+               [255,   0,   0, 255, 255, 255, 255, 255],
+               [255,   0,   0, 255, 255, 255, 255, 255],
+               [255, 255, 255, 255, 255, 255, 255, 255]], dtype=uint8)
+
+        ```
+
+        Returns
+        -------
+        numpy.ndarray:
+            Screendata in `ndarray` of bytes with shape (144, 160, 4)
+        """
+
+    def _set_image(self):
+        self.image = Image.frombuffer(
+            self.mb.lcd.renderer.color_format, self.mb.lcd.renderer.buffer_dims[::-1],
+            self.mb.lcd.renderer._screenbuffer_raw
+        )
+
+    @property
+    def tilemap_position_list(self):
+        """
+        This function provides the screen (SCX, SCY) and window (WX. WY) position for each horizontal line in the
+        screen buffer. These parameters are often used for visual effects, and some games will reset the registers at
+        the end of each call to `pyboy.PyBoy.tick()`.
+
+        See `Screen.get_tilemap_position` for more information.
+
+        Example:
+        ```python
+        >>> pyboy.tick(25)
+        True
+        >>> swoosh = pyboy.screen.tilemap_position_list[67:78]
+        >>> print(*swoosh, sep=newline) # Just to pretty-print it
+        [0, 0, -7, 0]
+        [1, 0, -7, 0]
+        [2, 0, -7, 0]
+        [2, 0, -7, 0]
+        [3, 0, -7, 0]
+        [3, 0, -7, 0]
+        [3, 0, -7, 0]
+        [3, 0, -7, 0]
+        [2, 0, -7, 0]
+        [1, 0, -7, 0]
+        [0, 0, -7, 0]
+
+        ```
+
+        Returns
+        -------
+        list:
+            Nested list of SCX, SCY, WX and WY for each scanline (144x4). Returns (0, 0, 0, 0) when LCD is off.
+        """
+        # self.tilemap_position_list = np.asarray(self.mb.lcd.renderer._scanlineparameters, dtype=np.uint8).reshape(144, 5)[:, :4]
+        # self.tilemap_position_list = self.mb.lcd.renderer._scanlineparameters
+
+        # # return self.mb.lcd.renderer._scanlineparameters
+        if self.mb.lcd._LCDC.lcd_enable:
+            return [[line[0], line[1], line[2], line[3]] for line in self.mb.lcd.renderer._scanlineparameters]
+        else:
+            return [[0, 0, 0, 0] for line in range(144)]
+
+    def get_tilemap_position(self):
+        """
+        These coordinates define the offset in the tile map from where the top-left corner of the screen is place. Note
+        that the tile map defines 256x256 pixels, but the screen can only show 160x144 pixels. When the offset is closer
+        to the right or bottom edge than 160x144 pixels, the screen will wrap around and render from the opposite site
+        of the tile map.
+
+        For more details, see "7.4 Viewport" in the [report](https://github.com/Baekalfen/PyBoy/raw/master/extras/PyBoy.pdf),
+        or the Pan Docs under [LCD Position and Scrolling](https://gbdev.io/pandocs/Scrolling.html).
+
+        Example:
+        ```python
+        >>> pyboy.screen.get_tilemap_position()
+        ((0, 0), (-7, 0))
+
+        ```
+
+        Returns
+        -------
+        tuple:
+            Returns the tuple of registers ((SCX, SCY), (WX - 7, WY))
+        """
+        return (self.mb.lcd.getviewport(), self.mb.lcd.getwindowpos())
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Screen +(mb) +
+
+

As part of the emulation, we generate a screen buffer in 32-bit RGBA format. This class has several helper methods +to make it possible to read this buffer out.

+

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.tilemap_background, PyBoy.tilemap_window, and PyBoy.get_sprite() instead.

+
+ +Expand source code + +
class Screen:
+    """
+    As part of the emulation, we generate a screen buffer in 32-bit RGBA format. This class has several helper methods
+    to make it possible to read this buffer out.
+
+    If you're making an AI or bot, it's highly recommended to _not_ use this class for detecting objects on the screen.
+    It's much more efficient to use `pyboy.PyBoy.tilemap_background`, `pyboy.PyBoy.tilemap_window`, and `pyboy.PyBoy.get_sprite` instead.
+    """
+    def __init__(self, mb):
+        self.mb = mb
+
+        self.raw_buffer = self.mb.lcd.renderer._screenbuffer
+        """
+        Provides a raw, unfiltered `bytes` object with the data from the screen. Check
+        `Screen.raw_buffer_format` to see which dataformat is used. **The returned type and dataformat are
+        subject to change.** The screen buffer is row-major.
+
+        Use this, only if you need to bypass the overhead of `Screen.image` or `Screen.ndarray`.
+
+        Example:
+        ```python
+        >>> import numpy as np
+        >>> rows, cols = pyboy.screen.raw_buffer_dims
+        >>> ndarray = np.frombuffer(
+        ...     pyboy.screen.raw_buffer,
+        ...     dtype=np.uint8,
+        ... ).reshape(rows, cols, 4) # Just an example, use pyboy.screen.ndarray instead
+
+        ```
+
+        Returns
+        -------
+        bytes:
+            92160 bytes of screen data in a `bytes` object.
+        """
+        self.raw_buffer_dims = self.mb.lcd.renderer.buffer_dims
+        """
+        Returns the dimensions of the raw screen buffer. The screen buffer is row-major.
+
+        Example:
+        ```python
+        >>> pyboy.screen.raw_buffer_dims
+        (144, 160)
+
+        ```
+
+        Returns
+        -------
+        tuple:
+            A two-tuple of the buffer dimensions. E.g. (144, 160).
+        """
+        self.raw_buffer_format = self.mb.lcd.renderer.color_format
+        """
+        Returns the color format of the raw screen buffer. **This format is subject to change.**
+
+        Example:
+        ```python
+        >>> from PIL import Image
+        >>> pyboy.screen.raw_buffer_format
+        'RGBA'
+        >>> image = Image.frombuffer(
+        ...    pyboy.screen.raw_buffer_format,
+        ...    pyboy.screen.raw_buffer_dims[::-1],
+        ...    pyboy.screen.raw_buffer,
+        ... ) # Just an example, use pyboy.screen.image instead
+        >>> image.save('frame.png')
+
+        ```
+
+        Returns
+        -------
+        str:
+            Color format of the raw screen buffer. E.g. 'RGBA'.
+        """
+        self.image = None
+        """
+        Reference to a PIL Image from the screen buffer. **Remember to copy, resize or convert this object** if you
+        intend to store it. The backing buffer will update, but it will be the same `PIL.Image` object.
+
+        Convenient for screen captures, but might be a bottleneck, if you use it to train a neural network. In which
+        case, read up on the `pyboy.api` features, [Pan Docs](https://gbdev.io/pandocs/) on tiles/sprites,
+        and join our Discord channel for more help.
+
+        Example:
+        ```python
+        >>> image = pyboy.screen.image
+        >>> type(image)
+        <class 'PIL.Image.Image'>
+        >>> image.save('frame.png')
+
+        ```
+
+        Returns
+        -------
+        PIL.Image:
+            RGB image of (160, 144) pixels
+        """
+        if not Image:
+            logger.warning("Cannot generate screen image. Missing dependency \"Pillow\".")
+        else:
+            self._set_image()
+
+        self.ndarray = np.frombuffer(
+            self.mb.lcd.renderer._screenbuffer_raw,
+            dtype=np.uint8,
+        ).reshape(ROWS, COLS, 4)
+        """
+        References the screen data in NumPy format. **Remember to copy this object** if you intend to store it.
+        The backing buffer will update, but it will be the same `ndarray` object.
+
+        The format is given by `pyboy.api.screen.Screen.raw_buffer_format`. The screen buffer is row-major.
+
+        Example:
+        ```python
+        >>> pyboy.screen.ndarray.shape
+        (144, 160, 4)
+        >>> # Display "P" on screen from the PyBoy bootrom
+        >>> pyboy.screen.ndarray[66:80,64:72,0]
+        array([[255, 255, 255, 255, 255, 255, 255, 255],
+               [255,   0,   0,   0,   0,   0, 255, 255],
+               [255,   0,   0,   0,   0,   0,   0, 255],
+               [255,   0,   0, 255, 255,   0,   0, 255],
+               [255,   0,   0, 255, 255,   0,   0, 255],
+               [255,   0,   0, 255, 255,   0,   0, 255],
+               [255,   0,   0,   0,   0,   0,   0, 255],
+               [255,   0,   0,   0,   0,   0, 255, 255],
+               [255,   0,   0, 255, 255, 255, 255, 255],
+               [255,   0,   0, 255, 255, 255, 255, 255],
+               [255,   0,   0, 255, 255, 255, 255, 255],
+               [255,   0,   0, 255, 255, 255, 255, 255],
+               [255,   0,   0, 255, 255, 255, 255, 255],
+               [255, 255, 255, 255, 255, 255, 255, 255]], dtype=uint8)
+
+        ```
+
+        Returns
+        -------
+        numpy.ndarray:
+            Screendata in `ndarray` of bytes with shape (144, 160, 4)
+        """
+
+    def _set_image(self):
+        self.image = Image.frombuffer(
+            self.mb.lcd.renderer.color_format, self.mb.lcd.renderer.buffer_dims[::-1],
+            self.mb.lcd.renderer._screenbuffer_raw
+        )
+
+    @property
+    def tilemap_position_list(self):
+        """
+        This function provides the screen (SCX, SCY) and window (WX. WY) position for each horizontal line in the
+        screen buffer. These parameters are often used for visual effects, and some games will reset the registers at
+        the end of each call to `pyboy.PyBoy.tick()`.
+
+        See `Screen.get_tilemap_position` for more information.
+
+        Example:
+        ```python
+        >>> pyboy.tick(25)
+        True
+        >>> swoosh = pyboy.screen.tilemap_position_list[67:78]
+        >>> print(*swoosh, sep=newline) # Just to pretty-print it
+        [0, 0, -7, 0]
+        [1, 0, -7, 0]
+        [2, 0, -7, 0]
+        [2, 0, -7, 0]
+        [3, 0, -7, 0]
+        [3, 0, -7, 0]
+        [3, 0, -7, 0]
+        [3, 0, -7, 0]
+        [2, 0, -7, 0]
+        [1, 0, -7, 0]
+        [0, 0, -7, 0]
+
+        ```
+
+        Returns
+        -------
+        list:
+            Nested list of SCX, SCY, WX and WY for each scanline (144x4). Returns (0, 0, 0, 0) when LCD is off.
+        """
+        # self.tilemap_position_list = np.asarray(self.mb.lcd.renderer._scanlineparameters, dtype=np.uint8).reshape(144, 5)[:, :4]
+        # self.tilemap_position_list = self.mb.lcd.renderer._scanlineparameters
+
+        # # return self.mb.lcd.renderer._scanlineparameters
+        if self.mb.lcd._LCDC.lcd_enable:
+            return [[line[0], line[1], line[2], line[3]] for line in self.mb.lcd.renderer._scanlineparameters]
+        else:
+            return [[0, 0, 0, 0] for line in range(144)]
+
+    def get_tilemap_position(self):
+        """
+        These coordinates define the offset in the tile map from where the top-left corner of the screen is place. Note
+        that the tile map defines 256x256 pixels, but the screen can only show 160x144 pixels. When the offset is closer
+        to the right or bottom edge than 160x144 pixels, the screen will wrap around and render from the opposite site
+        of the tile map.
+
+        For more details, see "7.4 Viewport" in the [report](https://github.com/Baekalfen/PyBoy/raw/master/extras/PyBoy.pdf),
+        or the Pan Docs under [LCD Position and Scrolling](https://gbdev.io/pandocs/Scrolling.html).
+
+        Example:
+        ```python
+        >>> pyboy.screen.get_tilemap_position()
+        ((0, 0), (-7, 0))
+
+        ```
+
+        Returns
+        -------
+        tuple:
+            Returns the tuple of registers ((SCX, SCY), (WX - 7, WY))
+        """
+        return (self.mb.lcd.getviewport(), self.mb.lcd.getwindowpos())
+
+

Instance variables

+
+
var tilemap_position_list
+
+

This function provides the screen (SCX, SCY) and window (WX. WY) position for each horizontal line in the +screen buffer. These parameters are often used for visual effects, and some games will reset the registers at +the end of each call to PyBoy.tick().

+

See Screen.get_tilemap_position() for more information.

+

Example:

+
>>> pyboy.tick(25)
+True
+>>> swoosh = pyboy.screen.tilemap_position_list[67:78]
+>>> print(*swoosh, sep=newline) # Just to pretty-print it
+[0, 0, -7, 0]
+[1, 0, -7, 0]
+[2, 0, -7, 0]
+[2, 0, -7, 0]
+[3, 0, -7, 0]
+[3, 0, -7, 0]
+[3, 0, -7, 0]
+[3, 0, -7, 0]
+[2, 0, -7, 0]
+[1, 0, -7, 0]
+[0, 0, -7, 0]
+
+
+

Returns

+
+
list:
+
Nested list of SCX, SCY, WX and WY for each scanline (144x4). Returns (0, 0, 0, 0) when LCD is off.
+
+
+ +Expand source code + +
@property
+def tilemap_position_list(self):
+    """
+    This function provides the screen (SCX, SCY) and window (WX. WY) position for each horizontal line in the
+    screen buffer. These parameters are often used for visual effects, and some games will reset the registers at
+    the end of each call to `pyboy.PyBoy.tick()`.
+
+    See `Screen.get_tilemap_position` for more information.
+
+    Example:
+    ```python
+    >>> pyboy.tick(25)
+    True
+    >>> swoosh = pyboy.screen.tilemap_position_list[67:78]
+    >>> print(*swoosh, sep=newline) # Just to pretty-print it
+    [0, 0, -7, 0]
+    [1, 0, -7, 0]
+    [2, 0, -7, 0]
+    [2, 0, -7, 0]
+    [3, 0, -7, 0]
+    [3, 0, -7, 0]
+    [3, 0, -7, 0]
+    [3, 0, -7, 0]
+    [2, 0, -7, 0]
+    [1, 0, -7, 0]
+    [0, 0, -7, 0]
+
+    ```
+
+    Returns
+    -------
+    list:
+        Nested list of SCX, SCY, WX and WY for each scanline (144x4). Returns (0, 0, 0, 0) when LCD is off.
+    """
+    # self.tilemap_position_list = np.asarray(self.mb.lcd.renderer._scanlineparameters, dtype=np.uint8).reshape(144, 5)[:, :4]
+    # self.tilemap_position_list = self.mb.lcd.renderer._scanlineparameters
+
+    # # return self.mb.lcd.renderer._scanlineparameters
+    if self.mb.lcd._LCDC.lcd_enable:
+        return [[line[0], line[1], line[2], line[3]] for line in self.mb.lcd.renderer._scanlineparameters]
+    else:
+        return [[0, 0, 0, 0] for line in range(144)]
+
+
+
var raw_buffer
+
+

Provides a raw, unfiltered bytes object with the data from the screen. Check +Screen.raw_buffer_format to see which dataformat is used. The returned type and dataformat are +subject to change. The screen buffer is row-major.

+

Use this, only if you need to bypass the overhead of Screen.image or Screen.ndarray.

+

Example:

+
>>> import numpy as np
+>>> rows, cols = pyboy.screen.raw_buffer_dims
+>>> ndarray = np.frombuffer(
+...     pyboy.screen.raw_buffer,
+...     dtype=np.uint8,
+... ).reshape(rows, cols, 4) # Just an example, use pyboy.screen.ndarray instead
+
+
+

Returns

+
+
bytes:
+
92160 bytes of screen data in a bytes object.
+
+
+
var raw_buffer_dims
+
+

Returns the dimensions of the raw screen buffer. The screen buffer is row-major.

+

Example:

+
>>> pyboy.screen.raw_buffer_dims
+(144, 160)
+
+
+

Returns

+
+
tuple:
+
A two-tuple of the buffer dimensions. E.g. (144, 160).
+
+
+
var raw_buffer_format
+
+

Returns the color format of the raw screen buffer. This format is subject to change.

+

Example:

+
>>> from PIL import Image
+>>> pyboy.screen.raw_buffer_format
+'RGBA'
+>>> image = Image.frombuffer(
+...    pyboy.screen.raw_buffer_format,
+...    pyboy.screen.raw_buffer_dims[::-1],
+...    pyboy.screen.raw_buffer,
+... ) # Just an example, use pyboy.screen.image instead
+>>> image.save('frame.png')
+
+
+

Returns

+
+
str:
+
Color format of the raw screen buffer. E.g. 'RGBA'.
+
+
+
var image
+
+

Reference to a PIL Image from the screen buffer. Remember to copy, resize or convert this object if you +intend to store it. The backing buffer will update, but it will be the same PIL.Image object.

+

Convenient for screen captures, but might be a bottleneck, if you use it to train a neural network. In which +case, read up on the pyboy.api features, Pan Docs on tiles/sprites, +and join our Discord channel for more help.

+

Example:

+
>>> image = pyboy.screen.image
+>>> type(image)
+<class 'PIL.Image.Image'>
+>>> image.save('frame.png')
+
+
+

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.

+

The format is given by Screen.raw_buffer_format. The screen buffer is row-major.

+

Example:

+
>>> pyboy.screen.ndarray.shape
+(144, 160, 4)
+>>> # Display "P" on screen from the PyBoy bootrom
+>>> pyboy.screen.ndarray[66:80,64:72,0]
+array([[255, 255, 255, 255, 255, 255, 255, 255],
+       [255,   0,   0,   0,   0,   0, 255, 255],
+       [255,   0,   0,   0,   0,   0,   0, 255],
+       [255,   0,   0, 255, 255,   0,   0, 255],
+       [255,   0,   0, 255, 255,   0,   0, 255],
+       [255,   0,   0, 255, 255,   0,   0, 255],
+       [255,   0,   0,   0,   0,   0,   0, 255],
+       [255,   0,   0,   0,   0,   0, 255, 255],
+       [255,   0,   0, 255, 255, 255, 255, 255],
+       [255,   0,   0, 255, 255, 255, 255, 255],
+       [255,   0,   0, 255, 255, 255, 255, 255],
+       [255,   0,   0, 255, 255, 255, 255, 255],
+       [255,   0,   0, 255, 255, 255, 255, 255],
+       [255, 255, 255, 255, 255, 255, 255, 255]], dtype=uint8)
+
+
+

Returns

+
+
numpy.ndarray:
+
Screendata in ndarray of bytes with shape (144, 160, 4)
+
+
+
+

Methods

+
+
+def get_tilemap_position(self) +
+
+

These coordinates define the offset in the tile map from where the top-left corner of the screen is place. Note +that the tile map defines 256x256 pixels, but the screen can only show 160x144 pixels. When the offset is closer +to the right or bottom edge than 160x144 pixels, the screen will wrap around and render from the opposite site +of the tile map.

+

For more details, see "7.4 Viewport" in the report, +or the Pan Docs under LCD Position and Scrolling.

+

Example:

+
>>> pyboy.screen.get_tilemap_position()
+((0, 0), (-7, 0))
+
+
+

Returns

+
+
tuple:
+
Returns the tuple of registers ((SCX, SCY), (WX - 7, WY))
+
+
+ +Expand source code + +
def get_tilemap_position(self):
+    """
+    These coordinates define the offset in the tile map from where the top-left corner of the screen is place. Note
+    that the tile map defines 256x256 pixels, but the screen can only show 160x144 pixels. When the offset is closer
+    to the right or bottom edge than 160x144 pixels, the screen will wrap around and render from the opposite site
+    of the tile map.
+
+    For more details, see "7.4 Viewport" in the [report](https://github.com/Baekalfen/PyBoy/raw/master/extras/PyBoy.pdf),
+    or the Pan Docs under [LCD Position and Scrolling](https://gbdev.io/pandocs/Scrolling.html).
+
+    Example:
+    ```python
+    >>> pyboy.screen.get_tilemap_position()
+    ((0, 0), (-7, 0))
+
+    ```
+
+    Returns
+    -------
+    tuple:
+        Returns the tuple of registers ((SCX, SCY), (WX - 7, WY))
+    """
+    return (self.mb.lcd.getviewport(), self.mb.lcd.getwindowpos())
+
+
+
+
+
+
+
+ +
+ + + + + \ No newline at end of file diff --git a/docs/botsupport/sprite.html b/docs/api/sprite.html similarity index 77% rename from docs/botsupport/sprite.html rename to docs/api/sprite.html index 7c6c40099..7c119dff7 100644 --- a/docs/botsupport/sprite.html +++ b/docs/api/sprite.html @@ -4,7 +4,7 @@ -pyboy.botsupport.sprite API documentation +pyboy.api.sprite API documentation @@ -18,7 +18,7 @@
-

Module pyboy.botsupport.sprite

+

Module pyboy.api.sprite

This class presents an interface to the sprites held in the OAM data on the Game Boy.

@@ -56,7 +56,7 @@

Module pyboy.botsupport.sprite

call to `pyboy.PyBoy.tick`, so make sure to verify the `Sprite.tile_identifier` hasn't changed. By knowing the tile identifiers of players, enemies, power-ups and so on, you'll be able to search for them - using `pyboy.botsupport.BotSupportManager.sprite_by_tile_identifier` and feed it to your bot or AI. + using `pyboy.sprite_by_tile_identifier` and feed it to your bot or AI. """ assert 0 <= sprite_index < SPRITES, f"Sprite index of {sprite_index} is out of range (0-{SPRITES})" self.mb = mb @@ -99,7 +99,7 @@

Module pyboy.botsupport.sprite

self.tile_identifier = self.mb.getitem(OAM_OFFSET + self._offset + 2) """ The identifier of the tile the sprite uses. To get a better representation, see the method - `pyboy.botsupport.sprite.Sprite.tiles`. + `pyboy.api.sprite.Sprite.tiles`. For double-height sprites, this will only give the identifier of the first tile. The second tile will always be the one immediately following the first (`tile_identifier + 1`). @@ -114,7 +114,7 @@

Module pyboy.botsupport.sprite

self.attr_obj_bg_priority = _bit(attr, 7) """ To better understand this values, look in the [Pan Docs: VRAM Sprite Attribute Table - (OAM)](http://bgb.bircd.org/pandocs.htm#vramspriteattributetableoam). + (OAM)](https://gbdev.io/pandocs/OAM.html). Returns ------- @@ -125,7 +125,7 @@

Module pyboy.botsupport.sprite

self.attr_y_flip = _bit(attr, 6) """ To better understand this values, look in the [Pan Docs: VRAM Sprite Attribute Table - (OAM)](http://bgb.bircd.org/pandocs.htm#vramspriteattributetableoam). + (OAM)](https://gbdev.io/pandocs/OAM.html). Returns ------- @@ -136,7 +136,7 @@

Module pyboy.botsupport.sprite

self.attr_x_flip = _bit(attr, 5) """ To better understand this values, look in the [Pan Docs: VRAM Sprite Attribute Table - (OAM)](http://bgb.bircd.org/pandocs.htm#vramspriteattributetableoam). + (OAM)](https://gbdev.io/pandocs/OAM.html). Returns ------- @@ -144,10 +144,21 @@

Module pyboy.botsupport.sprite

The state of the bit in the attributes lookup. """ - self.attr_palette_number = _bit(attr, 4) + self.attr_palette_number = 0 """ To better understand this values, look in the [Pan Docs: VRAM Sprite Attribute Table - (OAM)](http://bgb.bircd.org/pandocs.htm#vramspriteattributetableoam). + (OAM)](https://gbdev.io/pandocs/OAM.html). + + Returns + ------- + int: + The state of the bit(s) in the attributes lookup. + """ + + self.attr_cgb_bank_number = 0 + """ + To better understand this values, look in the [Pan Docs: VRAM Sprite Attribute Table + (OAM)](https://gbdev.io/pandocs/OAM.html). Returns ------- @@ -155,8 +166,14 @@

Module pyboy.botsupport.sprite

The state of the bit in the attributes lookup. """ + if self.mb.cgb: + self.attr_palette_number = attr & 0b111 + self.attr_cgb_bank_number = _bit(attr, 3) + else: + self.attr_palette_number = _bit(attr, 4) + LCDC = LCDCRegister(self.mb.getitem(LCDC_OFFSET)) - sprite_height = 16 if LCDC.sprite_height else 8 + sprite_height = 16 if LCDC._get_sprite_height() else 8 self.shape = (8, sprite_height) """ Sprites can be set to be 8x8 or 8x16 pixels (16 pixels tall). This is defined globally for the rendering @@ -176,12 +193,12 @@

Module pyboy.botsupport.sprite

immediately following the identifier given, and render it below the first. More information can be found in the [Pan Docs: VRAM Sprite Attribute Table - (OAM)](http://bgb.bircd.org/pandocs.htm#vramspriteattributetableoam) + (OAM)](https://gbdev.io/pandocs/OAM.html) Returns ------- list: - A list of `pyboy.botsupport.tile.Tile` object(s) representing the graphics data for the sprite + A list of `pyboy.api.tile.Tile` object(s) representing the graphics data for the sprite """ if sprite_height == 16: self.tiles += [Tile(self.mb, self.tile_identifier + 1)] @@ -222,7 +239,7 @@

Module pyboy.botsupport.sprite

Classes

-
+
class Sprite (mb, sprite_index)
@@ -234,9 +251,9 @@

Classes

grid-size of 8x8 pixels precision, and can have no transparency.

Sprites on the Game Boy are tightly associated with tiles. The sprites can be seen as "upgraded" tiles, as the image data still refers back to one (or two) tiles. The tile that a sprite will show, can change between each -call to PyBoy.tick(), so make sure to verify the Sprite.tile_identifier hasn't changed.

+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.

+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).

Returns

@@ -440,9 +474,9 @@

Returns

unsigned tile index
-
var attr_obj_bg_priority
+
var attr_obj_bg_priority
-

To better understand this values, look in the Pan Docs: VRAM Sprite Attribute Table +

To better understand this values, look in the Pan Docs: VRAM Sprite Attribute Table (OAM).

Returns

@@ -450,9 +484,9 @@

Returns

The state of the bit in the attributes lookup.
-
var attr_y_flip
+
var attr_y_flip
-

To better understand this values, look in the Pan Docs: VRAM Sprite Attribute Table +

To better understand this values, look in the Pan Docs: VRAM Sprite Attribute Table (OAM).

Returns

@@ -460,9 +494,9 @@

Returns

The state of the bit in the attributes lookup.
-
var attr_x_flip
+
var attr_x_flip
-

To better understand this values, look in the Pan Docs: VRAM Sprite Attribute Table +

To better understand this values, look in the Pan Docs: VRAM Sprite Attribute Table (OAM).

Returns

@@ -470,9 +504,19 @@

Returns

The state of the bit in the attributes lookup.
-
var attr_palette_number
+
var attr_palette_number
+
+

To better understand this values, look in the Pan Docs: VRAM Sprite Attribute Table +(OAM).

+

Returns

+
+
int:
+
The state of the bit(s) in the attributes lookup.
+
+
+
var attr_cgb_bank_number
-

To better understand this values, look in the Pan Docs: VRAM Sprite Attribute Table +

To better understand this values, look in the Pan Docs: VRAM Sprite Attribute Table (OAM).

Returns

@@ -480,7 +524,7 @@

Returns

The state of the bit in the attributes lookup.
-
var shape
+
var shape

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.

-

More information can be found in the Pan Docs: VRAM Sprite Attribute Table +

More information can be found in the Pan Docs: VRAM Sprite Attribute Table (OAM)

Returns

list:
-
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.

@@ -526,24 +570,25 @@

Index

  • Super-module

  • Classes

    diff --git a/docs/botsupport/tile.html b/docs/api/tile.html similarity index 51% rename from docs/botsupport/tile.html rename to docs/api/tile.html index 54fdfe878..195df2998 100644 --- a/docs/botsupport/tile.html +++ b/docs/api/tile.html @@ -4,9 +4,9 @@ -pyboy.botsupport.tile API documentation +pyboy.api.tile API documentation +`pyboy.api.sprite.Sprite` and …" /> @@ -19,11 +19,11 @@
    -

    Module pyboy.botsupport.tile

    +

    Module pyboy.api.tile

    The Game Boy uses tiles as the building block for all graphics on the screen. This base-class is used both for -Sprite and TileMap, when refering to graphics.

    +Sprite and TileMap, when refering to graphics.

    Expand source code @@ -34,50 +34,58 @@

    Module pyboy.botsupport.tile

    # """ The Game Boy uses tiles as the building block for all graphics on the screen. This base-class is used both for -`pyboy.botsupport.sprite.Sprite` and `pyboy.botsupport.tilemap.TileMap`, when refering to graphics. +`pyboy.api.sprite.Sprite` and `pyboy.api.tilemap.TileMap`, when refering to graphics. """ -import logging - import numpy as np + +import pyboy from pyboy import utils -from .constants import LOW_TILEDATA, VRAM_OFFSET +from .constants import LOW_TILEDATA, TILES, TILES_CGB, VRAM_OFFSET -logger = logging.getLogger(__name__) +logger = pyboy.logging.get_logger(__name__) try: from PIL import Image except ImportError: Image = None +try: + from cython import compiled + cythonmode = compiled +except ImportError: + cythonmode = False + class Tile: def __init__(self, mb, identifier): """ The Game Boy uses tiles as the building block for all graphics on the screen. This base-class is used for - `pyboy.botsupport.BotSupportManager.tile`, `pyboy.botsupport.sprite.Sprite` and `pyboy.botsupport.tilemap.TileMap`, when - refering to graphics. + `pyboy.PyBoy.get_tile`, `pyboy.api.sprite.Sprite` and `pyboy.api.tilemap.TileMap`, when refering to graphics. This class is not meant to be instantiated by developers reading this documentation, but it will be created - internally and returned by `pyboy.botsupport.sprite.Sprite.tiles` and - `pyboy.botsupport.tilemap.TileMap.tile`. + internally and returned by `pyboy.api.sprite.Sprite.tiles` and + `pyboy.api.tilemap.TileMap.tile`. The data of this class is static, apart from the image data, which is loaded from the Game Boy's memory when needed. Beware that the graphics for the tile can change between each call to `pyboy.PyBoy.tick`. """ self.mb = mb - assert 0 <= identifier < 384, "Identifier out of range" + if self.mb.cgb: + assert 0 <= identifier < TILES_CGB, "Identifier out of range" + else: + assert 0 <= identifier < TILES, "Identifier out of range" - self.data_address = LOW_TILEDATA + (16*identifier) + self.data_address = LOW_TILEDATA + (16 * (identifier%TILES)) """ The tile data is defined in a specific area of the Game Boy. This function returns the address of the tile data - corresponding to the tile identifier. It is advised to use `pyboy.botsupport.tile.Tile.image` or one of the + corresponding to the tile identifier. It is advised to use `pyboy.api.tile.Tile.image` or one of the other `image`-functions if you want to view the tile. You can read how the data is read in the - [Pan Docs: VRAM Tile Data](http://bgb.bircd.org/pandocs.htm#vramtiledata). + [Pan Docs: VRAM Tile Data](https://gbdev.io/pandocs/Tile_Data.html). Returns ------- @@ -85,10 +93,16 @@

    Module pyboy.botsupport.tile

    address in VRAM where tile data starts """ - self.tile_identifier = (self.data_address - LOW_TILEDATA) // 16 + if identifier < TILES: + self.vram_bank = 0 + else: + self.vram_bank = 1 + + self.tile_identifier = identifier """ The Game Boy has a slightly complicated indexing system for tiles. This identifier unifies the otherwise - complicated indexing system on the Game Boy into a single range of 0-383 (both included). + complicated indexing system on the Game Boy into a single range of 0-383 (both included) or 0-767 for Game Boy + Color. Returns ------- @@ -106,12 +120,29 @@

    Module pyboy.botsupport.tile

    The width and height of the tile. """ + self.raw_buffer_format = self.mb.lcd.renderer.color_format + """ + Returns the color format of the raw screen buffer. + + Returns + ------- + str: + Color format of the raw screen buffer. E.g. 'RGBA'. + """ + def image(self): """ - Use this function to get an easy-to-use `PIL.Image` object of the tile. The image is 8x8 pixels in RGBA colors. + Use this function to get an `PIL.Image` object of the tile. The image is 8x8 pixels. The format or "mode" might change at any time. Be aware, that the graphics for this tile can change between each call to `pyboy.PyBoy.tick`. + Example: + ```python + >>> tile = pyboy.get_tile(1) + >>> tile.image().save('tile_1.png') + + ``` + Returns ------- PIL.Image : @@ -120,24 +151,55 @@

    Module pyboy.botsupport.tile

    if Image is None: logger.error(f"{__name__}: Missing dependency \"Pillow\".") return None - return Image.frombytes("RGBA", (8, 8), bytes(self.image_data())) - def image_ndarray(self): + if cythonmode: + return Image.fromarray(self._image_data().base, mode=self.raw_buffer_format) + else: + return Image.frombytes(self.raw_buffer_format, (8, 8), self._image_data()) + + def ndarray(self): """ - Use this function to get an easy-to-use `numpy.ndarray` object of the tile. The array has a shape of (8, 8, 4) - and each value is of `numpy.uint8`. The values corresponds to and RGBA image of 8x8 pixels with each sub-color - in a separate cell. + Use this function to get an `numpy.ndarray` object of the tile. The array has a shape of (8, 8, 4) + and each value is of `numpy.uint8`. The values corresponds to an image of 8x8 pixels with each sub-color + in a separate cell. The format is given by `pyboy.api.tile.Tile.raw_buffer_format`. Be aware, that the graphics for this tile can change between each call to `pyboy.PyBoy.tick`. + Example: + ```python + >>> tile1 = pyboy.get_tile(1) + >>> tile1.ndarray()[:,:,0] # Upper part of "P" + array([[255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255], + [255, 0, 0, 0, 0, 0, 255, 255], + [255, 0, 0, 0, 0, 0, 0, 255], + [255, 0, 0, 255, 255, 0, 0, 255], + [255, 0, 0, 255, 255, 0, 0, 255], + [255, 0, 0, 255, 255, 0, 0, 255]], dtype=uint8) + >>> tile2 = pyboy.get_tile(2) + >>> tile2.ndarray()[:,:,0] # Lower part of "P" + array([[255, 0, 0, 0, 0, 0, 0, 255], + [255, 0, 0, 0, 0, 0, 255, 255], + [255, 0, 0, 255, 255, 255, 255, 255], + [255, 0, 0, 255, 255, 255, 255, 255], + [255, 0, 0, 255, 255, 255, 255, 255], + [255, 0, 0, 255, 255, 255, 255, 255], + [255, 0, 0, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255]], dtype=uint8) + + ``` + Returns ------- numpy.ndarray : Array of shape (8, 8, 4) with data type of `numpy.uint8`. """ - return np.asarray(self.image_data()).view(dtype=np.uint8).reshape(8, 8, 4) + # The data is laid out as (X, red, green, blue), where X is currently always zero, but this is not guarenteed + # across versions of PyBoy. + return np.asarray(self._image_data()).view(dtype=np.uint8).reshape(8, 8, 4) - def image_data(self): + def _image_data(self): """ Use this function to get the raw tile data. The data is a `memoryview` corresponding to 8x8 pixels in RGBA colors. @@ -147,23 +209,24 @@

    Module pyboy.botsupport.tile

    Returns ------- memoryview : - Image data of tile in 8x8 pixels and RGBA colors. + Image data of tile in 8x8 pixels and RGB colors. """ self.data = np.zeros((8, 8), dtype=np.uint32) for k in range(0, 16, 2): # 2 bytes for each line - byte1 = self.mb.lcd.VRAM0[self.data_address + k - VRAM_OFFSET] - byte2 = self.mb.lcd.VRAM0[self.data_address + k + 1 - VRAM_OFFSET] + if self.vram_bank == 0: + byte1 = self.mb.lcd.VRAM0[self.data_address + k - VRAM_OFFSET] + byte2 = self.mb.lcd.VRAM0[self.data_address + k + 1 - VRAM_OFFSET] + else: + byte1 = self.mb.lcd.VRAM1[self.data_address + k - VRAM_OFFSET] + byte2 = self.mb.lcd.VRAM1[self.data_address + k + 1 - VRAM_OFFSET] for x in range(8): colorcode = utils.color_code(byte1, byte2, 7 - x) - # NOTE: ">> 8 | 0xFF000000" to keep compatibility with earlier code - old_A_format = 0xFF000000 - self.data[k // 2][x] = self.mb.lcd.BGP.getcolor(colorcode) >> 8 | old_A_format - + self.data[k // 2][x] = self.mb.lcd.BGP.getcolor(colorcode) return self.data def __eq__(self, other): - return self.data_address == other.data_address + return self.data_address == other.data_address and self.vram_bank == other.vram_bank def __repr__(self): return f"Tile: {self.tile_identifier}" @@ -178,17 +241,16 @@

    Module pyboy.botsupport.tile

    Classes

    -
    +
    class Tile (mb, identifier)

    The Game Boy uses tiles as the building block for all graphics on the screen. This base-class is used for -BotSupportManager.tile(), Sprite and TileMap, when -refering to graphics.

    +PyBoy.get_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().

    +internally and returned by Sprite.tiles and +TileMap.tile().

    The data of this class is static, apart from the image data, which is loaded from the Game Boy's memory when needed. Beware that the graphics for the tile can change between each call to PyBoy.tick().

    @@ -199,28 +261,30 @@

    Classes

    def __init__(self, mb, identifier): """ The Game Boy uses tiles as the building block for all graphics on the screen. This base-class is used for - `pyboy.botsupport.BotSupportManager.tile`, `pyboy.botsupport.sprite.Sprite` and `pyboy.botsupport.tilemap.TileMap`, when - refering to graphics. + `pyboy.PyBoy.get_tile`, `pyboy.api.sprite.Sprite` and `pyboy.api.tilemap.TileMap`, when refering to graphics. This class is not meant to be instantiated by developers reading this documentation, but it will be created - internally and returned by `pyboy.botsupport.sprite.Sprite.tiles` and - `pyboy.botsupport.tilemap.TileMap.tile`. + internally and returned by `pyboy.api.sprite.Sprite.tiles` and + `pyboy.api.tilemap.TileMap.tile`. The data of this class is static, apart from the image data, which is loaded from the Game Boy's memory when needed. Beware that the graphics for the tile can change between each call to `pyboy.PyBoy.tick`. """ self.mb = mb - assert 0 <= identifier < 384, "Identifier out of range" + if self.mb.cgb: + assert 0 <= identifier < TILES_CGB, "Identifier out of range" + else: + assert 0 <= identifier < TILES, "Identifier out of range" - self.data_address = LOW_TILEDATA + (16*identifier) + self.data_address = LOW_TILEDATA + (16 * (identifier%TILES)) """ The tile data is defined in a specific area of the Game Boy. This function returns the address of the tile data - corresponding to the tile identifier. It is advised to use `pyboy.botsupport.tile.Tile.image` or one of the + corresponding to the tile identifier. It is advised to use `pyboy.api.tile.Tile.image` or one of the other `image`-functions if you want to view the tile. You can read how the data is read in the - [Pan Docs: VRAM Tile Data](http://bgb.bircd.org/pandocs.htm#vramtiledata). + [Pan Docs: VRAM Tile Data](https://gbdev.io/pandocs/Tile_Data.html). Returns ------- @@ -228,10 +292,16 @@

    Classes

    address in VRAM where tile data starts """ - self.tile_identifier = (self.data_address - LOW_TILEDATA) // 16 + if identifier < TILES: + self.vram_bank = 0 + else: + self.vram_bank = 1 + + self.tile_identifier = identifier """ The Game Boy has a slightly complicated indexing system for tiles. This identifier unifies the otherwise - complicated indexing system on the Game Boy into a single range of 0-383 (both included). + complicated indexing system on the Game Boy into a single range of 0-383 (both included) or 0-767 for Game Boy + Color. Returns ------- @@ -249,12 +319,29 @@

    Classes

    The width and height of the tile. """ + self.raw_buffer_format = self.mb.lcd.renderer.color_format + """ + Returns the color format of the raw screen buffer. + + Returns + ------- + str: + Color format of the raw screen buffer. E.g. 'RGBA'. + """ + def image(self): """ - Use this function to get an easy-to-use `PIL.Image` object of the tile. The image is 8x8 pixels in RGBA colors. + Use this function to get an `PIL.Image` object of the tile. The image is 8x8 pixels. The format or "mode" might change at any time. Be aware, that the graphics for this tile can change between each call to `pyboy.PyBoy.tick`. + Example: + ```python + >>> tile = pyboy.get_tile(1) + >>> tile.image().save('tile_1.png') + + ``` + Returns ------- PIL.Image : @@ -263,24 +350,55 @@

    Classes

    if Image is None: logger.error(f"{__name__}: Missing dependency \"Pillow\".") return None - return Image.frombytes("RGBA", (8, 8), bytes(self.image_data())) - def image_ndarray(self): + if cythonmode: + return Image.fromarray(self._image_data().base, mode=self.raw_buffer_format) + else: + return Image.frombytes(self.raw_buffer_format, (8, 8), self._image_data()) + + def ndarray(self): """ - Use this function to get an easy-to-use `numpy.ndarray` object of the tile. The array has a shape of (8, 8, 4) - and each value is of `numpy.uint8`. The values corresponds to and RGBA image of 8x8 pixels with each sub-color - in a separate cell. + Use this function to get an `numpy.ndarray` object of the tile. The array has a shape of (8, 8, 4) + and each value is of `numpy.uint8`. The values corresponds to an image of 8x8 pixels with each sub-color + in a separate cell. The format is given by `pyboy.api.tile.Tile.raw_buffer_format`. Be aware, that the graphics for this tile can change between each call to `pyboy.PyBoy.tick`. + Example: + ```python + >>> tile1 = pyboy.get_tile(1) + >>> tile1.ndarray()[:,:,0] # Upper part of "P" + array([[255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255], + [255, 0, 0, 0, 0, 0, 255, 255], + [255, 0, 0, 0, 0, 0, 0, 255], + [255, 0, 0, 255, 255, 0, 0, 255], + [255, 0, 0, 255, 255, 0, 0, 255], + [255, 0, 0, 255, 255, 0, 0, 255]], dtype=uint8) + >>> tile2 = pyboy.get_tile(2) + >>> tile2.ndarray()[:,:,0] # Lower part of "P" + array([[255, 0, 0, 0, 0, 0, 0, 255], + [255, 0, 0, 0, 0, 0, 255, 255], + [255, 0, 0, 255, 255, 255, 255, 255], + [255, 0, 0, 255, 255, 255, 255, 255], + [255, 0, 0, 255, 255, 255, 255, 255], + [255, 0, 0, 255, 255, 255, 255, 255], + [255, 0, 0, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255]], dtype=uint8) + + ``` + Returns ------- numpy.ndarray : Array of shape (8, 8, 4) with data type of `numpy.uint8`. """ - return np.asarray(self.image_data()).view(dtype=np.uint8).reshape(8, 8, 4) + # The data is laid out as (X, red, green, blue), where X is currently always zero, but this is not guarenteed + # across versions of PyBoy. + return np.asarray(self._image_data()).view(dtype=np.uint8).reshape(8, 8, 4) - def image_data(self): + def _image_data(self): """ Use this function to get the raw tile data. The data is a `memoryview` corresponding to 8x8 pixels in RGBA colors. @@ -290,68 +408,84 @@

    Classes

    Returns ------- memoryview : - Image data of tile in 8x8 pixels and RGBA colors. + Image data of tile in 8x8 pixels and RGB colors. """ self.data = np.zeros((8, 8), dtype=np.uint32) for k in range(0, 16, 2): # 2 bytes for each line - byte1 = self.mb.lcd.VRAM0[self.data_address + k - VRAM_OFFSET] - byte2 = self.mb.lcd.VRAM0[self.data_address + k + 1 - VRAM_OFFSET] + if self.vram_bank == 0: + byte1 = self.mb.lcd.VRAM0[self.data_address + k - VRAM_OFFSET] + byte2 = self.mb.lcd.VRAM0[self.data_address + k + 1 - VRAM_OFFSET] + else: + byte1 = self.mb.lcd.VRAM1[self.data_address + k - VRAM_OFFSET] + byte2 = self.mb.lcd.VRAM1[self.data_address + k + 1 - VRAM_OFFSET] for x in range(8): colorcode = utils.color_code(byte1, byte2, 7 - x) - # NOTE: ">> 8 | 0xFF000000" to keep compatibility with earlier code - old_A_format = 0xFF000000 - self.data[k // 2][x] = self.mb.lcd.BGP.getcolor(colorcode) >> 8 | old_A_format - + self.data[k // 2][x] = self.mb.lcd.BGP.getcolor(colorcode) return self.data def __eq__(self, other): - return self.data_address == other.data_address + return self.data_address == other.data_address and self.vram_bank == other.vram_bank def __repr__(self): return f"Tile: {self.tile_identifier}"

    Instance variables

    -
    var data_address
    +
    var data_address

    The tile data is defined in a specific area of the Game Boy. This function returns the address of the tile data -corresponding to the tile identifier. It is advised to use Tile.image() or one of the +corresponding to the tile identifier. It is advised to use Tile.image() or one of the other image-functions if you want to view the tile.

    You can read how the data is read in the -Pan Docs: VRAM Tile Data.

    +Pan Docs: VRAM Tile Data.

    Returns

    int:
    address in VRAM where tile data starts
    -
    var tile_identifier
    +
    var tile_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

    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().

    +

    Example:

    +
    >>> tile = pyboy.get_tile(1)
    +>>> tile.image().save('tile_1.png')
    +
    +

    Returns

    PIL.Image :
    @@ -363,10 +497,17 @@

    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`.
     
    +    Example:
    +    ```python
    +    >>> tile = pyboy.get_tile(1)
    +    >>> tile.image().save('tile_1.png')
    +
    +    ```
    +
         Returns
         -------
         PIL.Image :
    @@ -375,17 +516,44 @@ 

    Returns

    if Image is None: logger.error(f"{__name__}: Missing dependency \"Pillow\".") return None - return Image.frombytes("RGBA", (8, 8), bytes(self.image_data()))
    + + 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) +
+def ndarray(self)
-

Use this function to get an easy-to-use numpy.ndarray object of the tile. The array has a shape of (8, 8, 4) -and each value is of numpy.uint8. The values corresponds to and RGBA image of 8x8 pixels with each sub-color -in a separate cell.

+

Use this function to get an numpy.ndarray object of the tile. The array has a shape of (8, 8, 4) +and each value is of numpy.uint8. The values corresponds to an image of 8x8 pixels with each sub-color +in a separate cell. The format is given by Tile.raw_buffer_format.

Be aware, that the graphics for this tile can change between each call to PyBoy.tick().

+

Example:

+
>>> tile1 = pyboy.get_tile(1)
+>>> tile1.ndarray()[:,:,0] # Upper part of "P"
+array([[255, 255, 255, 255, 255, 255, 255, 255],
+       [255, 255, 255, 255, 255, 255, 255, 255],
+       [255, 255, 255, 255, 255, 255, 255, 255],
+       [255,   0,   0,   0,   0,   0, 255, 255],
+       [255,   0,   0,   0,   0,   0,   0, 255],
+       [255,   0,   0, 255, 255,   0,   0, 255],
+       [255,   0,   0, 255, 255,   0,   0, 255],
+       [255,   0,   0, 255, 255,   0,   0, 255]], dtype=uint8)
+>>> tile2 = pyboy.get_tile(2)
+>>> tile2.ndarray()[:,:,0] # Lower part of "P"
+array([[255,   0,   0,   0,   0,   0,   0, 255],
+       [255,   0,   0,   0,   0,   0, 255, 255],
+       [255,   0,   0, 255, 255, 255, 255, 255],
+       [255,   0,   0, 255, 255, 255, 255, 255],
+       [255,   0,   0, 255, 255, 255, 255, 255],
+       [255,   0,   0, 255, 255, 255, 255, 255],
+       [255,   0,   0, 255, 255, 255, 255, 255],
+       [255, 255, 255, 255, 255, 255, 255, 255]], dtype=uint8)
+
+

Returns

numpy.ndarray :
@@ -395,62 +563,47 @@

Returns

Expand source code -
def image_ndarray(self):
+
def ndarray(self):
     """
-    Use this function to get an easy-to-use `numpy.ndarray` object of the tile. The array has a shape of (8, 8, 4)
-    and each value is of `numpy.uint8`. The values corresponds to and RGBA image of 8x8 pixels with each sub-color
-    in a separate cell.
+    Use this function to get an `numpy.ndarray` object of the tile. The array has a shape of (8, 8, 4)
+    and each value is of `numpy.uint8`. The values corresponds to an image of 8x8 pixels with each sub-color
+    in a separate cell. The format is given by `pyboy.api.tile.Tile.raw_buffer_format`.
 
     Be aware, that the graphics for this tile can change between each call to `pyboy.PyBoy.tick`.
 
+    Example:
+    ```python
+    >>> tile1 = pyboy.get_tile(1)
+    >>> tile1.ndarray()[:,:,0] # Upper part of "P"
+    array([[255, 255, 255, 255, 255, 255, 255, 255],
+           [255, 255, 255, 255, 255, 255, 255, 255],
+           [255, 255, 255, 255, 255, 255, 255, 255],
+           [255,   0,   0,   0,   0,   0, 255, 255],
+           [255,   0,   0,   0,   0,   0,   0, 255],
+           [255,   0,   0, 255, 255,   0,   0, 255],
+           [255,   0,   0, 255, 255,   0,   0, 255],
+           [255,   0,   0, 255, 255,   0,   0, 255]], dtype=uint8)
+    >>> tile2 = pyboy.get_tile(2)
+    >>> tile2.ndarray()[:,:,0] # Lower part of "P"
+    array([[255,   0,   0,   0,   0,   0,   0, 255],
+           [255,   0,   0,   0,   0,   0, 255, 255],
+           [255,   0,   0, 255, 255, 255, 255, 255],
+           [255,   0,   0, 255, 255, 255, 255, 255],
+           [255,   0,   0, 255, 255, 255, 255, 255],
+           [255,   0,   0, 255, 255, 255, 255, 255],
+           [255,   0,   0, 255, 255, 255, 255, 255],
+           [255, 255, 255, 255, 255, 255, 255, 255]], dtype=uint8)
+
+    ```
+
     Returns
     -------
     numpy.ndarray :
         Array of shape (8, 8, 4) with data type of `numpy.uint8`.
     """
-    return np.asarray(self.image_data()).view(dtype=np.uint8).reshape(8, 8, 4)
- -
-
-def image_data(self) -
-
-

Use this function to get the raw tile data. The data is a memoryview corresponding to 8x8 pixels in RGBA -colors.

-

Be aware, that the graphics for this tile can change between each call to PyBoy.tick().

-

Returns

-
-
memoryview :
-
Image data of tile in 8x8 pixels and RGBA colors.
-
-
- -Expand source code - -
def image_data(self):
-    """
-    Use this function to get the raw tile data. The data is a `memoryview` corresponding to 8x8 pixels in RGBA
-    colors.
-
-    Be aware, that the graphics for this tile can change between each call to `pyboy.PyBoy.tick`.
-
-    Returns
-    -------
-    memoryview :
-        Image data of tile in 8x8 pixels and RGBA colors.
-    """
-    self.data = np.zeros((8, 8), dtype=np.uint32)
-    for k in range(0, 16, 2): # 2 bytes for each line
-        byte1 = self.mb.lcd.VRAM0[self.data_address + k - VRAM_OFFSET]
-        byte2 = self.mb.lcd.VRAM0[self.data_address + k + 1 - VRAM_OFFSET]
-
-        for x in range(8):
-            colorcode = utils.color_code(byte1, byte2, 7 - x)
-            # NOTE: ">> 8 | 0xFF000000" to keep compatibility with earlier code
-            old_A_format = 0xFF000000
-            self.data[k // 2][x] = self.mb.lcd.BGP.getcolor(colorcode) >> 8 | old_A_format
-
-    return self.data
+ # The data is laid out as (X, red, green, blue), where X is currently always zero, but this is not guarenteed + # across versions of PyBoy. + return np.asarray(self._image_data()).view(dtype=np.uint8).reshape(8, 8, 4)
@@ -466,20 +619,20 @@

Index

  • Super-module

  • Classes

    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 @@

    Module pyboy.botsupport.tilemap

    int: Tile identifier. """ - + self._refresh_lcdc() tile = self.mb.getitem(self._tile_address(column, row)) if self.signed_tile_data: return ((tile ^ 0x80) - 128) + LOW_TILEDATA_NTILES @@ -210,6 +213,7 @@

    Module pyboy.botsupport.tilemap

    return tile def __repr__(self): + self._refresh_lcdc() adjust = 4 _use_tile_objects = self._use_tile_objects self.use_tile_objects(False) @@ -235,24 +239,51 @@

    Module pyboy.botsupport.tilemap

    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".

    -

    Use BotSupportManager.tilemap_background() and -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[:,:].

    @@ -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 @@

    Classes

    int: Tile identifier. """ - + self._refresh_lcdc() tile = self.mb.getitem(self._tile_address(column, row)) if self.signed_tile_data: return ((tile ^ 0x80) - 128) + LOW_TILEDATA_NTILES @@ -477,6 +506,7 @@

    Classes

    return tile def __repr__(self): + self._refresh_lcdc() adjust = 4 _use_tile_objects = self._use_tile_objects self.use_tile_objects(False) @@ -502,24 +532,51 @@

    Classes

    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.

    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 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.

    Args

    @@ -646,7 +677,7 @@

    Args

    Row in this tile map.

    Returns

    -

    Tile: +

    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).

    You can read how the indexes work in the -Pan Docs: VRAM Tile Data.

    +Pan Docs: VRAM Tile Data.

    Args

    column : int
    @@ -707,7 +738,7 @@

    Returns

    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 @@

    Returns

    int: Tile identifier. """ - + self._refresh_lcdc() tile = self.mb.getitem(self._tile_address(column, row)) if self.signed_tile_data: return ((tile ^ 0x80) - 128) + LOW_TILEDATA_NTILES @@ -726,7 +757,7 @@

    Returns

    return tile
    -
    +
    def use_tile_objects(self, switch)
    @@ -734,7 +765,7 @@

    Returns

    Args

    switch : bool
    -
    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 @@

    Index

    • Super-module

    • Classes

      diff --git a/docs/botsupport/index.html b/docs/botsupport/index.html deleted file mode 100644 index 61c1c66dc..000000000 --- a/docs/botsupport/index.html +++ /dev/null @@ -1,480 +0,0 @@ - - - - - - -pyboy.botsupport API documentation - - - - - - - - - - -
      -
      -
      -

      Module pyboy.botsupport

      -
      -
      -

      Tools to help interfacing with the Game Boy hardware

      -
      - -Expand source code - -
      #
      -# License: See LICENSE.md file
      -# GitHub: https://github.com/Baekalfen/PyBoy
      -#
      -"""
      -Tools to help interfacing with the Game Boy hardware
      -"""
      -
      -__pdoc__ = {
      -    "constants": False,
      -    "manager": False,
      -}
      -__all__ = ["BotSupportManager"]
      -
      -from .manager import BotSupportManager
      -
      -
      -
      -

      Sub-modules

      -
      -
      pyboy.botsupport.screen
      -
      -

      This class gives access to the frame buffer and other screen parameters of PyBoy.

      -
      -
      pyboy.botsupport.sprite
      -
      -

      This class presents an interface to the sprites held in the OAM data on the Game Boy.

      -
      -
      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 -Sprite and …

      -
      -
      pyboy.botsupport.tilemap
      -
      -

      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.

      -

      Args

      -
      -
      index : int
      -
      Sprite index from 0 to 39.
      -
      -

      Returns

      -

      Sprite: -Sprite corresponding to the given index.

      -
      - -Expand source code - -
      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 -BotSupportManager.sprite() function to get a 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
      -
      -
      - -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.

      -

      Returns

      -

      Tile: -A Tile object for the given identifier.

      -
      - -Expand source code - -
      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.

      -

      Returns

      -

      TileMap: -A TileMap object for the tile map.

      -
      - -Expand source code - -
      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.

      -

      Returns

      -

      TileMap: -A TileMap object for the tile map.

      -
      - -Expand source code - -
      def tilemap_window(self):
      -    """
      -    The Game Boy uses two tile maps at the same time to draw graphics on the screen. This method will provide one
      -    for the _window_ tiles. The game chooses whether it wants to use the low or the high tilemap.
      -
      -    Read more details about it, in the [Pan Docs](http://bgb.bircd.org/pandocs.htm#vrambackgroundmaps).
      -
      -    Returns
      -    -------
      -    `pyboy.botsupport.tilemap.TileMap`:
      -        A TileMap object for the tile map.
      -    """
      -    return _tilemap.TileMap(self.mb, "WINDOW")
      -
      -
      -
      -
      -
      -
      -
      - -
      - - - - - \ No newline at end of file diff --git a/docs/botsupport/screen.html b/docs/botsupport/screen.html deleted file mode 100644 index 27f9864d0..000000000 --- a/docs/botsupport/screen.html +++ /dev/null @@ -1,579 +0,0 @@ - - - - - - -pyboy.botsupport.screen API documentation - - - - - - - - - - -
      -
      -
      -

      Module pyboy.botsupport.screen

      -
      -
      -

      This class gives access to the frame buffer and other screen parameters of PyBoy.

      -
      - -Expand source code - -
      #
      -# License: See LICENSE.md file
      -# GitHub: https://github.com/Baekalfen/PyBoy
      -#
      -"""
      -This class gives access to the frame buffer and other screen parameters of PyBoy.
      -"""
      -
      -import logging
      -
      -import numpy as np
      -
      -from .constants import COLS, ROWS
      -
      -logger = logging.getLogger(__name__)
      -
      -try:
      -    from PIL import Image
      -except ImportError:
      -    Image = None
      -
      -
      -class Screen:
      -    """
      -    As part of the emulation, we generate a screen buffer in 32-bit RGBA format. This class has several helper methods
      -    to make it possible to read this buffer out.
      -
      -    If you're making an AI or bot, it's highly recommended to _not_ use this class for detecting objects on the screen.
      -    It's much more efficient to use `pyboy.botsupport.BotSupportManager.tilemap_background`, `pyboy.botsupport.BotSupportManager.tilemap_window`, and
      -    `pyboy.botsupport.BotSupportManager.sprite` instead.
      -    """
      -    def __init__(self, mb):
      -        self.mb = mb
      -
      -    def tilemap_position(self):
      -        """
      -        These coordinates define the offset in the tile map from where the top-left corner of the screen is place. Note
      -        that the tile map defines 256x256 pixels, but the screen can only show 160x144 pixels. When the offset is closer
      -        to the right or bottom edge than 160x144 pixels, the screen will wrap around and render from the opposite site
      -        of the tile map.
      -
      -        For more details, see "7.4 Viewport" in the [report](https://github.com/Baekalfen/PyBoy/raw/master/extras/PyBoy.pdf),
      -        or the Pan Docs under [LCD Position and Scrolling](http://bgb.bircd.org/pandocs.htm#lcdpositionandscrolling).
      -
      -        Returns
      -        -------
      -        tuple:
      -            Returns the tuple of registers ((SCX, SCY), (WX - 7, WY))
      -        """
      -        return (self.mb.lcd.getviewport(), self.mb.lcd.getwindowpos())
      -
      -    def tilemap_position_list(self):
      -        """
      -        This function provides the screen (SCX, SCY) and window (WX. WY) position for each horizontal line in the
      -        screen buffer. These parameters are often used for visual effects, and some games will reset the registers at
      -        the end of each call to `pyboy.PyBoy.tick()`. For such games, `Screen.tilemap_position` becomes useless.
      -
      -        See `Screen.tilemap_position` for more information.
      -
      -        Returns
      -        -------
      -        list:
      -            Nested list of SCX, SCY, WX and WY for each scanline (144x4). Returns (0, 0, 0, 0) when LCD is off.
      -        """
      -        if self.mb.lcd._LCDC.lcd_enable:
      -            return [[line[0], line[1], line[2], line[3]] for line in self.mb.lcd.renderer._scanlineparameters]
      -        else:
      -            return [[0, 0, 0, 0] for line in range(144)]
      -
      -    def raw_screen_buffer(self):
      -        """
      -        Provides a raw, unfiltered `bytes` object with the data from the screen. Check
      -        `Screen.raw_screen_buffer_format` to see which dataformat is used. The returned type and dataformat are
      -        subject to change.
      -
      -        Use this, only if you need to bypass the overhead of `Screen.screen_image` or `Screen.screen_ndarray`.
      -
      -        Returns
      -        -------
      -        bytes:
      -            92160 bytes of screen data in a `bytes` object.
      -        """
      -        return self.mb.lcd.renderer._screenbuffer_raw.tobytes()
      -
      -    def raw_screen_buffer_dims(self):
      -        """
      -        Returns the dimensions of the raw screen buffer.
      -
      -        Returns
      -        -------
      -        tuple:
      -            A two-tuple of the buffer dimensions. E.g. (160, 144).
      -        """
      -        return self.mb.lcd.renderer.buffer_dims
      -
      -    def raw_screen_buffer_format(self):
      -        """
      -        Returns the color format of the raw screen buffer.
      -
      -        Returns
      -        -------
      -        str:
      -            Color format of the raw screen buffer. E.g. 'RGB'.
      -        """
      -        return self.mb.lcd.renderer.color_format
      -
      -    def screen_ndarray(self):
      -        """
      -        Provides the screen data in NumPy format. The dataformat is always RGB.
      -
      -        Returns
      -        -------
      -        numpy.ndarray:
      -            Screendata in `ndarray` of bytes with shape (160, 144, 3)
      -        """
      -        return np.frombuffer(self.mb.lcd.renderer._screenbuffer_raw, dtype=np.uint8).reshape(ROWS, COLS, 4)[:, :, 1:]
      -
      -    def screen_image(self):
      -        """
      -        Generates a PIL Image from the screen buffer.
      -
      -        Convenient for screen captures, but might be a bottleneck, if you use it to train a neural network. In which
      -        case, read up on the `pyboy.botsupport` features, [Pan Docs](http://bgb.bircd.org/pandocs.htm) on tiles/sprites,
      -        and join our Discord channel for more help.
      -
      -        Returns
      -        -------
      -        PIL.Image:
      -            RGB image of (160, 144) pixels
      -        """
      -        if not Image:
      -            logger.error("Cannot generate screen image. Missing dependency \"Pillow\".")
      -            return None
      -
      -        # NOTE: Might have room for performance improvement
      -        # It's not possible to use the following, as the byte-order (endianess) isn't supported in Pillow
      -        # Image.frombytes('RGBA', self.buffer_dims, self.screen_buffer()).show()
      -        # FIXME: FORMAT IS BGR NOT RGB!!!
      -        return Image.fromarray(self.screen_ndarray()[:, :, [2, 1, 0]], "RGB")
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -

      Classes

      -
      -
      -class Screen -(mb) -
      -
      -

      As part of the emulation, we generate a screen buffer in 32-bit RGBA format. This class has several helper methods -to make it possible to read this buffer out.

      -

      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 BotSupportManager.tilemap_background(), BotSupportManager.tilemap_window(), and -BotSupportManager.sprite() instead.

      -
      - -Expand source code - -
      class Screen:
      -    """
      -    As part of the emulation, we generate a screen buffer in 32-bit RGBA format. This class has several helper methods
      -    to make it possible to read this buffer out.
      -
      -    If you're making an AI or bot, it's highly recommended to _not_ use this class for detecting objects on the screen.
      -    It's much more efficient to use `pyboy.botsupport.BotSupportManager.tilemap_background`, `pyboy.botsupport.BotSupportManager.tilemap_window`, and
      -    `pyboy.botsupport.BotSupportManager.sprite` instead.
      -    """
      -    def __init__(self, mb):
      -        self.mb = mb
      -
      -    def tilemap_position(self):
      -        """
      -        These coordinates define the offset in the tile map from where the top-left corner of the screen is place. Note
      -        that the tile map defines 256x256 pixels, but the screen can only show 160x144 pixels. When the offset is closer
      -        to the right or bottom edge than 160x144 pixels, the screen will wrap around and render from the opposite site
      -        of the tile map.
      -
      -        For more details, see "7.4 Viewport" in the [report](https://github.com/Baekalfen/PyBoy/raw/master/extras/PyBoy.pdf),
      -        or the Pan Docs under [LCD Position and Scrolling](http://bgb.bircd.org/pandocs.htm#lcdpositionandscrolling).
      -
      -        Returns
      -        -------
      -        tuple:
      -            Returns the tuple of registers ((SCX, SCY), (WX - 7, WY))
      -        """
      -        return (self.mb.lcd.getviewport(), self.mb.lcd.getwindowpos())
      -
      -    def tilemap_position_list(self):
      -        """
      -        This function provides the screen (SCX, SCY) and window (WX. WY) position for each horizontal line in the
      -        screen buffer. These parameters are often used for visual effects, and some games will reset the registers at
      -        the end of each call to `pyboy.PyBoy.tick()`. For such games, `Screen.tilemap_position` becomes useless.
      -
      -        See `Screen.tilemap_position` for more information.
      -
      -        Returns
      -        -------
      -        list:
      -            Nested list of SCX, SCY, WX and WY for each scanline (144x4). Returns (0, 0, 0, 0) when LCD is off.
      -        """
      -        if self.mb.lcd._LCDC.lcd_enable:
      -            return [[line[0], line[1], line[2], line[3]] for line in self.mb.lcd.renderer._scanlineparameters]
      -        else:
      -            return [[0, 0, 0, 0] for line in range(144)]
      -
      -    def raw_screen_buffer(self):
      -        """
      -        Provides a raw, unfiltered `bytes` object with the data from the screen. Check
      -        `Screen.raw_screen_buffer_format` to see which dataformat is used. The returned type and dataformat are
      -        subject to change.
      -
      -        Use this, only if you need to bypass the overhead of `Screen.screen_image` or `Screen.screen_ndarray`.
      -
      -        Returns
      -        -------
      -        bytes:
      -            92160 bytes of screen data in a `bytes` object.
      -        """
      -        return self.mb.lcd.renderer._screenbuffer_raw.tobytes()
      -
      -    def raw_screen_buffer_dims(self):
      -        """
      -        Returns the dimensions of the raw screen buffer.
      -
      -        Returns
      -        -------
      -        tuple:
      -            A two-tuple of the buffer dimensions. E.g. (160, 144).
      -        """
      -        return self.mb.lcd.renderer.buffer_dims
      -
      -    def raw_screen_buffer_format(self):
      -        """
      -        Returns the color format of the raw screen buffer.
      -
      -        Returns
      -        -------
      -        str:
      -            Color format of the raw screen buffer. E.g. 'RGB'.
      -        """
      -        return self.mb.lcd.renderer.color_format
      -
      -    def screen_ndarray(self):
      -        """
      -        Provides the screen data in NumPy format. The dataformat is always RGB.
      -
      -        Returns
      -        -------
      -        numpy.ndarray:
      -            Screendata in `ndarray` of bytes with shape (160, 144, 3)
      -        """
      -        return np.frombuffer(self.mb.lcd.renderer._screenbuffer_raw, dtype=np.uint8).reshape(ROWS, COLS, 4)[:, :, 1:]
      -
      -    def screen_image(self):
      -        """
      -        Generates a PIL Image from the screen buffer.
      -
      -        Convenient for screen captures, but might be a bottleneck, if you use it to train a neural network. In which
      -        case, read up on the `pyboy.botsupport` features, [Pan Docs](http://bgb.bircd.org/pandocs.htm) on tiles/sprites,
      -        and join our Discord channel for more help.
      -
      -        Returns
      -        -------
      -        PIL.Image:
      -            RGB image of (160, 144) pixels
      -        """
      -        if not Image:
      -            logger.error("Cannot generate screen image. Missing dependency \"Pillow\".")
      -            return None
      -
      -        # NOTE: Might have room for performance improvement
      -        # It's not possible to use the following, as the byte-order (endianess) isn't supported in Pillow
      -        # Image.frombytes('RGBA', self.buffer_dims, self.screen_buffer()).show()
      -        # FIXME: FORMAT IS BGR NOT RGB!!!
      -        return Image.fromarray(self.screen_ndarray()[:, :, [2, 1, 0]], "RGB")
      -
      -

      Methods

      -
      -
      -def tilemap_position(self) -
      -
      -

      These coordinates define the offset in the tile map from where the top-left corner of the screen is place. Note -that the tile map defines 256x256 pixels, but the screen can only show 160x144 pixels. When the offset is closer -to the right or bottom edge than 160x144 pixels, the screen will wrap around and render from the opposite site -of the tile map.

      -

      For more details, see "7.4 Viewport" in the report, -or the Pan Docs under LCD Position and Scrolling.

      -

      Returns

      -
      -
      tuple:
      -
      Returns the tuple of registers ((SCX, SCY), (WX - 7, WY))
      -
      -
      - -Expand source code - -
      def tilemap_position(self):
      -    """
      -    These coordinates define the offset in the tile map from where the top-left corner of the screen is place. Note
      -    that the tile map defines 256x256 pixels, but the screen can only show 160x144 pixels. When the offset is closer
      -    to the right or bottom edge than 160x144 pixels, the screen will wrap around and render from the opposite site
      -    of the tile map.
      -
      -    For more details, see "7.4 Viewport" in the [report](https://github.com/Baekalfen/PyBoy/raw/master/extras/PyBoy.pdf),
      -    or the Pan Docs under [LCD Position and Scrolling](http://bgb.bircd.org/pandocs.htm#lcdpositionandscrolling).
      -
      -    Returns
      -    -------
      -    tuple:
      -        Returns the tuple of registers ((SCX, SCY), (WX - 7, WY))
      -    """
      -    return (self.mb.lcd.getviewport(), self.mb.lcd.getwindowpos())
      -
      -
      -
      -def tilemap_position_list(self) -
      -
      -

      This function provides the screen (SCX, SCY) and window (WX. WY) position for each horizontal line in the -screen buffer. These parameters are often used for visual effects, and some games will reset the registers at -the end of each call to PyBoy.tick(). For such games, Screen.tilemap_position() becomes useless.

      -

      See Screen.tilemap_position() for more information.

      -

      Returns

      -
      -
      list:
      -
      Nested list of SCX, SCY, WX and WY for each scanline (144x4). Returns (0, 0, 0, 0) when LCD is off.
      -
      -
      - -Expand source code - -
      def tilemap_position_list(self):
      -    """
      -    This function provides the screen (SCX, SCY) and window (WX. WY) position for each horizontal line in the
      -    screen buffer. These parameters are often used for visual effects, and some games will reset the registers at
      -    the end of each call to `pyboy.PyBoy.tick()`. For such games, `Screen.tilemap_position` becomes useless.
      -
      -    See `Screen.tilemap_position` for more information.
      -
      -    Returns
      -    -------
      -    list:
      -        Nested list of SCX, SCY, WX and WY for each scanline (144x4). Returns (0, 0, 0, 0) when LCD is off.
      -    """
      -    if self.mb.lcd._LCDC.lcd_enable:
      -        return [[line[0], line[1], line[2], line[3]] for line in self.mb.lcd.renderer._scanlineparameters]
      -    else:
      -        return [[0, 0, 0, 0] for line in range(144)]
      -
      -
      -
      -def raw_screen_buffer(self) -
      -
      -

      Provides a raw, unfiltered bytes object with the data from the screen. Check -Screen.raw_screen_buffer_format() to see which dataformat is used. The returned type and dataformat are -subject to change.

      -

      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.
      -
      -
      - -Expand source code - -
      def raw_screen_buffer(self):
      -    """
      -    Provides a raw, unfiltered `bytes` object with the data from the screen. Check
      -    `Screen.raw_screen_buffer_format` to see which dataformat is used. The returned type and dataformat are
      -    subject to change.
      -
      -    Use this, only if you need to bypass the overhead of `Screen.screen_image` or `Screen.screen_ndarray`.
      -
      -    Returns
      -    -------
      -    bytes:
      -        92160 bytes of screen data in a `bytes` object.
      -    """
      -    return self.mb.lcd.renderer._screenbuffer_raw.tobytes()
      -
      -
      -
      -def raw_screen_buffer_dims(self) -
      -
      -

      Returns the dimensions of the raw screen buffer.

      -

      Returns

      -
      -
      tuple:
      -
      A two-tuple of the buffer dimensions. E.g. (160, 144).
      -
      -
      - -Expand source code - -
      def raw_screen_buffer_dims(self):
      -    """
      -    Returns the dimensions of the raw screen buffer.
      -
      -    Returns
      -    -------
      -    tuple:
      -        A two-tuple of the buffer dimensions. E.g. (160, 144).
      -    """
      -    return self.mb.lcd.renderer.buffer_dims
      -
      -
      -
      -def raw_screen_buffer_format(self) -
      -
      -

      Returns the color format of the raw screen buffer.

      -

      Returns

      -
      -
      str:
      -
      Color format of the raw screen buffer. E.g. 'RGB'.
      -
      -
      - -Expand source code - -
      def raw_screen_buffer_format(self):
      -    """
      -    Returns the color format of the raw screen buffer.
      -
      -    Returns
      -    -------
      -    str:
      -        Color format of the raw screen buffer. E.g. 'RGB'.
      -    """
      -    return self.mb.lcd.renderer.color_format
      -
      -
      -
      -def screen_ndarray(self) -
      -
      -

      Provides the screen data in NumPy format. The dataformat is always RGB.

      -

      Returns

      -
      -
      numpy.ndarray:
      -
      Screendata in ndarray of bytes with shape (160, 144, 3)
      -
      -
      - -Expand source code - -
      def screen_ndarray(self):
      -    """
      -    Provides the screen data in NumPy format. The dataformat is always RGB.
      -
      -    Returns
      -    -------
      -    numpy.ndarray:
      -        Screendata in `ndarray` of bytes with shape (160, 144, 3)
      -    """
      -    return np.frombuffer(self.mb.lcd.renderer._screenbuffer_raw, dtype=np.uint8).reshape(ROWS, COLS, 4)[:, :, 1:]
      -
      -
      -
      -def screen_image(self) -
      -
      -

      Generates a PIL Image from the screen buffer.

      -

      Convenient for screen captures, but might be a bottleneck, if you use it to train a neural network. In which -case, read up on the pyboy.botsupport features, Pan Docs on tiles/sprites, -and join our Discord channel for more help.

      -

      Returns

      -
      -
      PIL.Image:
      -
      RGB image of (160, 144) pixels
      -
      -
      - -Expand source code - -
      def screen_image(self):
      -    """
      -    Generates a PIL Image from the screen buffer.
      -
      -    Convenient for screen captures, but might be a bottleneck, if you use it to train a neural network. In which
      -    case, read up on the `pyboy.botsupport` features, [Pan Docs](http://bgb.bircd.org/pandocs.htm) on tiles/sprites,
      -    and join our Discord channel for more help.
      -
      -    Returns
      -    -------
      -    PIL.Image:
      -        RGB image of (160, 144) pixels
      -    """
      -    if not Image:
      -        logger.error("Cannot generate screen image. Missing dependency \"Pillow\".")
      -        return None
      -
      -    # NOTE: Might have room for performance improvement
      -    # It's not possible to use the following, as the byte-order (endianess) isn't supported in Pillow
      -    # Image.frombytes('RGBA', self.buffer_dims, self.screen_buffer()).show()
      -    # FIXME: FORMAT IS BGR NOT RGB!!!
      -    return Image.fromarray(self.screen_ndarray()[:, :, [2, 1, 0]], "RGB")
      -
      -
      -
      -
      -
      -
      -
      - -
      - - - - - \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index b7c45692f..dbcc86013 100644 --- a/docs/index.html +++ b/docs/index.html @@ -32,31 +32,31 @@

      Module pyboy

      __pdoc__ = { "core": False, - "logger": False, + "logging": False, "pyboy": False, - "utils": False, + "conftest": False, } -__all__ = ["PyBoy", "WindowEvent"] -from .pyboy import PyBoy -from .utils import WindowEvent
      +__all__ = ["PyBoy", "PyBoyMemoryView"] + +from .pyboy import PyBoy, PyBoyMemoryView

    Sub-modules

    -
    pyboy.botsupport
    +
    pyboy.api

    Tools to help interfacing with the Game Boy hardware

    -
    pyboy.openai_gym
    -
    -
    -
    pyboy.plugins

    Plugins that extend PyBoy's functionality. The only publicly exposed, are the game wrappers.

    +
    pyboy.utils
    +
    +
    +
    @@ -68,30 +68,42 @@

    Classes

    class PyBoy -(gamerom_file, *, bootrom_file=None, disable_renderer=False, sound=False, sound_emulated=False, cgb=None, randomize=False, **kwargs) +(gamerom, *, window='SDL2', scale=3, symbols=None, bootrom=None, sound=False, sound_emulated=False, cgb=None, **kwargs)

    PyBoy is loadable as an object in Python. This means, it can be initialized from another script, and be controlled and probed by the script. It is supported to spawn multiple emulators, just instantiate the class multiple times.

    -

    This object, WindowEvent, and the pyboy.botsupport module, are the only official user-facing -interfaces. All other parts of the emulator, are subject to change.

    A range of methods are exposed, which should allow for complete control of the emulator. Please open an issue on GitHub, if other methods are needed for your projects. Take a look at the files in examples/ for a crude "bots", which interact with the game.

    -

    Only the gamerom_file argument is required.

    +

    Only the gamerom argument is required.

    +

    Example:

    +
    >>> pyboy = PyBoy('game_rom.gb')
    +>>> for _ in range(60): # Use 'while True:' for infinite
    +...     pyboy.tick()
    +True...
    +>>> pyboy.stop()
    +
    +

    Args

    -
    gamerom_file : str
    +
    gamerom : str
    Filepath to a game-ROM for Game Boy or Game Boy Color.

    Kwargs

    -

    bootrom_file (str): Filepath to a boot-ROM to use. If unsure, specify None. -disable_renderer (bool): Can be used to optimize performance, by internally disable rendering of the screen. -color_palette (tuple): Specify the color palette to use for rendering. -cgb_color_palette (list of tuple): Specify the color palette to use for rendering in CGB-mode for non-color games.

    -

    Other keyword arguments may exist for plugins that are not listed here. They can be viewed with the -parser_arguments() method in the pyboy.plugins.manager module, or by running pyboy –help in the terminal.

    +
      +
    • window (str): "SDL2", "OpenGL", or "null"
    • +
    • scale (int): Window scale factor. Doesn't apply to API.
    • +
    • symbols (str): Filepath to a .sym file to use. If unsure, specify None.
    • +
    • bootrom (str): Filepath to a boot-ROM to use. If unsure, specify None.
    • +
    • sound (bool): Enable sound emulation and output.
    • +
    • sound_emulated (bool): Enable sound emulation without any output. Used for compatibility.
    • +
    • cgb (bool): Forcing Game Boy Color mode.
    • +
    • color_palette (tuple): Specify the color palette to use for rendering.
    • +
    • cgb_color_palette (list of tuple): Specify the color palette to use for rendering in CGB-mode for non-color games.
    • +
    +

    Other keyword arguments may exist for plugins that are not listed here. They can be viewed by running pyboy --help in the terminal.

    Expand source code @@ -99,14 +111,15 @@

    Kwargs

    class PyBoy:
         def __init__(
             self,
    -        gamerom_file,
    +        gamerom,
             *,
    -        bootrom_file=None,
    -        disable_renderer=False,
    +        window=defaults["window"],
    +        scale=defaults["scale"],
    +        symbols=None,
    +        bootrom=None,
             sound=False,
             sound_emulated=False,
             cgb=None,
    -        randomize=False,
             **kwargs
         ):
             """
    @@ -114,50 +127,100 @@ 

    Kwargs

    controlled and probed by the script. It is supported to spawn multiple emulators, just instantiate the class multiple times. - This object, `pyboy.WindowEvent`, and the `pyboy.botsupport` module, are the only official user-facing - interfaces. All other parts of the emulator, are subject to change. - A range of methods are exposed, which should allow for complete control of the emulator. Please open an issue on GitHub, if other methods are needed for your projects. Take a look at the files in `examples/` for a crude "bots", which interact with the game. - Only the `gamerom_file` argument is required. + Only the `gamerom` argument is required. + + Example: + ```python + >>> pyboy = PyBoy('game_rom.gb') + >>> for _ in range(60): # Use 'while True:' for infinite + ... pyboy.tick() + True... + >>> pyboy.stop() + + ``` Args: - gamerom_file (str): Filepath to a game-ROM for Game Boy or Game Boy Color. + gamerom (str): Filepath to a game-ROM for Game Boy or Game Boy Color. Kwargs: - bootrom_file (str): Filepath to a boot-ROM to use. If unsure, specify `None`. - disable_renderer (bool): Can be used to optimize performance, by internally disable rendering of the screen. - color_palette (tuple): Specify the color palette to use for rendering. - cgb_color_palette (list of tuple): Specify the color palette to use for rendering in CGB-mode for non-color games. - - Other keyword arguments may exist for plugins that are not listed here. They can be viewed with the - `parser_arguments()` method in the pyboy.plugins.manager module, or by running pyboy --help in the terminal. + * window (str): "SDL2", "OpenGL", or "null" + * scale (int): Window scale factor. Doesn't apply to API. + * symbols (str): Filepath to a .sym file to use. If unsure, specify `None`. + * bootrom (str): Filepath to a boot-ROM to use. If unsure, specify `None`. + * sound (bool): Enable sound emulation and output. + * sound_emulated (bool): Enable sound emulation without any output. Used for compatibility. + * cgb (bool): Forcing Game Boy Color mode. + * color_palette (tuple): Specify the color palette to use for rendering. + * cgb_color_palette (list of tuple): Specify the color palette to use for rendering in CGB-mode for non-color games. + + Other keyword arguments may exist for plugins that are not listed here. They can be viewed by running `pyboy --help` in the terminal. """ self.initialized = False + if "bootrom_file" in kwargs: + logger.error( + "Deprecated use of 'bootrom_file'. Use 'bootrom' keyword argument instead. https://github.com/Baekalfen/PyBoy/wiki/Migrating-from-v1.x.x-to-v2.0.0" + ) + bootrom = kwargs.pop("bootrom_file") + + if "window_type" in kwargs: + logger.error( + "Deprecated use of 'window_type'. Use 'window' keyword argument instead. https://github.com/Baekalfen/PyBoy/wiki/Migrating-from-v1.x.x-to-v2.0.0" + ) + window = kwargs.pop("window_type") + + if window not in ["SDL2", "OpenGL", "null", "headless", "dummy"]: + raise KeyError(f'Unknown window type: {window}. Use "SDL2", "OpenGL", or "null"') + + kwargs["window"] = window + kwargs["scale"] = scale + randomize = kwargs.pop("randomize", False) # Undocumented feature + for k, v in defaults.items(): if k not in kwargs: kwargs[k] = kwargs.get(k, defaults[k]) - if not os.path.isfile(gamerom_file): - raise FileNotFoundError(f"ROM file {gamerom_file} was not found!") - self.gamerom_file = gamerom_file + log_level(kwargs.pop("log_level")) + + if not os.path.isfile(gamerom): + raise FileNotFoundError(f"ROM file {gamerom} was not found!") + self.gamerom = gamerom + + self.rom_symbols = {} + if symbols is not None: + if not os.path.isfile(symbols): + raise FileNotFoundError(f"Symbols file {symbols} was not found!") + self.symbols_file = symbols + self._load_symbols() self.mb = Motherboard( - gamerom_file, - bootrom_file or kwargs.get("bootrom"), # Our current way to provide cli arguments is broken + gamerom, + bootrom, kwargs["color_palette"], kwargs["cgb_color_palette"], - disable_renderer, sound, sound_emulated, cgb, randomize=randomize, ) + # Validate all kwargs + plugin_manager_keywords = [] + for x in parser_arguments(): + if not x: + continue + plugin_manager_keywords.extend(z.strip("-").replace("-", "_") for y in x for z in y[:-1]) + + for k, v in kwargs.items(): + if k not in defaults and k not in plugin_manager_keywords: + logger.error("Unknown keyword argument: %s", k) + raise KeyError(f"Unknown keyword argument: {k}") + # Performance measures self.avg_pre = 0 self.avg_tick = 0 @@ -169,39 +232,207 @@

    Kwargs

    self.set_emulation_speed(1) self.paused = False self.events = [] - self.old_events = [] + self.queued_input = [] self.quitting = False self.stopped = False self.window_title = "PyBoy" ################### - # Plugins + # API attributes + self.screen = Screen(self.mb) + """ + Use this method to get a `pyboy.api.screen.Screen` object. This can be used to get the screen buffer in + a variety of formats. - self.plugin_manager = PluginManager(self, self.mb, kwargs) - self.initialized = True + It's also here you can find the screen position (SCX, SCY, WX, WY) for each scan line in the screen buffer. See + `pyboy.api.screen.Screen.tilemap_position_list` for more information. + + Example: + ```python + >>> pyboy.screen.image.show() + >>> pyboy.screen.ndarray.shape + (144, 160, 4) + >>> pyboy.screen.raw_buffer_format + 'RGBA' - def tick(self): + ``` + + Returns + ------- + `pyboy.api.screen.Screen`: + A Screen object with helper functions for reading the screen buffer. + """ + self.memory = PyBoyMemoryView(self.mb) """ - Progresses the emulator ahead by one frame. + Provides a `pyboy.PyBoyMemoryView` object for reading and writing the memory space of the Game Boy. - To run the emulator in real-time, this will need to be called 60 times a second (for example in a while-loop). - This function will block for roughly 16,67ms at a time, to not run faster than real-time, unless you specify - otherwise with the `PyBoy.set_emulation_speed` method. + For a more comprehensive description, see the `pyboy.PyBoyMemoryView` class. + + Example: + ```python + >>> pyboy.memory[0x0000:0x0010] # Read 16 bytes from ROM bank 0 + [49, 254, 255, 33, 0, 128, 175, 34, 124, 254, 160, 32, 249, 6, 48, 33] + >>> pyboy.memory[1, 0x2000] = 12 # Override address 0x2000 from ROM bank 1 with the value 12 + >>> pyboy.memory[0xC000] = 1 # Write to address 0xC000 with value 1 + ``` + + """ + + self.memory_scanner = MemoryScanner(self) + """ + Provides a `pyboy.api.memory_scanner.MemoryScanner` object for locating addresses of interest in the memory space + of the Game Boy. This might require some trial and error. Values can be represented in memory in surprising ways. _Open an issue on GitHub if you need finer control, and we will take a look at it._ + + Example: + ```python + >>> current_score = 4 # You write current score in game + >>> pyboy.memory_scanner.scan_memory(current_score, start_addr=0xC000, end_addr=0xDFFF) + [] + >>> for _ in range(175): + ... pyboy.tick(1, True) # Progress the game to change score + True... + >>> current_score = 8 # You write the new score in game + >>> from pyboy.api.memory_scanner import DynamicComparisonType + >>> addresses = pyboy.memory_scanner.rescan_memory(current_score, DynamicComparisonType.MATCH) + >>> print(addresses) # If repeated enough, only one address will remain + [] + + ``` + """ + + self.tilemap_background = TileMap(self, self.mb, "BACKGROUND") + """ + The Game Boy uses two tile maps at the same time to draw graphics on the screen. This method will provide one + for the _background_ tiles. The game chooses whether it wants to use the low or the high tilemap. + + Read more details about it, in the [Pan Docs](https://gbdev.io/pandocs/Tile_Maps.html). + + Example: + ``` + >>> pyboy.tilemap_background[8,8] + 1 + >>> pyboy.tilemap_background[7:12,8] + [0, 1, 0, 1, 0] + >>> pyboy.tilemap_background[7:12,8:11] + [[0, 1, 0, 1, 0], [0, 2, 3, 4, 5], [0, 0, 6, 0, 0]] + + ``` + + Returns + ------- + `pyboy.api.tilemap.TileMap`: + A TileMap object for the tile map. + """ + + self.tilemap_window = TileMap(self, self.mb, "WINDOW") + """ + The Game Boy uses two tile maps at the same time to draw graphics on the screen. This method will provide one + for the _window_ tiles. The game chooses whether it wants to use the low or the high tilemap. + + Read more details about it, in the [Pan Docs](https://gbdev.io/pandocs/Tile_Maps.html). + + Example: + ``` + >>> pyboy.tilemap_window[8,8] + 1 + >>> pyboy.tilemap_window[7:12,8] + [0, 1, 0, 1, 0] + >>> pyboy.tilemap_window[7:12,8:11] + [[0, 1, 0, 1, 0], [0, 2, 3, 4, 5], [0, 0, 6, 0, 0]] + + ``` + + Returns + ------- + `pyboy.api.tilemap.TileMap`: + A TileMap object for the tile map. + """ + + self.cartridge_title = self.mb.cartridge.gamename + """ + The title stored on the currently loaded cartridge ROM. The title is all upper-case ASCII and may + have been truncated to 11 characters. + + Example: + ```python + >>> pyboy.cartridge_title # Title of PyBoy's default ROM + 'DEFAULT-ROM' + + ``` + + Returns + ------- + str : + Game title + """ + + self._hooks = {} + + self._plugin_manager = PluginManager(self, self.mb, kwargs) + """ + Returns + ------- + `pyboy.plugins.manager.PluginManager`: + Object for handling plugins in PyBoy + """ + + self.game_wrapper = self._plugin_manager.gamewrapper() + """ + Provides an instance of a game-specific or generic wrapper. The game is detected by the cartridge's hard-coded + game title (see `pyboy.PyBoy.cartridge_title`). + + If a game-specific wrapper is not found, a generic wrapper will be returned. + + To get more information, find the wrapper for your game in `pyboy.plugins`. + + Example: + ```python + >>> pyboy.game_wrapper.start_game() + >>> pyboy.game_wrapper.reset_game() + + ``` + + Returns + ------- + `pyboy.plugins.base_plugin.PyBoyGameWrapper`: + A game-specific wrapper object. """ + + self.initialized = True + + def _tick(self, render): if self.stopped: - return True + return False t_start = time.perf_counter_ns() self._handle_events(self.events) t_pre = time.perf_counter_ns() if not self.paused: - if self.mb.tick(): - # breakpoint reached - self.plugin_manager.handle_breakpoint() - else: - self.frame_count += 1 + self.__rendering(render) + # Reenter mb.tick until we eventually get a clean exit without breakpoints + while self.mb.tick(): + # Breakpoint reached + # NOTE: Potentially reinject breakpoint that we have now stepped passed + self.mb.breakpoint_reinject() + + # NOTE: PC has not been incremented when hitting breakpoint! + breakpoint_index = self.mb.breakpoint_reached() + if breakpoint_index != -1: + self.mb.breakpoint_remove(breakpoint_index) + self.mb.breakpoint_singlestep_latch = 0 + + if not self._handle_hooks(): + self._plugin_manager.handle_breakpoint() + else: + if self.mb.breakpoint_singlestep_latch: + if not self._handle_hooks(): + self._plugin_manager.handle_breakpoint() + # Keep singlestepping on, if that's what we're doing + self.mb.breakpoint_singlestep = self.mb.breakpoint_singlestep_latch + + self.frame_count += 1 t_tick = time.perf_counter_ns() self._post_tick() t_post = time.perf_counter_ns() @@ -215,25 +446,77 @@

    Kwargs

    nsecs = t_post - t_tick self.avg_post = 0.9 * self.avg_post + (0.1*nsecs/1_000_000_000) - return self.quitting + return not self.quitting + + def tick(self, count=1, render=True): + """ + Progresses the emulator ahead by `count` frame(s). + + To run the emulator in real-time, it will need to process 60 frames a second (for example in a while-loop). + This function will block for roughly 16,67ms per frame, to not run faster than real-time, unless you specify + otherwise with the `PyBoy.set_emulation_speed` method. + + If you need finer control than 1 frame, have a look at `PyBoy.hook_register` to inject code at a specific point + in the game. + + Setting `render` to `True` will make PyBoy render the screen for *the last frame* of this tick. This can be seen + as a type of "frameskipping" optimization. + + For AI training, it's adviced to use as high a count as practical, as it will otherwise reduce performance + substantially. While setting `render` to `False`, you can still access the `PyBoy.game_area` to get a simpler + representation of the game. + + If `render` was enabled, use `pyboy.api.screen.Screen` to get a NumPy buffer or raw memory buffer. + + Example: + ```python + >>> pyboy.tick() # Progress 1 frame with rendering + True + >>> pyboy.tick(1) # Progress 1 frame with rendering + True + >>> pyboy.tick(60, False) # Progress 60 frames *without* rendering + True + >>> pyboy.tick(60, True) # Progress 60 frames and render *only the last frame* + True + >>> for _ in range(60): # Progress 60 frames and render every frame + ... if not pyboy.tick(1, True): + ... break + >>> + ``` + + Args: + count (int): Number of ticks to process + render (bool): Whether to render an image for this tick + Returns + ------- + (True or False): + False if emulation has ended otherwise True + """ + + running = False + while count != 0: + _render = render and count == 1 # Only render on last tick to improve performance + running = self._tick(_render) + count -= 1 + return running def _handle_events(self, events): # This feeds events into the tick-loop from the window. There might already be events in the list from the API. - events = self.plugin_manager.handle_events(events) + events = self._plugin_manager.handle_events(events) for event in events: if event == WindowEvent.QUIT: self.quitting = True elif event == WindowEvent.RELEASE_SPEED_UP: # Switch between unlimited and 1x real-time emulation speed self.target_emulationspeed = int(bool(self.target_emulationspeed) ^ True) - logger.debug("Speed limit: %s" % self.target_emulationspeed) + logger.debug("Speed limit: %d", self.target_emulationspeed) elif event == WindowEvent.STATE_SAVE: - with open(self.gamerom_file + ".state", "wb") as f: + with open(self.gamerom + ".state", "wb") as f: self.mb.save_state(IntIOWrapper(f)) elif event == WindowEvent.STATE_LOAD: - state_path = self.gamerom_file + ".state" + state_path = self.gamerom + ".state" if not os.path.isfile(state_path): - logger.error(f"State file not found: {state_path}") + logger.error("State file not found: %s", state_path) continue with open(state_path, "rb") as f: self.mb.load_state(IntIOWrapper(f)) @@ -249,7 +532,7 @@

    Kwargs

    elif event == WindowEvent.UNPAUSE: self._unpause() elif event == WindowEvent._INTERNAL_RENDERER_FLUSH: - self.plugin_manager._post_tick_windows() + self._plugin_manager._post_tick_windows() else: self.mb.buttonevent(event) @@ -271,23 +554,30 @@

    Kwargs

    self._update_window_title() def _post_tick(self): + # Fix buggy PIL. They will copy our image buffer and destroy the + # reference on some user operations like .save(). + if not self.screen.image.readonly: + self.screen._set_image() + if self.frame_count % 60 == 0: self._update_window_title() - self.plugin_manager.post_tick() - self.plugin_manager.frame_limiter(self.target_emulationspeed) + self._plugin_manager.post_tick() + self._plugin_manager.frame_limiter(self.target_emulationspeed) # Prepare an empty list, as the API might be used to send in events between ticks - self.old_events = self.events self.events = [] + while self.queued_input and self.frame_count == self.queued_input[0][0]: + _, _event = heapq.heappop(self.queued_input) + self.events.append(WindowEvent(_event)) def _update_window_title(self): avg_emu = self.avg_pre + self.avg_tick + self.avg_post - self.window_title = "CPU/frame: %0.2f%%" % ((self.avg_pre + self.avg_tick) / SPF * 100) - self.window_title += " Emulation: x%s" % (round(SPF / avg_emu) if avg_emu > 0 else "INF") + self.window_title = f"CPU/frame: {(self.avg_pre + self.avg_tick) / SPF * 100:0.2f}%" + self.window_title += f' Emulation: x{(round(SPF / avg_emu) if avg_emu > 0 else "INF")}' if self.paused: self.window_title += "[PAUSED]" - self.window_title += self.plugin_manager.window_title() - self.plugin_manager._set_title() + self.window_title += self._plugin_manager.window_title() + self._plugin_manager._set_title() def __del__(self): self.stop(save=False) @@ -302,6 +592,13 @@

    Kwargs

    """ Gently stops the emulator and all sub-modules. + Example: + ```python + >>> pyboy.stop() # Stop emulator and save game progress (cartridge RAM) + >>> pyboy.stop(False) # Stop emulator and discard game progress (cartridge RAM) + + ``` + Args: save (bool): Specify whether to save the game upon stopping. It will always be saved in a file next to the provided game-ROM. @@ -310,7 +607,7 @@

    Kwargs

    logger.info("###########################") logger.info("# Emulator is turning off #") logger.info("###########################") - self.plugin_manager.stop() + self._plugin_manager.stop() self.mb.stop(save) self.stopped = True @@ -318,147 +615,172 @@

    Kwargs

    # Scripts and bot methods # - def botsupport_manager(self): - """ - - Returns - ------- - `pyboy.botsupport.BotSupportManager`: - The manager, which gives easier access to the emulated game through the classes in `pyboy.botsupport`. - """ - return botsupport.BotSupportManager(self, self.mb) - - def openai_gym(self, observation_type="tiles", action_type="press", simultaneous_actions=False, **kwargs): + def button(self, input, delay=1): """ - For Reinforcement learning, it is often easier to use the standard gym environment. This method will provide one. - This function requires PyBoy to implement a Game Wrapper for the loaded ROM. You can find the supported games in pyboy.plugins. - Additional kwargs are passed to the start_game method of the game_wrapper. + Send input to PyBoy in the form of "a", "b", "start", "select", "left", "right", "up" and "down". + + The button will automatically be released at the following call to `PyBoy.tick`. + + Example: + ```python + >>> pyboy.button('a') # Press button 'a' and release after `pyboy.tick()` + >>> pyboy.tick() # Button 'a' pressed + True + >>> pyboy.tick() # Button 'a' released + True + >>> pyboy.button('a', 3) # Press button 'a' and release after 3 `pyboy.tick()` + >>> pyboy.tick() # Button 'a' pressed + True + >>> pyboy.tick() # Button 'a' still pressed + True + >>> pyboy.tick() # Button 'a' still pressed + True + >>> pyboy.tick() # Button 'a' released + True + ``` Args: - observation_type (str): Define what the agent will be able to see: - * `"raw"`: Gives the raw pixels color - * `"tiles"`: Gives the id of the sprites in 8x8 pixel zones of the game_area defined by the game_wrapper. - * `"compressed"`: Gives a more detailled but heavier representation than `"minimal"`. - * `"minimal"`: Gives a minimal representation defined by the game_wrapper (recommended). - - action_type (str): Define how the agent will interact with button inputs - * `"press"`: The agent will only press inputs for 1 frame an then release it. - * `"toggle"`: The agent will toggle inputs, first time it press and second time it release. - * `"all"`: The agent have access to all inputs, press and release are separated. - - simultaneous_actions (bool): Allow to inject multiple input at once. This dramatically increases the action_space: \\(n \\rightarrow 2^n\\) - - Returns - ------- - `pyboy.openai_gym.PyBoyGymEnv`: - A Gym environment based on the `Pyboy` object. + input (str): button to press + delay (int, optional): Number of frames to delay the release. Defaults to 1 """ - if gym_enabled: - return PyBoyGymEnv(self, observation_type, action_type, simultaneous_actions, **kwargs) + input = input.lower() + if input == "left": + self.send_input(WindowEvent.PRESS_ARROW_LEFT) + heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_ARROW_LEFT)) + elif input == "right": + self.send_input(WindowEvent.PRESS_ARROW_RIGHT) + heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_ARROW_RIGHT)) + elif input == "up": + self.send_input(WindowEvent.PRESS_ARROW_UP) + heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_ARROW_UP)) + elif input == "down": + self.send_input(WindowEvent.PRESS_ARROW_DOWN) + heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_ARROW_DOWN)) + elif input == "a": + self.send_input(WindowEvent.PRESS_BUTTON_A) + heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_BUTTON_A)) + elif input == "b": + self.send_input(WindowEvent.PRESS_BUTTON_B) + heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_BUTTON_B)) + elif input == "start": + self.send_input(WindowEvent.PRESS_BUTTON_START) + heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_BUTTON_START)) + elif input == "select": + self.send_input(WindowEvent.PRESS_BUTTON_SELECT) + heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_BUTTON_SELECT)) else: - logger.error(f"{__name__}: Missing dependency \"gym\". ") - return None - - def game_wrapper(self): - """ - Provides an instance of a game-specific wrapper. The game is detected by the cartridge's hard-coded game title - (see `pyboy.PyBoy.cartridge_title`). - - If the game isn't supported, None will be returned. - - To get more information, find the wrapper for your game in `pyboy.plugins`. - - Returns - ------- - `pyboy.plugins.base_plugin.PyBoyGameWrapper`: - A game-specific wrapper object. - """ - return self.plugin_manager.gamewrapper() - - def get_memory_value(self, addr): - """ - Reads a given memory address of the Game Boy's current memory state. This will not directly give you access to - all switchable memory banks. Open an issue on GitHub if that is needed, or use `PyBoy.set_memory_value` to send - MBC commands to the virtual cartridge. - - Returns - ------- - int: - An integer with the value of the memory address - """ - return self.mb.getitem(addr) + raise Exception("Unrecognized input:", input) - def set_memory_value(self, addr, value): + def button_press(self, input): """ - Write one byte to a given memory address of the Game Boy's current memory state. + Send input to PyBoy in the form of "a", "b", "start", "select", "left", "right", "up" and "down". - This will not directly give you access to all switchable memory banks. + The button will remain press until explicitly released with `PyBoy.button_release` or `PyBoy.send_input`. - __NOTE:__ This function will not let you change ROM addresses (0x0000 to 0x8000). If you write to these - addresses, it will send commands to the "Memory Bank Controller" (MBC) of the virtual cartridge. You can read - about the MBC at [Pan Docs](http://bgb.bircd.org/pandocs.htm). + Example: + ```python + >>> pyboy.button_press('a') # Press button 'a' and keep pressed after `PyBoy.tick()` + >>> pyboy.tick() # Button 'a' pressed + True + >>> pyboy.tick() # Button 'a' still pressed + True + >>> pyboy.button_release('a') # Release button 'a' on next call to `PyBoy.tick()` + >>> pyboy.tick() # Button 'a' released + True - If you need to change ROM values, see `pyboy.PyBoy.override_memory_value`. + ``` Args: - addr (int): Address to write the byte - value (int): A byte of data + input (str): button to press """ - self.mb.setitem(addr, value) + input = input.lower() + + if input == "left": + self.send_input(WindowEvent.PRESS_ARROW_LEFT) + elif input == "right": + self.send_input(WindowEvent.PRESS_ARROW_RIGHT) + elif input == "up": + self.send_input(WindowEvent.PRESS_ARROW_UP) + elif input == "down": + self.send_input(WindowEvent.PRESS_ARROW_DOWN) + elif input == "a": + self.send_input(WindowEvent.PRESS_BUTTON_A) + elif input == "b": + self.send_input(WindowEvent.PRESS_BUTTON_B) + elif input == "start": + self.send_input(WindowEvent.PRESS_BUTTON_START) + elif input == "select": + self.send_input(WindowEvent.PRESS_BUTTON_SELECT) + else: + raise Exception("Unrecognized input") - def override_memory_value(self, rom_bank, addr, value): + def button_release(self, input): """ - Override one byte at a given memory address of the Game Boy's ROM. + Send input to PyBoy in the form of "a", "b", "start", "select", "left", "right", "up" and "down". - This will let you override data in the ROM at any given bank. This is the memory allocated at 0x0000 to 0x8000, where 0x4000 to 0x8000 can be changed from the MBC. + This will release a button after a call to `PyBoy.button_press` or `PyBoy.send_input`. - __NOTE__: Any changes here are not saved or loaded to game states! Use this function with caution and reapply - any overrides when reloading the ROM. + Example: + ```python + >>> pyboy.button_press('a') # Press button 'a' and keep pressed after `PyBoy.tick()` + >>> pyboy.tick() # Button 'a' pressed + True + >>> pyboy.tick() # Button 'a' still pressed + True + >>> pyboy.button_release('a') # Release button 'a' on next call to `PyBoy.tick()` + >>> pyboy.tick() # Button 'a' released + True - If you need to change a RAM address, see `pyboy.PyBoy.set_memory_value`. + ``` Args: - rom_bank (int): ROM bank to do the overwrite in - addr (int): Address to write the byte inside the ROM bank - value (int): A byte of data + input (str): button to release """ - # TODO: If you change a RAM value outside of the ROM banks above, the memory value will stay the same no matter - # what the game writes to the address. This can be used so freeze the value for health, cash etc. - self.mb.cartridge.overrideitem(rom_bank, addr, value) + input = input.lower() + if input == "left": + self.send_input(WindowEvent.RELEASE_ARROW_LEFT) + elif input == "right": + self.send_input(WindowEvent.RELEASE_ARROW_RIGHT) + elif input == "up": + self.send_input(WindowEvent.RELEASE_ARROW_UP) + elif input == "down": + self.send_input(WindowEvent.RELEASE_ARROW_DOWN) + elif input == "a": + self.send_input(WindowEvent.RELEASE_BUTTON_A) + elif input == "b": + self.send_input(WindowEvent.RELEASE_BUTTON_B) + elif input == "start": + self.send_input(WindowEvent.RELEASE_BUTTON_START) + elif input == "select": + self.send_input(WindowEvent.RELEASE_BUTTON_SELECT) + else: + raise Exception("Unrecognized input") def send_input(self, event): """ - Send a single input to control the emulator. This is both Game Boy buttons and emulator controls. - - See `pyboy.WindowEvent` for which events to send. + Send a single input to control the emulator. This is both Game Boy buttons and emulator controls. See + `pyboy.utils.WindowEvent` for which events to send. - Args: - event (pyboy.WindowEvent): The event to send - """ - self.events.append(WindowEvent(event)) + Consider using `PyBoy.button` instead for easier access. - def get_input( - self, - ignore=( - WindowEvent.PASS, WindowEvent._INTERNAL_TOGGLE_DEBUG, WindowEvent._INTERNAL_RENDERER_FLUSH, - WindowEvent._INTERNAL_MOUSE, WindowEvent._INTERNAL_MARK_TILE - ) - ): - """ - Get current inputs except the events specified in "ignore" tuple. - This is both Game Boy buttons and emulator controls. + Example: + ```python + >>> from pyboy.utils import WindowEvent + >>> pyboy.send_input(WindowEvent.PRESS_BUTTON_A) # Press button 'a' and keep pressed after `PyBoy.tick()` + >>> pyboy.tick() # Button 'a' pressed + True + >>> pyboy.tick() # Button 'a' still pressed + True + >>> pyboy.send_input(WindowEvent.RELEASE_BUTTON_A) # Release button 'a' on next call to `PyBoy.tick()` + >>> pyboy.tick() # Button 'a' released + True - See `pyboy.WindowEvent` for which events to get. + ``` Args: - ignore (tuple): Events this function should ignore - - Returns - ------- - list: - List of the `pyboy.utils.WindowEvent`s processed for the last call to `pyboy.PyBoy.tick` + event (pyboy.WindowEvent): The event to send """ - return [x for x in self.old_events if x not in ignore] + self.events.append(WindowEvent(event)) def save_state(self, file_like_object): """ @@ -468,13 +790,19 @@

    Kwargs

    You can either save it to a file, or in-memory. The following two examples will provide the file handle in each case. Remember to `seek` the in-memory buffer to the beginning before calling `PyBoy.load_state`: - # Save to file - file_like_object = open("state_file.state", "wb") + ```python + >>> # Save to file + >>> with open("state_file.state", "wb") as f: + ... pyboy.save_state(f) + >>> + >>> # Save to memory + >>> import io + >>> with io.BytesIO() as f: + ... f.seek(0) + ... pyboy.save_state(f) + 0 - # Save to memory - import io - file_like_object = io.BytesIO() - file_like_object.seek(0) + ``` Args: file_like_object (io.BufferedIOBase): A file-like object for which to write the emulator state. @@ -483,6 +811,9 @@

    Kwargs

    if isinstance(file_like_object, str): raise Exception("String not allowed. Did you specify a filepath instead of a file-like object?") + if file_like_object.__class__.__name__ == "TextIOWrapper": + raise Exception("Text file not allowed. Did you specify open(..., 'wb')?") + self.mb.save_state(IntIOWrapper(file_like_object)) def load_state(self, file_like_object): @@ -494,10 +825,12 @@

    Kwargs

    can load it here. To load a file, remember to load it as bytes: - - # Load file - file_like_object = open("state_file.state", "rb") - + ```python + >>> # Load file + >>> with open("state_file.state", "rb") as f: + ... pyboy.load_state(f) + >>> + ``` Args: file_like_object (io.BufferedIOBase): A file-like object for which to read the emulator state. @@ -506,24 +839,142 @@

    Kwargs

    if isinstance(file_like_object, str): raise Exception("String not allowed. Did you specify a filepath instead of a file-like object?") + if file_like_object.__class__.__name__ == "TextIOWrapper": + raise Exception("Text file not allowed. Did you specify open(..., 'rb')?") + self.mb.load_state(IntIOWrapper(file_like_object)) - def screen_image(self): + def game_area_dimensions(self, x, y, width, height, follow_scrolling=True): + """ + If using the generic game wrapper (see `pyboy.PyBoy.game_wrapper`), you can use this to set the section of the + tilemaps to extract. This will default to the entire tilemap. + + Example: + ```python + >>> pyboy.game_wrapper.shape + (32, 32) + >>> pyboy.game_area_dimensions(2, 2, 10, 18, False) + >>> pyboy.game_wrapper.shape + (10, 18) + ``` + + Args: + x (int): Offset from top-left corner of the screen + y (int): Offset from top-left corner of the screen + width (int): Width of game area + height (int): Height of game area + follow_scrolling (bool): Whether to follow the scrolling of [SCX and SCY](https://gbdev.io/pandocs/Scrolling.html) + """ + self.game_wrapper._set_dimensions(x, y, width, height, follow_scrolling=True) + + def game_area_collision(self): + """ + Some game wrappers define a collision map. Check if your game wrapper has this feature implemented: `pyboy.plugins`. + + The output will be unique for each game wrapper. + + Example: + ```python + >>> # This example show nothing, but a supported game will + >>> pyboy.game_area_collision() + array([[0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint32) + + ``` + + Returns + ------- + memoryview: + Simplified 2-dimensional memoryview of the collision map + """ + return self.game_wrapper.game_area_collision() + + def game_area_mapping(self, mapping, sprite_offset=0): + """ + Define custom mappings for tile identifiers in the game area. + + Example of custom mapping: + ```python + >>> mapping = [x for x in range(384)] # 1:1 mapping + >>> mapping[0] = 0 # Map tile identifier 0 -> 0 + >>> mapping[1] = 0 # Map tile identifier 1 -> 0 + >>> mapping[2] = 0 # Map tile identifier 2 -> 0 + >>> mapping[3] = 0 # Map tile identifier 3 -> 0 + >>> pyboy.game_area_mapping(mapping, 1000) + + ``` + + Some game wrappers will supply mappings as well. See the specific documentation for your game wrapper: + `pyboy.plugins`. + ```python + >>> pyboy.game_area_mapping(pyboy.game_wrapper.mapping_one_to_one, 0) + + ``` + + Args: + mapping (list or ndarray): list of 384 (DMG) or 768 (CGB) tile mappings. Use `None` to reset to a 1:1 mapping. + sprite_offest (int): Optional offset add to tile id for sprites """ - Shortcut for `pyboy.botsupport_manager.screen.screen_image`. - Generates a PIL Image from the screen buffer. + if mapping is None: + mapping = [x for x in range(768)] + + assert isinstance(sprite_offset, int) + assert isinstance(mapping, (np.ndarray, list)) + assert len(mapping) == 384 or len(mapping) == 768 + + self.game_wrapper.game_area_mapping(mapping, sprite_offset) - Convenient for screen captures, but might be a bottleneck, if you use it to train a neural network. In which - case, read up on the `pyboy.botsupport` features, [Pan Docs](http://bgb.bircd.org/pandocs.htm) on tiles/sprites, - and join our Discord channel for more help. + def game_area(self): + """ + Use this method to get a matrix of the "game area" of the screen. This view is simplified to be perfect for + machine learning applications. + + The layout will vary from game to game. Below is an example from Tetris: + + Example: + ```python + >>> pyboy.game_area() + array([[ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [ 47, 47, 47, 130, 130, 47, 47, 47, 47, 47], + [ 47, 47, 47, 47, 130, 130, 47, 47, 47, 47], + [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47], + [ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47]], dtype=uint32) + + ``` + + If you want a "compressed", "minimal" or raw mapping of tiles, you can change the mapping using + `pyboy.PyBoy.game_area_mapping`. Either you'll have to supply your own mapping, or you can find one + that is built-in with the game wrapper plugin for your game. See `pyboy.PyBoy.game_area_mapping`. Returns ------- - PIL.Image: - RGB image of (160, 144) pixels + memoryview: + Simplified 2-dimensional memoryview of the screen """ - return self.botsupport_manager().screen().screen_image() + + return self.game_wrapper.game_area() def _serial(self): """ @@ -547,366 +998,803 @@

    Kwargs

    Some window types do not implement a frame-limiter, and will always run at full speed. + Example: + ```python + >>> pyboy.tick() # Delays 16.67ms + True + >>> pyboy.set_emulation_speed(0) # Disable limit + >>> pyboy.tick() # As fast as possible + True + ``` + Args: target_speed (int): Target emulation speed as multiplier of real-time. """ + if self.initialized and self._plugin_manager.window_null_enabled: + logger.warning( + 'This window type does not support frame-limiting. `pyboy.set_emulation_speed(...)` will have no effect, as it\'s always running at full speed.' + ) + if target_speed > 5: logger.warning("The emulation speed might not be accurate when speed-target is higher than 5") self.target_emulationspeed = target_speed - def cartridge_title(self): + def __rendering(self, value): """ - Get the title stored on the currently loaded cartridge ROM. The title is all upper-case ASCII and may - have been truncated to 11 characters. + Disable or enable rendering + """ + self.mb.lcd.disable_renderer = not value + def _is_cpu_stuck(self): + return self.mb.cpu.is_stuck + + def _load_symbols(self): + gamerom_file_no_ext, rom_ext = os.path.splitext(self.gamerom) + for sym_path in [self.symbols_file, gamerom_file_no_ext + ".sym", gamerom_file_no_ext + rom_ext + ".sym"]: + if sym_path and os.path.isfile(sym_path): + logger.info("Loading symbol file: %s", sym_path) + with open(sym_path) as f: + for _line in f.readlines(): + line = _line.strip() + if line == "": + continue + elif line.startswith(";"): + continue + elif line.startswith("["): + # Start of key group + # [labels] + # [definitions] + continue + + try: + bank, addr, sym_label = re.split(":| ", line.strip()) + bank = int(bank, 16) + addr = int(addr, 16) + if not bank in self.rom_symbols: + self.rom_symbols[bank] = {} + + if not addr in self.rom_symbols[bank]: + self.rom_symbols[bank][addr] = [] + + self.rom_symbols[bank][addr].append(sym_label) + except ValueError as ex: + logger.warning("Skipping .sym line: %s", line.strip()) + return self.rom_symbols + + def _lookup_symbol(self, symbol): + for bank, addresses in self.rom_symbols.items(): + for addr, labels in addresses.items(): + if symbol in labels: + return bank, addr + raise ValueError("Symbol not found: %s" % symbol) + + def symbol_lookup(self, symbol): + """ + Look up a specific symbol from provided symbols file. + + This can be useful in combination with `PyBoy.memory` or even `PyBoy.hook_register`. + + See `PyBoy.hook_register` for how to load symbol into PyBoy. + + Example: + ```python + >>> # Directly + >>> pyboy.memory[pyboy.symbol_lookup("Tileset")] + 0 + >>> # By bank and address + >>> bank, addr = pyboy.symbol_lookup("Tileset") + >>> pyboy.memory[bank, addr] + 0 + >>> pyboy.memory[bank, addr:addr+10] + [0, 0, 0, 0, 0, 0, 102, 102, 102, 102] + + ``` Returns ------- - str : - Game title + (int, int): + ROM/RAM bank, address """ - return self.mb.cartridge.gamename + return self._lookup_symbol(symbol) - def _rendering(self, value): + def hook_register(self, bank, addr, callback, context): """ - Disable or enable rendering - """ - self.mb.lcd.disable_renderer = not value + Adds a hook into a specific bank and memory address. + When the Game Boy executes this address, the provided callback function will be called. - def _is_cpu_stuck(self): - return self.mb.cpu.is_stuck
    -
    -

    Methods

    -
    -
    -def tick(self) -
    -
    -

    Progresses the emulator ahead by one frame.

    -

    To run the emulator in real-time, this will need to be called 60 times a second (for example in a while-loop). -This function will block for roughly 16,67ms at a time, to not run faster than real-time, unless you specify -otherwise with the PyBoy.set_emulation_speed() method.

    -

    Open an issue on GitHub if you need finer control, and we will take a look at it.

    -
    - -Expand source code - -
    def tick(self):
    -    """
    -    Progresses the emulator ahead by one frame.
    +        By providing an object as `context`, you can later get access to information inside and outside of the callback.
     
    -    To run the emulator in real-time, this will need to be called 60 times a second (for example in a while-loop).
    -    This function will block for roughly 16,67ms at a time, to not run faster than real-time, unless you specify
    -    otherwise with the `PyBoy.set_emulation_speed` method.
    +        Example:
    +        ```python
    +        >>> context = "Hello from hook"
    +        >>> def my_callback(context):
    +        ...     print(context)
    +        >>> pyboy.hook_register(0, 0x100, my_callback, context)
    +        >>> pyboy.tick(70)
    +        Hello from hook
    +        True
     
    -    _Open an issue on GitHub if you need finer control, and we will take a look at it._
    -    """
    -    if self.stopped:
    -        return True
    -
    -    t_start = time.perf_counter_ns()
    -    self._handle_events(self.events)
    -    t_pre = time.perf_counter_ns()
    -    if not self.paused:
    -        if self.mb.tick():
    -            # breakpoint reached
    -            self.plugin_manager.handle_breakpoint()
    -        else:
    -            self.frame_count += 1
    -    t_tick = time.perf_counter_ns()
    -    self._post_tick()
    -    t_post = time.perf_counter_ns()
    +        ```
     
    -    nsecs = t_pre - t_start
    -    self.avg_pre = 0.9 * self.avg_pre + (0.1*nsecs/1_000_000_000)
    +        If a symbol file is loaded, this function can also automatically resolve a bank and address from a symbol. To
    +        enable this, you'll need to place a `.sym` file next to your ROM, or provide it using:
    +        `PyBoy(..., symbols_file="game_rom.gb.sym")`.
     
    -    nsecs = t_tick - t_pre
    -    self.avg_tick = 0.9 * self.avg_tick + (0.1*nsecs/1_000_000_000)
    +        Then provide `None` for `bank` and the symbol for `addr` to trigger the automatic lookup.
     
    -    nsecs = t_post - t_tick
    -    self.avg_post = 0.9 * self.avg_post + (0.1*nsecs/1_000_000_000)
    +        Example:
    +        ```python
    +        >>> # Continued example above
    +        >>> pyboy.hook_register(None, "Main.move", lambda x: print(x), "Hello from hook2")
    +        >>> pyboy.tick(80)
    +        Hello from hook2
    +        True
     
    -    return self.quitting
    -
    -
    -
    -def stop(self, save=True) -
    -
    -

    Gently stops the emulator and all sub-modules.

    -

    Args

    -
    -
    save : bool
    -
    Specify whether to save the game upon stopping. It will always be saved in a file next to the -provided game-ROM.
    -
    -
    - -Expand source code - -
    def stop(self, save=True):
    -    """
    -    Gently stops the emulator and all sub-modules.
    +        ```
     
    -    Args:
    -        save (bool): Specify whether to save the game upon stopping. It will always be saved in a file next to the
    -            provided game-ROM.
    -    """
    -    if self.initialized and not self.stopped:
    -        logger.info("###########################")
    -        logger.info("# Emulator is turning off #")
    -        logger.info("###########################")
    -        self.plugin_manager.stop()
    -        self.mb.stop(save)
    -        self.stopped = True
    -
    -
    -
    -def botsupport_manager(self) -
    -
    -

    Returns

    -

    BotSupportManager: -The manager, which gives easier access to the emulated game through the classes in pyboy.botsupport.

    -
    - -Expand source code - -
    def botsupport_manager(self):
    -    """
    +        Args:
    +            bank (int or None): ROM or RAM bank (None for symbol lookup)
    +            addr (int or str): Address in the Game Boy's address space (str for symbol lookup)
    +            callback (func): A function which takes `context` as argument
    +            context (object): Argument to pass to callback when hook is called
    +        """
    +        if bank is None and isinstance(addr, str):
    +            bank, addr = self._lookup_symbol(addr)
     
    -    Returns
    -    -------
    -    `pyboy.botsupport.BotSupportManager`:
    -        The manager, which gives easier access to the emulated game through the classes in `pyboy.botsupport`.
    -    """
    -    return botsupport.BotSupportManager(self, self.mb)
    + opcode = self.memory[bank, addr] + if opcode == 0xDB: + raise ValueError("Hook already registered for this bank and address.") + self.mb.breakpoint_add(bank, addr) + bank_addr_opcode = (bank & 0xFF) << 24 | (addr & 0xFFFF) << 8 | (opcode & 0xFF) + self._hooks[bank_addr_opcode] = (callback, context) + + def hook_deregister(self, bank, addr): + """ + Remove a previously registered hook from a specific bank and memory address. + + Example: + ```python + >>> context = "Hello from hook" + >>> def my_callback(context): + ... print(context) + >>> pyboy.hook_register(0, 0x2000, my_callback, context) + >>> pyboy.hook_deregister(0, 0x2000) + + ``` + + This function can also deregister a hook based on a symbol. See `PyBoy.hook_register` for details. + + Example: + ```python + >>> pyboy.hook_register(None, "Main", lambda x: print(x), "Hello from hook") + >>> pyboy.hook_deregister(None, "Main") + + ``` + + Args: + bank (int or None): ROM or RAM bank (None for symbol lookup) + addr (int or str): Address in the Game Boy's address space (str for symbol lookup) + """ + if bank is None and isinstance(addr, str): + bank, addr = self._lookup_symbol(addr) + + index = self.mb.breakpoint_find(bank, addr) + if index == -1: + raise ValueError("Breakpoint not found for bank and addr") + + _, _, opcode = self.mb.breakpoints_list[index] + self.mb.breakpoint_remove(index) + bank_addr_opcode = (bank & 0xFF) << 24 | (addr & 0xFFFF) << 8 | (opcode & 0xFF) + self._hooks.pop(bank_addr_opcode) + + def _handle_hooks(self): + if _handler := self._hooks.get(self.mb.breakpoint_waiting): + (callback, context) = _handler + callback(context) + return True + return False + + def get_sprite(self, sprite_index): + """ + Provides a `pyboy.api.sprite.Sprite` object, which makes the OAM data more presentable. The given index + corresponds to index of the sprite in the "Object Attribute Memory" (OAM). + + The Game Boy supports 40 sprites in total. Read more details about it, in the [Pan + Docs](http://bgb.bircd.org/pandocs.htm). + + ```python + >>> s = pyboy.get_sprite(12) + >>> s + Sprite [12]: Position: (-8, -16), Shape: (8, 8), Tiles: (Tile: 0), On screen: False + >>> s.on_screen + False + >>> s.tiles + [Tile: 0] + + ``` + + Args: + index (int): Sprite index from 0 to 39. + Returns + ------- + `pyboy.api.sprite.Sprite`: + Sprite corresponding to the given index. + """ + return Sprite(self.mb, sprite_index) + + def get_sprite_by_tile_identifier(self, tile_identifiers, on_screen=True): + """ + Provided a list of tile identifiers, this function will find all occurrences of sprites using the tile + identifiers and return the sprite indexes where each identifier is found. Use the sprite indexes in the + `pyboy.PyBoy.get_sprite` function to get a `pyboy.api.sprite.Sprite` object. + + Example: + ```python + >>> print(pyboy.get_sprite_by_tile_identifier([43, 123])) + [[0, 2, 4], []] + + ``` + + Meaning, that tile identifier `43` is found at the sprite indexes: 0, 2, and 4, while tile identifier + `123` was not found anywhere. + + Args: + identifiers (list): List of tile identifiers (int) + on_screen (bool): Require that the matched sprite is on screen + + Returns + ------- + list: + list of sprite matches for every tile identifier in the input + """ + + matches = [] + for i in tile_identifiers: + match = [] + for s in range(constants.SPRITES): + sprite = Sprite(self.mb, s) + for t in sprite.tiles: + if t.tile_identifier == i and (not on_screen or (on_screen and sprite.on_screen)): + match.append(s) + matches.append(match) + return matches + + def get_tile(self, identifier): + """ + The Game Boy can have 384 tiles loaded in memory at once (768 for Game Boy Color). Use this method to get a + `pyboy.api.tile.Tile`-object for given identifier. + + The identifier is a PyBoy construct, which unifies two different scopes of indexes in the Game Boy hardware. See + the `pyboy.api.tile.Tile` object for more information. + + Example: + ```python + >>> t = pyboy.get_tile(2) + >>> t + Tile: 2 + >>> t.shape + (8, 8) + + ``` + + Returns + ------- + `pyboy.api.tile.Tile`: + A Tile object for the given identifier. + """ + return Tile(self.mb, identifier=identifier)
    +

    Instance variables

    +
    +
    var screen
    +
    +

    Use this method to get a Screen object. This can be used to get the screen buffer in +a variety of formats.

    +

    It's also here you can find the screen position (SCX, SCY, WX, WY) for each scan line in the screen buffer. See +Screen.tilemap_position_list for more information.

    +

    Example:

    +
    >>> pyboy.screen.image.show()
    +>>> pyboy.screen.ndarray.shape
    +(144, 160, 4)
    +>>> pyboy.screen.raw_buffer_format
    +'RGBA'
    +
    +
    +

    Returns

    +

    Screen: +A Screen object with helper functions for reading the screen buffer.

    -
    -def openai_gym(self, observation_type='tiles', action_type='press', simultaneous_actions=False, **kwargs) -
    +
    var memory
    -

    For Reinforcement learning, it is often easier to use the standard gym environment. This method will provide one. -This function requires PyBoy to implement a Game Wrapper for the loaded ROM. You can find the supported games in pyboy.plugins. -Additional kwargs are passed to the start_game method of the game_wrapper.

    -

    Args

    +

    Provides a PyBoyMemoryView object for reading and writing the memory space of the Game Boy.

    +

    For a more comprehensive description, see the PyBoyMemoryView class.

    +

    Example:

    +
    >>> pyboy.memory[0x0000:0x0010] # Read 16 bytes from ROM bank 0
    +[49, 254, 255, 33, 0, 128, 175, 34, 124, 254, 160, 32, 249, 6, 48, 33]
    +>>> pyboy.memory[1, 0x2000] = 12 # Override address 0x2000 from ROM bank 1 with the value 12
    +>>> pyboy.memory[0xC000] = 1 # Write to address 0xC000 with value 1
    +
    +
    +
    var memory_scanner
    +
    +

    Provides a MemoryScanner object for locating addresses of interest in the memory space +of the Game Boy. This might require some trial and error. Values can be represented in memory in surprising ways.

    +

    Open an issue on GitHub if you need finer control, and we will take a look at it.

    +

    Example:

    +
    >>> current_score = 4 # You write current score in game
    +>>> pyboy.memory_scanner.scan_memory(current_score, start_addr=0xC000, end_addr=0xDFFF)
    +[]
    +>>> for _ in range(175):
    +...     pyboy.tick(1, True) # Progress the game to change score
    +True...
    +>>> current_score = 8 # You write the new score in game
    +>>> from pyboy.api.memory_scanner import DynamicComparisonType
    +>>> addresses = pyboy.memory_scanner.rescan_memory(current_score, DynamicComparisonType.MATCH)
    +>>> print(addresses) # If repeated enough, only one address will remain
    +[]
    +
    +
    +
    +
    var tilemap_background
    +
    +

    The Game Boy uses two tile maps at the same time to draw graphics on the screen. This method will provide one +for the background tiles. The game chooses whether it wants to use the low or the high tilemap.

    +

    Read more details about it, in the Pan Docs.

    +

    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

    +

    TileMap: +A TileMap object for the tile map.

    +
    +
    var tilemap_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.

    +

    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

    +

    TileMap: +A TileMap object for the tile map.

    +
    +
    var cartridge_title
    +
    +

    The title stored on the currently loaded cartridge ROM. The title is all upper-case ASCII and may +have been truncated to 11 characters.

    +

    Example:

    +
    >>> pyboy.cartridge_title # Title of PyBoy's default ROM
    +'DEFAULT-ROM'
    +
    +
    +

    Returns

    -
    observation_type : str
    -
    Define what the agent will be able to see:
    +
    str :
    +
    Game title
    +
    +
    +
    var game_wrapper
    +
    +

    Provides an instance of a game-specific or generic wrapper. The game is detected by the cartridge's hard-coded +game title (see PyBoy.cartridge_title).

    +

    If a game-specific wrapper is not found, a generic wrapper will be returned.

    +

    To get more information, find the wrapper for your game in pyboy.plugins.

    +

    Example:

    +
    >>> pyboy.game_wrapper.start_game()
    +>>> pyboy.game_wrapper.reset_game()
    +
    +
    +

    Returns

    +

    PyBoyGameWrapper: +A game-specific wrapper object.

    +
    -
      -
    • "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).
    • -
    +

    Methods

    -
    action_type : str
    -
    Define how the agent will interact with button inputs
    -
    -
      -
    • "press": The agent will only press inputs for 1 frame an then release it.
    • -
    • "toggle": The agent will toggle inputs, first time it press and second time it release.
    • -
    • "all": The agent have access to all inputs, press and release are separated.
    • -
    +
    +def tick(self, count=1, render=True) +
    +
    +

    Progresses the emulator ahead by count frame(s).

    +

    To run the emulator in real-time, it will need to process 60 frames a second (for example in a while-loop). +This function will block for roughly 16,67ms per frame, to not run faster than real-time, unless you specify +otherwise with the PyBoy.set_emulation_speed() method.

    +

    If you need finer control than 1 frame, have a look at PyBoy.hook_register() to inject code at a specific point +in the game.

    +

    Setting render to True will make PyBoy render the screen for the last frame of this tick. This can be seen +as a type of "frameskipping" optimization.

    +

    For AI training, it's adviced to use as high a count as practical, as it will otherwise reduce performance +substantially. While setting render to False, you can still access the PyBoy.game_area() to get a simpler +representation of the game.

    +

    If render was enabled, use Screen to get a NumPy buffer or raw memory buffer.

    +

    Example:

    +
    >>> pyboy.tick() # Progress 1 frame with rendering
    +True
    +>>> pyboy.tick(1) # Progress 1 frame with rendering
    +True
    +>>> pyboy.tick(60, False) # Progress 60 frames *without* rendering
    +True
    +>>> pyboy.tick(60, True) # Progress 60 frames and render *only the last frame*
    +True
    +>>> for _ in range(60): # Progress 60 frames and render every frame
    +...     if not pyboy.tick(1, True):
    +...         break
    +>>>
    +
    +

    Args

    -
    simultaneous_actions : bool
    -
    Allow to inject multiple input at once. This dramatically increases the action_space: n \rightarrow 2^n
    +
    count : int
    +
    Number of ticks to process
    +
    render : bool
    +
    Whether to render an image for this tick

    Returns

    -

    PyBoyGymEnv: -A Gym environment based on the Pyboy object.

    +

    (True or False): +False if emulation has ended otherwise True

    Expand source code -
    def openai_gym(self, observation_type="tiles", action_type="press", simultaneous_actions=False, **kwargs):
    +
    def tick(self, count=1, render=True):
         """
    -    For Reinforcement learning, it is often easier to use the standard gym environment. This method will provide one.
    -    This function requires PyBoy to implement a Game Wrapper for the loaded ROM. You can find the supported games in pyboy.plugins.
    -    Additional kwargs are passed to the start_game method of the game_wrapper.
    -
    -    Args:
    -        observation_type (str): Define what the agent will be able to see:
    -        * `"raw"`: Gives the raw pixels color
    -        * `"tiles"`:  Gives the id of the sprites in 8x8 pixel zones of the game_area defined by the game_wrapper.
    -        * `"compressed"`: Gives a more detailled but heavier representation than `"minimal"`.
    -        * `"minimal"`: Gives a minimal representation defined by the game_wrapper (recommended).
    +    Progresses the emulator ahead by `count` frame(s).
     
    -        action_type (str): Define how the agent will interact with button inputs
    -        * `"press"`: The agent will only press inputs for 1 frame an then release it.
    -        * `"toggle"`: The agent will toggle inputs, first time it press and second time it release.
    -        * `"all"`: The agent have access to all inputs, press and release are separated.
    +    To run the emulator in real-time, it will need to process 60 frames a second (for example in a while-loop).
    +    This function will block for roughly 16,67ms per frame, to not run faster than real-time, unless you specify
    +    otherwise with the `PyBoy.set_emulation_speed` method.
     
    -        simultaneous_actions (bool): Allow to inject multiple input at once. This dramatically increases the action_space: \\(n \\rightarrow 2^n\\)
    +    If you need finer control than 1 frame, have a look at `PyBoy.hook_register` to inject code at a specific point
    +    in the game.
    +
    +    Setting `render` to `True` will make PyBoy render the screen for *the last frame* of this tick. This can be seen
    +    as a type of "frameskipping" optimization.
    +
    +    For AI training, it's adviced to use as high a count as practical, as it will otherwise reduce performance
    +    substantially. While setting `render` to `False`, you can still access the `PyBoy.game_area` to get a simpler
    +    representation of the game.
    +
    +    If `render` was enabled, use `pyboy.api.screen.Screen` to get a NumPy buffer or raw memory buffer.
    +
    +    Example:
    +    ```python
    +    >>> pyboy.tick() # Progress 1 frame with rendering
    +    True
    +    >>> pyboy.tick(1) # Progress 1 frame with rendering
    +    True
    +    >>> pyboy.tick(60, False) # Progress 60 frames *without* rendering
    +    True
    +    >>> pyboy.tick(60, True) # Progress 60 frames and render *only the last frame*
    +    True
    +    >>> for _ in range(60): # Progress 60 frames and render every frame
    +    ...     if not pyboy.tick(1, True):
    +    ...         break
    +    >>>
    +    ```
     
    +    Args:
    +        count (int): Number of ticks to process
    +        render (bool): Whether to render an image for this tick
         Returns
         -------
    -    `pyboy.openai_gym.PyBoyGymEnv`:
    -        A Gym environment based on the `Pyboy` object.
    +    (True or False):
    +        False if emulation has ended otherwise True
         """
    -    if gym_enabled:
    -        return PyBoyGymEnv(self, observation_type, action_type, simultaneous_actions, **kwargs)
    -    else:
    -        logger.error(f"{__name__}: Missing dependency \"gym\". ")
    -        return None
    + + running = False + while count != 0: + _render = render and count == 1 # Only render on last tick to improve performance + running = self._tick(_render) + count -= 1 + return running
    -
    -def game_wrapper(self) +
    +def stop(self, save=True)
    -

    Provides an instance of a game-specific wrapper. The game is detected by the cartridge's hard-coded game title -(see PyBoy.cartridge_title()).

    -

    If the game isn't supported, None will be returned.

    -

    To get more information, find the wrapper for your game in pyboy.plugins.

    -

    Returns

    -

    PyBoyGameWrapper: -A game-specific wrapper object.

    +

    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

    +
    +
    save : bool
    +
    Specify whether to save the game upon stopping. It will always be saved in a file next to the +provided game-ROM.
    +
    Expand source code -
    def game_wrapper(self):
    +
    def stop(self, save=True):
         """
    -    Provides an instance of a game-specific wrapper. The game is detected by the cartridge's hard-coded game title
    -    (see `pyboy.PyBoy.cartridge_title`).
    +    Gently stops the emulator and all sub-modules.
     
    -    If the game isn't supported, None will be returned.
    +    Example:
    +    ```python
    +    >>> pyboy.stop() # Stop emulator and save game progress (cartridge RAM)
    +    >>> pyboy.stop(False) # Stop emulator and discard game progress (cartridge RAM)
     
    -    To get more information, find the wrapper for your game in `pyboy.plugins`.
    +    ```
     
    -    Returns
    -    -------
    -    `pyboy.plugins.base_plugin.PyBoyGameWrapper`:
    -        A game-specific wrapper object.
    +    Args:
    +        save (bool): Specify whether to save the game upon stopping. It will always be saved in a file next to the
    +            provided game-ROM.
         """
    -    return self.plugin_manager.gamewrapper()
    + if self.initialized and not self.stopped: + logger.info("###########################") + logger.info("# Emulator is turning off #") + logger.info("###########################") + self._plugin_manager.stop() + self.mb.stop(save) + self.stopped = True
    -
    -def get_memory_value(self, addr) +
    +def button(self, input, delay=1)
    -

    Reads a given memory address of the Game Boy's current memory state. This will not directly give you access to -all switchable memory banks. Open an issue on GitHub if that is needed, or use PyBoy.set_memory_value() to send -MBC commands to the virtual cartridge.

    -

    Returns

    +

    Send input to PyBoy in the form of "a", "b", "start", "select", "left", "right", "up" and "down".

    +

    The button will automatically be released at the following call to PyBoy.tick().

    +

    Example:

    +
    >>> pyboy.button('a') # Press button 'a' and release after `pyboy.tick()`
    +>>> pyboy.tick() # Button 'a' pressed
    +True
    +>>> pyboy.tick() # Button 'a' released
    +True
    +>>> pyboy.button('a', 3) # Press button 'a' and release after 3 `pyboy.tick()`
    +>>> pyboy.tick() # Button 'a' pressed
    +True
    +>>> pyboy.tick() # Button 'a' still pressed
    +True
    +>>> pyboy.tick() # Button 'a' still pressed
    +True
    +>>> pyboy.tick() # Button 'a' released
    +True
    +
    +

    Args

    -
    int:
    -
    An integer with the value of the memory address
    +
    input : str
    +
    button to press
    +
    delay : int, optional
    +
    Number of frames to delay the release. Defaults to 1
    Expand source code -
    def get_memory_value(self, addr):
    +
    def button(self, input, delay=1):
         """
    -    Reads a given memory address of the Game Boy's current memory state. This will not directly give you access to
    -    all switchable memory banks. Open an issue on GitHub if that is needed, or use `PyBoy.set_memory_value` to send
    -    MBC commands to the virtual cartridge.
    +    Send input to PyBoy in the form of "a", "b", "start", "select", "left", "right", "up" and "down".
    +
    +    The button will automatically be released at the following call to `PyBoy.tick`.
    +
    +    Example:
    +    ```python
    +    >>> pyboy.button('a') # Press button 'a' and release after `pyboy.tick()`
    +    >>> pyboy.tick() # Button 'a' pressed
    +    True
    +    >>> pyboy.tick() # Button 'a' released
    +    True
    +    >>> pyboy.button('a', 3) # Press button 'a' and release after 3 `pyboy.tick()`
    +    >>> pyboy.tick() # Button 'a' pressed
    +    True
    +    >>> pyboy.tick() # Button 'a' still pressed
    +    True
    +    >>> pyboy.tick() # Button 'a' still pressed
    +    True
    +    >>> pyboy.tick() # Button 'a' released
    +    True
    +    ```
     
    -    Returns
    -    -------
    -    int:
    -        An integer with the value of the memory address
    +    Args:
    +        input (str): button to press
    +        delay (int, optional): Number of frames to delay the release. Defaults to 1
         """
    -    return self.mb.getitem(addr)
    + input = input.lower() + if input == "left": + self.send_input(WindowEvent.PRESS_ARROW_LEFT) + heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_ARROW_LEFT)) + elif input == "right": + self.send_input(WindowEvent.PRESS_ARROW_RIGHT) + heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_ARROW_RIGHT)) + elif input == "up": + self.send_input(WindowEvent.PRESS_ARROW_UP) + heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_ARROW_UP)) + elif input == "down": + self.send_input(WindowEvent.PRESS_ARROW_DOWN) + heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_ARROW_DOWN)) + elif input == "a": + self.send_input(WindowEvent.PRESS_BUTTON_A) + heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_BUTTON_A)) + elif input == "b": + self.send_input(WindowEvent.PRESS_BUTTON_B) + heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_BUTTON_B)) + elif input == "start": + self.send_input(WindowEvent.PRESS_BUTTON_START) + heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_BUTTON_START)) + elif input == "select": + self.send_input(WindowEvent.PRESS_BUTTON_SELECT) + heapq.heappush(self.queued_input, (self.frame_count + delay, WindowEvent.RELEASE_BUTTON_SELECT)) + else: + raise Exception("Unrecognized input:", input)
    -
    -def set_memory_value(self, addr, value) +
    +def button_press(self, input)
    -

    Write one byte to a given memory address of the Game Boy's current memory state.

    -

    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.

    -

    If you need to change ROM values, see PyBoy.override_memory_value().

    +

    Send input to PyBoy in the form of "a", "b", "start", "select", "left", "right", "up" and "down".

    +

    The button will remain press until explicitly released with PyBoy.button_release() or PyBoy.send_input().

    +

    Example:

    +
    >>> pyboy.button_press('a') # Press button 'a' and keep pressed after `PyBoy.tick()`
    +>>> pyboy.tick() # Button 'a' pressed
    +True
    +>>> pyboy.tick() # Button 'a' still pressed
    +True
    +>>> pyboy.button_release('a') # Release button 'a' on next call to `PyBoy.tick()`
    +>>> pyboy.tick() # Button 'a' released
    +True
    +
    +

    Args

    -
    addr : int
    -
    Address to write the byte
    -
    value : int
    -
    A byte of data
    +
    input : str
    +
    button to press
    Expand source code -
    def set_memory_value(self, addr, value):
    +
    def button_press(self, input):
         """
    -    Write one byte to a given memory address of the Game Boy's current memory state.
    +    Send input to PyBoy in the form of "a", "b", "start", "select", "left", "right", "up" and "down".
     
    -    This will not directly give you access to all switchable memory banks.
    +    The button will remain press until explicitly released with `PyBoy.button_release` or `PyBoy.send_input`.
     
    -    __NOTE:__ This function will not let you change ROM addresses (0x0000 to 0x8000). If you write to these
    -    addresses, it will send commands to the "Memory Bank Controller" (MBC) of the virtual cartridge. You can read
    -    about the MBC at [Pan Docs](http://bgb.bircd.org/pandocs.htm).
    +    Example:
    +    ```python
    +    >>> pyboy.button_press('a') # Press button 'a' and keep pressed after `PyBoy.tick()`
    +    >>> pyboy.tick() # Button 'a' pressed
    +    True
    +    >>> pyboy.tick() # Button 'a' still pressed
    +    True
    +    >>> pyboy.button_release('a') # Release button 'a' on next call to `PyBoy.tick()`
    +    >>> pyboy.tick() # Button 'a' released
    +    True
     
    -    If you need to change ROM values, see `pyboy.PyBoy.override_memory_value`.
    +    ```
     
         Args:
    -        addr (int): Address to write the byte
    -        value (int): A byte of data
    +        input (str): button to press
         """
    -    self.mb.setitem(addr, value)
    + 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.

    -

    This will let you override data in the ROM at any given bank. This is the memory allocated at 0x0000 to 0x8000, where 0x4000 to 0x8000 can be changed from the MBC.

    -

    NOTE: Any changes here are not saved or loaded to game states! Use this function with caution and reapply -any overrides when reloading the ROM.

    -

    If you need to change a RAM address, see PyBoy.set_memory_value().

    +

    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().

    +

    Example:

    +
    >>> 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

    -
    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
    Expand source code -
    def override_memory_value(self, rom_bank, addr, value):
    +
    def button_release(self, input):
         """
    -    Override one byte at a given memory address of the Game Boy's ROM.
    +    Send input to PyBoy in the form of "a", "b", "start", "select", "left", "right", "up" and "down".
     
    -    This will let you override data in the ROM at any given bank. This is the memory allocated at 0x0000 to 0x8000, where 0x4000 to 0x8000 can be changed from the MBC.
    +    This will release a button after a call to `PyBoy.button_press` or `PyBoy.send_input`.
     
    -    __NOTE__: Any changes here are not saved or loaded to game states! Use this function with caution and reapply
    -    any overrides when reloading the ROM.
    +    Example:
    +    ```python
    +    >>> pyboy.button_press('a') # Press button 'a' and keep pressed after `PyBoy.tick()`
    +    >>> pyboy.tick() # Button 'a' pressed
    +    True
    +    >>> pyboy.tick() # Button 'a' still pressed
    +    True
    +    >>> pyboy.button_release('a') # Release button 'a' on next call to `PyBoy.tick()`
    +    >>> pyboy.tick() # Button 'a' released
    +    True
     
    -    If you need to change a RAM address, see `pyboy.PyBoy.set_memory_value`.
    +    ```
     
         Args:
    -        rom_bank (int): ROM bank to do the overwrite in
    -        addr (int): Address to write the byte inside the ROM bank
    -        value (int): A byte of data
    +        input (str): button to release
         """
    -    # TODO: If you change a RAM value outside of the ROM banks above, the memory value will stay the same no matter
    -    # what the game writes to the address. This can be used so freeze the value for health, cash etc.
    -    self.mb.cartridge.overrideitem(rom_bank, addr, value)
    + input = input.lower() + if input == "left": + self.send_input(WindowEvent.RELEASE_ARROW_LEFT) + elif input == "right": + self.send_input(WindowEvent.RELEASE_ARROW_RIGHT) + elif input == "up": + self.send_input(WindowEvent.RELEASE_ARROW_UP) + elif input == "down": + self.send_input(WindowEvent.RELEASE_ARROW_DOWN) + elif input == "a": + self.send_input(WindowEvent.RELEASE_BUTTON_A) + elif input == "b": + self.send_input(WindowEvent.RELEASE_BUTTON_B) + elif input == "start": + self.send_input(WindowEvent.RELEASE_BUTTON_START) + elif input == "select": + self.send_input(WindowEvent.RELEASE_BUTTON_SELECT) + else: + raise Exception("Unrecognized input")
    def send_input(self, event)
    -

    Send a single input to control the emulator. This is both Game Boy buttons and emulator controls.

    -

    See WindowEvent for which events to send.

    +

    Send a single input to control the emulator. This is both Game Boy buttons and emulator controls. See +WindowEvent for which events to send.

    +

    Consider using PyBoy.button() instead for easier access.

    +

    Example:

    +
    >>> 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
    +
    +

    Args

    -
    event : WindowEvent
    +
    event : pyboy.WindowEvent
    The event to send
    @@ -915,59 +1803,29 @@

    Args

    def send_input(self, event):
         """
    -    Send a single input to control the emulator. This is both Game Boy buttons and emulator controls.
    +    Send a single input to control the emulator. This is both Game Boy buttons and emulator controls. See
    +    `pyboy.utils.WindowEvent` for which events to send.
     
    -    See `pyboy.WindowEvent` for which events to send.
    +    Consider using `PyBoy.button` instead for easier access.
     
    -    Args:
    -        event (pyboy.WindowEvent): The event to send
    -    """
    -    self.events.append(WindowEvent(event))
    -
    -
    -
    -def get_input(self, ignore=(22, 17, 33, 34, 35)) -
    -
    -

    Get current inputs except the events specified in "ignore" tuple. -This is both Game Boy buttons and emulator controls.

    -

    See WindowEvent for which events to get.

    -

    Args

    -
    -
    ignore : tuple
    -
    Events this function should ignore
    -
    -

    Returns

    -
    -
    list:
    -
    List of the pyboy.utils.WindowEvents processed for the last call to PyBoy.tick()
    -
    -
    - -Expand source code - -
    def get_input(
    -    self,
    -    ignore=(
    -        WindowEvent.PASS, WindowEvent._INTERNAL_TOGGLE_DEBUG, WindowEvent._INTERNAL_RENDERER_FLUSH,
    -        WindowEvent._INTERNAL_MOUSE, WindowEvent._INTERNAL_MARK_TILE
    -    )
    -):
    -    """
    -    Get current inputs except the events specified in "ignore" tuple.
    -    This is both Game Boy buttons and emulator controls.
    +    Example:
    +    ```python
    +    >>> from pyboy.utils import WindowEvent
    +    >>> pyboy.send_input(WindowEvent.PRESS_BUTTON_A) # Press button 'a' and keep pressed after `PyBoy.tick()`
    +    >>> pyboy.tick() # Button 'a' pressed
    +    True
    +    >>> pyboy.tick() # Button 'a' still pressed
    +    True
    +    >>> pyboy.send_input(WindowEvent.RELEASE_BUTTON_A) # Release button 'a' on next call to `PyBoy.tick()`
    +    >>> pyboy.tick() # Button 'a' released
    +    True
     
    -    See `pyboy.WindowEvent` for which events to get.
    +    ```
     
         Args:
    -        ignore (tuple): Events this function should ignore
    -
    -    Returns
    -    -------
    -    list:
    -        List of the `pyboy.utils.WindowEvent`s processed for the last call to `pyboy.PyBoy.tick`
    +        event (pyboy.WindowEvent): The event to send
         """
    -    return [x for x in self.old_events if x not in ignore]
    + self.events.append(WindowEvent(event))
    @@ -978,13 +1836,17 @@

    Returns

    a game.

    You can either save it to a file, or in-memory. The following two examples will provide the file handle in each case. Remember to seek the in-memory buffer to the beginning before calling PyBoy.load_state():

    -
    # Save to file
    -file_like_object = open("state_file.state", "wb")
    +
    >>> # Save to file
    +>>> with open("state_file.state", "wb") as f:
    +...     pyboy.save_state(f)
    +>>>
    +>>> # Save to memory
    +>>> import io
    +>>> with io.BytesIO() as f:
    +...     f.seek(0)
    +...     pyboy.save_state(f)
    +0
     
    -# Save to memory
    -import io
    -file_like_object = io.BytesIO()
    -file_like_object.seek(0)
     

    Args

    @@ -1003,13 +1865,19 @@

    Args

    You can either save it to a file, or in-memory. The following two examples will provide the file handle in each case. Remember to `seek` the in-memory buffer to the beginning before calling `PyBoy.load_state`: - # Save to file - file_like_object = open("state_file.state", "wb") + ```python + >>> # Save to file + >>> with open("state_file.state", "wb") as f: + ... pyboy.save_state(f) + >>> + >>> # Save to memory + >>> import io + >>> with io.BytesIO() as f: + ... f.seek(0) + ... pyboy.save_state(f) + 0 - # Save to memory - import io - file_like_object = io.BytesIO() - file_like_object.seek(0) + ``` Args: file_like_object (io.BufferedIOBase): A file-like object for which to write the emulator state. @@ -1018,6 +1886,9 @@

    Args

    if isinstance(file_like_object, str): raise Exception("String not allowed. Did you specify a filepath instead of a file-like object?") + if file_like_object.__class__.__name__ == "TextIOWrapper": + raise Exception("Text file not allowed. Did you specify open(..., 'wb')?") + self.mb.save_state(IntIOWrapper(file_like_object))
    @@ -1030,8 +1901,10 @@

    Args

    You can either load it from a file, or from memory. See PyBoy.save_state() for how to save the state, before you can load it here.

    To load a file, remember to load it as bytes:

    -
    # Load file
    -file_like_object = open("state_file.state", "rb")
    +
    >>> # Load file
    +>>> with open("state_file.state", "rb") as f:
    +...     pyboy.load_state(f)
    +>>>
     

    Args

    @@ -1051,10 +1924,12 @@

    Args

    can load it here. To load a file, remember to load it as bytes: - - # Load file - file_like_object = open("state_file.state", "rb") - + ```python + >>> # Load file + >>> with open("state_file.state", "rb") as f: + ... pyboy.load_state(f) + >>> + ``` Args: file_like_object (io.BufferedIOBase): A file-like object for which to read the emulator state. @@ -1063,43 +1938,277 @@

    Args

    if isinstance(file_like_object, str): raise Exception("String not allowed. Did you specify a filepath instead of a file-like object?") + if file_like_object.__class__.__name__ == "TextIOWrapper": + raise Exception("Text file not allowed. Did you specify open(..., 'rb')?") + self.mb.load_state(IntIOWrapper(file_like_object))
    -
    -def screen_image(self) +
    +def game_area_dimensions(self, x, y, width, height, follow_scrolling=True) +
    +
    +

    If using the generic game wrapper (see PyBoy.game_wrapper), you can use this to set the section of the +tilemaps to extract. This will default to the entire tilemap.

    +

    Example:

    +
    >>> pyboy.game_wrapper.shape
    +(32, 32)
    +>>> pyboy.game_area_dimensions(2, 2, 10, 18, False)
    +>>> pyboy.game_wrapper.shape
    +(10, 18)
    +
    +

    Args

    +
    +
    x : int
    +
    Offset from top-left corner of the screen
    +
    y : int
    +
    Offset from top-left corner of the screen
    +
    width : int
    +
    Width of game area
    +
    height : int
    +
    Height of game area
    +
    follow_scrolling : bool
    +
    Whether to follow the scrolling of SCX and SCY
    +
    +
    + +Expand source code + +
    def game_area_dimensions(self, x, y, width, height, follow_scrolling=True):
    +    """
    +    If using the generic game wrapper (see `pyboy.PyBoy.game_wrapper`), you can use this to set the section of the
    +    tilemaps to extract. This will default to the entire tilemap.
    +
    +    Example:
    +    ```python
    +    >>> pyboy.game_wrapper.shape
    +    (32, 32)
    +    >>> pyboy.game_area_dimensions(2, 2, 10, 18, False)
    +    >>> pyboy.game_wrapper.shape
    +    (10, 18)
    +    ```
    +
    +    Args:
    +        x (int): Offset from top-left corner of the screen
    +        y (int): Offset from top-left corner of the screen
    +        width (int): Width of game area
    +        height (int): Height of game area
    +        follow_scrolling (bool): Whether to follow the scrolling of [SCX and SCY](https://gbdev.io/pandocs/Scrolling.html)
    +    """
    +    self.game_wrapper._set_dimensions(x, y, width, height, follow_scrolling=True)
    +
    +
    +
    +def game_area_collision(self)
    -

    Shortcut for pyboy.botsupport_manager.screen.screen_image.

    -

    Generates a PIL Image from the screen buffer.

    -

    Convenient for screen captures, but might be a bottleneck, if you use it to train a neural network. In which -case, read up on the pyboy.botsupport features, Pan Docs on tiles/sprites, -and join our Discord channel for more help.

    +

    Some game wrappers define a collision map. Check if your game wrapper has this feature implemented: pyboy.plugins.

    +

    The output will be unique for each game wrapper.

    +

    Example:

    +
    >>> # This example show nothing, but a supported game will
    +>>> pyboy.game_area_collision()
    +array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
    +       [0, 0, 0, 0, 0, 0, 0, 0, 0],
    +       [0, 0, 0, 0, 0, 0, 0, 0, 0],
    +       [0, 0, 0, 0, 0, 0, 0, 0, 0],
    +       [0, 0, 0, 0, 0, 0, 0, 0, 0],
    +       [0, 0, 0, 0, 0, 0, 0, 0, 0],
    +       [0, 0, 0, 0, 0, 0, 0, 0, 0],
    +       [0, 0, 0, 0, 0, 0, 0, 0, 0],
    +       [0, 0, 0, 0, 0, 0, 0, 0, 0],
    +       [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint32)
    +
    +

    Returns

    -
    PIL.Image:
    -
    RGB image of (160, 144) pixels
    +
    memoryview:
    +
    Simplified 2-dimensional memoryview of the collision map
    +
    +
    + +Expand source code + +
    def game_area_collision(self):
    +    """
    +    Some game wrappers define a collision map. Check if your game wrapper has this feature implemented: `pyboy.plugins`.
    +
    +    The output will be unique for each game wrapper.
    +
    +    Example:
    +    ```python
    +    >>> # This example show nothing, but a supported game will
    +    >>> pyboy.game_area_collision()
    +    array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
    +           [0, 0, 0, 0, 0, 0, 0, 0, 0],
    +           [0, 0, 0, 0, 0, 0, 0, 0, 0],
    +           [0, 0, 0, 0, 0, 0, 0, 0, 0],
    +           [0, 0, 0, 0, 0, 0, 0, 0, 0],
    +           [0, 0, 0, 0, 0, 0, 0, 0, 0],
    +           [0, 0, 0, 0, 0, 0, 0, 0, 0],
    +           [0, 0, 0, 0, 0, 0, 0, 0, 0],
    +           [0, 0, 0, 0, 0, 0, 0, 0, 0],
    +           [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint32)
    +
    +    ```
    +
    +    Returns
    +    -------
    +    memoryview:
    +        Simplified 2-dimensional memoryview of the collision map
    +    """
    +    return self.game_wrapper.game_area_collision()
    +
    +
    +
    +def game_area_mapping(self, mapping, sprite_offset=0) +
    +
    +

    Define custom mappings for tile identifiers in the game area.

    +

    Example of custom mapping:

    +
    >>> 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.

    +
    >>> pyboy.game_area_mapping(pyboy.game_wrapper.mapping_one_to_one, 0)
    +
    +
    +

    Args

    +
    +
    mapping : list or ndarray
    +
    list of 384 (DMG) or 768 (CGB) tile mappings. Use None to reset to a 1:1 mapping.
    +
    sprite_offest : int
    +
    Optional offset add to tile id for sprites
    Expand source code -
    def screen_image(self):
    +
    def game_area_mapping(self, mapping, sprite_offset=0):
    +    """
    +    Define custom mappings for tile identifiers in the game area.
    +
    +    Example of custom mapping:
    +    ```python
    +    >>> mapping = [x for x in range(384)] # 1:1 mapping
    +    >>> mapping[0] = 0 # Map tile identifier 0 -> 0
    +    >>> mapping[1] = 0 # Map tile identifier 1 -> 0
    +    >>> mapping[2] = 0 # Map tile identifier 2 -> 0
    +    >>> mapping[3] = 0 # Map tile identifier 3 -> 0
    +    >>> pyboy.game_area_mapping(mapping, 1000)
    +
    +    ```
    +
    +    Some game wrappers will supply mappings as well. See the specific documentation for your game wrapper:
    +    `pyboy.plugins`.
    +    ```python
    +    >>> pyboy.game_area_mapping(pyboy.game_wrapper.mapping_one_to_one, 0)
    +
    +    ```
    +
    +    Args:
    +        mapping (list or ndarray): list of 384 (DMG) or 768 (CGB) tile mappings. Use `None` to reset to a 1:1 mapping.
    +        sprite_offest (int): Optional offset add to tile id for sprites
         """
    -    Shortcut for `pyboy.botsupport_manager.screen.screen_image`.
     
    -    Generates a PIL Image from the screen buffer.
    +    if mapping is None:
    +        mapping = [x for x in range(768)]
    +
    +    assert isinstance(sprite_offset, int)
    +    assert isinstance(mapping, (np.ndarray, list))
    +    assert len(mapping) == 384 or len(mapping) == 768
    +
    +    self.game_wrapper.game_area_mapping(mapping, sprite_offset)
    +
    +
    +
    +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:

    +
    >>> 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)
     
    -    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.
    +
    +

    If you want a "compressed", "minimal" or raw mapping of tiles, you can change the mapping using +PyBoy.game_area_mapping(). Either you'll have to supply your own mapping, or you can find one +that is built-in with the game wrapper plugin for your game. See PyBoy.game_area_mapping().

    +

    Returns

    +
    +
    memoryview:
    +
    Simplified 2-dimensional memoryview of the screen
    +
    +
    + +Expand source code + +
    def game_area(self):
    +    """
    +    Use this method to get a matrix of the "game area" of the screen. This view is simplified to be perfect for
    +    machine learning applications.
    +
    +    The layout will vary from game to game. Below is an example from Tetris:
    +
    +    Example:
    +    ```python
    +    >>> pyboy.game_area()
    +    array([[ 47,  47,  47,  47,  47,  47,  47,  47,  47,  47],
    +           [ 47,  47,  47,  47,  47,  47,  47,  47,  47,  47],
    +           [ 47,  47,  47,  47,  47,  47,  47,  47,  47,  47],
    +           [ 47,  47,  47,  47,  47,  47,  47,  47,  47,  47],
    +           [ 47,  47,  47, 130, 130,  47,  47,  47,  47,  47],
    +           [ 47,  47,  47,  47, 130, 130,  47,  47,  47,  47],
    +           [ 47,  47,  47,  47,  47,  47,  47,  47,  47,  47],
    +           [ 47,  47,  47,  47,  47,  47,  47,  47,  47,  47],
    +           [ 47,  47,  47,  47,  47,  47,  47,  47,  47,  47],
    +           [ 47,  47,  47,  47,  47,  47,  47,  47,  47,  47],
    +           [ 47,  47,  47,  47,  47,  47,  47,  47,  47,  47],
    +           [ 47,  47,  47,  47,  47,  47,  47,  47,  47,  47],
    +           [ 47,  47,  47,  47,  47,  47,  47,  47,  47,  47],
    +           [ 47,  47,  47,  47,  47,  47,  47,  47,  47,  47],
    +           [ 47,  47,  47,  47,  47,  47,  47,  47,  47,  47],
    +           [ 47,  47,  47,  47,  47,  47,  47,  47,  47,  47],
    +           [ 47,  47,  47,  47,  47,  47,  47,  47,  47,  47],
    +           [ 47,  47,  47,  47,  47,  47,  47,  47,  47,  47]], dtype=uint32)
    +
    +    ```
    +
    +    If you want a "compressed", "minimal" or raw mapping of tiles, you can change the mapping using
    +    `pyboy.PyBoy.game_area_mapping`. Either you'll have to supply your own mapping, or you can find one
    +    that is built-in with the game wrapper plugin for your game. See `pyboy.PyBoy.game_area_mapping`.
     
         Returns
         -------
    -    PIL.Image:
    -        RGB image of (160, 144) pixels
    +    memoryview:
    +        Simplified 2-dimensional memoryview of the screen
         """
    -    return self.botsupport_manager().screen().screen_image()
    + + return self.game_wrapper.game_area()
    @@ -1111,6 +2220,13 @@

    Returns

    The speed is defined as a multiple of real-time. I.e target_speed=2 is double speed.

    A target_speed of 0 means unlimited. I.e. fastest possible execution.

    Some window types do not implement a frame-limiter, and will always run at full speed.

    +

    Example:

    +
    >>> pyboy.tick() # Delays 16.67ms
    +True
    +>>> pyboy.set_emulation_speed(0) # Disable limit
    +>>> pyboy.tick() # As fast as possible
    +True
    +

    Args

    target_speed : int
    @@ -1131,334 +2247,895 @@

    Args

    Some window types do not implement a frame-limiter, and will always run at full speed. + Example: + ```python + >>> pyboy.tick() # Delays 16.67ms + True + >>> pyboy.set_emulation_speed(0) # Disable limit + >>> pyboy.tick() # As fast as possible + True + ``` + Args: target_speed (int): Target emulation speed as multiplier of real-time. """ + if self.initialized and self._plugin_manager.window_null_enabled: + logger.warning( + 'This window type does not support frame-limiting. `pyboy.set_emulation_speed(...)` will have no effect, as it\'s always running at full speed.' + ) + if target_speed > 5: logger.warning("The emulation speed might not be accurate when speed-target is higher than 5") self.target_emulationspeed = target_speed
    -
    -def cartridge_title(self) +
    +def symbol_lookup(self, symbol)
    -

    Get the title stored on the currently loaded cartridge ROM. The title is all upper-case ASCII and may -have been truncated to 11 characters.

    +

    Look up a specific symbol from provided symbols file.

    +

    This can be useful in combination with PyBoy.memory or even PyBoy.hook_register().

    +

    See PyBoy.hook_register() for how to load symbol into PyBoy.

    +

    Example:

    +
    >>> # Directly
    +>>> pyboy.memory[pyboy.symbol_lookup("Tileset")]
    +0
    +>>> # By bank and address
    +>>> bank, addr = pyboy.symbol_lookup("Tileset")
    +>>> pyboy.memory[bank, addr]
    +0
    +>>> pyboy.memory[bank, addr:addr+10]
    +[0, 0, 0, 0, 0, 0, 102, 102, 102, 102]
    +
    +

    Returns

    -
    -
    str :
    -
    Game title
    -
    +

    (int, int): +ROM/RAM bank, address

    Expand source code -
    def cartridge_title(self):
    +
    def symbol_lookup(self, symbol):
         """
    -    Get the title stored on the currently loaded cartridge ROM. The title is all upper-case ASCII and may
    -    have been truncated to 11 characters.
    +    Look up a specific symbol from provided symbols file.
    +
    +    This can be useful in combination with `PyBoy.memory` or even `PyBoy.hook_register`.
     
    +    See `PyBoy.hook_register` for how to load symbol into PyBoy.
    +
    +    Example:
    +    ```python
    +    >>> # Directly
    +    >>> pyboy.memory[pyboy.symbol_lookup("Tileset")]
    +    0
    +    >>> # By bank and address
    +    >>> bank, addr = pyboy.symbol_lookup("Tileset")
    +    >>> pyboy.memory[bank, addr]
    +    0
    +    >>> pyboy.memory[bank, addr:addr+10]
    +    [0, 0, 0, 0, 0, 0, 102, 102, 102, 102]
    +
    +    ```
         Returns
         -------
    -    str :
    -        Game title
    +    (int, int):
    +        ROM/RAM bank, address
         """
    -    return self.mb.cartridge.gamename
    + return self._lookup_symbol(symbol)
    -
    - -
    -class WindowEvent -(event) +
    +def hook_register(self, bank, addr, callback, context)
    -

    All supported events can be found in the class description below.

    -

    It can be used as follows:

    -
    >>> from pyboy import PyBoy, WindowEvent
    ->>> pyboy = PyBoy('file.rom')
    ->>> pyboy.send_input(WindowEvent.PRESS_ARROW_RIGHT)
    -
    -
    - -Expand source code +

    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:

    +
    >>> 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(..., symbols_file="game_rom.gb.sym").

    +

    Then provide None for bank and the symbol for addr to trigger the automatic lookup.

    +

    Example:

    +
    >>> # Continued example above
    +>>> pyboy.hook_register(None, "Main.move", lambda x: print(x), "Hello from hook2")
    +>>> pyboy.tick(80)
    +Hello from hook2
    +True
    +
    +
    +

    Args

    +
    +
    bank : int or None
    +
    ROM or RAM bank (None for symbol lookup)
    +
    addr : int or str
    +
    Address in the Game Boy's address space (str for symbol lookup)
    +
    callback : func
    +
    A function which takes context as argument
    +
    context : object
    +
    Argument to pass to callback when hook is called
    +
    +
    + +Expand source code -
    class WindowEvent:
    +
    def hook_register(self, bank, addr, callback, context):
         """
    -    All supported events can be found in the class description below.
    +    Adds a hook into a specific bank and memory address.
    +    When the Game Boy executes this address, the provided callback function will be called.
     
    -    It can be used as follows:
    +    By providing an object as `context`, you can later get access to information inside and outside of the callback.
     
    -    >>> from pyboy import PyBoy, WindowEvent
    -    >>> pyboy = PyBoy('file.rom')
    -    >>> pyboy.send_input(WindowEvent.PRESS_ARROW_RIGHT)
    -    """
    +    Example:
    +    ```python
    +    >>> context = "Hello from hook"
    +    >>> def my_callback(context):
    +    ...     print(context)
    +    >>> pyboy.hook_register(0, 0x100, my_callback, context)
    +    >>> pyboy.tick(70)
    +    Hello from hook
    +    True
     
    -    # ONLY ADD NEW EVENTS AT THE END OF THE LIST!
    -    # Otherwise, it will break replays, which depend on the id of the event
    -    (
    -        QUIT,
    -        PRESS_ARROW_UP,
    -        PRESS_ARROW_DOWN,
    -        PRESS_ARROW_RIGHT,
    -        PRESS_ARROW_LEFT,
    -        PRESS_BUTTON_A,
    -        PRESS_BUTTON_B,
    -        PRESS_BUTTON_SELECT,
    -        PRESS_BUTTON_START,
    -        RELEASE_ARROW_UP,
    -        RELEASE_ARROW_DOWN,
    -        RELEASE_ARROW_RIGHT,
    -        RELEASE_ARROW_LEFT,
    -        RELEASE_BUTTON_A,
    -        RELEASE_BUTTON_B,
    -        RELEASE_BUTTON_SELECT,
    -        RELEASE_BUTTON_START,
    -        _INTERNAL_TOGGLE_DEBUG,
    -        PRESS_SPEED_UP,
    -        RELEASE_SPEED_UP,
    -        STATE_SAVE,
    -        STATE_LOAD,
    -        PASS,
    -        SCREEN_RECORDING_TOGGLE,
    -        PAUSE,
    -        UNPAUSE,
    -        PAUSE_TOGGLE,
    -        PRESS_REWIND_BACK,
    -        PRESS_REWIND_FORWARD,
    -        RELEASE_REWIND_BACK,
    -        RELEASE_REWIND_FORWARD,
    -        WINDOW_FOCUS,
    -        WINDOW_UNFOCUS,
    -        _INTERNAL_RENDERER_FLUSH,
    -        _INTERNAL_MOUSE,
    -        _INTERNAL_MARK_TILE,
    -        SCREENSHOT_RECORD,
    -        DEBUG_MEMORY_SCROLL_DOWN,
    -        DEBUG_MEMORY_SCROLL_UP,
    -        MOD_SHIFT_ON,
    -        MOD_SHIFT_OFF,
    -        FULL_SCREEN_TOGGLE,
    -    ) = range(42)
    -
    -    def __init__(self, event):
    -        self.event = event
    -
    -    def __eq__(self, x):
    -        if isinstance(x, int):
    -            return self.event == x
    -        else:
    -            return self.event == x.event
    -
    -    def __int__(self):
    -        return self.event
    -
    -    def __str__(self):
    -        return (
    -            "QUIT",
    -            "PRESS_ARROW_UP",
    -            "PRESS_ARROW_DOWN",
    -            "PRESS_ARROW_RIGHT",
    -            "PRESS_ARROW_LEFT",
    -            "PRESS_BUTTON_A",
    -            "PRESS_BUTTON_B",
    -            "PRESS_BUTTON_SELECT",
    -            "PRESS_BUTTON_START",
    -            "RELEASE_ARROW_UP",
    -            "RELEASE_ARROW_DOWN",
    -            "RELEASE_ARROW_RIGHT",
    -            "RELEASE_ARROW_LEFT",
    -            "RELEASE_BUTTON_A",
    -            "RELEASE_BUTTON_B",
    -            "RELEASE_BUTTON_SELECT",
    -            "RELEASE_BUTTON_START",
    -            "_INTERNAL_TOGGLE_DEBUG",
    -            "PRESS_SPEED_UP",
    -            "RELEASE_SPEED_UP",
    -            "STATE_SAVE",
    -            "STATE_LOAD",
    -            "PASS",
    -            "SCREEN_RECORDING_TOGGLE",
    -            "PAUSE",
    -            "UNPAUSE",
    -            "PAUSE_TOGGLE",
    -            "PRESS_REWIND_BACK",
    -            "PRESS_REWIND_FORWARD",
    -            "RELEASE_REWIND_BACK",
    -            "RELEASE_REWIND_FORWARD",
    -            "WINDOW_FOCUS",
    -            "WINDOW_UNFOCUS",
    -            "_INTERNAL_RENDERER_FLUSH",
    -            "_INTERNAL_MOUSE",
    -            "_INTERNAL_MARK_TILE",
    -            "SCREENSHOT_RECORD",
    -            "DEBUG_MEMORY_SCROLL_DOWN",
    -            "DEBUG_MEMORY_SCROLL_UP",
    -            "MOD_SHIFT_ON",
    -            "MOD_SHIFT_OFF",
    -            "FULL_SCREEN_TOGGLE",
    -        )[self.event]
    + ``` + + If a symbol file is loaded, this function can also automatically resolve a bank and address from a symbol. To + enable this, you'll need to place a `.sym` file next to your ROM, or provide it using: + `PyBoy(..., symbols_file="game_rom.gb.sym")`. + + Then provide `None` for `bank` and the symbol for `addr` to trigger the automatic lookup. + + Example: + ```python + >>> # Continued example above + >>> pyboy.hook_register(None, "Main.move", lambda x: print(x), "Hello from hook2") + >>> pyboy.tick(80) + Hello from hook2 + True + + ``` + + Args: + bank (int or None): ROM or RAM bank (None for symbol lookup) + addr (int or str): Address in the Game Boy's address space (str for symbol lookup) + callback (func): A function which takes `context` as argument + context (object): Argument to pass to callback when hook is called + """ + if bank is None and isinstance(addr, str): + bank, addr = self._lookup_symbol(addr) + + opcode = self.memory[bank, addr] + if opcode == 0xDB: + raise ValueError("Hook already registered for this bank and address.") + self.mb.breakpoint_add(bank, addr) + bank_addr_opcode = (bank & 0xFF) << 24 | (addr & 0xFFFF) << 8 | (opcode & 0xFF) + self._hooks[bank_addr_opcode] = (callback, context)
    -

    Subclasses

    -
      -
    • pyboy.utils.WindowEventMouse
    • -
    -

    Class variables

    -
    -
    var QUIT
    -
    -
    -
    -
    var PRESS_ARROW_UP
    -
    -
    -
    -
    var PRESS_ARROW_DOWN
    -
    -
    -
    -
    var PRESS_ARROW_RIGHT
    -
    -
    -
    -
    var PRESS_ARROW_LEFT
    -
    -
    -
    -
    var PRESS_BUTTON_A
    -
    -
    -
    -
    var PRESS_BUTTON_B
    -
    -
    -
    -
    var PRESS_BUTTON_SELECT
    -
    -
    -
    -
    var PRESS_BUTTON_START
    -
    -
    -
    -
    var RELEASE_ARROW_UP
    -
    -
    -
    -
    var RELEASE_ARROW_DOWN
    -
    -
    -
    -
    var RELEASE_ARROW_RIGHT
    -
    -
    -
    -
    var RELEASE_ARROW_LEFT
    -
    -
    -
    -
    var RELEASE_BUTTON_A
    -
    -
    -
    -
    var RELEASE_BUTTON_B
    -
    -
    -
    -
    var RELEASE_BUTTON_SELECT
    -
    -
    -
    -
    var RELEASE_BUTTON_START
    -
    -
    -
    -
    var PRESS_SPEED_UP
    -
    -
    -
    -
    var RELEASE_SPEED_UP
    -
    -
    -
    -
    var STATE_SAVE
    -
    -
    -
    -
    var STATE_LOAD
    -
    -
    -
    -
    var PASS
    -
    -
    -
    -
    var SCREEN_RECORDING_TOGGLE
    -
    -
    -
    -
    var PAUSE
    -
    -
    -
    -
    var UNPAUSE
    -
    -
    -
    -
    var PAUSE_TOGGLE
    -
    -
    -
    -
    var PRESS_REWIND_BACK
    -
    -
    -
    -
    var PRESS_REWIND_FORWARD
    -
    -
    -
    var RELEASE_REWIND_BACK
    -
    -
    -
    -
    var RELEASE_REWIND_FORWARD
    -
    -
    -
    -
    var WINDOW_FOCUS
    -
    -
    -
    -
    var WINDOW_UNFOCUS
    -
    -
    -
    -
    var SCREENSHOT_RECORD
    +
    +def hook_deregister(self, bank, addr) +
    -
    +

    Remove a previously registered hook from a specific bank and memory address.

    +

    Example:

    +
    >>> 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:

    +
    >>> 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)
    +
    +
    + +Expand source code + +
    def hook_deregister(self, bank, addr):
    +    """
    +    Remove a previously registered hook from a specific bank and memory address.
    +
    +    Example:
    +    ```python
    +    >>> context = "Hello from hook"
    +    >>> def my_callback(context):
    +    ...     print(context)
    +    >>> pyboy.hook_register(0, 0x2000, my_callback, context)
    +    >>> pyboy.hook_deregister(0, 0x2000)
    +
    +    ```
    +
    +    This function can also deregister a hook based on a symbol. See `PyBoy.hook_register` for details.
    +
    +    Example:
    +    ```python
    +    >>> pyboy.hook_register(None, "Main", lambda x: print(x), "Hello from hook")
    +    >>> pyboy.hook_deregister(None, "Main")
    +
    +    ```
    +
    +    Args:
    +        bank (int or None): ROM or RAM bank (None for symbol lookup)
    +        addr (int or str): Address in the Game Boy's address space (str for symbol lookup)
    +    """
    +    if bank is None and isinstance(addr, str):
    +        bank, addr = self._lookup_symbol(addr)
    +
    +    index = self.mb.breakpoint_find(bank, addr)
    +    if index == -1:
    +        raise ValueError("Breakpoint not found for bank and addr")
    +
    +    _, _, opcode = self.mb.breakpoints_list[index]
    +    self.mb.breakpoint_remove(index)
    +    bank_addr_opcode = (bank & 0xFF) << 24 | (addr & 0xFFFF) << 8 | (opcode & 0xFF)
    +    self._hooks.pop(bank_addr_opcode)
    +
    -
    var DEBUG_MEMORY_SCROLL_DOWN
    +
    +def get_sprite(self, sprite_index) +
    -
    +

    Provides a Sprite object, which makes the OAM data more presentable. The given index +corresponds to index of the sprite in the "Object Attribute Memory" (OAM).

    +

    The Game Boy supports 40 sprites in total. Read more details about it, in the Pan +Docs.

    +
    >>> s = pyboy.get_sprite(12)
    +>>> s
    +Sprite [12]: Position: (-8, -16), Shape: (8, 8), Tiles: (Tile: 0), On screen: False
    +>>> s.on_screen
    +False
    +>>> s.tiles
    +[Tile: 0]
    +
    +
    +

    Args

    +
    +
    index : int
    +
    Sprite index from 0 to 39.
    +
    +

    Returns

    +

    Sprite: +Sprite corresponding to the given index.

    +
    + +Expand source code + +
    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)
    +
    -
    var DEBUG_MEMORY_SCROLL_UP
    +
    +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.get_sprite() function to get a Sprite object.

    +

    Example:

    +
    >>> 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
    +
    +
    + +Expand source code + +
    def get_sprite_by_tile_identifier(self, tile_identifiers, on_screen=True):
    +    """
    +    Provided a list of tile identifiers, this function will find all occurrences of sprites using the tile
    +    identifiers and return the sprite indexes where each identifier is found. Use the sprite indexes in the
    +    `pyboy.PyBoy.get_sprite` function to get a `pyboy.api.sprite.Sprite` object.
    +
    +    Example:
    +    ```python
    +    >>> print(pyboy.get_sprite_by_tile_identifier([43, 123]))
    +    [[0, 2, 4], []]
    +
    +    ```
    +
    +    Meaning, that tile identifier `43` is found at the sprite indexes: 0, 2, and 4, while tile identifier
    +    `123` was not found anywhere.
    +
    +    Args:
    +        identifiers (list): List of tile identifiers (int)
    +        on_screen (bool): Require that the matched sprite is on screen
    +
    +    Returns
    +    -------
    +    list:
    +        list of sprite matches for every tile identifier in the input
    +    """
    +
    +    matches = []
    +    for i in tile_identifiers:
    +        match = []
    +        for s in range(constants.SPRITES):
    +            sprite = Sprite(self.mb, s)
    +            for t in sprite.tiles:
    +                if t.tile_identifier == i and (not on_screen or (on_screen and sprite.on_screen)):
    +                    match.append(s)
    +        matches.append(match)
    +    return matches
    +
    -
    var MOD_SHIFT_ON
    +
    +def get_tile(self, identifier) +
    -
    +

    The Game Boy can have 384 tiles loaded in memory at once (768 for Game Boy Color). Use this method to get a +Tile-object for given identifier.

    +

    The identifier is a PyBoy construct, which unifies two different scopes of indexes in the Game Boy hardware. See +the Tile object for more information.

    +

    Example:

    +
    >>> t = pyboy.get_tile(2)
    +>>> t
    +Tile: 2
    +>>> t.shape
    +(8, 8)
    +
    +
    +

    Returns

    +

    Tile: +A Tile object for the given identifier.

    +
    + +Expand source code + +
    def get_tile(self, identifier):
    +    """
    +    The Game Boy can have 384 tiles loaded in memory at once (768 for Game Boy Color). Use this method to get a
    +    `pyboy.api.tile.Tile`-object for given identifier.
    +
    +    The identifier is a PyBoy construct, which unifies two different scopes of indexes in the Game Boy hardware. See
    +    the `pyboy.api.tile.Tile` object for more information.
    +
    +    Example:
    +    ```python
    +    >>> t = pyboy.get_tile(2)
    +    >>> t
    +    Tile: 2
    +    >>> t.shape
    +    (8, 8)
    +
    +    ```
    +
    +    Returns
    +    -------
    +    `pyboy.api.tile.Tile`:
    +        A Tile object for the given identifier.
    +    """
    +    return Tile(self.mb, identifier=identifier)
    +
    -
    var MOD_SHIFT_OFF
    -
    -
    +
    -
    var FULL_SCREEN_TOGGLE
    +
    +class PyBoyMemoryView +(mb) +
    -
    -
    -
    +

    This class cannot be used directly, but is accessed through PyBoy.memory.

    +

    This class serves four purposes: Reading memory (ROM/RAM), writing memory (ROM/RAM), overriding memory (ROM/RAM) and special registers.

    +

    See the Pan Docs: Memory Map for a great overview of the memory space.

    +

    Memory can be accessed as individual bytes (pyboy.memory[0x00]) or as slices (pyboy.memory[0x00:0x10]). And if +applicable, a specific ROM/RAM bank can be defined before the address (pyboy.memory[0, 0x00] or pyboy.memory[0, 0x00:0x10]).

    +

    The boot ROM is accessed using the special "-1" ROM bank.

    +

    The find addresses of interest, either search online for something like: "[game title] RAM map", or find them yourself +using PyBoy.memory_scanner.

    +

    Read:

    +

    If you're developing a bot or AI with this API, you're most likely going to be using read the most. This is how you +would efficiently read the score, time, coins, positions etc. in a game's memory.

    +
    >>> pyboy.memory[0x0000] # Read one byte at address 0x0000
    +49
    +>>> pyboy.memory[0x0000:0x0010] # Read 16 bytes from 0x0000 to 0x0010 (excluding 0x0010)
    +[49, 254, 255, 33, 0, 128, 175, 34, 124, 254, 160, 32, 249, 6, 48, 33]
    +>>> pyboy.memory[-1, 0x0000:0x0010] # Read 16 bytes from 0x0000 to 0x0010 (excluding 0x0010) from the boot ROM
    +[49, 254, 255, 33, 0, 128, 175, 34, 124, 254, 160, 32, 249, 6, 48, 33]
    +>>> pyboy.memory[0, 0x0000:0x0010] # Read 16 bytes from 0x0000 to 0x0010 (excluding 0x0010) from ROM bank 0
    +[64, 65, 66, 67, 68, 69, 70, 65, 65, 65, 71, 65, 65, 65, 72, 73]
    +>>> pyboy.memory[2, 0xA000] # Read from external RAM on cartridge (if any) from bank 2 at address 0xA000
    +0
    +
    +

    Write:

    +

    Writing to Game Boy memory can be complicated because of the limited address space. There's a lot of memory that +isn't directly accessible, and can be hidden through "memory banking". This means that the same address range +(for example 0x4000 to 0x8000) can change depending on what state the game is in.

    +

    If you want to change an address in the ROM, then look at override below. Issuing writes to the ROM area actually +sends commands to the Memory Bank Controller (MBC) on the cartridge.

    +

    A write is done by assigning to the PyBoy.memory object. It's recommended to define the bank to avoid mistakes +(pyboy.memory[2, 0xA000]=1). Without defining the bank, PyBoy will pick the current bank for the given address if +needed (pyboy.memory[0xA000]=1).

    +

    At this point, all reads will return a new list of the values in the given range. The slices will not reference back to the PyBoy memory. This feature might come in the future.

    +
    >>> pyboy.memory[0xC000] = 123 # Write to WRAM at address 0xC000
    +>>> pyboy.memory[0xC000:0xC00A] = [0,1,2,3,4,5,6,7,8,9] # Write to WRAM from address 0xC000 to 0xC00A
    +>>> pyboy.memory[0xC010:0xC01A] = 0 # Write to WRAM from address 0xC010 to 0xC01A
    +>>> pyboy.memory[0x1000] = 123 # Not writing 123 at address 0x1000! This sends a command to the cartridge's MBC.
    +>>> pyboy.memory[2, 0xA000] = 123 # Write to external RAM on cartridge (if any) for bank 2 at address 0xA000
    +>>> # Game Boy Color (CGB) only:
    +>>> pyboy_cgb.memory[1, 0x8000] = 25 # Write to VRAM bank 1 at address 0xD000 when in CGB mode
    +>>> pyboy_cgb.memory[6, 0xD000] = 25 # Write to WRAM bank 6 at address 0xD000 when in CGB mode
    +
    +

    Override:

    +

    Override data at a given memory address of the Game Boy's ROM.

    +

    This can be used to reprogram a game ROM to change its behavior.

    +

    This will not let your override RAM or a special register. This will let you override data in the ROM at any given bank. +This is the memory allocated at 0x0000 to 0x8000, where 0x4000 to 0x8000 can be changed from the MBC.

    +

    NOTE: Any changes here are not saved or loaded to game states! Use this function with caution and reapply +any overrides when reloading the ROM.

    +

    To override, it's required to provide the ROM-bank you're changing. Otherwise, it'll be considered a regular 'write' as described above.

    +
    >>> pyboy.memory[0, 0x0010] = 10 # Override ROM-bank 0 at address 0x0010
    +>>> pyboy.memory[0, 0x0010:0x001A] = [0,1,2,3,4,5,6,7,8,9] # Override ROM-bank 0 at address 0x0010 to 0x001A
    +>>> pyboy.memory[-1, 0x0010] = 10 # Override boot ROM at address 0x0010
    +>>> pyboy.memory[1, 0x6000] = 12 # Override ROM-bank 1 at address 0x6000
    +>>> pyboy.memory[0x1000] = 12 # This will not override, as there is no ROM bank assigned!
    +
    +

    Special Registers:

    +

    The Game Boy has a range of memory addresses known as hardware registers. These control parts of the hardware like LCD, +Timer, DMA, serial and so on. Even though they might appear as regular RAM addresses, reading/writing these addresses +often results in special side-effects.

    +

    The DIV (0xFF04) register for example provides a number that increments 16 thousand times each second. This can be +used as a source of randomness in games. If you read the value, you'll get a pseudo-random number. But if you write +any value to the register, it'll reset to zero.

    +
    >>> pyboy.memory[0xFF04] # DIV register
    +163
    +>>> pyboy.memory[0xFF04] = 123 # Trying to write to it will always reset it to zero
    +>>> pyboy.memory[0xFF04]
    +0
    +
    +
    + +Expand source code + +
    class PyBoyMemoryView:
    +    """
    +    This class cannot be used directly, but is accessed through `PyBoy.memory`.
    +
    +    This class serves four purposes: Reading memory (ROM/RAM), writing memory (ROM/RAM), overriding memory (ROM/RAM) and special registers.
    +
    +    See the [Pan Docs: Memory Map](https://gbdev.io/pandocs/Memory_Map.html) for a great overview of the memory space.
    +
    +    Memory can be accessed as individual bytes (`pyboy.memory[0x00]`) or as slices (`pyboy.memory[0x00:0x10]`). And if
    +    applicable, a specific ROM/RAM bank can be defined before the address (`pyboy.memory[0, 0x00]` or `pyboy.memory[0, 0x00:0x10]`).
    +
    +    The boot ROM is accessed using the special "-1" ROM bank.
    +
    +    The find addresses of interest, either search online for something like: "[game title] RAM map", or find them yourself
    +    using `PyBoy.memory_scanner`.
    +
    +    **Read:**
    +
    +    If you're developing a bot or AI with this API, you're most likely going to be using read the most. This is how you
    +    would efficiently read the score, time, coins, positions etc. in a game's memory.
    +
    +    ```python
    +    >>> pyboy.memory[0x0000] # Read one byte at address 0x0000
    +    49
    +    >>> pyboy.memory[0x0000:0x0010] # Read 16 bytes from 0x0000 to 0x0010 (excluding 0x0010)
    +    [49, 254, 255, 33, 0, 128, 175, 34, 124, 254, 160, 32, 249, 6, 48, 33]
    +    >>> pyboy.memory[-1, 0x0000:0x0010] # Read 16 bytes from 0x0000 to 0x0010 (excluding 0x0010) from the boot ROM
    +    [49, 254, 255, 33, 0, 128, 175, 34, 124, 254, 160, 32, 249, 6, 48, 33]
    +    >>> pyboy.memory[0, 0x0000:0x0010] # Read 16 bytes from 0x0000 to 0x0010 (excluding 0x0010) from ROM bank 0
    +    [64, 65, 66, 67, 68, 69, 70, 65, 65, 65, 71, 65, 65, 65, 72, 73]
    +    >>> pyboy.memory[2, 0xA000] # Read from external RAM on cartridge (if any) from bank 2 at address 0xA000
    +    0
    +    ```
    +
    +    **Write:**
    +
    +    Writing to Game Boy memory can be complicated because of the limited address space. There's a lot of memory that
    +    isn't directly accessible, and can be hidden through "memory banking". This means that the same address range
    +    (for example 0x4000 to 0x8000) can change depending on what state the game is in.
    +
    +    If you want to change an address in the ROM, then look at override below. Issuing writes to the ROM area actually
    +    sends commands to the [Memory Bank Controller (MBC)](https://gbdev.io/pandocs/MBCs.html#mbcs) on the cartridge.
    +
    +    A write is done by assigning to the `PyBoy.memory` object. It's recommended to define the bank to avoid mistakes
    +    (`pyboy.memory[2, 0xA000]=1`). Without defining the bank, PyBoy will pick the current bank for the given address if
    +    needed (`pyboy.memory[0xA000]=1`).
    +
    +    At this point, all reads will return a new list of the values in the given range. The slices will not reference back to the PyBoy memory. This feature might come in the future.
    +
    +    ```python
    +    >>> pyboy.memory[0xC000] = 123 # Write to WRAM at address 0xC000
    +    >>> pyboy.memory[0xC000:0xC00A] = [0,1,2,3,4,5,6,7,8,9] # Write to WRAM from address 0xC000 to 0xC00A
    +    >>> pyboy.memory[0xC010:0xC01A] = 0 # Write to WRAM from address 0xC010 to 0xC01A
    +    >>> pyboy.memory[0x1000] = 123 # Not writing 123 at address 0x1000! This sends a command to the cartridge's MBC.
    +    >>> pyboy.memory[2, 0xA000] = 123 # Write to external RAM on cartridge (if any) for bank 2 at address 0xA000
    +    >>> # Game Boy Color (CGB) only:
    +    >>> pyboy_cgb.memory[1, 0x8000] = 25 # Write to VRAM bank 1 at address 0xD000 when in CGB mode
    +    >>> pyboy_cgb.memory[6, 0xD000] = 25 # Write to WRAM bank 6 at address 0xD000 when in CGB mode
    +    ```
    +
    +    **Override:**
    +
    +    Override data at a given memory address of the Game Boy's ROM.
    +
    +    This can be used to reprogram a game ROM to change its behavior.
    +
    +    This will not let your override RAM or a special register. This will let you override data in the ROM at any given bank.
    +    This is the memory allocated at 0x0000 to 0x8000, where 0x4000 to 0x8000 can be changed from the MBC.
    +
    +    _NOTE_: Any changes here are not saved or loaded to game states! Use this function with caution and reapply
    +    any overrides when reloading the ROM.
    +
    +    To override, it's required to provide the ROM-bank you're changing. Otherwise, it'll be considered a regular 'write' as described above.
    +
    +    ```python
    +    >>> pyboy.memory[0, 0x0010] = 10 # Override ROM-bank 0 at address 0x0010
    +    >>> pyboy.memory[0, 0x0010:0x001A] = [0,1,2,3,4,5,6,7,8,9] # Override ROM-bank 0 at address 0x0010 to 0x001A
    +    >>> pyboy.memory[-1, 0x0010] = 10 # Override boot ROM at address 0x0010
    +    >>> pyboy.memory[1, 0x6000] = 12 # Override ROM-bank 1 at address 0x6000
    +    >>> pyboy.memory[0x1000] = 12 # This will not override, as there is no ROM bank assigned!
    +    ```
    +
    +    **Special Registers:**
    +
    +    The Game Boy has a range of memory addresses known as [hardware registers](https://gbdev.io/pandocs/Hardware_Reg_List.html). These control parts of the hardware like LCD,
    +    Timer, DMA, serial and so on. Even though they might appear as regular RAM addresses, reading/writing these addresses
    +    often results in special side-effects.
    +
    +    The [DIV (0xFF04) register](https://gbdev.io/pandocs/Timer_and_Divider_Registers.html#ff04--div-divider-register) for example provides a number that increments 16 thousand times each second. This can be
    +    used as a source of randomness in games. If you read the value, you'll get a pseudo-random number. But if you write
    +    *any* value to the register, it'll reset to zero.
    +
    +    ```python
    +    >>> pyboy.memory[0xFF04] # DIV register
    +    163
    +    >>> pyboy.memory[0xFF04] = 123 # Trying to write to it will always reset it to zero
    +    >>> pyboy.memory[0xFF04]
    +    0
    +    ```
    +
    +    """
    +    def __init__(self, mb):
    +        self.mb = mb
    +
    +    def _fix_slice(self, addr):
    +        if addr.start is None:
    +            return (-1, 0, 0)
    +        if addr.stop is None:
    +            return (0, -1, 0)
    +        start = addr.start
    +        stop = addr.stop
    +        if start > stop:
    +            return (-1, -1, 0)
    +        if addr.step is None:
    +            step = 1
    +        else:
    +            step = addr.step
    +        return start, stop, step
    +
    +    def __getitem__(self, addr):
    +        is_bank = isinstance(addr, tuple)
    +        bank = 0
    +        if is_bank:
    +            bank, addr = addr
    +            assert isinstance(bank, int), "Bank has to be integer. Slicing is not supported."
    +        is_single = isinstance(addr, int)
    +        if not is_single:
    +            start, stop, step = self._fix_slice(addr)
    +            assert start >= 0 or stop >= 0, "Start address has to come before end address"
    +            assert start >= 0, "Start address required"
    +            assert stop >= 0, "End address required"
    +            return self.__getitem(start, stop, step, bank, is_single, is_bank)
    +        else:
    +            return self.__getitem(addr, 0, 1, bank, is_single, is_bank)
    +
    +    def __getitem(self, start, stop, step, bank, is_single, is_bank):
    +        slice_length = (stop-start) // step
    +        if is_bank:
    +            # Reading a specific bank
    +            if start < 0x8000:
    +                if start >= 0x4000:
    +                    start -= 0x4000
    +                    stop -= 0x4000
    +                # Cartridge ROM Banks
    +                assert stop < 0x4000, "Out of bounds for reading ROM bank"
    +                if bank == -1:
    +                    assert start <= 0xFF, "Start address out of range for bootrom"
    +                    assert stop <= 0xFF, "Start address out of range for bootrom"
    +                    if not is_single:
    +                        mem_slice = [0] * slice_length
    +                        for x in range(start, stop, step):
    +                            mem_slice[(x-start) // step] = self.mb.bootrom.bootrom[x]
    +                        return mem_slice
    +                    else:
    +                        return self.mb.bootrom.bootrom[start]
    +                else:
    +                    assert bank <= self.mb.cartridge.external_rom_count, "ROM Bank out of range"
    +                    if not is_single:
    +                        mem_slice = [0] * slice_length
    +                        for x in range(start, stop, step):
    +                            mem_slice[(x-start) // step] = self.mb.cartridge.rombanks[bank, x]
    +                        return mem_slice
    +                    else:
    +                        return self.mb.cartridge.rombanks[bank, start]
    +            elif start < 0xA000:
    +                start -= 0x8000
    +                stop -= 0x8000
    +                # CGB VRAM Banks
    +                assert self.mb.cgb or (bank == 0), "Selecting bank of VRAM is only supported for CGB mode"
    +                assert stop < 0x2000, "Out of bounds for reading VRAM bank"
    +                assert bank <= 1, "VRAM Bank out of range"
    +
    +                if bank == 0:
    +                    if not is_single:
    +                        mem_slice = [0] * slice_length
    +                        for x in range(start, stop, step):
    +                            mem_slice[(x-start) // step] = self.mb.lcd.VRAM0[x]
    +                        return mem_slice
    +                    else:
    +                        return self.mb.lcd.VRAM0[start]
    +                else:
    +                    if not is_single:
    +                        mem_slice = [0] * slice_length
    +                        for x in range(start, stop, step):
    +                            mem_slice[(x-start) // step] = self.mb.lcd.VRAM1[x]
    +                        return mem_slice
    +                    else:
    +                        return self.mb.lcd.VRAM1[start]
    +            elif start < 0xC000:
    +                start -= 0xA000
    +                stop -= 0xA000
    +                # Cartridge RAM banks
    +                assert stop < 0x2000, "Out of bounds for reading cartridge RAM bank"
    +                assert bank <= self.mb.cartridge.external_ram_count, "ROM Bank out of range"
    +                if not is_single:
    +                    mem_slice = [0] * slice_length
    +                    for x in range(start, stop, step):
    +                        mem_slice[(x-start) // step] = self.mb.cartridge.rambanks[bank, x]
    +                    return mem_slice
    +                else:
    +                    return self.mb.cartridge.rambanks[bank, start]
    +            elif start < 0xE000:
    +                start -= 0xC000
    +                stop -= 0xC000
    +                if start >= 0x1000:
    +                    start -= 0x1000
    +                    stop -= 0x1000
    +                # CGB VRAM banks
    +                assert self.mb.cgb or (bank == 0), "Selecting bank of WRAM is only supported for CGB mode"
    +                assert stop < 0x1000, "Out of bounds for reading VRAM bank"
    +                assert bank <= 7, "WRAM Bank out of range"
    +                if not is_single:
    +                    mem_slice = [0] * slice_length
    +                    for x in range(start, stop, step):
    +                        mem_slice[(x-start) // step] = self.mb.ram.internal_ram0[x + bank*0x1000]
    +                    return mem_slice
    +                else:
    +                    return self.mb.ram.internal_ram0[start + bank*0x1000]
    +            else:
    +                assert None, "Invalid memory address for bank"
    +        elif not is_single:
    +            # Reading slice of memory space
    +            mem_slice = [0] * slice_length
    +            for x in range(start, stop, step):
    +                mem_slice[(x-start) // step] = self.mb.getitem(x)
    +            return mem_slice
    +        else:
    +            # Reading specific address of memory space
    +            return self.mb.getitem(start)
    +
    +    def __setitem__(self, addr, v):
    +        is_bank = isinstance(addr, tuple)
    +        bank = 0
    +        if is_bank:
    +            bank, addr = addr
    +            assert isinstance(bank, int), "Bank has to be integer. Slicing is not supported."
    +        is_single = isinstance(addr, int)
    +        if not is_single:
    +            start, stop, step = self._fix_slice(addr)
    +            assert start >= 0, "Start address required"
    +            assert stop >= 0, "End address required"
    +            self.__setitem(start, stop, step, v, bank, is_single, is_bank)
    +        else:
    +            self.__setitem(addr, 0, 0, v, bank, is_single, is_bank)
    +
    +    def __setitem(self, start, stop, step, v, bank, is_single, is_bank):
    +        if is_bank:
    +            # Writing a specific bank
    +            if start < 0x8000:
    +                """
    +                Override one byte at a given memory address of the Game Boy's ROM.
    +
    +                This will let you override data in the ROM at any given bank. This is the memory allocated at 0x0000 to 0x8000, where 0x4000 to 0x8000 can be changed from the MBC.
    +
    +                __NOTE__: Any changes here are not saved or loaded to game states! Use this function with caution and reapply
    +                any overrides when reloading the ROM.
    +
    +                If you need to change a RAM address, see `pyboy.PyBoy.memory`.
    +
    +                Args:
    +                    rom_bank (int): ROM bank to do the overwrite in
    +                    addr (int): Address to write the byte inside the ROM bank
    +                    value (int): A byte of data
    +                """
    +                if start >= 0x4000:
    +                    start -= 0x4000
    +                    stop -= 0x4000
    +                # Cartridge ROM Banks
    +                assert stop < 0x4000, "Out of bounds for reading ROM bank"
    +                assert bank <= self.mb.cartridge.external_rom_count, "ROM Bank out of range"
    +
    +                # TODO: If you change a RAM value outside of the ROM banks above, the memory value will stay the same no matter
    +                # what the game writes to the address. This can be used so freeze the value for health, cash etc.
    +                if bank == -1:
    +                    assert start <= 0xFF, "Start address out of range for bootrom"
    +                    assert stop <= 0xFF, "Start address out of range for bootrom"
    +                    if not is_single:
    +                        # Writing slice of memory space
    +                        if hasattr(v, "__iter__"):
    +                            assert (stop-start) // step == len(v), "slice does not match length of data"
    +                            _v = iter(v)
    +                            for x in range(start, stop, step):
    +                                self.mb.bootrom.bootrom[x] = next(_v)
    +                        else:
    +                            for x in range(start, stop, step):
    +                                self.mb.bootrom.bootrom[x] = v
    +                    else:
    +                        self.mb.bootrom.bootrom[start] = v
    +                else:
    +                    if not is_single:
    +                        # Writing slice of memory space
    +                        if hasattr(v, "__iter__"):
    +                            assert (stop-start) // step == len(v), "slice does not match length of data"
    +                            _v = iter(v)
    +                            for x in range(start, stop, step):
    +                                self.mb.cartridge.overrideitem(bank, x, next(_v))
    +                        else:
    +                            for x in range(start, stop, step):
    +                                self.mb.cartridge.overrideitem(bank, x, v)
    +                    else:
    +                        self.mb.cartridge.overrideitem(bank, start, v)
    +
    +            elif start < 0xA000:
    +                start -= 0x8000
    +                stop -= 0x8000
    +                # CGB VRAM Banks
    +                assert self.mb.cgb or (bank == 0), "Selecting bank of VRAM is only supported for CGB mode"
    +                assert stop < 0x2000, "Out of bounds for reading VRAM bank"
    +                assert bank <= 1, "VRAM Bank out of range"
    +
    +                if bank == 0:
    +                    if not is_single:
    +                        # Writing slice of memory space
    +                        if hasattr(v, "__iter__"):
    +                            assert (stop-start) // step == len(v), "slice does not match length of data"
    +                            _v = iter(v)
    +                            for x in range(start, stop, step):
    +                                self.mb.lcd.VRAM0[x] = next(_v)
    +                        else:
    +                            for x in range(start, stop, step):
    +                                self.mb.lcd.VRAM0[x] = v
    +                    else:
    +                        self.mb.lcd.VRAM0[start] = v
    +                else:
    +                    if not is_single:
    +                        # Writing slice of memory space
    +                        if hasattr(v, "__iter__"):
    +                            assert (stop-start) // step == len(v), "slice does not match length of data"
    +                            _v = iter(v)
    +                            for x in range(start, stop, step):
    +                                self.mb.lcd.VRAM1[x] = next(_v)
    +                        else:
    +                            for x in range(start, stop, step):
    +                                self.mb.lcd.VRAM1[x] = v
    +                    else:
    +                        self.mb.lcd.VRAM1[start] = v
    +            elif start < 0xC000:
    +                start -= 0xA000
    +                stop -= 0xA000
    +                # Cartridge RAM banks
    +                assert stop < 0x2000, "Out of bounds for reading cartridge RAM bank"
    +                assert bank <= self.mb.cartridge.external_ram_count, "ROM Bank out of range"
    +                if not is_single:
    +                    # Writing slice of memory space
    +                    if hasattr(v, "__iter__"):
    +                        assert (stop-start) // step == len(v), "slice does not match length of data"
    +                        _v = iter(v)
    +                        for x in range(start, stop, step):
    +                            self.mb.cartridge.rambanks[bank, x] = next(_v)
    +                    else:
    +                        for x in range(start, stop, step):
    +                            self.mb.cartridge.rambanks[bank, x] = v
    +                else:
    +                    self.mb.cartridge.rambanks[bank, start] = v
    +            elif start < 0xE000:
    +                start -= 0xC000
    +                stop -= 0xC000
    +                if start >= 0x1000:
    +                    start -= 0x1000
    +                    stop -= 0x1000
    +                # CGB VRAM banks
    +                assert self.mb.cgb or (bank == 0), "Selecting bank of WRAM is only supported for CGB mode"
    +                assert stop < 0x1000, "Out of bounds for reading VRAM bank"
    +                assert bank <= 7, "WRAM Bank out of range"
    +                if not is_single:
    +                    # Writing slice of memory space
    +                    if hasattr(v, "__iter__"):
    +                        assert (stop-start) // step == len(v), "slice does not match length of data"
    +                        _v = iter(v)
    +                        for x in range(start, stop, step):
    +                            self.mb.ram.internal_ram0[x + bank*0x1000] = next(_v)
    +                    else:
    +                        for x in range(start, stop, step):
    +                            self.mb.ram.internal_ram0[x + bank*0x1000] = v
    +                else:
    +                    self.mb.ram.internal_ram0[start + bank*0x1000] = v
    +            else:
    +                assert None, "Invalid memory address for bank"
    +        elif not is_single:
    +            # Writing slice of memory space
    +            if hasattr(v, "__iter__"):
    +                assert (stop-start) // step == len(v), "slice does not match length of data"
    +                _v = iter(v)
    +                for x in range(start, stop, step):
    +                    self.mb.setitem(x, next(_v))
    +            else:
    +                for x in range(start, stop, step):
    +                    self.mb.setitem(x, v)
    +        else:
    +            # Writing specific address of memory space
    +            self.mb.setitem(start, v)
    +
    @@ -1471,9 +3148,9 @@

    Index

  • diff --git a/docs/openai_gym.html b/docs/openai_gym.html deleted file mode 100644 index 0af456a43..000000000 --- a/docs/openai_gym.html +++ /dev/null @@ -1,596 +0,0 @@ - - - - - - -pyboy.openai_gym API documentation - - - - - - - - - - -
    -
    -
    -

    Module pyboy.openai_gym

    -
    -
    -
    - -Expand source code - -
    #
    -# 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)
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -

    Classes

    -
    -
    -class PyBoyGymEnv -(pyboy, observation_type='tiles', action_type='toggle', simultaneous_actions=False, **kwargs) -
    -
    -

    A gym environement built from a 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 (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.
    -
    -
    - -Expand source code - -
    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

    -
    - -Expand source code - -
    def reset(self):
    -    """ Reset (or start) the gym environment throught the game_wrapper """
    -    if not self._started:
    -        self.game_wrapper.start_game(**self._kwargs)
    -        self._started = True
    -    else:
    -        self.game_wrapper.reset_game()
    -    self.last_fitness = self.game_wrapper.fitness
    -    self.button_is_pressed = {button: False for button in self._buttons}
    -    return self._get_observation()
    -
    -
    -
    -def render(self) -
    -
    -

    Compute the render frames as specified by render_mode attribute during initialization of the environment.

    -

    The set of supported modes varies per environment. (And some -third-party environments may not support rendering at all.) -By convention, if render_mode is:

    -
      -
    • None (default): no render is computed.
    • -
    • human: render return None. -The environment is continuously rendered in the current display or terminal. Usually for human consumption.
    • -
    • rgb_array: return a single frame representing the current state of the environment. -A frame is a numpy.ndarray with shape (x, y, 3) representing RGB values for an x-by-y pixel image.
    • -
    • rgb_array_list: return a list of frames representing the states of the environment since the last reset. -Each frame is a numpy.ndarray with shape (x, y, 3), as with rgb_array.
    • -
    • ansi: Return a strings (str) or StringIO.StringIO containing a -terminal-style text representation for each time step. -The text can include newlines and ANSI escape sequences (e.g. for colors).
    • -
    -

    Note

    -

    Make sure that your class's metadata 'render_modes' key includes -the list of supported modes. It's recommended to call super() -in implementations to use the functionality of this method.

    -
    - -Expand source code - -
    def render(self):
    -    pass
    -
    -
    -
    -def close(self) -
    -
    -

    Override close in your subclass to perform any necessary cleanup.

    -

    Environments will automatically :meth:close() themselves when -garbage collected or when the program exits.

    -
    - -Expand source code - -
    def close(self):
    -    self.pyboy.stop(save=False)
    -
    -
    -
    -
    -
    -
    -
    - -
    - - - - - \ No newline at end of file diff --git a/docs/plugins/base_plugin.html b/docs/plugins/base_plugin.html index 260852b8c..43575932b 100644 --- a/docs/plugins/base_plugin.html +++ b/docs/plugins/base_plugin.html @@ -39,14 +39,15 @@

    Module pyboy.plugins.base_plugin

    } import io -import logging import random from array import array import numpy as np -from pyboy.botsupport.sprite import Sprite -logger = logging.getLogger(__name__) +import pyboy +from pyboy.api.sprite import Sprite + +logger = pyboy.logging.get_logger(__name__) try: from cython import compiled @@ -99,7 +100,7 @@

    Module pyboy.plugins.base_plugin

    logger.debug("%s initialization" % self.__class__.__name__) self._scaledresolution = (scale * COLS, scale * ROWS) - logger.debug("Scale: x%s %s" % (self.scale, self._scaledresolution)) + logger.debug("Scale: x%d (%d, %d)", self.scale, self._scaledresolution[0], self._scaledresolution[1]) self.enable_title = True if not cythonmode: @@ -121,37 +122,51 @@

    Module pyboy.plugins.base_plugin

    , which shows both sprites and tiles on the screen as a simple matrix. """ - argv = [("--game-wrapper", {"action": "store_true", "help": "Enable game wrapper for the current game"})] + cartridge_title = None - def __init__(self, *args, game_area_section=(0, 0, 32, 32), game_area_wrap_around=False, **kwargs): + mapping_one_to_one = np.arange(384 * 2, dtype=np.uint8) + """ + Example mapping of 1:1 + """ + def __init__(self, *args, game_area_section=(0, 0, 32, 32), game_area_follow_scxy=False, **kwargs): super().__init__(*args, **kwargs) - self.tilemap_background = self.pyboy.botsupport_manager().tilemap_background() - self.tilemap_window = self.pyboy.botsupport_manager().tilemap_window() + if not cythonmode: + self.tilemap_background = self.pyboy.tilemap_background + self.tilemap_window = self.pyboy.tilemap_window self.tilemap_use_background = True + self.mapping = np.asarray([x for x in range(768)], dtype=np.uint32) self.sprite_offset = 0 self.game_has_started = False self._tile_cache_invalid = True self._sprite_cache_invalid = True - self.game_area_section = game_area_section - self.game_area_wrap_around = game_area_wrap_around - width = self.game_area_section[2] - self.game_area_section[0] - height = self.game_area_section[3] - self.game_area_section[1] + self.shape = None + """ + The shape of the game area. This can be modified with `pyboy.PyBoy.game_area_dimensions`. + + Example: + ```python + >>> pyboy.game_wrapper.shape + (32, 32) + ``` + """ + self._set_dimensions(*game_area_section, game_area_follow_scxy) + width, height = self.shape self._cached_game_area_tiles_raw = array("B", [0xFF] * (width*height*4)) + self._cached_game_area_tiles = memoryview(self._cached_game_area_tiles_raw).cast("I", shape=(width, height)) self.saved_state = io.BytesIO() - if cythonmode: - self._cached_game_area_tiles = memoryview(self._cached_game_area_tiles_raw).cast("I", shape=(width, height)) - else: - v = memoryview(self._cached_game_area_tiles_raw).cast("I") - self._cached_game_area_tiles = [v[i:i + height] for i in range(0, height * width, height)] + def __cinit__(self, pyboy, mb, pyboy_argv, *args, **kwargs): + self.tilemap_background = self.pyboy.tilemap_background + self.tilemap_window = self.pyboy.tilemap_window def enabled(self): - return self.pyboy_argv.get("game_wrapper") and self.pyboy.cartridge_title() == self.cartridge_title + return self.cartridge_title is None or self.pyboy.cartridge_title == self.cartridge_title def post_tick(self): - raise NotImplementedError("post_tick not implemented in game wrapper") + self._tile_cache_invalid = True + self._sprite_cache_invalid = True def _set_timer_div(self, timer_div): if timer_div is None: @@ -175,6 +190,9 @@

    Module pyboy.plugins.base_plugin

    if not self.pyboy.frame_count == 0: 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) def reset_game(self, timer_div=None): """ @@ -213,9 +231,9 @@

    Module pyboy.plugins.base_plugin

    yy = self.game_area_section[1] width = self.game_area_section[2] height = self.game_area_section[3] - scanline_parameters = self.pyboy.botsupport_manager().screen().tilemap_position_list() + scanline_parameters = self.pyboy.screen.tilemap_position_list - if self.game_area_wrap_around: + if self.game_area_follow_scxy: self._cached_game_area_tiles = np.ndarray(shape=(height, width), dtype=np.uint32) for y in range(height): SCX = scanline_parameters[(yy+y) * 8][0] // 8 @@ -224,9 +242,9 @@

    Module pyboy.plugins.base_plugin

    _x = (xx+x+SCX) % 32 _y = (yy+y+SCY) % 32 if self.tilemap_use_background: - self._cached_game_area_tiles[y][x] = self.tilemap_background.tile_identifier(_x, _y) + self._cached_game_area_tiles[y, x] = self.tilemap_background.tile_identifier(_x, _y) else: - self._cached_game_area_tiles[y][x] = self.tilemap_window.tile_identifier(_x, _y) + self._cached_game_area_tiles[y, x] = self.tilemap_window.tile_identifier(_x, _y) else: if self.tilemap_use_background: self._cached_game_area_tiles = np.asarray( @@ -251,7 +269,7 @@

    Module pyboy.plugins.base_plugin

    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] @@ -261,36 +279,54 @@

    Module pyboy.plugins.base_plugin

    _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 - def _game_area_np(self, observation_type="tiles"): - if observation_type == "tiles": - return np.asarray(self.game_area(), dtype=np.uint16) - elif observation_type == "compressed": - try: - return self.tiles_compressed[np.asarray(self.game_area(), dtype=np.uint16)] - except AttributeError: - raise AttributeError( - f"Game wrapper miss the attribute tiles_compressed for observation_type : {observation_type}" - ) - elif observation_type == "minimal": - try: - return self.tiles_minimal[np.asarray(self.game_area(), dtype=np.uint16)] - except AttributeError: - raise AttributeError( - f"Game wrapper miss the attribute tiles_minimal for observation_type : {observation_type}" - ) - else: - raise ValueError(f"Invalid observation_type : {observation_type}") + def game_area_mapping(self, mapping, sprite_offest): + self.mapping = np.asarray(mapping, dtype=np.uint32) + self.sprite_offset = sprite_offest + + def _set_dimensions(self, x, y, width, height, follow_scrolling=True): + self.shape = (width, height) + self.game_area_section = (x, y, width, height) + self.game_area_follow_scxy = follow_scrolling def _sum_number_on_screen(self, x, y, length, blank_tile_identifier, tile_identifier_offset): number = 0 for i, x in enumerate(self.tilemap_background[x:x + length, y]): if x != blank_tile_identifier: number += (x+tile_identifier_offset) * (10**(length - 1 - i)) - return number
    + return number + + def __repr__(self): + adjust = 4 + # yapf: disable + + sprites = "\n".join([str(s) for s in self._sprites_on_screen()]) + + tiles_header = ( + " "*4 + "".join([f"{i: >4}" for i in range(self.shape[0])]) + "\n" + + "_"*(adjust*self.shape[0]+4) + ) + + tiles = "\n".join( + [ + (f"{i: <3}|" + "".join([str(tile).rjust(adjust) for tile in line])).strip() + for i, line in enumerate(self.game_area()) + ] + ) + + return ( + "Sprites on screen:\n" + + sprites + + "\n" + + "Tiles on screen:\n" + + tiles_header + + "\n" + + tiles + ) + # yapf: enable
    @@ -304,7 +340,7 @@

    Classes

    class PyBoyGameWrapper -(*args, game_area_section=(0, 0, 32, 32), game_area_wrap_around=False, **kwargs) +(*args, game_area_section=(0, 0, 32, 32), game_area_follow_scxy=False, **kwargs)

    This is the base-class for the game-wrappers. It provides some generic game-wrapping functionality, like game_area @@ -319,37 +355,51 @@

    Classes

    , which shows both sprites and tiles on the screen as a simple matrix. """ - argv = [("--game-wrapper", {"action": "store_true", "help": "Enable game wrapper for the current game"})] + cartridge_title = None - def __init__(self, *args, game_area_section=(0, 0, 32, 32), game_area_wrap_around=False, **kwargs): + mapping_one_to_one = np.arange(384 * 2, dtype=np.uint8) + """ + Example mapping of 1:1 + """ + def __init__(self, *args, game_area_section=(0, 0, 32, 32), game_area_follow_scxy=False, **kwargs): super().__init__(*args, **kwargs) - self.tilemap_background = self.pyboy.botsupport_manager().tilemap_background() - self.tilemap_window = self.pyboy.botsupport_manager().tilemap_window() + if not cythonmode: + self.tilemap_background = self.pyboy.tilemap_background + self.tilemap_window = self.pyboy.tilemap_window self.tilemap_use_background = True + self.mapping = np.asarray([x for x in range(768)], dtype=np.uint32) self.sprite_offset = 0 self.game_has_started = False self._tile_cache_invalid = True self._sprite_cache_invalid = True - self.game_area_section = game_area_section - self.game_area_wrap_around = game_area_wrap_around - width = self.game_area_section[2] - self.game_area_section[0] - height = self.game_area_section[3] - self.game_area_section[1] + self.shape = None + """ + The shape of the game area. This can be modified with `pyboy.PyBoy.game_area_dimensions`. + + Example: + ```python + >>> pyboy.game_wrapper.shape + (32, 32) + ``` + """ + self._set_dimensions(*game_area_section, game_area_follow_scxy) + width, height = self.shape self._cached_game_area_tiles_raw = array("B", [0xFF] * (width*height*4)) + self._cached_game_area_tiles = memoryview(self._cached_game_area_tiles_raw).cast("I", shape=(width, height)) self.saved_state = io.BytesIO() - if cythonmode: - self._cached_game_area_tiles = memoryview(self._cached_game_area_tiles_raw).cast("I", shape=(width, height)) - else: - v = memoryview(self._cached_game_area_tiles_raw).cast("I") - self._cached_game_area_tiles = [v[i:i + height] for i in range(0, height * width, height)] + def __cinit__(self, pyboy, mb, pyboy_argv, *args, **kwargs): + self.tilemap_background = self.pyboy.tilemap_background + self.tilemap_window = self.pyboy.tilemap_window def enabled(self): - return self.pyboy_argv.get("game_wrapper") and self.pyboy.cartridge_title() == self.cartridge_title + return self.cartridge_title is None or self.pyboy.cartridge_title == self.cartridge_title def post_tick(self): - raise NotImplementedError("post_tick not implemented in game wrapper") + self._tile_cache_invalid = True + self._sprite_cache_invalid = True def _set_timer_div(self, timer_div): if timer_div is None: @@ -373,6 +423,9 @@

    Classes

    if not self.pyboy.frame_count == 0: 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) def reset_game(self, timer_div=None): """ @@ -411,9 +464,9 @@

    Classes

    yy = self.game_area_section[1] width = self.game_area_section[2] height = self.game_area_section[3] - scanline_parameters = self.pyboy.botsupport_manager().screen().tilemap_position_list() + scanline_parameters = self.pyboy.screen.tilemap_position_list - if self.game_area_wrap_around: + if self.game_area_follow_scxy: self._cached_game_area_tiles = np.ndarray(shape=(height, width), dtype=np.uint32) for y in range(height): SCX = scanline_parameters[(yy+y) * 8][0] // 8 @@ -422,9 +475,9 @@

    Classes

    _x = (xx+x+SCX) % 32 _y = (yy+y+SCY) % 32 if self.tilemap_use_background: - self._cached_game_area_tiles[y][x] = self.tilemap_background.tile_identifier(_x, _y) + self._cached_game_area_tiles[y, x] = self.tilemap_background.tile_identifier(_x, _y) else: - self._cached_game_area_tiles[y][x] = self.tilemap_window.tile_identifier(_x, _y) + self._cached_game_area_tiles[y, x] = self.tilemap_window.tile_identifier(_x, _y) else: if self.tilemap_use_background: self._cached_game_area_tiles = np.asarray( @@ -449,7 +502,7 @@

    Classes

    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] @@ -459,36 +512,54 @@

    Classes

    _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 - def _game_area_np(self, observation_type="tiles"): - if observation_type == "tiles": - return np.asarray(self.game_area(), dtype=np.uint16) - elif observation_type == "compressed": - try: - return self.tiles_compressed[np.asarray(self.game_area(), dtype=np.uint16)] - except AttributeError: - raise AttributeError( - f"Game wrapper miss the attribute tiles_compressed for observation_type : {observation_type}" - ) - elif observation_type == "minimal": - try: - return self.tiles_minimal[np.asarray(self.game_area(), dtype=np.uint16)] - except AttributeError: - raise AttributeError( - f"Game wrapper miss the attribute tiles_minimal for observation_type : {observation_type}" - ) - else: - raise ValueError(f"Invalid observation_type : {observation_type}") + def game_area_mapping(self, mapping, sprite_offest): + self.mapping = np.asarray(mapping, dtype=np.uint32) + self.sprite_offset = sprite_offest + + def _set_dimensions(self, x, y, width, height, follow_scrolling=True): + self.shape = (width, height) + self.game_area_section = (x, y, width, height) + self.game_area_follow_scxy = follow_scrolling def _sum_number_on_screen(self, x, y, length, blank_tile_identifier, tile_identifier_offset): number = 0 for i, x in enumerate(self.tilemap_background[x:x + length, y]): if x != blank_tile_identifier: number += (x+tile_identifier_offset) * (10**(length - 1 - i)) - return number
    + return number + + def __repr__(self): + adjust = 4 + # yapf: disable + + sprites = "\n".join([str(s) for s in self._sprites_on_screen()]) + + tiles_header = ( + " "*4 + "".join([f"{i: >4}" for i in range(self.shape[0])]) + "\n" + + "_"*(adjust*self.shape[0]+4) + ) + + tiles = "\n".join( + [ + (f"{i: <3}|" + "".join([str(tile).rjust(adjust) for tile in line])).strip() + for i, line in enumerate(self.game_area()) + ] + ) + + return ( + "Sprites on screen:\n" + + sprites + + "\n" + + "Tiles on screen:\n" + + tiles_header + + "\n" + + tiles + ) + # yapf: enable

    Ancestors

    +

    Class variables

    +
    +
    var cartridge_title
    +
    +
    +
    +
    var mapping_one_to_one
    +
    +

    Example mapping of 1:1

    +
    +
    +

    Instance variables

    +
    +
    var shape
    +
    +

    The shape of the game area. This can be modified with PyBoy.game_area_dimensions().

    +

    Example:

    +
    >>> pyboy.game_wrapper.shape
    +(32, 32)
    +
    +
    +

    Methods

    @@ -536,7 +629,10 @@

    Args

    """ if not self.pyboy.frame_count == 0: - logger.warning("Calling start_game from an already running game. This might not work.")
    + logger.warning("Calling start_game from an already running game. This might not work.") + self.game_has_started = True + self.saved_state.seek(0) + self.pyboy.save_state(self.saved_state)
    @@ -621,7 +717,7 @@

    Returns

    memoryview: Simplified 2-dimensional memoryview of the screen """ - tiles_matrix = self._game_area_tiles() + tiles_matrix = self.mapping[self._game_area_tiles()] sprites = self._sprites_on_screen() xx = self.game_area_section[0] yy = self.game_area_section[1] @@ -631,11 +727,25 @@

    Returns

    _x = (s.x // 8) - xx _y = (s.y // 8) - yy if 0 <= _y < height and 0 <= _x < width: - tiles_matrix[_y][ - _x] = s.tile_identifier + self.sprite_offset # Adding offset to try to seperate sprites from tiles + tiles_matrix[_y][_x] = self.mapping[ + s.tile_identifier] + self.sprite_offset # Adding offset to try to seperate sprites from tiles return tiles_matrix
    +
    +def game_area_mapping(self, mapping, sprite_offest) +
    +
    +
    +
    + +Expand source code + +
    def game_area_mapping(self, mapping, sprite_offest):
    +    self.mapping = np.asarray(mapping, dtype=np.uint32)
    +    self.sprite_offset = sprite_offest
    +
    +
    @@ -656,12 +766,16 @@

    Index

    • PyBoyGameWrapper

      - diff --git a/docs/plugins/game_wrapper_kirby_dream_land.html b/docs/plugins/game_wrapper_kirby_dream_land.html index 0140e758e..04fc5bd93 100644 --- a/docs/plugins/game_wrapper_kirby_dream_land.html +++ b/docs/plugins/game_wrapper_kirby_dream_land.html @@ -34,32 +34,24 @@

      Module pyboy.plugins.game_wrapper_kirby_dream_landModule pyboy.plugins.game_wrapper_kirby_dream_landModule pyboy.plugins.game_wrapper_kirby_dream_landModule pyboy.plugins.game_wrapper_kirby_dream_landModule pyboy.plugins.game_wrapper_kirby_dream_land @@ -232,7 +192,7 @@

      Classes

      (*args, **kwargs)
      -

      This class wraps Kirby Dream Land, and provides easy access to score and a "fitness" score for AIs.

      +

      This class wraps Kirby Dream Land, and provides easy access for AIs.

      If you call print on an instance of this object, it will show an overview of everything this object provides.

      @@ -240,15 +200,13 @@

      Classes

      class GameWrapperKirbyDreamLand(PyBoyGameWrapper):
           """
      -    This class wraps Kirby Dream Land, and provides easy access to score and a "fitness" score for AIs.
      +    This class wraps Kirby Dream Land, and provides easy access for AIs.
       
           If you call `print` on an instance of this object, it will show an overview of everything this object provides.
           """
           cartridge_title = "KIRBY DREAM LA"
       
           def __init__(self, *args, **kwargs):
      -        self.shape = (20, 16)
      -        """The shape of the game area"""
               self.score = 0
               """The score provided by the game"""
               self.health = 0
      @@ -257,35 +215,26 @@ 

      Classes

      """The lives remaining provided by the game""" self._game_over = False """The game over state""" - self.fitness = 0 - """ - A built-in fitness scoring. Taking score, health, and lives left into account. - .. math:: - fitness = score \\cdot health \\cdot lives\\_left - """ - super().__init__(*args, game_area_section=(0, 0) + self.shape, game_area_wrap_around=True, **kwargs) + super().__init__(*args, game_area_section=(0, 0, 20, 16), game_area_follow_scxy=True, **kwargs) def post_tick(self): self._tile_cache_invalid = True self._sprite_cache_invalid = True self.score = 0 - score_digits = 5 + score_digits = 4 for n in range(score_digits): - self.score += self.pyboy.get_memory_value(0xD06F + n) * 10**(score_digits-n) + self.score += self.pyboy.memory[0xD070 + n] * 10**(score_digits - n) # Check if game is over prev_health = self.health - self.health = self.pyboy.get_memory_value(0xD086) + self.health = self.pyboy.memory[0xD086] if self.lives_left == 0: if prev_health > 0 and self.health == 0: self._game_over = True - self.lives_left = self.pyboy.get_memory_value(0xD089) - 1 - - if self.game_has_started: - self.fitness = self.score * self.health * self.lives_left + self.lives_left = self.pyboy.memory[0xD089] - 1 def start_game(self, timer_div=None): """ @@ -296,39 +245,30 @@

      Classes

      instantly. Kwargs: - timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. + * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. """ PyBoyGameWrapper.start_game(self, timer_div=timer_div) # Boot screen while True: - self.pyboy.tick() - self.tilemap_background.refresh_lcdc() + self.pyboy.tick(1, False) if self.tilemap_background[0:3, 16] == [231, 224, 235]: # 'HAL' on the first screen break # Wait for transition to finish (start screen) - for _ in range(25): - self.pyboy.tick() - - self.pyboy.send_input(WindowEvent.PRESS_BUTTON_START) + self.pyboy.tick(25, False) + self.pyboy.button("start") self.pyboy.tick() - self.pyboy.send_input(WindowEvent.RELEASE_BUTTON_START) # Wait for transition to finish (exit start screen, enter level intro screen) - for _ in range(60): - self.pyboy.tick() + self.pyboy.tick(60, False) # Skip level intro - self.pyboy.send_input(WindowEvent.PRESS_BUTTON_START) + self.pyboy.button("start") self.pyboy.tick() - self.pyboy.send_input(WindowEvent.RELEASE_BUTTON_START) # Wait for transition to finish (exit level intro screen, enter game) - for _ in range(60): - self.pyboy.tick() - - self.game_has_started = True + self.pyboy.tick(60, False) self.saved_state.seek(0) self.pyboy.save_state(self.saved_state) @@ -340,7 +280,7 @@

      Classes

      After calling `start_game`, you can call this method at any time to reset the game. Kwargs: - timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. + * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. """ PyBoyGameWrapper.reset_game(self, timer_div=timer_div) @@ -382,27 +322,13 @@

      Classes

      return self._game_over def __repr__(self): - adjust = 4 # yapf: disable return ( f"Kirby Dream Land:\n" + f"Score: {self.score}\n" + f"Health: {self.health}\n" + f"Lives left: {self.lives_left}\n" + - f"Fitness: {self.fitness}\n" + - "Sprites on screen:\n" + - "\n".join([str(s) for s in self._sprites_on_screen()]) + - "\n" + - "Tiles on screen:\n" + - " "*5 + "".join([f"{i: <4}" for i in range(10)]) + "\n" + - "_"*(adjust*20+4) + - "\n" + - "\n".join( - [ - f"{i: <3}| " + "".join([str(tile).ljust(adjust) for tile in line]) - for i, line in enumerate(self.game_area()) - ] - ) + super().__repr__() ) # yapf: enable
      @@ -413,10 +339,6 @@

      Ancestors

    Instance variables

    -
    var shape
    -
    -

    The shape of the game area

    -
    var score

    The score provided by the game

    @@ -429,11 +351,6 @@

    Instance variables

    The lives remaining provided by the game

    -
    var fitness
    -
    -

    A built-in fitness scoring. Taking score, health, and lives left into account.

    -

    fitness = score \cdot health \cdot lives\_left

    -

    Methods

    @@ -446,7 +363,9 @@

    Methods

    The state of the emulator is saved, and using reset_game, you can get back to this point of the game instantly.

    Kwargs

    -

    timer_div (int): Replace timer's DIV register with this value. Use None to randomize.

    +
      +
    • timer_div (int): Replace timer's DIV register with this value. Use None to randomize.
    • +
    Expand source code @@ -460,39 +379,30 @@

    Kwargs

    instantly. Kwargs: - timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. + * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. """ PyBoyGameWrapper.start_game(self, timer_div=timer_div) # Boot screen while True: - self.pyboy.tick() - self.tilemap_background.refresh_lcdc() + self.pyboy.tick(1, False) if self.tilemap_background[0:3, 16] == [231, 224, 235]: # 'HAL' on the first screen break # Wait for transition to finish (start screen) - for _ in range(25): - self.pyboy.tick() - - self.pyboy.send_input(WindowEvent.PRESS_BUTTON_START) + self.pyboy.tick(25, False) + self.pyboy.button("start") self.pyboy.tick() - self.pyboy.send_input(WindowEvent.RELEASE_BUTTON_START) # Wait for transition to finish (exit start screen, enter level intro screen) - for _ in range(60): - self.pyboy.tick() + self.pyboy.tick(60, False) # Skip level intro - self.pyboy.send_input(WindowEvent.PRESS_BUTTON_START) + self.pyboy.button("start") self.pyboy.tick() - self.pyboy.send_input(WindowEvent.RELEASE_BUTTON_START) # Wait for transition to finish (exit level intro screen, enter game) - for _ in range(60): - self.pyboy.tick() - - self.game_has_started = True + self.pyboy.tick(60, False) self.saved_state.seek(0) self.pyboy.save_state(self.saved_state) @@ -506,7 +416,9 @@

    Kwargs

    After calling start_game, you can call this method at any time to reset the game.

    Kwargs

    -

    timer_div (int): Replace timer's DIV register with this value. Use None to randomize.

    +
      +
    • timer_div (int): Replace timer's DIV register with this value. Use None to randomize.
    • +
    Expand source code @@ -516,7 +428,7 @@

    Kwargs

    After calling `start_game`, you can call this method at any time to reset the game. Kwargs: - timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. + * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. """ PyBoyGameWrapper.reset_game(self, timer_div=timer_div) @@ -595,6 +507,8 @@

    Inherited members

  • PyBoyGameWrapper:
@@ -621,11 +535,9 @@

start_game
  • reset_game
  • game_area
  • -
  • shape
  • score
  • health
  • lives_left
  • -
  • fitness
  • diff --git a/docs/plugins/game_wrapper_pokemon_gen1.html b/docs/plugins/game_wrapper_pokemon_gen1.html index 12b1630d9..92c7bdae8 100644 --- a/docs/plugins/game_wrapper_pokemon_gen1.html +++ b/docs/plugins/game_wrapper_pokemon_gen1.html @@ -34,21 +34,15 @@

    Module pyboy.plugins.game_wrapper_pokemon_gen1Module pyboy.plugins.game_wrapper_pokemon_gen1Module pyboy.plugins.game_wrapper_pokemon_gen1 @@ -165,40 +144,38 @@

    Classes

    cartridge_title = None def __init__(self, *args, **kwargs): - self.shape = (20, 18) - super().__init__(*args, game_area_section=(0, 0) + self.shape, game_area_wrap_around=True, **kwargs) - self.sprite_offset = 0x1000 + super().__init__(*args, game_area_section=(0, 0, 20, 18), game_area_follow_scxy=True, **kwargs) + self.sprite_offset = 0 def enabled(self): - return self.pyboy_argv.get("game_wrapper") and ((self.pyboy.cartridge_title() == "POKEMON RED") or - (self.pyboy.cartridge_title() == "POKEMON BLUE")) + return (self.pyboy.cartridge_title == "POKEMON RED") or (self.pyboy.cartridge_title == "POKEMON BLUE") def post_tick(self): self._tile_cache_invalid = True self._sprite_cache_invalid = True - scanline_parameters = self.pyboy.botsupport_manager().screen().tilemap_position_list() + scanline_parameters = self.pyboy.screen.tilemap_position_list WX = scanline_parameters[0][2] WY = scanline_parameters[0][3] self.use_background(WY != 0) def _get_screen_background_tilemap(self): - ### SIMILAR TO CURRENT pyboy.game_wrapper()._game_area_np(), BUT ONLY FOR BACKGROUND TILEMAP, SO NPC ARE SKIPPED + ### SIMILAR TO CURRENT pyboy.game_wrapper.game_area(), BUT ONLY FOR BACKGROUND TILEMAP, SO NPC ARE SKIPPED bsm = self.pyboy.botsupport_manager() ((scx, scy), (wx, wy)) = bsm.screen().tilemap_position() - tilemap = np.array(bsm.tilemap_background()[:, :]) + tilemap = np.array(bsm.tilemap_background[:, :]) return np.roll(np.roll(tilemap, -scy // 8, axis=0), -scx // 8, axis=1)[:18, :20] def _get_screen_walkable_matrix(self): walkable_tiles_indexes = [] - collision_ptr = self.pyboy.get_memory_value(0xD530) + (self.pyboy.get_memory_value(0xD531) << 8) - tileset_type = self.pyboy.get_memory_value(0xFFD7) + collision_ptr = self.pyboy.memory[0xD530] + (self.pyboy.memory[0xD531] << 8) + tileset_type = self.pyboy.memory[0xFFD7] if tileset_type > 0: - grass_tile_index = self.pyboy.get_memory_value(0xD535) + grass_tile_index = self.pyboy.memory[0xD535] if grass_tile_index != 0xFF: walkable_tiles_indexes.append(grass_tile_index + 0x100) for i in range(0x180): - tile_index = self.pyboy.get_memory_value(collision_ptr + i) + tile_index = self.pyboy.memory[collision_ptr + i] if tile_index == 0xFF: break else: @@ -220,23 +197,10 @@

    Classes

    return game_area def __repr__(self): - adjust = 4 # yapf: disable return ( f"Pokemon Gen 1:\n" + - "Sprites on screen:\n" + - "\n".join([str(s) for s in self._sprites_on_screen()]) + - "\n" + - "Tiles on screen:\n" + - " "*5 + "".join([f"{i: <4}" for i in range(10)]) + "\n" + - "_"*(adjust*20+4) + - "\n" + - "\n".join( - [ - f"{i: <3}| " + "".join([str(tile).ljust(adjust) for tile in line]) - for i, line in enumerate(self.game_area()) - ] - ) + super().__repr__() ) # yapf: enable @@ -257,8 +221,7 @@

    Methods

    Expand source code
    def enabled(self):
    -    return self.pyboy_argv.get("game_wrapper") and ((self.pyboy.cartridge_title() == "POKEMON RED") or
    -                                                    (self.pyboy.cartridge_title() == "POKEMON BLUE"))
    + return (self.pyboy.cartridge_title == "POKEMON RED") or (self.pyboy.cartridge_title == "POKEMON BLUE")
    @@ -289,7 +252,9 @@

    Inherited members

    diff --git a/docs/plugins/game_wrapper_super_mario_land.html b/docs/plugins/game_wrapper_super_mario_land.html index bc127196b..d65b8d315 100644 --- a/docs/plugins/game_wrapper_super_mario_land.html +++ b/docs/plugins/game_wrapper_super_mario_land.html @@ -34,20 +34,14 @@

    Module pyboy.plugins.game_wrapper_super_mario_landModule pyboy.plugins.game_wrapper_super_mario_landModule pyboy.plugins.game_wrapper_super_mario_landModule pyboy.plugins.game_wrapper_super_mario_landModule pyboy.plugins.game_wrapper_super_mario_landModule pyboy.plugins.game_wrapper_super_mario_landModule pyboy.plugins.game_wrapper_super_mario_landModule pyboy.plugins.game_wrapper_super_mario_landModule pyboy.plugins.game_wrapper_super_mario_landModule pyboy.plugins.game_wrapper_super_mario_landModule pyboy.plugins.game_wrapper_super_mario_landModule pyboy.plugins.game_wrapper_super_mario_land @@ -384,81 +538,269 @@

    Classes

    (*args, **kwargs)
    -

    This class wraps Super Mario Land, and provides easy access to score, coins, lives left, time left, world and a -"fitness" score 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.

    +

    This class wraps Super Mario Land, and provides easy access to score, coins, lives left, time left and world for AIs.

    +

    If you call print on an instance of this object, it will show an overview of everything this object provides.

    +

    Super Mario Land

    +
    >>> pyboy = PyBoy(supermarioland_rom)
    +>>> pyboy.game_wrapper.start_game()
    +>>> pyboy.tick(1, True)
    +True
    +>>> pyboy.screen.image.resize((320,288)).save('docs/plugins/supermarioland.png') # The exact screenshot shown above
    +>>> pyboy.game_wrapper
    +Super Mario Land: World 1-1
    +Coins: 0
    +lives_left: 2
    +Score: 0
    +Time left: 400
    +Level progress: 251
    +Sprites on screen:
    +Sprite [3]: Position: (35, 112), Shape: (8, 8), Tiles: (Tile: 0), On screen: True
    +Sprite [4]: Position: (43, 112), Shape: (8, 8), Tiles: (Tile: 1), On screen: True
    +Sprite [5]: Position: (35, 120), Shape: (8, 8), Tiles: (Tile: 16), On screen: True
    +Sprite [6]: Position: (43, 120), Shape: (8, 8), Tiles: (Tile: 17), On screen: True
    +Tiles on screen:
    +       0   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19
    +____________________________________________________________________________________
    +0  | 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339
    +1  | 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320
    +2  | 300 300 300 300 300 300 300 300 300 300 300 300 321 322 321 322 323 300 300 300
    +3  | 300 300 300 300 300 300 300 300 300 300 300 324 325 326 325 326 327 300 300 300
    +4  | 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300
    +5  | 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300
    +6  | 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300
    +7  | 300 300 300 300 300 300 300 300 310 350 300 300 300 300 300 300 300 300 300 300
    +8  | 300 300 300 300 300 300 300 310 300 300 350 300 300 300 300 300 300 300 300 300
    +9  | 300 300 300 300 300 129 310 300 300 300 300 350 300 300 300 300 300 300 300 300
    +10 | 300 300 300 300 300 310 300 300 300 300 300 300 350 300 300 300 300 300 300 300
    +11 | 300 300 310 350 310 300 300 300 300 306 307 300 300 350 300 300 300 300 300 300
    +12 | 300 368 369 300   0   1 300 306 307 305 300 300 300 300 350 300 300 300 300 300
    +13 | 310 370 371 300  16  17 300 305 300 305 300 300 300 300 300 350 300 300 300 300
    +14 | 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352
    +15 | 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353
    +
    Expand source code
    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.
    -
    -    __Only world 1-1 is officially supported at the moment. Support for more worlds coming soon.__
    +    This class wraps Super Mario Land, and provides easy access to score, coins, lives left, time left and world for AIs.
     
         If you call `print` on an instance of this object, it will show an overview of everything this object provides.
    +
    +    ![Super Mario Land](supermarioland.png)
    +    ```python
    +    >>> pyboy = PyBoy(supermarioland_rom)
    +    >>> pyboy.game_wrapper.start_game()
    +    >>> pyboy.tick(1, True)
    +    True
    +    >>> pyboy.screen.image.resize((320,288)).save('docs/plugins/supermarioland.png') # The exact screenshot shown above
    +    >>> pyboy.game_wrapper
    +    Super Mario Land: World 1-1
    +    Coins: 0
    +    lives_left: 2
    +    Score: 0
    +    Time left: 400
    +    Level progress: 251
    +    Sprites on screen:
    +    Sprite [3]: Position: (35, 112), Shape: (8, 8), Tiles: (Tile: 0), On screen: True
    +    Sprite [4]: Position: (43, 112), Shape: (8, 8), Tiles: (Tile: 1), On screen: True
    +    Sprite [5]: Position: (35, 120), Shape: (8, 8), Tiles: (Tile: 16), On screen: True
    +    Sprite [6]: Position: (43, 120), Shape: (8, 8), Tiles: (Tile: 17), On screen: True
    +    Tiles on screen:
    +           0   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19
    +    ____________________________________________________________________________________
    +    0  | 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339
    +    1  | 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320
    +    2  | 300 300 300 300 300 300 300 300 300 300 300 300 321 322 321 322 323 300 300 300
    +    3  | 300 300 300 300 300 300 300 300 300 300 300 324 325 326 325 326 327 300 300 300
    +    4  | 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300
    +    5  | 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300
    +    6  | 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300
    +    7  | 300 300 300 300 300 300 300 300 310 350 300 300 300 300 300 300 300 300 300 300
    +    8  | 300 300 300 300 300 300 300 310 300 300 350 300 300 300 300 300 300 300 300 300
    +    9  | 300 300 300 300 300 129 310 300 300 300 300 350 300 300 300 300 300 300 300 300
    +    10 | 300 300 300 300 300 310 300 300 300 300 300 300 350 300 300 300 300 300 300 300
    +    11 | 300 300 310 350 310 300 300 300 300 306 307 300 300 350 300 300 300 300 300 300
    +    12 | 300 368 369 300   0   1 300 306 307 305 300 300 300 300 350 300 300 300 300 300
    +    13 | 310 370 371 300  16  17 300 305 300 305 300 300 300 300 300 350 300 300 300 300
    +    14 | 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352
    +    15 | 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353
    +    ```
    +
         """
         cartridge_title = "SUPER MARIOLAN"
    -    tiles_compressed = tiles_compressed
    -    tiles_minimal = tiles_minimal
    -
    +    mapping_compressed = mapping_compressed
    +    """
    +    Compressed mapping for `pyboy.PyBoy.game_area_mapping`
    +
    +    Example using `mapping_compressed`, Mario is `1`. He is standing on the ground which is `10`:
    +    ```python
    +    >>> pyboy = PyBoy(supermarioland_rom)
    +    >>> pyboy.game_wrapper.start_game()
    +    >>> pyboy.game_wrapper.game_area_mapping(pyboy.game_wrapper.mapping_compressed, 0)
    +    >>> pyboy.game_wrapper.game_area()
    +    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,  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],
    +           [ 0,  0,  0,  0,  0, 13,  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, 14, 14,  0,  1,  1,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0],
    +           [ 0, 14, 14,  0,  1,  1,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0],
    +           [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10],
    +           [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10]], dtype=uint32)
    +    ```
    +    """
    +    mapping_minimal = mapping_minimal
    +    """
    +    Minimal mapping for `pyboy.PyBoy.game_area_mapping`
    +
    +    Example using `mapping_minimal`, Mario is `1`. He is standing on the ground which is `3`:
    +    ```python
    +    >>> pyboy = PyBoy(supermarioland_rom)
    +    >>> pyboy.game_wrapper.start_game()
    +    >>> pyboy.game_wrapper.game_area_mapping(pyboy.game_wrapper.mapping_minimal, 0)
    +    >>> pyboy.game_wrapper.game_area()
    +    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, 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],
    +           [0, 0, 0, 0, 0, 3, 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, 3, 3, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    +           [0, 3, 3, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    +           [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
    +           [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]], dtype=uint32)
    +    ```
    +    """
         def __init__(self, *args, **kwargs):
    -        self.shape = (20, 16)
    -        """The shape of the game area"""
             self.world = (0, 0)
    -        """Provides the current "world" Mario is in, as a tuple of as two integers (world, level)."""
    +        """
    +        Provides the current "world" Mario is in, as a tuple of as two integers (world, level).
    +
    +        You can force a level change with `set_world_level`.
    +
    +        Example:
    +        ```python
    +        >>> pyboy = PyBoy(supermarioland_rom)
    +        >>> pyboy.game_wrapper.start_game()
    +        >>> pyboy.game_wrapper.world
    +        (1, 1)
    +        ```
    +
    +        """
             self.coins = 0
    -        """The number of collected coins."""
    +        """
    +        The number of collected coins.
    +
    +        Example:
    +        ```python
    +        >>> pyboy = PyBoy(supermarioland_rom)
    +        >>> pyboy.game_wrapper.start_game()
    +        >>> pyboy.game_wrapper.coins
    +        0
    +        ```
    +        """
             self.lives_left = 0
    -        """The number of lives Mario has left"""
    +        """
    +        The number of lives Mario has left
    +
    +        Example:
    +        ```python
    +        >>> pyboy = PyBoy(supermarioland_rom)
    +        >>> pyboy.game_wrapper.start_game()
    +        >>> pyboy.game_wrapper.lives_left
    +        2
    +        ```
    +        """
             self.score = 0
    -        """The score provided by the game"""
    +        """
    +        The score provided by the game
    +
    +        Example:
    +        ```python
    +        >>> pyboy = PyBoy(supermarioland_rom)
    +        >>> pyboy.game_wrapper.start_game()
    +        >>> pyboy.game_wrapper.score
    +        0
    +        ```
    +        """
             self.time_left = 0
    -        """The number of seconds left to finish the level"""
    +        """
    +        The number of seconds left to finish the level
    +
    +        Example:
    +        ```python
    +        >>> pyboy = PyBoy(supermarioland_rom)
    +        >>> pyboy.game_wrapper.start_game()
    +        >>> pyboy.game_wrapper.time_left
    +        400
    +        ```
    +        """
             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)
    +        An integer of the current "global" X position in this level. Can be used for AI scoring.
    +
    +        Example:
    +        ```python
    +        >>> pyboy = PyBoy(supermarioland_rom)
    +        >>> pyboy.game_wrapper.start_game()
    +        >>> pyboy.game_wrapper.level_progress
    +        251
    +        ```
             """
     
    -        super().__init__(*args, game_area_section=(0, 2) + self.shape, game_area_wrap_around=True, **kwargs)
    +        super().__init__(*args, game_area_section=(0, 2, 20, 16), 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.
     
             This should only be called when the game has started.
     
    +        Example:
    +        ```python
    +        >>> pyboy = PyBoy(supermarioland_rom)
    +        >>> pyboy.game_wrapper.start_game()
    +        >>> pyboy.game_wrapper.lives_left
    +        2
    +        >>> pyboy.game_wrapper.set_lives_left(10)
    +        >>> pyboy.tick(1, False)
    +        True
    +        >>> pyboy.game_wrapper.lives_left
    +        10
    +        ```
    +
             Args:
                 amount (int): The wanted number of lives
             """
    @@ -468,23 +810,32 @@ 

    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): """ Patches the handler for pressing start in the menu. It hardcodes a world and level to always "continue" from. + Example: + ```python + >>> pyboy = PyBoy(supermarioland_rom) + >>> pyboy.game_wrapper.set_world_level(3, 2) + >>> pyboy.game_wrapper.start_game() + >>> pyboy.game_wrapper.world + (3, 2) + ``` + Args: world (int): The world to select a level from, 0-3 level (int): The level to start from, 0-2 """ for i in range(0x450, 0x461): - self.pyboy.override_memory_value(0, i, 0x00) + self.pyboy.memory[0, i] = 0x00 patch1 = [ 0x3E, # LD A, d8 @@ -492,7 +843,7 @@

    Classes

    ] for i, byte in enumerate(patch1): - self.pyboy.override_memory_value(0, 0x451 + i, byte) + self.pyboy.memory[0, 0x451 + i] = byte def start_game(self, timer_div=None, world_level=None, unlock_level_select=False): """ @@ -508,10 +859,18 @@

    Classes

    If you're not using the game wrapper for unattended use, you can unlock the level selector for the main menu. Enabling the selector, will make this function return before entering the game. + Example: + ```python + >>> pyboy = PyBoy(supermarioland_rom) + >>> pyboy.game_wrapper.start_game(world_level=(4,1)) + >>> pyboy.game_wrapper.world + (4, 1) + ``` + Kwargs: - timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. - world_level (tuple): (world, level) to start the game from - unlock_level_select (bool): Unlock level selector menu + * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. + * world_level (tuple): (world, level) to start the game from + * unlock_level_select (bool): Unlock level selector menu """ PyBoyGameWrapper.start_game(self, timer_div=timer_div) @@ -520,28 +879,24 @@

    Classes

    # Boot screen while True: - self.pyboy.tick() + self.pyboy.tick(1, False) if self.tilemap_background[6:11, 13] == [284, 285, 266, 283, 285]: # "START" on the main menu break - self.pyboy.tick() - self.pyboy.tick() - self.pyboy.tick() - self.pyboy.send_input(WindowEvent.PRESS_BUTTON_START) - self.pyboy.tick() - self.pyboy.send_input(WindowEvent.RELEASE_BUTTON_START) + self.pyboy.tick(3, False) + self.pyboy.button("start") + self.pyboy.tick(1, False) while True: if unlock_level_select and self.pyboy.frame_count == 71: # An arbitrary frame count, where the write will work - self.pyboy.set_memory_value(ADDR_WIN_COUNT, 2 if unlock_level_select else 0) + self.pyboy.memory[ADDR_WIN_COUNT] = 2 if unlock_level_select else 0 break - self.pyboy.tick() - self.tilemap_background.refresh_lcdc() + self.pyboy.tick(1, False) # "MARIO" in the title bar and 0 is placed at score if self.tilemap_background[0:5, 0] == [278, 266, 283, 274, 280] and \ self.tilemap_background[5, 1] == 256: - self.game_has_started = True + # Game has started break self.saved_state.seek(0) @@ -551,13 +906,20 @@

    Classes

    def reset_game(self, timer_div=None): """ - After calling `start_game`, use this method to reset Mario to the beginning of world 1-1. + After calling `start_game`, use this method to reset Mario to the beginning of `start_game`. - If you want to reset to later parts of the game -- for example world 1-2 or 3-1 -- use the methods - `pyboy.PyBoy.save_state` and `pyboy.PyBoy.load_state`. + If you want to reset to other worlds or levels of the game -- for example world 1-2 or 3-1 -- reset the entire + emulator and provide the `world_level` on `start_game`. + + Example: + ```python + >>> pyboy = PyBoy(supermarioland_rom) + >>> pyboy.game_wrapper.start_game() + >>> pyboy.game_wrapper.reset_game() + ``` Kwargs: - timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. + * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. """ PyBoyGameWrapper.reset_game(self, timer_div=timer_div) @@ -571,26 +933,28 @@

    Classes

    In Super Mario Land, this is almost the entire screen, expect for the top part showing the score, lives left and so on. These values can be found in the variables of this class. - In this example, Mario is `0`, `1`, `16` and `17`. He is standing on the ground which is `352` and `353`: - ```text - 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 - ____________________________________________________________________________________ - 0 | 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 - 1 | 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 - 2 | 300 300 300 300 300 300 300 300 300 300 300 300 321 322 321 322 323 300 300 300 - 3 | 300 300 300 300 300 300 300 300 300 300 300 324 325 326 325 326 327 300 300 300 - 4 | 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 - 5 | 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 - 6 | 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 - 7 | 300 300 300 300 300 300 300 300 310 350 300 300 300 300 300 300 300 300 300 300 - 8 | 300 300 300 300 300 300 300 310 300 300 350 300 300 300 300 300 300 300 300 300 - 9 | 300 300 300 300 300 129 310 300 300 300 300 350 300 300 300 300 300 300 300 300 - 10 | 300 300 300 300 300 310 300 300 300 300 300 300 350 300 300 300 300 300 300 300 - 11 | 300 300 310 350 310 300 300 300 300 306 307 300 300 350 300 300 300 300 300 300 - 12 | 300 368 369 300 0 1 300 306 307 305 300 300 300 300 350 300 300 300 300 300 - 13 | 310 370 371 300 16 17 300 305 300 305 300 300 300 300 300 350 300 300 300 300 - 14 | 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 - 15 | 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 + In this example using `mapping_minimal`, Mario is `1`. He is standing on the ground which is `3`: + ```python + >>> pyboy = PyBoy(supermarioland_rom) + >>> pyboy.game_wrapper.start_game() + >>> pyboy.game_wrapper.game_area_mapping(pyboy.game_wrapper.mapping_minimal, 0) + >>> pyboy.game_wrapper.game_area() + 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, 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], + [0, 0, 0, 0, 0, 3, 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, 3, 3, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 3, 3, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3], + [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]], dtype=uint32) ``` Returns @@ -603,32 +967,18 @@

    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 # yapf: disable return ( - f"Super Mario Land: World {'-'.join([str(i) for i in self.world])}\n" + + f"Super Mario Land: World {'-'.join([str(i) for i in self.world])}\n" f"Coins: {self.coins}\n" + f"lives_left: {self.lives_left}\n" + f"Score: {self.score}\n" + f"Time left: {self.time_left}\n" + f"Level progress: {self.level_progress}\n" + - f"Fitness: {self.fitness}\n" + - "Sprites on screen:\n" + - "\n".join([str(s) for s in self._sprites_on_screen()]) + - "\n" + - "Tiles on screen:\n" + - " "*5 + "".join([f"{i: <4}" for i in range(20)]) + "\n" + - "_"*(adjust*20+4) + - "\n" + - "\n".join( - [ - f"{i: <3}| " + "".join([str(tile).ljust(adjust) for tile in line]) - for i, line in enumerate(self.game_area()) - ] - ) + super().__repr__() ) # yapf: enable
    @@ -639,49 +989,121 @@

    Ancestors

    Class variables

    -
    var tiles_compressed
    +
    var mapping_compressed
    -
    +

    Compressed mapping for PyBoy.game_area_mapping()

    +

    Example using mapping_compressed, Mario is 1. He is standing on the ground which is 10:

    +
    >>> pyboy = PyBoy(supermarioland_rom)
    +>>> pyboy.game_wrapper.start_game()
    +>>> pyboy.game_wrapper.game_area_mapping(pyboy.game_wrapper.mapping_compressed, 0)
    +>>> pyboy.game_wrapper.game_area()
    +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,  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],
    +       [ 0,  0,  0,  0,  0, 13,  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, 14, 14,  0,  1,  1,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0],
    +       [ 0, 14, 14,  0,  1,  1,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0],
    +       [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10],
    +       [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10]], dtype=uint32)
    +
    -
    var tiles_minimal
    +
    var mapping_minimal
    -
    +

    Minimal mapping for PyBoy.game_area_mapping()

    +

    Example using mapping_minimal, Mario is 1. He is standing on the ground which is 3:

    +
    >>> pyboy = PyBoy(supermarioland_rom)
    +>>> pyboy.game_wrapper.start_game()
    +>>> pyboy.game_wrapper.game_area_mapping(pyboy.game_wrapper.mapping_minimal, 0)
    +>>> pyboy.game_wrapper.game_area()
    +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, 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],
    +       [0, 0, 0, 0, 0, 3, 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, 3, 3, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    +       [0, 3, 3, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    +       [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
    +       [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]], dtype=uint32)
    +

    Instance variables

    -
    var shape
    -
    -

    The shape of the game area

    -
    var world
    -

    Provides the current "world" Mario is in, as a tuple of as two integers (world, level).

    +

    Provides the current "world" Mario is in, as a tuple of as two integers (world, level).

    +

    You can force a level change with set_world_level.

    +

    Example:

    +
    >>> pyboy = PyBoy(supermarioland_rom)
    +>>> pyboy.game_wrapper.start_game()
    +>>> pyboy.game_wrapper.world
    +(1, 1)
    +
    var coins
    -

    The number of collected coins.

    +

    The number of collected coins.

    +

    Example:

    +
    >>> pyboy = PyBoy(supermarioland_rom)
    +>>> pyboy.game_wrapper.start_game()
    +>>> pyboy.game_wrapper.coins
    +0
    +
    var lives_left
    -

    The number of lives Mario has left

    +

    The number of lives Mario has left

    +

    Example:

    +
    >>> pyboy = PyBoy(supermarioland_rom)
    +>>> pyboy.game_wrapper.start_game()
    +>>> pyboy.game_wrapper.lives_left
    +2
    +
    var score
    -

    The score provided by the game

    +

    The score provided by the game

    +

    Example:

    +
    >>> pyboy = PyBoy(supermarioland_rom)
    +>>> pyboy.game_wrapper.start_game()
    +>>> pyboy.game_wrapper.score
    +0
    +
    var time_left
    -

    The number of seconds left to finish the level

    +

    The number of seconds left to finish the level

    +

    Example:

    +
    >>> pyboy = PyBoy(supermarioland_rom)
    +>>> pyboy.game_wrapper.start_game()
    +>>> pyboy.game_wrapper.time_left
    +400
    +
    var level_progress
    -

    An integer of the current "global" X position in this level. Can be used for AI scoring.

    -
    -
    var fitness
    -
    -

    A built-in fitness scoring. Taking points, level progression, time left, and lives left into account.

    -

    fitness = (lives\_left \cdot 10000) + (score + time\_left \cdot 10) + (\_level\_progress\_max \cdot 10)

    +

    An integer of the current "global" X position in this level. Can be used for AI scoring.

    +

    Example:

    +
    >>> pyboy = PyBoy(supermarioland_rom)
    +>>> pyboy.game_wrapper.start_game()
    +>>> pyboy.game_wrapper.level_progress
    +251
    +

    Methods

    @@ -692,6 +1114,17 @@

    Methods

    Set the amount lives to any number between 0 and 99.

    This should only be called when the game has started.

    +

    Example:

    +
    >>> pyboy = PyBoy(supermarioland_rom)
    +>>> pyboy.game_wrapper.start_game()
    +>>> pyboy.game_wrapper.lives_left
    +2
    +>>> pyboy.game_wrapper.set_lives_left(10)
    +>>> pyboy.tick(1, False)
    +True
    +>>> pyboy.game_wrapper.lives_left
    +10
    +

    Args

    amount : int
    @@ -707,6 +1140,19 @@

    Args

    This should only be called when the game has started. + Example: + ```python + >>> pyboy = PyBoy(supermarioland_rom) + >>> pyboy.game_wrapper.start_game() + >>> pyboy.game_wrapper.lives_left + 2 + >>> pyboy.game_wrapper.set_lives_left(10) + >>> pyboy.tick(1, False) + True + >>> pyboy.game_wrapper.lives_left + 10 + ``` + Args: amount (int): The wanted number of lives """ @@ -716,11 +1162,11 @@

    Args

    if 0 <= amount <= 99: tens = amount // 10 ones = amount % 10 - self.pyboy.set_memory_value(ADDR_LIVES_LEFT, (tens << 4) | ones) - self.pyboy.set_memory_value(ADDR_LIVES_LEFT_DISPLAY, tens) - self.pyboy.set_memory_value(ADDR_LIVES_LEFT_DISPLAY + 1, ones) + self.pyboy.memory[ADDR_LIVES_LEFT] = (tens << 4) | ones + self.pyboy.memory[ADDR_LIVES_LEFT_DISPLAY] = tens + self.pyboy.memory[ADDR_LIVES_LEFT_DISPLAY + 1] = ones else: - logger.error(f"{amount} is out of bounds. Only values between 0 and 99 allowed.") + logger.error("%d is out of bounds. Only values between 0 and 99 allowed.", amount)
    @@ -728,6 +1174,13 @@

    Args

    Patches the handler for pressing start in the menu. It hardcodes a world and level to always "continue" from.

    +

    Example:

    +
    >>> pyboy = PyBoy(supermarioland_rom)
    +>>> pyboy.game_wrapper.set_world_level(3, 2)
    +>>> pyboy.game_wrapper.start_game()
    +>>> pyboy.game_wrapper.world
    +(3, 2)
    +

    Args

    world : int
    @@ -743,13 +1196,22 @@

    Args

    """ Patches the handler for pressing start in the menu. It hardcodes a world and level to always "continue" from. + Example: + ```python + >>> pyboy = PyBoy(supermarioland_rom) + >>> pyboy.game_wrapper.set_world_level(3, 2) + >>> pyboy.game_wrapper.start_game() + >>> pyboy.game_wrapper.world + (3, 2) + ``` + Args: world (int): The world to select a level from, 0-3 level (int): The level to start from, 0-2 """ for i in range(0x450, 0x461): - self.pyboy.override_memory_value(0, i, 0x00) + self.pyboy.memory[0, i] = 0x00 patch1 = [ 0x3E, # LD A, d8 @@ -757,7 +1219,7 @@

    Args

    ] for i, byte in enumerate(patch1): - self.pyboy.override_memory_value(0, 0x451 + i, byte) + self.pyboy.memory[0, 0x451 + i] = byte
    @@ -772,10 +1234,18 @@

    Args

    the optional keyword-argument world_level.

    If you're not using the game wrapper for unattended use, you can unlock the level selector for the main menu. Enabling the selector, will make this function return before entering the game.

    +

    Example:

    +
    >>> pyboy = PyBoy(supermarioland_rom)
    +>>> pyboy.game_wrapper.start_game(world_level=(4,1))
    +>>> pyboy.game_wrapper.world
    +(4, 1)
    +

    Kwargs

    -

    timer_div (int): Replace timer's DIV register with this value. Use None to randomize. -world_level (tuple): (world, level) to start the game from -unlock_level_select (bool): Unlock level selector menu

    +
      +
    • timer_div (int): Replace timer's DIV register with this value. Use None to randomize.
    • +
    • world_level (tuple): (world, level) to start the game from
    • +
    • unlock_level_select (bool): Unlock level selector menu
    • +
    Expand source code @@ -794,10 +1264,18 @@

    Kwargs

    If you're not using the game wrapper for unattended use, you can unlock the level selector for the main menu. Enabling the selector, will make this function return before entering the game. + Example: + ```python + >>> pyboy = PyBoy(supermarioland_rom) + >>> pyboy.game_wrapper.start_game(world_level=(4,1)) + >>> pyboy.game_wrapper.world + (4, 1) + ``` + Kwargs: - timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. - world_level (tuple): (world, level) to start the game from - unlock_level_select (bool): Unlock level selector menu + * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. + * world_level (tuple): (world, level) to start the game from + * unlock_level_select (bool): Unlock level selector menu """ PyBoyGameWrapper.start_game(self, timer_div=timer_div) @@ -806,28 +1284,24 @@

    Kwargs

    # Boot screen while True: - self.pyboy.tick() + self.pyboy.tick(1, False) if self.tilemap_background[6:11, 13] == [284, 285, 266, 283, 285]: # "START" on the main menu break - self.pyboy.tick() - self.pyboy.tick() - self.pyboy.tick() - self.pyboy.send_input(WindowEvent.PRESS_BUTTON_START) - self.pyboy.tick() - self.pyboy.send_input(WindowEvent.RELEASE_BUTTON_START) + self.pyboy.tick(3, False) + self.pyboy.button("start") + self.pyboy.tick(1, False) while True: if unlock_level_select and self.pyboy.frame_count == 71: # An arbitrary frame count, where the write will work - self.pyboy.set_memory_value(ADDR_WIN_COUNT, 2 if unlock_level_select else 0) + self.pyboy.memory[ADDR_WIN_COUNT] = 2 if unlock_level_select else 0 break - self.pyboy.tick() - self.tilemap_background.refresh_lcdc() + self.pyboy.tick(1, False) # "MARIO" in the title bar and 0 is placed at score if self.tilemap_background[0:5, 0] == [278, 266, 283, 274, 280] and \ self.tilemap_background[5, 1] == 256: - self.game_has_started = True + # Game has started break self.saved_state.seek(0) @@ -840,24 +1314,38 @@

    Kwargs

    def reset_game(self, timer_div=None)
    -

    After calling start_game, use this method to reset Mario to the beginning of world 1-1.

    -

    If you want to reset to later parts of the game – for example world 1-2 or 3-1 – use the methods -PyBoy.save_state() and PyBoy.load_state().

    +

    After calling start_game, use this method to reset Mario to the beginning of start_game.

    +

    If you want to reset to other worlds or levels of the game – for example world 1-2 or 3-1 – reset the entire +emulator and provide the world_level on start_game.

    +

    Example:

    +
    >>> pyboy = PyBoy(supermarioland_rom)
    +>>> pyboy.game_wrapper.start_game()
    +>>> pyboy.game_wrapper.reset_game()
    +

    Kwargs

    -

    timer_div (int): Replace timer's DIV register with this value. Use None to randomize.

    +
      +
    • timer_div (int): Replace timer's DIV register with this value. Use None to randomize.
    • +
    Expand source code
    def reset_game(self, timer_div=None):
         """
    -    After calling `start_game`, use this method to reset Mario to the beginning of world 1-1.
    +    After calling `start_game`, use this method to reset Mario to the beginning of `start_game`.
    +
    +    If you want to reset to other worlds or levels of the game -- for example world 1-2 or 3-1 -- reset the entire
    +    emulator and provide the `world_level` on `start_game`.
     
    -    If you want to reset to later parts of the game -- for example world 1-2 or 3-1 -- use the methods
    -    `pyboy.PyBoy.save_state` and `pyboy.PyBoy.load_state`.
    +    Example:
    +    ```python
    +    >>> pyboy = PyBoy(supermarioland_rom)
    +    >>> pyboy.game_wrapper.start_game()
    +    >>> pyboy.game_wrapper.reset_game()
    +    ```
     
         Kwargs:
    -        timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
    +        * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize.
         """
         PyBoyGameWrapper.reset_game(self, timer_div=timer_div)
     
    @@ -872,25 +1360,27 @@ 

    Kwargs

    machine learning applications.

    In Super Mario Land, this is almost the entire screen, expect for the top part showing the score, lives left and so on. These values can be found in the variables of this class.

    -

    In this example, Mario is 0, 1, 16 and 17. He is standing on the ground which is 352 and 353:

    -
         0   1   2   3   4   5   6   7   8   9   10  11  12  13  14  15  16  17  18  19
    -____________________________________________________________________________________
    -0  | 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339
    -1  | 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320
    -2  | 300 300 300 300 300 300 300 300 300 300 300 300 321 322 321 322 323 300 300 300
    -3  | 300 300 300 300 300 300 300 300 300 300 300 324 325 326 325 326 327 300 300 300
    -4  | 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300
    -5  | 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300
    -6  | 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300
    -7  | 300 300 300 300 300 300 300 300 310 350 300 300 300 300 300 300 300 300 300 300
    -8  | 300 300 300 300 300 300 300 310 300 300 350 300 300 300 300 300 300 300 300 300
    -9  | 300 300 300 300 300 129 310 300 300 300 300 350 300 300 300 300 300 300 300 300
    -10 | 300 300 300 300 300 310 300 300 300 300 300 300 350 300 300 300 300 300 300 300
    -11 | 300 300 310 350 310 300 300 300 300 306 307 300 300 350 300 300 300 300 300 300
    -12 | 300 368 369 300 0   1   300 306 307 305 300 300 300 300 350 300 300 300 300 300
    -13 | 310 370 371 300 16  17  300 305 300 305 300 300 300 300 300 350 300 300 300 300
    -14 | 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352
    -15 | 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353
    +

    In this example using mapping_minimal, Mario is 1. He is standing on the ground which is 3:

    +
    >>> pyboy = PyBoy(supermarioland_rom)
    +>>> pyboy.game_wrapper.start_game()
    +>>> pyboy.game_wrapper.game_area_mapping(pyboy.game_wrapper.mapping_minimal, 0)
    +>>> pyboy.game_wrapper.game_area()
    +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, 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],
    +       [0, 0, 0, 0, 0, 3, 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, 3, 3, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    +       [0, 3, 3, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    +       [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
    +       [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]], dtype=uint32)
     

    Returns

    @@ -909,26 +1399,28 @@

    Returns

    In Super Mario Land, this is almost the entire screen, expect for the top part showing the score, lives left and so on. These values can be found in the variables of this class. - In this example, Mario is `0`, `1`, `16` and `17`. He is standing on the ground which is `352` and `353`: - ```text - 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 - ____________________________________________________________________________________ - 0 | 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 339 - 1 | 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 320 - 2 | 300 300 300 300 300 300 300 300 300 300 300 300 321 322 321 322 323 300 300 300 - 3 | 300 300 300 300 300 300 300 300 300 300 300 324 325 326 325 326 327 300 300 300 - 4 | 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 - 5 | 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 - 6 | 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 300 - 7 | 300 300 300 300 300 300 300 300 310 350 300 300 300 300 300 300 300 300 300 300 - 8 | 300 300 300 300 300 300 300 310 300 300 350 300 300 300 300 300 300 300 300 300 - 9 | 300 300 300 300 300 129 310 300 300 300 300 350 300 300 300 300 300 300 300 300 - 10 | 300 300 300 300 300 310 300 300 300 300 300 300 350 300 300 300 300 300 300 300 - 11 | 300 300 310 350 310 300 300 300 300 306 307 300 300 350 300 300 300 300 300 300 - 12 | 300 368 369 300 0 1 300 306 307 305 300 300 300 300 350 300 300 300 300 300 - 13 | 310 370 371 300 16 17 300 305 300 305 300 300 300 300 300 350 300 300 300 300 - 14 | 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 352 - 15 | 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 353 + In this example using `mapping_minimal`, Mario is `1`. He is standing on the ground which is `3`: + ```python + >>> pyboy = PyBoy(supermarioland_rom) + >>> pyboy.game_wrapper.start_game() + >>> pyboy.game_wrapper.game_area_mapping(pyboy.game_wrapper.mapping_minimal, 0) + >>> pyboy.game_wrapper.game_area() + 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, 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], + [0, 0, 0, 0, 0, 3, 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, 3, 3, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 3, 3, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3], + [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]], dtype=uint32) ``` Returns @@ -945,6 +1437,8 @@

    Inherited members

  • PyBoyGameWrapper:
  • @@ -973,16 +1467,14 @@

    start_game
  • reset_game
  • game_area
  • -
  • shape
  • world
  • coins
  • lives_left
  • score
  • time_left
  • level_progress
  • -
  • fitness
  • -
  • tiles_compressed
  • -
  • tiles_minimal
  • +
  • mapping_compressed
  • +
  • mapping_minimal
  • diff --git a/docs/plugins/game_wrapper_tetris.html b/docs/plugins/game_wrapper_tetris.html index 3d4a533e4..e2a22eed3 100644 --- a/docs/plugins/game_wrapper_tetris.html +++ b/docs/plugins/game_wrapper_tetris.html @@ -34,21 +34,16 @@

    Module pyboy.plugins.game_wrapper_tetris

    "GameWrapperTetris.post_tick": False, } -import logging from array import array import numpy as np + +import pyboy from pyboy.utils import WindowEvent from .base_plugin import PyBoyGameWrapper -logger = logging.getLogger(__name__) - -try: - from cython import compiled - cythonmode = compiled -except ImportError: - cythonmode = False +logger = pyboy.logging.get_logger(__name__) # Table for translating game-representation of Tetromino types (8-bit int) to string tetromino_table = { @@ -68,57 +63,49 @@

    Module pyboy.plugins.game_wrapper_tetris

    TILES = 384 # Compressed assigns an ID to each Tetromino type -tiles_compressed = np.zeros(TILES, dtype=np.uint8) +mapping_compressed = np.zeros(TILES, dtype=np.uint8) # BLANK, J, Z, O, L, T, S, I, BLACK tiles_types = [[47], [129], [130], [131], [132], [133], [134], [128, 136, 137, 138, 139, 143], [135]] for tiles_type_ID, tiles_type in enumerate(tiles_types): for tile_ID in tiles_type: - tiles_compressed[tile_ID] = tiles_type_ID + mapping_compressed[tile_ID] = tiles_type_ID # Minimal has 3 id's: Background, Tetromino and "losing tile" (which fills the board when losing) -tiles_minimal = np.ones(TILES, dtype=np.uint8) # For minimal everything is 1 -tiles_minimal[47] = 0 # Except BLANK which is 0 -tiles_minimal[135] = 2 # And background losing tiles BLACK which is 2 +mapping_minimal = np.ones(TILES, dtype=np.uint8) # For minimal everything is 1 +mapping_minimal[47] = 0 # Except BLANK which is 0 +mapping_minimal[135] = 2 # And background losing tiles BLACK which is 2 class GameWrapperTetris(PyBoyGameWrapper): """ - This class wraps Tetris, and provides easy access to score, lines, level and a "fitness" score for AIs. + This class wraps Tetris, and provides easy access to score, lines and level for AIs. If you call `print` on an instance of this object, it will show an overview of everything this object provides. """ cartridge_title = "TETRIS" - tiles_compressed = tiles_compressed - tiles_minimal = tiles_minimal - + mapping_compressed = mapping_compressed + """ + Compressed mapping for `pyboy.PyBoy.game_area_mapping` + """ + mapping_minimal = mapping_minimal + """ + Minimal mapping for `pyboy.PyBoy.game_area_mapping` + """ def __init__(self, *args, **kwargs): - self.shape = (10, 18) - """The shape of the game area""" self.score = 0 """The score provided by the game""" self.level = 0 """The current level""" self.lines = 0 """The number of cleared lines""" - self.fitness = 0 - """ - A built-in fitness scoring. The scoring is equals to `score`. - - .. math:: - fitness = score - """ - super().__init__(*args, **kwargs) - ROWS, COLS = self.shape - self._cached_game_area_tiles_raw = array("B", [0xFF] * (ROWS*COLS*4)) + # super().__init__(*args, **kwargs) - if cythonmode: - self._cached_game_area_tiles = memoryview(self._cached_game_area_tiles_raw).cast("I", shape=(ROWS, COLS)) - else: - v = memoryview(self._cached_game_area_tiles_raw).cast("I") - self._cached_game_area_tiles = [v[i:i + COLS] for i in range(0, COLS * ROWS, COLS)] + # ROWS, COLS = self.shape + # self._cached_game_area_tiles_raw = array("B", [0xFF] * (ROWS*COLS*4)) + # self._cached_game_area_tiles = memoryview(self._cached_game_area_tiles_raw).cast("I", shape=(ROWS, COLS)) - super().__init__(*args, game_area_section=(2, 0) + self.shape, game_area_wrap_around=True, **kwargs) + super().__init__(*args, game_area_section=(2, 0, 10, 18), game_area_follow_scxy=False, **kwargs) def _game_area_tiles(self): if self._tile_cache_invalid: @@ -131,14 +118,10 @@

    Module pyboy.plugins.game_wrapper_tetris

    self._sprite_cache_invalid = True blank = 47 - self.tilemap_background.refresh_lcdc() self.score = self._sum_number_on_screen(13, 3, 6, blank, 0) self.level = self._sum_number_on_screen(14, 7, 4, blank, 0) self.lines = self._sum_number_on_screen(14, 10, 4, blank, 0) - if self.game_has_started: - self.fitness = self.score - def start_game(self, timer_div=None): """ Call this function right after initializing PyBoy. This will navigate through menus to start the game at the @@ -148,32 +131,25 @@

    Module pyboy.plugins.game_wrapper_tetris

    instantly. Kwargs: - timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. + * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. """ # We don't supply the timer_div arg here, as it won't have the desired effect PyBoyGameWrapper.start_game(self) # Boot screen while True: - self.pyboy.tick() - self.tilemap_background.refresh_lcdc() + self.pyboy.tick(1, False) if self.tilemap_background[2:9, 14] == [89, 25, 21, 10, 34, 14, 27]: # '1PLAYER' on the first screen break # Start game. Just press Start when the game allows us. for i in range(2): - self.pyboy.send_input(WindowEvent.PRESS_BUTTON_START) - self.pyboy.tick() - self.pyboy.send_input(WindowEvent.RELEASE_BUTTON_START) - - for _ in range(6): - self.pyboy.tick() + self.pyboy.button("start") + self.pyboy.tick(7, False) self.saved_state.seek(0) self.pyboy.save_state(self.saved_state) - self.game_has_started = True - self.reset_game(timer_div=timer_div) def reset_game(self, timer_div=None): @@ -181,18 +157,13 @@

    Module pyboy.plugins.game_wrapper_tetris

    After calling `start_game`, you can call this method at any time to reset the game. Kwargs: - timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. + * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. """ PyBoyGameWrapper.reset_game(self, timer_div=timer_div) self._set_timer_div(timer_div) - - self.pyboy.send_input(WindowEvent.PRESS_BUTTON_START) - self.pyboy.tick() - self.pyboy.send_input(WindowEvent.RELEASE_BUTTON_START) - - for _ in range(6): - self.pyboy.tick() + self.pyboy.button("start") + self.pyboy.tick(7, False) def game_area(self): """ @@ -252,7 +223,7 @@

    Module pyboy.plugins.game_wrapper_tetris

    * `"T"`: T-shape """ # Bitmask, as the last two bits determine the direction - return inverse_tetromino_table[self.pyboy.get_memory_value(NEXT_TETROMINO_ADDR) & 0b11111100] + return inverse_tetromino_table[self.pyboy.memory[NEXT_TETROMINO_ADDR] & 0b11111100] def set_tetromino(self, shape): """ @@ -291,7 +262,7 @@

    Module pyboy.plugins.game_wrapper_tetris

    ] for i, byte in enumerate(patch1): - self.pyboy.override_memory_value(0, 0x206E + i, byte) + self.pyboy.memory[0, 0x206E + i] = byte patch2 = [ 0x3E, # LD A, Tetromino @@ -299,7 +270,7 @@

    Module pyboy.plugins.game_wrapper_tetris

    ] for i, byte in enumerate(patch2): - self.pyboy.override_memory_value(0, 0x20B0 + i, byte) + self.pyboy.memory[0, 0x20B0 + i] = byte def game_over(self): """ @@ -317,20 +288,7 @@

    Module pyboy.plugins.game_wrapper_tetris

    f"Score: {self.score}\n" + f"Level: {self.level}\n" + f"Lines: {self.lines}\n" + - f"Fitness: {self.fitness}\n" + - "Sprites on screen:\n" + - "\n".join([str(s) for s in self._sprites_on_screen()]) + - "\n" + - "Tiles on screen:\n" + - " "*5 + "".join([f"{i: <4}" for i in range(10)]) + "\n" + - "_"*(adjust*10+4) + - "\n" + - "\n".join( - [ - f"{i: <3}| " + "".join([str(tile).ljust(adjust) for tile in line]) - for i, line in enumerate(self._game_area_np()) - ] - ) + super().__repr__() ) # yapf: enable
    @@ -349,7 +307,7 @@

    Classes

    (*args, **kwargs)
    -

    This class wraps Tetris, and provides easy access to score, lines, level and a "fitness" score for AIs.

    +

    This class wraps Tetris, and provides easy access to score, lines and level for AIs.

    If you call print on an instance of this object, it will show an overview of everything this object provides.

    @@ -357,42 +315,34 @@

    Classes

    class GameWrapperTetris(PyBoyGameWrapper):
         """
    -    This class wraps Tetris, and provides easy access to score, lines, level and a "fitness" score for AIs.
    +    This class wraps Tetris, and provides easy access to score, lines and level for AIs.
     
         If you call `print` on an instance of this object, it will show an overview of everything this object provides.
         """
         cartridge_title = "TETRIS"
    -    tiles_compressed = tiles_compressed
    -    tiles_minimal = tiles_minimal
    -
    +    mapping_compressed = mapping_compressed
    +    """
    +    Compressed mapping for `pyboy.PyBoy.game_area_mapping`
    +    """
    +    mapping_minimal = mapping_minimal
    +    """
    +    Minimal mapping for `pyboy.PyBoy.game_area_mapping`
    +    """
         def __init__(self, *args, **kwargs):
    -        self.shape = (10, 18)
    -        """The shape of the game area"""
             self.score = 0
             """The score provided by the game"""
             self.level = 0
             """The current level"""
             self.lines = 0
             """The number of cleared lines"""
    -        self.fitness = 0
    -        """
    -        A built-in fitness scoring. The scoring is equals to `score`.
    -
    -        .. math::
    -            fitness = score
    -        """
    -        super().__init__(*args, **kwargs)
     
    -        ROWS, COLS = self.shape
    -        self._cached_game_area_tiles_raw = array("B", [0xFF] * (ROWS*COLS*4))
    +        # super().__init__(*args, **kwargs)
     
    -        if cythonmode:
    -            self._cached_game_area_tiles = memoryview(self._cached_game_area_tiles_raw).cast("I", shape=(ROWS, COLS))
    -        else:
    -            v = memoryview(self._cached_game_area_tiles_raw).cast("I")
    -            self._cached_game_area_tiles = [v[i:i + COLS] for i in range(0, COLS * ROWS, COLS)]
    +        # ROWS, COLS = self.shape
    +        # self._cached_game_area_tiles_raw = array("B", [0xFF] * (ROWS*COLS*4))
    +        # self._cached_game_area_tiles = memoryview(self._cached_game_area_tiles_raw).cast("I", shape=(ROWS, COLS))
     
    -        super().__init__(*args, game_area_section=(2, 0) + self.shape, game_area_wrap_around=True, **kwargs)
    +        super().__init__(*args, game_area_section=(2, 0, 10, 18), game_area_follow_scxy=False, **kwargs)
     
         def _game_area_tiles(self):
             if self._tile_cache_invalid:
    @@ -405,14 +355,10 @@ 

    Classes

    self._sprite_cache_invalid = True blank = 47 - self.tilemap_background.refresh_lcdc() self.score = self._sum_number_on_screen(13, 3, 6, blank, 0) self.level = self._sum_number_on_screen(14, 7, 4, blank, 0) self.lines = self._sum_number_on_screen(14, 10, 4, blank, 0) - if self.game_has_started: - self.fitness = self.score - def start_game(self, timer_div=None): """ Call this function right after initializing PyBoy. This will navigate through menus to start the game at the @@ -422,32 +368,25 @@

    Classes

    instantly. Kwargs: - timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. + * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. """ # We don't supply the timer_div arg here, as it won't have the desired effect PyBoyGameWrapper.start_game(self) # Boot screen while True: - self.pyboy.tick() - self.tilemap_background.refresh_lcdc() + self.pyboy.tick(1, False) if self.tilemap_background[2:9, 14] == [89, 25, 21, 10, 34, 14, 27]: # '1PLAYER' on the first screen break # Start game. Just press Start when the game allows us. for i in range(2): - self.pyboy.send_input(WindowEvent.PRESS_BUTTON_START) - self.pyboy.tick() - self.pyboy.send_input(WindowEvent.RELEASE_BUTTON_START) - - for _ in range(6): - self.pyboy.tick() + self.pyboy.button("start") + self.pyboy.tick(7, False) self.saved_state.seek(0) self.pyboy.save_state(self.saved_state) - self.game_has_started = True - self.reset_game(timer_div=timer_div) def reset_game(self, timer_div=None): @@ -455,18 +394,13 @@

    Classes

    After calling `start_game`, you can call this method at any time to reset the game. Kwargs: - timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. + * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. """ PyBoyGameWrapper.reset_game(self, timer_div=timer_div) self._set_timer_div(timer_div) - - self.pyboy.send_input(WindowEvent.PRESS_BUTTON_START) - self.pyboy.tick() - self.pyboy.send_input(WindowEvent.RELEASE_BUTTON_START) - - for _ in range(6): - self.pyboy.tick() + self.pyboy.button("start") + self.pyboy.tick(7, False) def game_area(self): """ @@ -526,7 +460,7 @@

    Classes

    * `"T"`: T-shape """ # Bitmask, as the last two bits determine the direction - return inverse_tetromino_table[self.pyboy.get_memory_value(NEXT_TETROMINO_ADDR) & 0b11111100] + return inverse_tetromino_table[self.pyboy.memory[NEXT_TETROMINO_ADDR] & 0b11111100] def set_tetromino(self, shape): """ @@ -565,7 +499,7 @@

    Classes

    ] for i, byte in enumerate(patch1): - self.pyboy.override_memory_value(0, 0x206E + i, byte) + self.pyboy.memory[0, 0x206E + i] = byte patch2 = [ 0x3E, # LD A, Tetromino @@ -573,7 +507,7 @@

    Classes

    ] for i, byte in enumerate(patch2): - self.pyboy.override_memory_value(0, 0x20B0 + i, byte) + self.pyboy.memory[0, 0x20B0 + i] = byte def game_over(self): """ @@ -591,20 +525,7 @@

    Classes

    f"Score: {self.score}\n" + f"Level: {self.level}\n" + f"Lines: {self.lines}\n" + - f"Fitness: {self.fitness}\n" + - "Sprites on screen:\n" + - "\n".join([str(s) for s in self._sprites_on_screen()]) + - "\n" + - "Tiles on screen:\n" + - " "*5 + "".join([f"{i: <4}" for i in range(10)]) + "\n" + - "_"*(adjust*10+4) + - "\n" + - "\n".join( - [ - f"{i: <3}| " + "".join([str(tile).ljust(adjust) for tile in line]) - for i, line in enumerate(self._game_area_np()) - ] - ) + super().__repr__() ) # yapf: enable
    @@ -615,21 +536,17 @@

    Ancestors

    Class variables

    -
    var tiles_compressed
    +
    var mapping_compressed
    -
    +

    Compressed mapping for PyBoy.game_area_mapping()

    -
    var tiles_minimal
    +
    var mapping_minimal
    -
    +

    Minimal mapping for PyBoy.game_area_mapping()

    Instance variables

    -
    var shape
    -
    -

    The shape of the game area

    -
    var score

    The score provided by the game

    @@ -642,11 +559,6 @@

    Instance variables

    The number of cleared lines

    -
    var fitness
    -
    -

    A built-in fitness scoring. The scoring is equals to score.

    -

    fitness = score

    -

    Methods

    @@ -659,7 +571,9 @@

    Methods

    The state of the emulator is saved, and using reset_game, you can get back to this point of the game instantly.

    Kwargs

    -

    timer_div (int): Replace timer's DIV register with this value. Use None to randomize.

    +
      +
    • timer_div (int): Replace timer's DIV register with this value. Use None to randomize.
    • +
    Expand source code @@ -673,32 +587,25 @@

    Kwargs

    instantly. Kwargs: - timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. + * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. """ # We don't supply the timer_div arg here, as it won't have the desired effect PyBoyGameWrapper.start_game(self) # Boot screen while True: - self.pyboy.tick() - self.tilemap_background.refresh_lcdc() + self.pyboy.tick(1, False) if self.tilemap_background[2:9, 14] == [89, 25, 21, 10, 34, 14, 27]: # '1PLAYER' on the first screen break # Start game. Just press Start when the game allows us. for i in range(2): - self.pyboy.send_input(WindowEvent.PRESS_BUTTON_START) - self.pyboy.tick() - self.pyboy.send_input(WindowEvent.RELEASE_BUTTON_START) - - for _ in range(6): - self.pyboy.tick() + self.pyboy.button("start") + self.pyboy.tick(7, False) self.saved_state.seek(0) self.pyboy.save_state(self.saved_state) - self.game_has_started = True - self.reset_game(timer_div=timer_div)
    @@ -708,7 +615,9 @@

    Kwargs

    After calling start_game, you can call this method at any time to reset the game.

    Kwargs

    -

    timer_div (int): Replace timer's DIV register with this value. Use None to randomize.

    +
      +
    • timer_div (int): Replace timer's DIV register with this value. Use None to randomize.
    • +
    Expand source code @@ -718,18 +627,13 @@

    Kwargs

    After calling `start_game`, you can call this method at any time to reset the game. Kwargs: - timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. + * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. """ PyBoyGameWrapper.reset_game(self, timer_div=timer_div) self._set_timer_div(timer_div) - - self.pyboy.send_input(WindowEvent.PRESS_BUTTON_START) - self.pyboy.tick() - self.pyboy.send_input(WindowEvent.RELEASE_BUTTON_START) - - for _ in range(6): - self.pyboy.tick() + self.pyboy.button("start") + self.pyboy.tick(7, False)
    @@ -852,7 +756,7 @@

    Returns

    * `"T"`: T-shape """ # Bitmask, as the last two bits determine the direction - return inverse_tetromino_table[self.pyboy.get_memory_value(NEXT_TETROMINO_ADDR) & 0b11111100]
    + return inverse_tetromino_table[self.pyboy.memory[NEXT_TETROMINO_ADDR] & 0b11111100]
    @@ -919,7 +823,7 @@

    Args

    ] for i, byte in enumerate(patch1): - self.pyboy.override_memory_value(0, 0x206E + i, byte) + self.pyboy.memory[0, 0x206E + i] = byte patch2 = [ 0x3E, # LD A, Tetromino @@ -927,7 +831,7 @@

    Args

    ] for i, byte in enumerate(patch2): - self.pyboy.override_memory_value(0, 0x20B0 + i, byte)
    + self.pyboy.memory[0, 0x20B0 + i] = byte
    @@ -950,6 +854,15 @@

    Args

    +

    Inherited members

    + @@ -976,13 +889,11 @@

    next_tetromino
  • set_tetromino
  • game_over
  • -
  • shape
  • score
  • level
  • lines
  • -
  • fitness
  • -
  • tiles_compressed
  • -
  • tiles_minimal
  • +
  • mapping_compressed
  • +
  • mapping_minimal
  • diff --git a/docs/plugins/index.html b/docs/plugins/index.html index 414c1fe5e..9632b8256 100644 --- a/docs/plugins/index.html +++ b/docs/plugins/index.html @@ -36,19 +36,19 @@

    Module pyboy.plugins

    __pdoc__ = { # docs exclude - "window_headless": False, - "window_open_gl": False, - "screen_recorder": False, - "rewind": False, - "window_dummy": False, "disable_input": False, - "manager_gen": False, - "auto_pause": False, - "manager": False, - "record_replay": False, + "rewind": False, + "window_sdl2": False, "screenshot_recorder": False, + "debug_prompt": False, + "screen_recorder": False, "debug": False, - "window_sdl2": False, + "manager": False, + "record_replay": False, + "manager_gen": False, + "window_open_gl": False, + "auto_pause": False, + "window_null": False, # docs exclude end }
    diff --git a/docs/utils.html b/docs/utils.html new file mode 100644 index 000000000..34ddbd65d --- /dev/null +++ b/docs/utils.html @@ -0,0 +1,889 @@ + + + + + + +pyboy.utils API documentation + + + + + + + + + + +
    +
    +
    +

    Module pyboy.utils

    +
    +
    +
    + +Expand source code + +
    #
    +# License: See LICENSE.md file
    +# GitHub: https://github.com/Baekalfen/PyBoy
    +#
    +
    +__all__ = ["WindowEvent", "dec_to_bcd", "bcd_to_dec"]
    +
    +STATE_VERSION = 10
    +
    +##############################################################
    +# Buffer classes
    +
    +
    +class IntIOInterface:
    +    def __init__(self, buf):
    +        pass
    +
    +    def write(self, byte):
    +        raise Exception("Not implemented!")
    +
    +    def write_64bit(self, value):
    +        self.write(value & 0xFF)
    +        self.write((value >> 8) & 0xFF)
    +        self.write((value >> 16) & 0xFF)
    +        self.write((value >> 24) & 0xFF)
    +        self.write((value >> 32) & 0xFF)
    +        self.write((value >> 40) & 0xFF)
    +        self.write((value >> 48) & 0xFF)
    +        self.write((value >> 56) & 0xFF)
    +
    +    def read_64bit(self):
    +        a = self.read()
    +        b = self.read()
    +        c = self.read()
    +        d = self.read()
    +        e = self.read()
    +        f = self.read()
    +        g = self.read()
    +        h = self.read()
    +        return a | (b << 8) | (c << 16) | (d << 24) | (e << 32) | (f << 40) | (g << 48) | (h << 56)
    +
    +    def write_32bit(self, value):
    +        self.write(value & 0xFF)
    +        self.write((value >> 8) & 0xFF)
    +        self.write((value >> 16) & 0xFF)
    +        self.write((value >> 24) & 0xFF)
    +
    +    def read_32bit(self):
    +        a = self.read()
    +        b = self.read()
    +        c = self.read()
    +        d = self.read()
    +        return int(a | (b << 8) | (c << 16) | (d << 24))
    +
    +    def write_16bit(self, value):
    +        self.write(value & 0xFF)
    +        self.write((value >> 8) & 0xFF)
    +
    +    def read_16bit(self):
    +        a = self.read()
    +        b = self.read()
    +        return int(a | (b << 8))
    +
    +    def read(self):
    +        raise Exception("Not implemented!")
    +
    +    def seek(self, pos):
    +        raise Exception("Not implemented!")
    +
    +    def flush(self):
    +        raise Exception("Not implemented!")
    +
    +    def new(self):
    +        raise Exception("Not implemented!")
    +
    +    def commit(self):
    +        raise Exception("Not implemented!")
    +
    +    def seek_frame(self, _):
    +        raise Exception("Not implemented!")
    +
    +    def tell(self):
    +        raise Exception("Not implemented!")
    +
    +
    +class IntIOWrapper(IntIOInterface):
    +    """
    +    Wraps a file-like object to allow writing integers to it.
    +    This allows for higher performance, when writing to a memory map in rewind.
    +    """
    +    def __init__(self, buf):
    +        self.buffer = buf
    +
    +    def write(self, byte):
    +        assert isinstance(byte, int)
    +        assert 0 <= byte <= 0xFF
    +        return self.buffer.write(byte.to_bytes(1, "little"))
    +
    +    def read(self):
    +        # assert count == 1, "Only a count of 1 is supported"
    +        data = self.buffer.read(1)
    +        assert len(data) == 1, "No data"
    +        return ord(data)
    +
    +    def seek(self, pos):
    +        self.buffer.seek(pos)
    +
    +    def flush(self):
    +        self.buffer.flush()
    +
    +    def tell(self):
    +        return self.buffer.tell()
    +
    +
    +##############################################################
    +# Misc
    +
    +
    +# TODO: Would a lookup-table increase performance? For example a lookup table of each 4-bit nibble?
    +# That's 16**2 = 256 values. Index calculated as: (byte1 & 0xF0) | ((byte2 & 0xF0) >> 4)
    +# and then: (byte1 & 0x0F) | ((byte2 & 0x0F) >> 4)
    +# Then could even be preloaded for each color palette
    +def color_code(byte1, byte2, offset):
    +    """Convert 2 bytes into color code at a given offset.
    +
    +    The colors are 2 bit and are found like this:
    +
    +    Color of the first pixel is 0b10
    +    | Color of the second pixel is 0b01
    +    v v
    +    1 0 0 1 0 0 0 1 <- byte1
    +    0 1 1 1 1 1 0 0 <- byte2
    +    """
    +    return (((byte2 >> (offset)) & 0b1) << 1) + ((byte1 >> (offset)) & 0b1)
    +
    +
    +##############################################################
    +# Window Events
    +# Temporarily placed here to not be exposed on public API
    +
    +
    +class WindowEvent:
    +    """
    +    All supported events can be found in the class description below.
    +
    +    It can be used as follows:
    +
    +    ```python
    +    >>> from pyboy.utils import WindowEvent
    +    >>> pyboy.send_input(WindowEvent.PAUSE)
    +
    +    ```
    +
    +    Just for button presses, it might be easier to use: `pyboy.PyBoy.button`,
    +    `pyboy.PyBoy.button_press` and `pyboy.PyBoy.button_release`.
    +    """
    +
    +    # 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]
    +
    +
    +class WindowEventMouse(WindowEvent):
    +    def __init__(
    +        self, *args, window_id=-1, mouse_x=-1, mouse_y=-1, mouse_scroll_x=-1, mouse_scroll_y=-1, mouse_button=-1
    +    ):
    +        super().__init__(*args)
    +        self.window_id = window_id
    +        self.mouse_x = mouse_x
    +        self.mouse_y = mouse_y
    +        self.mouse_scroll_x = mouse_scroll_x
    +        self.mouse_scroll_y = mouse_scroll_y
    +        self.mouse_button = mouse_button
    +
    +
    +##############################################################
    +# Memory Scanning
    +#
    +
    +
    +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
    +
    +
    +
    +
    +
    +
    +
    +

    Functions

    +
    +
    +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 for more details.
    +
    +

    Example:

    +
    >>> 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.
    +
    +
    + +Expand source code + +
    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 for more details.
    +
    +

    Example:

    +
    >>> 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.
    +
    +
    + +Expand source code + +
    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)
    +
    +
    +

    Just for button presses, it might be easier to use: PyBoy.button(), +PyBoy.button_press() and PyBoy.button_release().

    +
    + +Expand source code + +
    class WindowEvent:
    +    """
    +    All supported events can be found in the class description below.
    +
    +    It can be used as follows:
    +
    +    ```python
    +    >>> from pyboy.utils import WindowEvent
    +    >>> pyboy.send_input(WindowEvent.PAUSE)
    +
    +    ```
    +
    +    Just for button presses, it might be easier to use: `pyboy.PyBoy.button`,
    +    `pyboy.PyBoy.button_press` and `pyboy.PyBoy.button_release`.
    +    """
    +
    +    # 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]
    +
    +

    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
    +
    +
    +
    +
    var MOD_SHIFT_OFF
    +
    +
    +
    +
    var FULL_SCREEN_TOGGLE
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + + + \ No newline at end of file From 26d2197e104a13e6ee3d2e9b40cde1eb21c2fe04 Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Sat, 20 Jan 2024 13:46:15 +0100 Subject: [PATCH 65/65] Bump version to v2.0.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cc729663d..c4d189e42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pyboy" -version = "1.6.14" +version = "2.0.0" authors = [ {name = "Mads Ynddal", email = "mads@ynddal.dk"} ]