Source code for miniworldmaker.boards.board_templates.pixel_board.board

import math
from typing import Tuple, Union, Type, Optional, List, cast

import miniworldmaker.appearances.appearance as appearance
import miniworldmaker.appearances.background as background_mod
import miniworldmaker.appearances.backgrounds_manager as backgrounds_manager
import miniworldmaker.base.app as app
import miniworldmaker.boards.board_base as board_base
import miniworldmaker.boards.board_manager.board_collision_manager as coll_manager
import miniworldmaker.boards.board_manager.board_event_manager as event_manager
import miniworldmaker.boards.board_manager.board_mouse_manager as mouse_manager
import miniworldmaker.boards.board_manager.board_music_manager as board_music_manager
import miniworldmaker.boards.board_manager.board_sound_manager as board_sound_manager
import miniworldmaker.dialogs.ask as ask
import miniworldmaker.positions.direction as board_direction
import miniworldmaker.positions.position as board_position
import miniworldmaker.positions.rect as board_rect
import miniworldmaker.tokens.token as token_mod
import miniworldmaker.tools.board_inspection as board_inspection
import miniworldmaker.tools.color as color
import miniworldmaker.tools.timer as timer
import pygame
from miniworldmaker.exceptions.miniworldmaker_exception import (
    BoardArgumentsError,
)


[docs]class Board(board_base.BaseBoard): """A board is a playing field on which tokens can move. A board has a `background` and provides basic functions for the positioning of tokens and for the collision detection of tokens, which can be queried via the sensors of the tokens. You can create your own board by creating a class that inherits from Board or you can directly create a board object of type `Board` or one of its child classes (`TiledBoard`, `PhysicsBoard`, ...). *Board* A board for pixel accurate games. * The position of a token on a Board is the pixel at topleft of token. * New tokens are created with top-left corner of token rect at position. * Two tokens collide when their sprites overlap. .. image:: ../_images/asteroids.jpg :alt: Asteroids **Other Boards:** * TiledBoard: For Boards using Tiles, like rogue-like rpgs, see :doc:`TiledBoard <../api/board.tiledboard>`) * PhysicsBoard: For Boards using the PhysicsEngine, see :doc:`PhysicsBoard <../api/board_physicsboard>`) Examples: Creating a TiledBoard Object: .. code-block:: python from miniworldmaker import * my_board = TiledBoard() my_board.columns = 30 my_board.rows = 20 my_board.tile_size = 20 Creating a TiledBoard-Subclass. .. code-block:: python import miniworldmaker class MyBoard(miniworldmaker.TiledBoard): def on_setup(self): self.columns = 30 self.rows = 20 self.tile_size = 20 Creating a Board Object: .. code-block:: python from miniworldmaker import * my_board = Board() my_board.columns = 300 my_board.rows = 200 Creating a Board Subclass .. code-block:: python import miniworldmaker class MyBoard(miniworldmaker.Board): def on_setup(self): self.columns = 300 self.rows = 200 See also: * See: :doc:`Board <../api/board>` * See: :doc:`TiledBoard <../api/board.tiledboard>` Args: view_x: columns of new board (default: 40) view_y: rows of new board (default:40) tile_size: Size of tiles (1 for normal Boards, can differ for TiledBoards) """ def __init__( self, view_x: Union[int, Tuple[int]] = 400, view_y: int = 400, tile_size: int = 1, ): if type(view_x) != int or type(view_y) != int: # If first param is tuple, generate board from tuple-data if type(view_x) == tuple: size = view_x view_x = size[0] view_y = size[1] else: raise BoardArgumentsError(view_x, view_y) self._tile_size = tile_size self.camera = self._get_camera_manager_class()(view_x, view_y, self) self._tokens = pygame.sprite.LayeredDirty() self.event_manager: event_manager.BoardEventManager = self._create_event_manager() super().__init__() self.backgrounds_manager: "backgrounds_manager.BackgroundsManager" = backgrounds_manager.BackgroundsManager( self ) self.mouse_manager: "mouse_manager.StageMouseManager" = mouse_manager.StageMouseManager(self) self.ask: "ask.Ask" = ask.Ask(self) self.is_display_initialized: bool = False self._fps: int = 60 self._key_pressed: bool = False self._animated: bool = False self._is_filled: bool = False self._orientation: int = 0 self._static: bool = False self._speed: int = 1 # All tokens are acting on n:th frame with n = self.speed self._default_is_filled = False self._default_fill_color = None self._default_border_color = None self._default_border = None self.is_running: bool = True self.is_listening: bool = True self.frame: int = 0 self.clock: pygame.time.Clock = pygame.time.Clock() if not app.App.init: app.App.init = True self.app: "app.App" = app.App("miniworldmaker") app.App.running_app = self.app app.App.running_board = self app.App.running_boards.append(self) else: self.app = app.App.running_app self.music: "board_music_manager.BoardMusicManager" = board_music_manager.BoardMusicManager(self.app) self.sound: "board_sound_manager.BoardSoundManager" = board_sound_manager.BoardSoundManager(self.app) self.background = background_mod.Background(self) self.background.update() self.collision_manager: "coll_manager.BoardCollisionManager" = coll_manager.BoardCollisionManager(self) self.timed_objects: list = [] self.app.event_manager.to_event_queue("setup", None) self.dynamic_tokens = pygame.sprite.Group() self._registered_methods = [] self.tokens_fixed_size = False self.container_width = self.camera.get_viewport_width_in_pixels() self.container_height = self.camera.get_viewport_height_in_pixels() self.app.container_manager.add_topleft(self) def _create_event_manager(self): return event_manager.BoardEventManager(self)
[docs] def is_position_on_the_board(self, pos): """Checks if position is on the board. Returns: True, if Position is on the board. """ if 0 <= pos[0] < self.boundary_x and 0 <= pos[1] < self.boundary_y: return True else: return False
is_position_on_board = is_position_on_the_board contains_position = is_position_on_the_board
[docs] def contains_rect(self, rect: Union[tuple, pygame.Rect]): """Detects if rect is completely on the board. Args: rect: A rectangle as tuple (top, left, width, height) """ rect = board_rect.Rect.create(rect) topleft_on_the_board = self.contains_position(rect.topleft) bottom_right_on_the_board = self.contains_position(rect.bottomright) return topleft_on_the_board or bottom_right_on_the_board
[docs] def setup_board(self): # Implemented in TiledBoards pass
@property def speed(self) -> int: """speed defines how often the method ``act()`` will be called. If e.g. ``speed = 30``, the game logic will be called every 30th-frame. .. note:: You can adjust the frame-rate with ``board.fps`` Examples: Set speed and fps. .. code-block:: python from miniworldmaker import * board = Board() board.size = (120,210) @board.register def on_setup(self): board.fps = 1 board.speed = 3 @board.register def act(self): board.run() Output: ``` 3 6 9 12 15 ``` """ return self._speed @speed.setter def speed(self, value: int): self._speed = value @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 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: """Gets width of board in pixels. (for tiledboard: columns * tile_size) """ return self.container_width @property def height(self) -> int: """Gets height of board in pixels. (for `tiledboard`: rows * tile_size) """ return self.container_height @property def boundary_x(self) -> int: """The x-boundary (defaults to view_size)""" return self.camera.boundary_x @boundary_x.setter def boundary_x(self, value: int): self.camera.boundary_x = value @property def boundary_y(self) -> int: """The y-boundary (defaults to view_size)""" return self.camera.boundary_y @boundary_y.setter def boundary_y(self, value: int): self.camera.boundary_y = value @property def viewport_width(self) -> int: return self.camera.viewport_width @viewport_width.setter def viewport_width(self, value: int): self.camera.viewport_width = value @property def viewport_height(self) -> int: """The y-boundary (defaults to view_size)""" return self.camera.viewport_height @viewport_height.setter def viewport_height(self, value: int): self.camera.viewport_height = value @property def columns(self) -> int: return self.camera.viewport_width @columns.setter def columns(self, value: int): self.setup_board() self.camera.viewport_width = value self.boundary_x = value @property def rows(self) -> int: return self.camera.viewport_height @rows.setter def rows(self, value: int): self.setup_board() self.viewport_height = value self.boundary_y = value
[docs] def borders(self, value: Union[tuple, "board_position.Position", pygame.Rect]) -> list: """Gets all borders from a source (`Position` or `Rect`). Args: value: Position or rect Returns: A list of borders, e.g. ["left", "top"], if rect is touching the left a top border. """ pass
@property def camera_x(self): return self.camera.x @camera_x.setter def camera_x(self, value): self.camera.x = value @property def camera_y(self): return self.camera.y @camera_y.setter def camera_y(self, value): self.camera.y = value @property def tile_size(self) -> int: """Tile size of each tile, if board has tiles Returns: The tile-size in pixels. """ return self._tile_size @tile_size.setter def tile_size(self, value: int): self.set_tile_size(value)
[docs] def set_tile_size(self, value): self._tile_size = value self.camera.reload_camera() self.background.set_dirty("all", background_mod.Background.RELOAD_ACTUAL_IMAGE)
@property def size(self) -> tuple: """Set the size of board Examples: Create a board with 800 columns and 600 rows: .. code-block:: python board = miniworldmaker.PixelBoard() board.size = (800, 600) """ return self.boundary_x, self.boundary_y @size.setter def size(self, value: tuple): self.boundary_x = value[0] self.boundary_y = value[1] self.viewport_width = value[0] self.viewport_height = value[1] @property def default_fill_color(self): """Set default fill color for borders and lines""" return self._default_fill_color @default_fill_color.setter def default_fill_color(self, value): self._default_fill_color = color.Color(value).get()
[docs] def default_fill(self, value): """Set default fill color for borders and lines""" self._is_filled = value if self.default_is_filled is not None and self.default_is_filled: self._default_fill_color = color.Color(value).get()
@property def default_is_filled(self): return self._default_is_filled @default_is_filled.setter def default_is_filled(self, value): self.default_fill(value) @property def default_stroke_color(self): """Set default stroke color for borders and lines. (equivalent to border-color)""" return self.default_border_color @default_stroke_color.setter def default_stroke_color(self, value): """Set default stroke color for borders and lines. (equivalent to border-color)""" self.default_border_color = value @property def default_border_color(self): """Set default border color for borders and lines. .. note:: ``board.default_border_color`` does not have an effect, if no border is set. You must also set ``board.border`` > 0. Examples: Create tokens with and without with border .. code-block:: python from miniworldmaker import * board = Board(210,80) board.default_border_color = (0,0, 255) board.default_border = 1 t = Token((10,10)) t2 = Token ((60, 10)) t2.border_color = (0,255, 0) t2.border = 5 # overwrites default border t3 = Token ((110, 10)) t3.border = None # removes border t4 = Token ((160, 10)) t4.add_costume("images/player.png") # border for sprite board.run() Output: .. image:: ../_images/border_color.png :width: 200px :alt: borders """ return self._default_border_color @default_border_color.setter def default_border_color(self, value): self._default_border_color = value @property def default_border(self): """Sets default border color for tokens .. note:: You must also set a border for token. Examples: Set default border for tokens: .. code-block:: python from miniworldmaker import * board = Board(210,80) board.default_border_color = (0,0, 255) board.default_border = 1 t = Token((10,10)) board.run() """ return self._default_border @default_border.setter def default_border(self, value): self._default_border = value @property def tokens(self) -> pygame.sprite.LayeredDirty: """A list of all tokens registered to the board.""" return self._tokens @property def backgrounds(self) -> list: """Returns all backgrounds of the board as list.""" return self.backgrounds_manager.backgrounds @property def background(self) -> "background_mod.Background": """Returns the current background""" return self.get_background()
[docs] def get_background(self) -> "background_mod.Background": """Returns the current background""" return self.backgrounds_manager.background
@background.setter def background(self, source): if isinstance(source, appearance.Appearance): self.backgrounds_manager.background = source else: self.backgrounds_manager.add_background(source)
[docs] def switch_background(self, background: Union[int, Type["appearance.Appearance"]]) -> "background_mod.Background": """Switches the background Args: background: The index of the new background or an Appearance. If index = -1, the next background will be selected Examples: Switch between different backgrounds: .. code-block:: python from miniworldmaker import * board = Board() token = Token() board.add_background("images/1.png") board.add_background((255, 0, 0, 255)) board.add_background("images/2.png") @timer(frames = 40) def switch(): board.switch_background(0) @timer(frames = 80) def switch(): board.switch_background(1) @timer(frames = 160) def switch(): board.switch_background(2) board.run() Output: .. image:: ../_images/switch_background.png :width: 100% :alt: Switch background Returns: The new background """ return self.backgrounds_manager.switch_appearance(background)
[docs] def remove_background(self, background=None): """Removes a background from board Args: background: The index of the new background. Defaults to -1 (last background) or an Appearance """ return self.backgrounds_manager.remove_appearance(background)
[docs] def set_background(self, source: Union[str, tuple]) -> "background_mod.Background": """Adds a new background to the board If multiple backgrounds are added, the last adds background will be set as active background. Args: source: The path to the first image of the background or a color (e.g. (255,0,0) for red or "images/my_background.png" as path to a background. Examples: Add multiple Backgrounds: .. code-block:: pythonlist from miniworldmaker import * board = Board() board.add_background((255, 0 ,0)) # red board.add_background((0, 0 ,255)) # blue board.run() # Shows a blue board. Returns: The new created background. """ return self.backgrounds_manager.set_background(source)
[docs] def add_background(self, source: Union[str, tuple]) -> "background_mod.Background": """Adds a new background to the board If multiple backgrounds are added, the last adds background will be set as active background. Args: source: The path to the first image of the background or a color (e.g. (255,0,0) for red or "images/my_background.png" as path to a background. Examples: Add multiple Backgrounds: .. code-block:: pythonlist from miniworldmaker import * board = Board() board.add_background((255, 0 ,0)) # red board.add_background((0, 0 ,255)) # blue board.run() # Shows a blue board. Returns: The new created background. """ return self.backgrounds_manager.add_background(source)
[docs] def start(self): """Starts the board, if board is not running.""" self.is_running = True
[docs] def stop(self, frames=0): """Stops the board. Args: frames (int, optional): If ``frames`` is set, board will be stopped in n frames. . Defaults to 0. """ if frames == 0: self.is_running = False else: timer.ActionTimer(frames, self.stop, 0)
[docs] def start_listening(self): self.is_listening = True
[docs] def stop_listening(self): self.is_listening = False
[docs] def clear(self): """Alias of ``clean`` see: :py:meth:`Board.clean` """ self.clean()
[docs] def clean(self): """removes all tokens Examples: Clear a board: .. code-block:: python from miniworldmaker import * import random board = Board() for i in range(50): Token((random.randint(0,board.width), random.randint(0,board.height))) @timer(frames = 50) def clean(): board.clear() board.run() Output: .. image:: ../_images/clear.png :width: 100% :alt: Clean board """ for token in self.tokens: token.remove()
[docs] def run(self, fullscreen: bool = False, fit_desktop: bool = False, replit: bool = False, event=None, data=None): """ The method show() should always be called at the end of your program. It starts the mainloop. Examples: A minimal miniworldmaker-program: .. code-block:: python from miniworldmaker import * board = TiledBoard() token = Token() board.run() Output: .. image:: ../_images/min.png :width: 200px :alt: Minimal program """ self.app.prepare_mainloop() if hasattr(self, "on_setup"): self.on_setup() self.init_display() self.is_running = True if event: self.app.event_manager.to_event_queue(event, data) self.app.run(self.image, fullscreen=fullscreen, fit_desktop=fit_desktop, replit=replit)
[docs] def init_display(self): if not self.is_display_initialized: self.is_display_initialized = True self.background.set_dirty("all", self.background.LOAD_NEW_IMAGE)
[docs] def play_sound(self, path: str): """plays sound from path""" self.app.sound_manager.play_sound(path)
[docs] def stop_sounds(self): self.app.sound_manager.stop()
[docs] def play_music(self, path: str): """plays a music from path Args: path: The path to the music Returns: """ self.music.play(path)
[docs] def stop_music(self): """stops a music Returns: """ self.music.stop()
[docs] def get_mouse_position(self) -> Union["board_position.Position", None]: """ Gets the current mouse_position Returns: Returns the mouse position if mouse is on the board. Returns None otherwise Examples: Create circles at current mouse position: .. code-block:: python from miniworldmaker import * board = PixelBoard() @board.register def act(self): c = Circle(board.get_mouse_position(), 40) c.color = (255,255,255, 100) c.border = None board.run() Output: .. image:: ../_images/mousepos.png :width: 200px :alt: Circles at mouse-position """ return self.mouse_manager.mouse_position
[docs] def get_mouse_x(self) -> int: """Gets x-coordinate of mouse-position""" if self.mouse_manager.mouse_position: return self.mouse_manager.mouse_position[0] else: return 0
[docs] def get_mouse_y(self) -> int: """Gets y-coordinate of mouse-position""" if self.mouse_manager.mouse_position: return self.mouse_manager.mouse_position[1] else: return 0
[docs] def get_prev_mouse_position(self): """gets mouse-position of last frame""" return self.mouse_manager.prev_mouse_position
[docs] def is_mouse_pressed(self) -> bool: """Returns True, if mouse is pressed""" return self.mouse_manager.mouse_left_is_clicked() or self.mouse_manager.mouse_left_is_clicked()
[docs] def is_mouse_left_pressed(self) -> bool: """Returns True, if mouse left button is pressed""" return self.mouse_manager.mouse_left_is_clicked()
[docs] def is_mouse_right_pressed(self) -> bool: """Returns True, if mouse right button is pressed""" return self.mouse_manager.mouse_right_is_clicked()
[docs] def send_message(self, message, data=None): """Sends broadcast message A message can be received by the board or any token on board """ self.app.event_manager.to_event_queue("message", message)
[docs] def quit(self, exit_code=0): """quits app and closes the window""" self.app.quit(exit_code)
[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: .. code-block:: python def on_sensing_collision_with_pipe(self, other, info): self.board.is_running = False self.board.reset() """ self.app.event_manager.event_queue.clear() for background in self.backgrounds: self.backgrounds_manager.remove_appearance(background) self.clean() if hasattr(self, "on_setup"): self.on_setup()
[docs] def switch_board(self, new_board: "Board"): """Switches to another board Args: new_board (Board): _description_ """ self.stop() self.stop_listening() # for background in self.backgrounds: # self.backgrounds_manager.remove_appearance(background) self.app.event_manager.event_queue.clear() self.app.container_manager.switch_board(new_board) new_board.init_display() new_board.is_running = True new_board.reset() new_board.background.set_dirty("all", 2) new_board.start_listening()
[docs] def get_color_from_pixel(self, position: "board_position.Position") -> tuple: """ Returns the color at a specific position Examples: .. code-block:: python from miniworldmaker import * board = Board((100,60)) @board.register def on_setup(self): self.add_background((255,0,0)) print(self.get_color_from_pixel((5,5))) board.run() Output: (255, 0, 0, 255) .. image:: ../_images/get_color.png :width: 100px :alt: get color of red screen Args: position: The position to search for Returns: The color """ position = board_position.Position.create(position) return self.app.window.surface.get_at(position.to_int())
[docs] def get_from_pixel(self, position: Union["board_position.Position", Tuple]) -> Optional["board_position.Position"]: """Gets Position from pixel PixelBoard: the pixel position is returned TiledBoard: the tile-position is returned :param position: Position as pixel coordinates :return: The pixel position, if position is on the board, None if position is not on Board. """ column = position[0] row = position[1] position = board_position.Position(column, row) if column < self.container_width and row < self.container_height: return position else: return None
[docs] def get_board_position_from_pixel(self, pixel): """ Alias for get_from_pixel """ return self.get_from_pixel(pixel)
[docs] def to_pixel(self, position): x = position[0] y = position[1] return x, y
[docs] def on_setup(self): """Overwrite or register this method to call `on_setup`-Actions """ pass
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 """ return self.camera.get_viewport_width_in_pixels() @property def container_width(self) -> int: """ The width of the container """ return self.camera.get_viewport_width_in_pixels() @container_width.setter def container_width(self, value): self._container_width = value self.on_change() @property def container_height(self) -> int: """ The height of the container """ return self.camera.get_viewport_height_in_pixels() @container_height.setter def container_height(self, value): self._container_height = value self.on_change() @property def has_background(self) -> bool: return self.backgrounds_manager.has_appearance() @property def registered_events(self) -> set: return self.event_manager.registered_events @registered_events.setter def registered_events(self, value): return # setter is defined so that board_event_manager is not overwritten by board parent class container def add_to_board(self, token, position: tuple): """Adds a Token to the board. Is called in __init__-Method if position is set. Args: token: The token, which should be added to the board. position: The position on the board where the actor should be added. """ self.get_token_connector(token).add_token_to_board()
[docs] def detect_tokens(self, position: Union["board_position.Position", Tuple[float, float]]) -> List["token_mod.Token"]: """Gets all tokens which are found at a specific position. Args: position: Position, where tokens should be searched. Returns: A list of tokens Examples: Get all tokens at mouse position: .. code-block:: python position = board.get_mouse_position() tokens = board.get_tokens_by_pixel(position) """ # overwritten in tiled_board_sensor return cast(List["token_mod.Token"], [token for token in self.tokens if token.board_sensor.detect_point(position)])
get_tokens_at_position = detect_tokens
[docs] def get_tokens_from_pixel(self, pixel: Union["board_position.Position", Tuple[float, float]]): return self.detect_tokens(pixel) # overwritten in tiled_board
@property def image(self) -> pygame.Surface: """The current displayed image""" return self.backgrounds_manager.image
[docs] def repaint(self): self.background.repaint() # called 1/frame in container.repaint()
[docs] def update(self): """The mainloop, called once per frame. Called in app.update() when update() from all containers is called. """ if self.is_running or self.frame == 0: # Acting for all actors@static if self.frame > 0 and self.frame % self.speed == 0: self._act_all() self.collision_manager.handle_all_collisions() self.mouse_manager.update_positions() if self.frame == 0: self.init_display() # run animations self.background.update() self.background._update_all_costumes() # @TODO: Update costumes for animated costumes, performance self._tick_timed_objects() self.frame = self.frame + 1 self.clock.tick(self.fps) self.event_manager.executed_events.clear()
def _act_all(self): """Overwritten in subclasses, e.g. physics_board""" self.event_manager.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_manager.handle_event(event, data)
[docs] def register(self, method: callable) -> callable: """ Used as decorator e.g. @register def method... """ self._registered_methods.append(method) bound_method = board_inspection.BoardInspection(self).bind_method(method) self.event_manager.register_event(method.__name__, self) return bound_method
[docs] def unregister(self, method: callable): self._registered_methods.remove(method) board_inspection.BoardInspection(self).unbind_method(method)
@property def fill_color(self): return self.background.fill_color @fill_color.setter def fill_color(self, value): self.background.fill(value) # Alias color = fill_color
[docs] def direction(self, point1, point2): pass
[docs] @staticmethod def distance_to(pos1: "board_position.Position", pos2: "board_position.Position"): pos1 = board_position.Position.create(pos1) pos2 = board_position.Position.create(pos2) if type(pos2) == board_position.Position: return math.sqrt((pos1.x - pos2.x) ** 2 + (pos1.y - pos2.y) ** 2) elif type(pos2) == tuple: return math.sqrt((pos1.x - pos2[0]) ** 2 + (pos1.y - pos2[1]) ** 2)
[docs] @staticmethod def direction_to(pos1: "board_position.Position", pos2: "board_position.Position") -> "board_direction.Direction": return board_direction.Direction.from_two_points(pos1, pos2)