Source code for miniworldmaker.boards.board

from __future__ import annotations
from typing import Union, Type, List
import pygame
import inspect
from miniworldmaker.app import app
from miniworldmaker.appearances import appearance
from miniworldmaker.appearances import background
from miniworldmaker.board_positions import board_position_factory
from miniworldmaker.board_positions import board_rect_factory
from miniworldmaker.board_positions import board_position
from miniworldmaker.boards.board_handler import board_event_handler
from miniworldmaker.boards.board_handler import board_collision_handler
from miniworldmaker.boards.board_handler import board_view_handler 
from miniworldmaker.boards.board_handler import board_position_handler 
from miniworldmaker.boards.token_connectors import token_connector 
from miniworldmaker.containers import container
from miniworldmaker.exceptions.miniworldmaker_exception import BoardArgumentsError, BoardInstanceError
from miniworldmaker.tools import timer
from miniworldmaker.tokens import token as token_module
from miniworldmaker.tools import inspection_methods
from miniworldmaker.boards.data import import_factory
from miniworldmaker.boards.data import export_factory
from typing import Tuple


[docs]class Board(container.Container): """Board is the Base Class for Boards, e.g. TiledBoard, PixelBoard or PhysicsBoard. You can create a custom board by inherit one of Board subclasses (TiledBoard, PixelBoard or PhysicsBoard) or by creating a board-object: **Tiledboard** A Board for Games based on Tiles (Like Rogue-Like RPGs). * Every token on a TiledBoard has the size of exactly on one Tile. (If your tile_size is 40, every token has the size 40x40. ) * The **position** of a token (*mytoken.position*) corresponds to the tile on which it is placed. * Two tokens **collide** when they are on the same tile. .. image:: ../_images/tiled_board.jpg :width: 100% :alt: TiledBoard **PixelBoard** A board for pixel accurate games. .. image:: ../_images/asteroids.jpg :width: 100% :alt: Asteroids * The position of a token on a PixelBoard is the pixel at toplef of token. * New tokens are created with top-left corner of token rect at position. * Two tokens collide when their sprites overlap. Examples: Creating a TiledBoard Object: .. code-block:: python myboard = miniworldmaker.TiledBoard() myboard.columns = 30 myboard.rows = 20 myboard.tile_size = 20 Creating a TiledBoard-Subclass as Class: .. code-block:: python class MyBoard(miniworldmaker.TiledBoard): def on_setup(self): self.columns = 30 self.rows = 20 self.tile_size = 20 Creating a PixelBoard Object: .. code-block:: python myboard = miniworldmaker.PixelBoard() myboard.columns = 300 myboard.rows = 200 Example: A PixelBoard Subclass .. code-block:: python class MyBoard(miniworldmaker.PixelBoard): def on_setup(self): self.columns = 300 self.rows = 200 See also: * See: :doc:`Board <../api/board>` * See: :doc:`TiledBoard <../api/board.tiledboard>` Args: columns: columns of new board (default: 40) rows: rows of new board (default:40) """ subclasses = None def __init__(self, columns: Union[int, Tuple[int]] = 40, rows: int = 40, tile_size: int = 1, tile_margin: int = 0, background_image=None ): if self.__class__ == Board: raise BoardInstanceError() if type(columns) != int or type(rows) != int: if type(columns) == tuple: size = columns columns = size[0] rows = size[1] else: raise BoardArgumentsError(columns, rows) self._columns, self._rows, self._tile_size, self._tile_margin = columns, rows, tile_size, tile_margin self._tokens = pygame.sprite.LayeredDirty() self.event_handler : board_event_handler.BoardEventHandler = board_event_handler.BoardEventHandler(self) super().__init__() self.view_handler : "board_view_handler.BoardViewHandler"= board_view_handler.BoardViewHandler(self) self.position_handler: "board_position_handler.BoardPositionHandler" = board_position_handler.BoardPositionHandler(self) pygame.init() self.is_running: bool = True self._is_setup: bool = False self._fps: int = 60 self._key_pressed: bool = False self._animated: bool = False self._orientation: int = 0 self.frame: int = 0 self._speed: int = 1 # All tokens are acting on n'th frame with n = self.speed self.clock: pygame.time.Clock = pygame.time.Clock() # Init graphics self.app: app.App = app.App("miniworldmaker") self.app.container_manager.add_container(self, "top_left") app.App.board = self self.view_handler.init_background(background_image) self.view_handler.update_background() self.collision_handler : "board_collision_handler.BoardCollisionHandler" = board_collision_handler.BoardCollisionHandler(self) self.dirty: int = 1 self.timed_objects: list = [] self.app.event_manager.send_event_to_containers("setup", self) self.cache = dict() self.cache["token_classes"] = self.get_token_classes() # @todo: No cache implemented yet def get_token_connector(self, token) -> token_connector.TokenConnector: return token_connector.TokenConnector(self, token)
[docs] def load_board_from_db(self, file: str): """ Loads a sqlite db file. """ return import_factory.ImportBoardFromDB(file, self.__class__).load()
[docs] def load_tokens_from_db(self, file: str, token_classes: list) -> List["token_module.Token"]: """Loads all tokens from db. Usually you load the tokens in __init__() or in on_setup() Args: file (str): reference to db file token_classes (list): a list of all Token Classes which should be imported. Returns: [type]: All Tokens """ return import_factory.ImportTokensFromDB(file, token_classes).load()
[docs] def save_to_db(self, file): """ Saves the current board an all actors to database. The file is stored as db file and can be opened with sqlite. Args: file: The file as relative location Returns: """ export = export_factory.ExportBoardToDBFactory(file, self) export.remove_file() export.save() export_factory.ExportTokensToDBFactory(file, self.tokens).save()
@classmethod def all_subclasses(cls): def rec_all_subs(base_cls) -> set: if cls.subclasses is None: return set(base_cls.__subclasses__()).union( [s for c in base_cls.__subclasses__() for s in rec_all_subs(c)]) else: return cls.subclasses return rec_all_subs(cls) def __str__(self): return "{0} with {1} columns and {2} rows" \ .format(self.__class__.__name__, self.columns, self.rows) @property def container_width(self) -> int: """ The width of the container """ if self.view_handler.repaint_all: self._container_width = self.columns * self.tile_size + \ (self.columns + 1) * self.tile_margin return self._container_width @property def container_height(self) -> int: """ The height of the container """ if self.view_handler.repaint_all: self._container_height = self.rows * \ self.tile_size + (self.rows + 1) * self.tile_margin return self._container_height @property def speed(self) -> int: """ How often are events and game logic processed? The game logic can be called more often or less often independently from board.fps Examples: .. code-block:: python board.speed = 10 board.fps = 24 def act(self): nonlocal i i = i + 1 print(board.frame, i) if board.frame == 120: test_instance.assertEqual(i, 13) test_instance.assertEqual(board.frame, 120) """ return self._speed @speed.setter def speed(self, value: int): self._speed = value @property def has_background(self) -> bool: return self.view_handler.has_background @property def fps(self) -> int: """ Frames per second shown on the screen. This controls how often the screen is redrawn. However, the game logic can be called more often or less often independently of this with board.speed. Examples: .. code-block:: python board.speed = 10 board.fps = 24 def act(self): nonlocal i i = i + 1 print(board.frame, i) if board.frame == 120: test_instance.assertEqual(i, 13) test_instance.assertEqual(board.frame, 120) """ return self._fps @fps.setter def fps(self, value: int): self._fps = value @property def width(self) -> int: """ See container_width """ return self.container_width @property def height(self) -> int: """ See container_height """ return self.container_height @property def window(self) -> app.App: """ Gets the parent window Returns: The window """ return self._window @property def rows(self) -> int: """ The number of rows """ return self._rows @rows.setter def rows(self, value: int): self._rows = value self.app.window.dirty = 1 self.view_handler.full_repaint() @property def columns(self) -> int: """ The number of columns """ return self._columns @columns.setter def columns(self, value: int): self._columns = value self.app.window.dirty = 1 self.view_handler.full_repaint() @property def size(self) -> tuple: return (self.columns, self.rows) @size.setter def size(self, value: tuple): self.columns = value[0] self.rows = value[1] self.view_handler.full_repaint() self.app.window.dirty = 1 @property def tile_size(self) -> int: """ The number of columns """ return self._tile_size @tile_size.setter def tile_size(self, value: int): self._tile_size = value self.app.window.dirty = 1 self.view_handler.full_repaint() @property def tile_margin(self) -> int: """ The number of columns """ return self._tile_margin @tile_margin.setter def tile_margin(self, value: int): self._tile_margin = value self.app.window.dirty = 1 self.view_handler.full_repaint() @property def tokens(self) -> pygame.sprite.LayeredDirty: """ A list of all tokens registered to the grid. """ return self._tokens @property def class_name(self) -> str: return self.__class__.__name__ @property def registered_events(self) -> set: return self.event_handler.registered_events @registered_events.setter def registered_events(self, value): return # @TODO setter is used so that board_event_handler is not overwritten by board parent class container @property def background(self): return self.view_handler.background
[docs] def remove_from_board(self, token: token_module.Token): """ removes a token from board. The method is called with token.remove() Args: token: The token to remove from board. Returns: """ self.get_token_connector(token).remove_token_from_board(token)
[docs] def remove_background(self, background=None): """Removes a background from board Args: index: The index of the new background. Defaults to -1 (last background) """ self.view_handler.remove_background()
[docs] def add_background(self, source: Union[str, tuple]) -> "background.Background": """ Adds a new background to the board Args: source: The path to the first image of the background or a color Examples: Multiple Backgrounds: .. code-block:: python board = miniworldmaker.TiledBoard() ... board.add_background("images/soccer_green.jpg") board.add_background("images/space.jpg") Returns: The new created background. """ self.view_handler.add_background(source)
def add_to_board(self, token, position: tuple): """ Adds a Token to the board. Is called in __init__-Method if position is set. Args: board: The board, the actor should be added position: The position on the board where the actor should be added. """ self.get_token_connector(token).add_token_to_board(token, position) def blit_surface_to_window_surface(self): self.app.window.surface.blit(self.surface, self.rect) def get_colors_at_position(self, position: tuple): position = board_position_factory.BoardPositionFactory(self).create(position) return self.position_handler.get_color(position)
[docs] def get_colors_at_line(self, line: list): """ Gets all colors in a line. A line is a list of board_positions Args: line: the line Returns: A list of all colors found at the line """ colors = [] for pos in line: pos = board_position_factory.BoardPositionFactory(self).create(pos) color_at_pos = self.get_colors_at_position(pos) if color_at_pos not in colors: colors.append(color_at_pos) return colors
def get_color_at_rect(self, rect: pygame.Rect, directions=None) -> list: return rect.colors()
[docs] def get_tokens_by_pixel(self, pixel: tuple) -> list: """Gets all tokens by Pixel. Args: pixel: the pixel-coordinates Returns: A list of tokens Examples: >>> position = board.get_mouse_position() >>> tokens = board.get_tokens_by_pixel(position) """ return [token for token in self.tokens if token.rect.collidepoint(pixel)]
[docs] def get_tokens_at_rect(self, rect: pygame.Rect) -> Union[token_module.Token, list]: """ Gets all Tokens which are colliding with a given rect. Args: rect: The rect singleitem: Should the method return a single token (faster) or all tokens at rect (slower) exclude: Exclude a token token_type: Filter return values by token type Returns: A single token or a list of tokens at rect """ pass def get_single_token_at_rect(self, rect: pygame.Rect) -> Union[token_module.Token, list]: """ Gets the first Token which is colliding with a given rect. Args: rect: The rect singleitem: Should the method return a single token (faster) or all tokens at rect (slower) exclude: Exclude a token token_type: Filter return values by token type Returns: A single token or a list of tokens at rect """ pass
@property def image(self) -> pygame.Surface: """ The current displayed image """ return self.view_handler.image @property def surface(self): return self.view_handler.surface @surface.setter def surface(self, value): """ Method is overwritten in subclasses """ pass
[docs] def remove_tokens_from_rect(self, rect, token_class=None, exclude=None): """Removes all tokens in an area Args: rect: A rectangle or a tuple (which is automated converted to a rectangle with tile_size token_class: The class of the tokens which should be removed exclude: A token which should not be removed e.g. the actor itself Returns: all tokens in the area """ board_rect = board_rect_factory.BoardRectFactory(self).create(rect) tokens = self.get_tokens_at_rect(board_rect) for token in tokens: if token is not None: [token.remove() for token in Board.filter_actor_list(tokens, token_class)]
def start(self): self.is_running = True
[docs] def stop(self, frames=1): """ stops the board in n-frames """ if frames == 0: self.is_running = False else: timer.ActionTimer(frames, self.stop, 0)
def clear(self): self.clean() def clean(self): for token in self.tokens: token.remove()
[docs] def reset(self): """Resets the board Creates a new board with init-function - recreates all tokens and actors on the board. Examples: Restarts flappy the bird game after collision with pipe: >>> def on_sensing_collision_with_pipe(self, other, info): >>> self.board.is_running = False >>> self.board.reset() """ self.app.event_manager.send_event_to_containers("reset", self)
[docs] def repaint(self): self.view_handler.repaint()
[docs] def run(self, fullscreen: bool = False, event = None, data = None): """ The method show() should always called at the end of your program. It starts the mainloop. Examples: >>> my_board = Board() # or a subclass of Board >>> my_board.show() """ if not self._is_setup and hasattr(self, "on_setup") and callable(getattr(self, "on_setup")): self.event_handler.handle_event("setup", None) self.app.event_manager.send_event_to_containers("setup", self) if event: self.app.event_manager.send_event_to_containers(event, data) self.app.run(self.image, full_screen=fullscreen)
[docs] def switch_background(self, background: Union[int, Type[appearance.Appearance]]) -> background.Background: """Switches the background of costume Args: index: The index of the new costume. If index=-1, the next costume will be selected Returns: The new costume """ self.view_handler.switch_background(background)
[docs] def update(self): # This is the board-mainloop() # Called in miniworldwindow.update as container.update() if self.is_running or self.frame == 0: # Acting for all actors if self.frame > 0 and self.frame % self.speed == 0: self.act_all() self.run_next_line_in_started_method() self.collision_handler.handle_all_collisions() # run animations self.view_handler.update_all_costumes() self.view_handler.update_background() self._tick_timed_objects() self.frame = self.frame + 1 self.clock.tick(self.fps) self.event_handler.executed_events.clear()
def run_next_line_in_started_method(self): for on_started in self.event_handler.registered_events["on_started"]: if on_started: on_started_source = inspect.getsourcelines(on_started)[0] if self.frame % self.speed == 0 and self.frame != 0: line_number = self.frame // self.speed + 2 if line_number < len(on_started_source): exec(on_started_source[line_number].strip()) def act_all(self): self.event_handler.act_all() def _tick_timed_objects(self): [obj.tick() for obj in self.timed_objects] def handle_event(self, event, data=None): """ Event handling Args: event (str): The event which was thrown, e.g. "key_up", "act", "reset", ... data: The data of the event (e.g. ["S","s"], (155,3), ... """ self.event_handler.handle_event(event, data) def play_sound(self, path: str): self.app.sound_manager.play_sound(path)
[docs] def play_music(self, path: str): """ plays a music by path Args: path: The path to the music Returns: """ self.app.sound_manager.play_music(path)
def find_colors(self, rect, color, threshold=(20, 20, 20, 20)): return self.view_handler.find_colors(rect, color, threshold)
[docs] def get_mouse_position(self) -> Union[board_position.BoardPosition, None]: """ Gets the current mouse_position Returns: Returns the mouse position if mouse is on board. Returns "None" otherwise Examples: This example shows you how to use the mouse_position >>> def act(self): >>> mouse = self.board.get_mouse_position() >>> if mouse: >>> self.point_towards_position(mouse) """ pos = board_position_factory.BoardPositionFactory(self).from_pixel(pygame.mouse.get_pos()) clicked_container = self.app.container_manager.get_container_by_pixel(pos[0], pos[1]) if clicked_container == self: return pos else: return None
def get_board_position_from_pixel(self, pixel): return board_position_factory.BoardPositionFactory(self).from_pixel(pixel) def _update_event_handling(self): self.event_handler.update_event_handling() def is_position_on_board(self, position: board_position.BoardPosition) -> bool: return self.position_handler.is_position_on_board(position)
[docs] def register(self, method): """ Used as decorator e.g. @register def method... """ bound_method = method.__get__(self, self.__class__) setattr(self, method.__name__, bound_method) self.event_handler.register_event(method.__name__, self) return bound_method
def send_message(self, message, data=None): self.app.event_manager.send_event_to_containers("message", message) def screenshot(self, filename="screenshot.jpg"): pygame.image.save(self.surface, filename) def quit(self, exit_code=0): self.app.quit(exit_code) def add_container(self, container, dock, size=None): return self.app.container_manager.add_container(container, dock, size) def switch_board(self, new_board : Board): self.event_handler.handle_switch_board_event(new_board) def get_tokens_by_class_name(self, classname : str): return [token for token in self._tokens if token.__class__.__name__ == classname] def get_token_classes(self): return set([token.__class__ for token in self._tokens]) def get_tokens_by_class(self, classname : str): return [token for token in self._tokens if isinstance(token, classname)] def find_token_class_for_name(self, classname : str): classname = classname.lower() for token_cls in self.get_token_classes(): if token_cls.__name__.lower() == classname: return token_cls return None def find_token_by_parent_class_for_name(self, classname : str): classname = classname.lower() for token_cls in self.get_token_classes(): if token_cls.__name__.lower() == classname: return token_cls parents = inspect.getmro(token_cls) for parent in parents: if parent.__name__.lower() == classname: return parent return None