Source code for pyrat.src.rendering.pygame_rendering_engine

##########################################################################################
########################################## INFO ##########################################
##########################################################################################

# This file is part of the PyRat library.
# It is meant to be used as a library, and not to be executed directly.
# Please import necessary elements using the following syntax:
#     from pyrat import <element_name>

"""
This module provides a rendering engine using the ``pygame`` library.
It creates a window and displays the game in it.
The window itself is built and run by the ``gui`` package, in a separate process, so that drawing never slows the game down.
"""

##########################################################################################
######################################### IMPORTS ########################################
##########################################################################################

# External imports
import multiprocessing
import threading

# PyRat imports
from pyrat.src.rendering.rendering_engine import RenderingEngine
from pyrat.src.rendering.gui.process import run_gui_process
from pyrat.src.players.player import Player
from pyrat.src.mazes.maze import Maze
from pyrat.src.game.game_state import GameState
from pyrat.src.game.exceptions import PyRatException
from pyrat.src.game.main_guard import check_main_guard

##########################################################################################
######################################### CLASSES ########################################
##########################################################################################

[docs] class PygameRenderingEngine (RenderingEngine): """ *(This class inherits from* ``RenderingEngine`` *).* This rendering engine uses the ``pygame`` library to render the game. It creates a window and displays the game in it. The window runs in a different process than the one running the game, and receives the successive states of the game through a queue. It shows the moves at its own pace, set by the ``rendering_speed`` argument, and not at the pace of the game. The window may therefore lag behind the game, and catches up once the game is over. """ ################################################################################## # CONSTRUCTOR # ##################################################################################
[docs] def __init__ ( self, fullscreen: bool = False, trace_length: int = 0, *args: object, **kwargs: object ) -> None: """ Initializes a new instance of the class. Args: fullscreen: Indicates if the GUI should be fullscreen. trace_length: Length of the trace to display. *args: Arguments to pass to the parent constructor. **kwargs: Keyword arguments to pass to the parent constructor. """ # Inherit from parent class super().__init__(*args, **kwargs) # Check validity if not isinstance(fullscreen, bool): raise PyRatException(f"Argument 'fullscreen' must be a boolean, got {fullscreen!r} of type {type(fullscreen).__name__}") if not isinstance(trace_length, int): raise PyRatException(f"Argument 'trace_length' must be an integer, got {trace_length!r} of type {type(trace_length).__name__}") if not (trace_length >= 0): raise PyRatException(f"Argument 'trace_length' must be non-negative, got {trace_length!r}") # The window is opened in another process, which needs the game to be created from a block 'if __name__ == "__main__":' check_main_guard("The window of the game is opened in another process") # Private attributes self.__fullscreen = fullscreen self.__trace_length = trace_length self.__gui_process = None self.__gui_queue = None self.__manager = None
################################################################################## # PUBLIC METHODS # ##################################################################################
[docs] def end (self) -> None: """ *(This method redefines the method of the parent class with the same name).* It waits for the window to be closed before exiting. """ # Wait for GUI to be exited to quit if there is one if self.__gui_process is not None: self.__gui_process.join()
##################################################################################
[docs] def render ( self, players: list[Player], maze: Maze, game_state: GameState, ) -> None: """ *(This method redefines the method of the parent class with the same name).* This function renders the game to a ``pygame`` window. The window is created in a different process than the one running the game. Args: players: Players of the game. maze: Maze of the game. game_state: State of the game. Raises: PyRatException: If no player is given, if a player is not in the game state, or if the window could not be opened. """ # Check validity if not isinstance(players, list): raise PyRatException(f"Argument 'players' must be a list, got an object of type {type(players).__name__}") if not all(isinstance(player, Player) for player in players): raise PyRatException(f"All elements of 'players' must be of type 'pyrat.Player', got an element of type {type(next(player for player in players if not isinstance(player, Player))).__name__}") if not isinstance(maze, Maze): raise PyRatException(f"Argument 'maze' must be of type 'pyrat.Maze', got an object of type {type(maze).__name__}") if not isinstance(game_state, GameState): raise PyRatException(f"Argument 'game_state' must be of type 'pyrat.GameState', got an object of type {type(game_state).__name__}") if not (len(players) > 0): raise PyRatException("Argument 'players' must contain at least one player, got an empty list") if not all(player.get_name() in game_state.player_locations for player in players): raise PyRatException(f"All players must be in the game state, got player {next(player.get_name() for player in players if player.get_name() not in game_state.player_locations)!r}, which is not in 'game_state.player_locations' (players of the game state: {game_state.get_players()!r})") # Initialize the GUI in a different process at turn 0, and wait for the window to be ready # If the window cannot be opened, the process breaks the barrier instead of letting us wait forever if game_state.turn == 0: self.__manager = multiprocessing.Manager() gui_initialized_synchronizer = self.__manager.Barrier(2) self.__gui_queue = self.__manager.Queue() self.__gui_process = multiprocessing.Process(target=run_gui_process, args=(gui_initialized_synchronizer, self.__gui_queue, maze, game_state, players, self.__fullscreen, self._render_simplified, self.__trace_length, self._rendering_speed)) self.__gui_process.start() try: gui_initialized_synchronizer.wait() except threading.BrokenBarrierError: raise PyRatException("The graphical interface could not be started, see the error reported above by the PyRat window, or render the game in the terminal with render_mode=RenderMode.ANSI") # At each turn, send current info to the process else: self.__gui_queue.put(game_state)
########################################################################################## ##########################################################################################