Module pyboy.plugins.base_plugin

Expand source code
#
# License: See LICENSE.md file
# GitHub: https://github.com/Baekalfen/PyBoy
#

__pdoc__ = {
    "PyBoyPlugin": False,
    "PyBoyWindowPlugin": False,
    "PyBoyGameWrapper.post_tick": False,
    "PyBoyGameWrapper.enabled": False,
    "PyBoyGameWrapper.argv": False,
}

import io
import logging
import random
from array import array

import numpy as np
from pyboy.botsupport.sprite import Sprite

logger = logging.getLogger(__name__)

try:
    from cython import compiled
    cythonmode = compiled
except ImportError:
    cythonmode = False

ROWS, COLS = 144, 160


class PyBoyPlugin:
    argv = []

    def __init__(self, pyboy, mb, pyboy_argv):
        if not cythonmode:
            self.pyboy = pyboy
            self.mb = mb
            self.pyboy_argv = pyboy_argv

    def __cinit__(self, pyboy, mb, pyboy_argv, *args, **kwargs):
        self.pyboy = pyboy
        self.mb = mb
        self.pyboy_argv = pyboy_argv

    def handle_events(self, events):
        return events

    def post_tick(self):
        pass

    def window_title(self):
        return ""

    def stop(self):
        pass

    def enabled(self):
        return True


class PyBoyWindowPlugin(PyBoyPlugin):
    def __init__(self, pyboy, mb, pyboy_argv, *args, **kwargs):
        super().__init__(pyboy, mb, pyboy_argv, *args, **kwargs)

        if not self.enabled():
            return

        scale = pyboy_argv.get("scale")
        self.scale = scale
        logger.info("%s initialization" % self.__class__.__name__)

        self._scaledresolution = (scale * COLS, scale * ROWS)
        logger.info("Scale: x%s %s" % (self.scale, self._scaledresolution))

        self.enable_title = True
        if not cythonmode:
            self.renderer = mb.renderer

    def __cinit__(self, *args, **kwargs):
        self.renderer = self.mb.renderer

    def frame_limiter(self, speed):
        return False

    def set_title(self, title):
        pass


class PyBoyGameWrapper(PyBoyPlugin):
    """
    This is the base-class for the game-wrappers. It provides some generic game-wrapping functionality, like `game_area`
    , 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"})]

    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.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._cached_game_area_tiles_raw = array("B", [0xFF] * (width*height*4))

        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 enabled(self):
        return self.pyboy_argv.get("game_wrapper") and self.pyboy.cartridge_title() == self.cartridge_title

    def post_tick(self):
        raise NotImplementedError("post_tick not implemented in game wrapper")

    def _set_timer_div(self, timer_div):
        if timer_div is None:
            self.mb.timer.DIV = random.getrandbits(8)
        else:
            self.mb.timer.DIV = timer_div & 0xFF

    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
        first playable state.

        A value can be passed to set the timer's DIV register. Some games depend on it for randomization.

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

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

        if not self.pyboy.frame_count == 0:
            logger.warning("Calling start_game from an already running game. This might not work.")

        self._set_timer_div(timer_div)

    def reset_game(self, timer_div=None):
        """
        After calling `start_game`, you can call this method at any time to reset the game.

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

        if self.game_has_started:
            self.saved_state.seek(0)
            self.pyboy.load_state(self.saved_state)
            self.post_tick()
        else:
            logger.error("Tried to reset game, but it hasn't been started yet!")

        self._set_timer_div(timer_div)

    def game_over(self):
        """
        After calling `start_game`, you can call this method at any time to know if the game is over.
        """
        raise NotImplementedError("game_over not implemented in game wrapper")

    def _sprites_on_screen(self):
        if self._sprite_cache_invalid:
            self._cached_sprites_on_screen = []
            for s in range(40):
                sprite = Sprite(self.mb, s)
                if sprite.on_screen:
                    self._cached_sprites_on_screen.append(sprite)
            self._sprite_cache_invalid = False
        return self._cached_sprites_on_screen

    def _game_area_tiles(self):
        if self._tile_cache_invalid:
            xx = self.game_area_section[0]
            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()

            if self.game_area_wrap_around:
                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
                    SCY = scanline_parameters[(yy+y) * 8][1] // 8
                    for x in range(width):
                        _x = (xx+x+SCX) % 32
                        _y = (yy+y+SCY) % 32
                        self._cached_game_area_tiles[y][x] = self.tilemap_background.tile_identifier(_x, _y)
            else:
                self._cached_game_area_tiles = np.asarray(
                    self.tilemap_background[xx:xx + width, yy:yy + height], dtype=np.uint32
                )
            self._tile_cache_invalid = False
        return self._cached_game_area_tiles

    def game_area(self):
        """
        This method returns a cut-out of the screen as a simplified matrix for use in machine learning applications.

        Returns
        -------
        memoryview:
            Simplified 2-dimensional memoryview of the screen
        """
        tiles_matrix = self._game_area_tiles()
        sprites = self._sprites_on_screen()
        xx = self.game_area_section[0]
        yy = self.game_area_section[1]
        width = self.game_area_section[2]
        height = self.game_area_section[3]
        for s in sprites:
            _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
        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 _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

Classes

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

This is the base-class for the game-wrappers. It provides some generic game-wrapping functionality, like game_area , which shows both sprites and tiles on the screen as a simple matrix.

Expand source code
class PyBoyGameWrapper(PyBoyPlugin):
    """
    This is the base-class for the game-wrappers. It provides some generic game-wrapping functionality, like `game_area`
    , 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"})]

    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.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._cached_game_area_tiles_raw = array("B", [0xFF] * (width*height*4))

        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 enabled(self):
        return self.pyboy_argv.get("game_wrapper") and self.pyboy.cartridge_title() == self.cartridge_title

    def post_tick(self):
        raise NotImplementedError("post_tick not implemented in game wrapper")

    def _set_timer_div(self, timer_div):
        if timer_div is None:
            self.mb.timer.DIV = random.getrandbits(8)
        else:
            self.mb.timer.DIV = timer_div & 0xFF

    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
        first playable state.

        A value can be passed to set the timer's DIV register. Some games depend on it for randomization.

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

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

        if not self.pyboy.frame_count == 0:
            logger.warning("Calling start_game from an already running game. This might not work.")

        self._set_timer_div(timer_div)

    def reset_game(self, timer_div=None):
        """
        After calling `start_game`, you can call this method at any time to reset the game.

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

        if self.game_has_started:
            self.saved_state.seek(0)
            self.pyboy.load_state(self.saved_state)
            self.post_tick()
        else:
            logger.error("Tried to reset game, but it hasn't been started yet!")

        self._set_timer_div(timer_div)

    def game_over(self):
        """
        After calling `start_game`, you can call this method at any time to know if the game is over.
        """
        raise NotImplementedError("game_over not implemented in game wrapper")

    def _sprites_on_screen(self):
        if self._sprite_cache_invalid:
            self._cached_sprites_on_screen = []
            for s in range(40):
                sprite = Sprite(self.mb, s)
                if sprite.on_screen:
                    self._cached_sprites_on_screen.append(sprite)
            self._sprite_cache_invalid = False
        return self._cached_sprites_on_screen

    def _game_area_tiles(self):
        if self._tile_cache_invalid:
            xx = self.game_area_section[0]
            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()

            if self.game_area_wrap_around:
                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
                    SCY = scanline_parameters[(yy+y) * 8][1] // 8
                    for x in range(width):
                        _x = (xx+x+SCX) % 32
                        _y = (yy+y+SCY) % 32
                        self._cached_game_area_tiles[y][x] = self.tilemap_background.tile_identifier(_x, _y)
            else:
                self._cached_game_area_tiles = np.asarray(
                    self.tilemap_background[xx:xx + width, yy:yy + height], dtype=np.uint32
                )
            self._tile_cache_invalid = False
        return self._cached_game_area_tiles

    def game_area(self):
        """
        This method returns a cut-out of the screen as a simplified matrix for use in machine learning applications.

        Returns
        -------
        memoryview:
            Simplified 2-dimensional memoryview of the screen
        """
        tiles_matrix = self._game_area_tiles()
        sprites = self._sprites_on_screen()
        xx = self.game_area_section[0]
        yy = self.game_area_section[1]
        width = self.game_area_section[2]
        height = self.game_area_section[3]
        for s in sprites:
            _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
        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 _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

Ancestors

  • pyboy.plugins.base_plugin.PyBoyPlugin

Subclasses

Methods

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 first playable state.

A value can be passed to set the timer's DIV register. Some games depend on it for randomization.

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

Args

timer_div : int
Replace timer's DIV register with this value. Use None to randomize.
Expand source code
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
    first playable state.

    A value can be passed to set the timer's DIV register. Some games depend on it for randomization.

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

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

    if not self.pyboy.frame_count == 0:
        logger.warning("Calling start_game from an already running game. This might not work.")

    self._set_timer_div(timer_div)
def reset_game(self, timer_div=None)

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

Args

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`, you can call this method at any time to reset the game.

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

    if self.game_has_started:
        self.saved_state.seek(0)
        self.pyboy.load_state(self.saved_state)
        self.post_tick()
    else:
        logger.error("Tried to reset game, but it hasn't been started yet!")

    self._set_timer_div(timer_div)
def game_over(self)

After calling start_game, you can call this method at any time to know if the game is over.

Expand source code
def game_over(self):
    """
    After calling `start_game`, you can call this method at any time to know if the game is over.
    """
    raise NotImplementedError("game_over not implemented in game wrapper")
def game_area(self)

This method returns a cut-out of the screen as a simplified matrix for use in machine learning applications.

Returns

memoryview:
Simplified 2-dimensional memoryview of the screen
Expand source code
def game_area(self):
    """
    This method returns a cut-out of the screen as a simplified matrix for use in machine learning applications.

    Returns
    -------
    memoryview:
        Simplified 2-dimensional memoryview of the screen
    """
    tiles_matrix = self._game_area_tiles()
    sprites = self._sprites_on_screen()
    xx = self.game_area_section[0]
    yy = self.game_area_section[1]
    width = self.game_area_section[2]
    height = self.game_area_section[3]
    for s in sprites:
        _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
    return tiles_matrix