Module pyboy.api.tilemap

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

Expand source code
#
# License: See LICENSE.md file
# GitHub: https://github.com/Baekalfen/PyBoy
#
"""
The Game Boy has two tile maps, which defines what is rendered on the screen.
"""

import numpy as np

from pyboy.core.lcd import LCDCRegister

from .constants import HIGH_TILEMAP, LCDC_OFFSET, LOW_TILEDATA_NTILES, LOW_TILEMAP
from .tile import Tile


class TileMap:
    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.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:
        ```
        >>> 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.frame_count_update = 0
        self.__refresh_lcdc()

        self.shape = (32, 32)
        """
        Tile maps are always 32x32 tiles.

        Returns
        -------
        (int, int):
            The width and height of the tile map.
        """

    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.
        """
        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:
        ```
        >>> 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
        `123`was not found anywhere.

        Args:
            identifiers (list): List of tile identifiers (int)

        Returns
        -------
        list:
            list of matches for every tile identifier in the input
        """
        # TODO: Crude implementation
        tilemap_identifiers = np.asarray(self[:, :], dtype=np.uint32)
        matches = []
        for i in identifiers:
            matches.append([[int(y) for y in x] for x in np.argwhere(tilemap_identifiers == i)])
        return matches

    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.api.tile.Tile.data_address`.

        This can be used as an global identifier for the specific location in a tile map.

        Be aware, that the tile index referenced at the memory address might change between calls to
        `pyboy.PyBoy.tick`. And the tile data for the same tile index might also change to display something else
        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.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](https://gbdev.io/pandocs/Tile_Data.html).

        Args:
            column (int): Column in this tile map.
            row (int): Row in this tile map.

        Returns
        -------
        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:
            raise IndexError("row is out of bounds. Value of 0 to 31 is allowed")
        return self.map_offset + 32*row + column

    def tile(self, column, row):
        """
        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:
            column (int): Column in this tile map.
            row (int): Row in this tile map.

        Returns
        -------
        `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):
        """
        Returns an identifier (integer) of the tile at the given coordinate in the tile map. The identifier can be used
        to quickly recognize what is on the screen through this tile view.

        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](https://gbdev.io/pandocs/Tile_Data.html).

        Args:
            column (int): Column in this tile map.
            row (int): Row in this tile map.

        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
        else:
            return tile

    def __repr__(self):
        self._refresh_lcdc()
        adjust = 4
        _use_tile_objects = self._use_tile_objects
        self.use_tile_objects(False)
        # yapf: disable
        return_data = (
            f"Tile Map Address: {self.map_offset:#0{6}x}, " +
            f"Signed Tile Data: {'Yes' if self.signed_tile_data else 'No'}\n" +
            " "*5 + "".join([f"{i: <4}" for i in range(32)]) + "\n" +
            "_"*(adjust*32+2) + "\n" +
            "\n".join(
                [
                    f"{i: <3}| " + "".join([str(tile).ljust(adjust) for tile in line])
                    for i, line in enumerate(self[:, :])
                ]
            )
        )
        # yapf: enable
        self.use_tile_objects(_use_tile_objects)
        return return_data

    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.api.tile.Tile`-object. If False, accesses will
                return an `int`.
        """
        self._use_tile_objects = switch

    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

    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
        else:
            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)] for _y in range(*y)]
        elif x_slice:
            return [tile_fun(_x, y) for _x in range(*x)]
        elif y_slice:
            return [tile_fun(x, _y) for _y in range(*y)]
        else:
            return tile_fun(x, y)

Classes

class TileMap (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.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:

>>> 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[:,:].

Expand source code
class TileMap:
    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.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:
        ```
        >>> 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.frame_count_update = 0
        self.__refresh_lcdc()

        self.shape = (32, 32)
        """
        Tile maps are always 32x32 tiles.

        Returns
        -------
        (int, int):
            The width and height of the tile map.
        """

    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.
        """
        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:
        ```
        >>> 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
        `123`was not found anywhere.

        Args:
            identifiers (list): List of tile identifiers (int)

        Returns
        -------
        list:
            list of matches for every tile identifier in the input
        """
        # TODO: Crude implementation
        tilemap_identifiers = np.asarray(self[:, :], dtype=np.uint32)
        matches = []
        for i in identifiers:
            matches.append([[int(y) for y in x] for x in np.argwhere(tilemap_identifiers == i)])
        return matches

    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.api.tile.Tile.data_address`.

        This can be used as an global identifier for the specific location in a tile map.

        Be aware, that the tile index referenced at the memory address might change between calls to
        `pyboy.PyBoy.tick`. And the tile data for the same tile index might also change to display something else
        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.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](https://gbdev.io/pandocs/Tile_Data.html).

        Args:
            column (int): Column in this tile map.
            row (int): Row in this tile map.

        Returns
        -------
        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:
            raise IndexError("row is out of bounds. Value of 0 to 31 is allowed")
        return self.map_offset + 32*row + column

    def tile(self, column, row):
        """
        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:
            column (int): Column in this tile map.
            row (int): Row in this tile map.

        Returns
        -------
        `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):
        """
        Returns an identifier (integer) of the tile at the given coordinate in the tile map. The identifier can be used
        to quickly recognize what is on the screen through this tile view.

        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](https://gbdev.io/pandocs/Tile_Data.html).

        Args:
            column (int): Column in this tile map.
            row (int): Row in this tile map.

        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
        else:
            return tile

    def __repr__(self):
        self._refresh_lcdc()
        adjust = 4
        _use_tile_objects = self._use_tile_objects
        self.use_tile_objects(False)
        # yapf: disable
        return_data = (
            f"Tile Map Address: {self.map_offset:#0{6}x}, " +
            f"Signed Tile Data: {'Yes' if self.signed_tile_data else 'No'}\n" +
            " "*5 + "".join([f"{i: <4}" for i in range(32)]) + "\n" +
            "_"*(adjust*32+2) + "\n" +
            "\n".join(
                [
                    f"{i: <3}| " + "".join([str(tile).ljust(adjust) for tile in line])
                    for i, line in enumerate(self[:, :])
                ]
            )
        )
        # yapf: enable
        self.use_tile_objects(_use_tile_objects)
        return return_data

    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.api.tile.Tile`-object. If False, accesses will
                return an `int`.
        """
        self._use_tile_objects = switch

    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

    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
        else:
            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)] for _y in range(*y)]
        elif x_slice:
            return [tile_fun(_x, y) for _x in range(*x)]
        elif y_slice:
            return [tile_fun(x, _y) for _y in range(*y)]
        else:
            return tile_fun(x, y)

Instance variables

var shape

Tile maps are always 32x32 tiles.

Returns

(int, int): The width and height of the tile map.

Methods

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:

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

Args

identifiers : list
List of tile identifiers (int)

Returns

list:
list of matches for every tile identifier in the input
Expand source code
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:
    ```
    >>> 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
    `123`was not found anywhere.

    Args:
        identifiers (list): List of tile identifiers (int)

    Returns
    -------
    list:
        list of matches for every tile identifier in the input
    """
    # TODO: Crude implementation
    tilemap_identifiers = np.asarray(self[:, :], dtype=np.uint32)
    matches = []
    for i in identifiers:
        matches.append([[int(y) for y in x] for x in np.argwhere(tilemap_identifiers == i)])
    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 be returned from two different coordinates in the tile map if they are shown different places on the screen.

Args

column : int
Column in this tile map.
row : int
Row in this tile map.

Returns

Tile: Tile object corresponding to the tile index at the given coordinate in the tile map.

Expand source code
def tile(self, column, row):
    """
    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:
        column (int): Column in this tile map.
        row (int): Row in this tile map.

    Returns
    -------
    `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)

Returns an identifier (integer) of the tile at the given coordinate in the tile map. The identifier can be used to quickly recognize what is on the screen through this tile view.

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.

Args

column : int
Column in this tile map.
row : int
Row in this tile map.

Returns

int:
Tile identifier.
Expand source code
def tile_identifier(self, column, row):
    """
    Returns an identifier (integer) of the tile at the given coordinate in the tile map. The identifier can be used
    to quickly recognize what is on the screen through this tile view.

    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](https://gbdev.io/pandocs/Tile_Data.html).

    Args:
        column (int): Column in this tile map.
        row (int): Row in this tile map.

    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
    else:
        return tile
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 Tile-object. If False, accesses will return an int.
Expand source code
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.api.tile.Tile`-object. If False, accesses will
            return an `int`.
    """
    self._use_tile_objects = switch