pyrat.src.PygameRenderingEngine

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

  1#####################################################################################################################################################
  2######################################################################## INFO #######################################################################
  3#####################################################################################################################################################
  4
  5"""
  6    This file is part of the PyRat library.
  7    It is meant to be used as a library, and not to be executed directly.
  8    Please import necessary elements using the following syntax:
  9        from pyrat import <element_name>
 10"""
 11
 12#####################################################################################################################################################
 13###################################################################### IMPORTS ######################################################################
 14#####################################################################################################################################################
 15
 16# External imports
 17from typing import *
 18from typing_extensions import *
 19from numbers import *
 20import copy
 21import multiprocessing
 22import os
 23import glob
 24import distinctipy
 25import math
 26import random
 27import time
 28import queue
 29
 30# PyRat imports
 31from pyrat.src.RenderingEngine import RenderingEngine
 32from pyrat.src.Player import Player
 33from pyrat.src.Maze import Maze
 34from pyrat.src.GameState import GameState
 35
 36#####################################################################################################################################################
 37###################################################################### CLASSES ######################################################################
 38#####################################################################################################################################################
 39
 40class PygameRenderingEngine (RenderingEngine):
 41
 42    """
 43        This class inherits from the RenderingEngine class.
 44        Therefore, it has the attributes and methods defined in the RenderingEngine class in addition to the ones defined below.
 45
 46        This rendering engine uses the pygame library to render the game.
 47        It will create a window and display the game in it.
 48        The window will run in a different process than the one running the game.
 49    """
 50
 51    #############################################################################################################################################
 52    #                                                               MAGIC METHODS                                                               #
 53    #############################################################################################################################################
 54
 55    def __init__ ( self:         Self,
 56                   fullscreen:   bool = False,
 57                   trace_length: Integral = 0,
 58                   gui_speed:    Number = 1.0,
 59                   *args:        Any,
 60                   **kwargs:     Any
 61                 ) ->            Self:
 62
 63        """
 64            This function is the constructor of the class.
 65            When an object is instantiated, this method is called to initialize the object.
 66            This is where you should define the attributes of the object and set their initial values.
 67            Arguments *args and **kwargs are used to pass arguments to the parent constructor.
 68            This is useful not to declare again all the parent's attributes in the child class.
 69            In:
 70                * self:         Reference to the current object.
 71                * fullscreen:   Indicates if the GUI should be fullscreen.
 72                * trace_length: Length of the trace to display.
 73                * gui_speed:    Speed of the GUI.
 74                * args:         Arguments to pass to the parent constructor.
 75                * kwargs:       Keyword arguments to pass to the parent constructor.
 76            Out:
 77                * A new instance of the class.
 78        """
 79
 80        # Inherit from parent class
 81        super().__init__(*args, **kwargs)
 82
 83        # Debug
 84        assert isinstance(fullscreen, bool) # Type check for fullscreen
 85        assert isinstance(trace_length, Integral) # Type check for trace_length
 86        assert isinstance(gui_speed, Number) # Type check for gui_speed
 87        assert trace_length >= 0 # trace_length must be positive
 88        assert gui_speed > 0 # gui_speed must be positive
 89
 90        # Private attributes
 91        self.__fullscreen = fullscreen
 92        self.__trace_length = trace_length
 93        self.__gui_speed = gui_speed
 94        self.__gui_process = None
 95        self.__gui_queue = None
 96
 97    #############################################################################################################################################
 98    #                                                               PUBLIC METHODS                                                              #
 99    #############################################################################################################################################
100
101    @override
102    def render ( self:       Self,
103                 players:    List[Player],
104                 maze:       Maze,
105                 game_state: GameState,
106               ) ->          None:
107        
108        """
109            This method redefines the method of the parent class.
110            This function renders the game to a Pyame window.
111            The window is created in a different process than the one running the game.
112            In:
113                * self:       Reference to the current object.
114                * players:    Players of the game.
115                * maze:       Maze of the game.
116                * game_state: State of the game.
117            Out:
118                * None.
119        """
120
121        # Debug
122        assert isinstance(players, list) # Type check for players
123        assert all(isinstance(player, Player) for player in players) # Type check for players
124        assert isinstance(maze, Maze) # Type check for maze
125        assert isinstance(game_state, GameState) # Type check for game_state
126
127        # Initialize the GUI in a different process at turn 0
128        if game_state.turn == 0:
129
130            # Initialize the GUI process
131            gui_initialized_synchronizer = multiprocessing.Manager().Barrier(2)
132            self.__gui_queue = multiprocessing.Manager().Queue()
133            self.__gui_process = multiprocessing.Process(target=_gui_process_function, args=(gui_initialized_synchronizer, self.__gui_queue, maze, game_state, players, self.__fullscreen, self._render_simplified, self.__trace_length, self.__gui_speed))
134            self.__gui_process.start()
135            gui_initialized_synchronizer.wait()
136        
137        # At each turn, send current info to the process
138        else:
139            self.__gui_queue.put(game_state)
140        
141    #############################################################################################################################################
142
143    @override
144    def end ( self: Self,
145            ) ->    None:
146        
147        """
148            This method redefines the method of the parent class.
149            It waits for the window to be closed before exiting.
150            In:
151                * self: Reference to the current object.
152            Out:
153                * None.
154        """
155
156        # Wait for GUI to be exited to quit if there is one
157        if self.__gui_process is not None:
158            self.__gui_process.join()
159
160#####################################################################################################################################################
161##################################################################### FUNCTIONS #####################################################################
162#####################################################################################################################################################
163
164def _gui_process_function ( gui_initialized_synchronizer: multiprocessing.Barrier,
165                            gui_queue:                    multiprocessing.Queue,
166                            maze:                         Maze,
167                            initial_game_state:           GameState,
168                            players:                      List[Player],
169                            fullscreen:                   bool,
170                            render_simplified:            bool,
171                            trace_length:                 Integral,
172                            gui_speed:                    Number
173                          ) ->                            None:
174    
175    """
176        This function is executed in a separate process for the GUI.
177        It handles rendering in a Pygame environment.
178        It is defined outside of the class due to multiprocessing limitations.
179        In:
180            * gui_initialized_synchronizer: Barrier to synchronize the initialization of the GUI.
181            * gui_queue:                    Queue to receive the game state.
182            * maze:                         Maze of the game.
183            * initial_game_state:           Initial game state.
184            * players:                      Players of the game.
185            * fullscreen:                   Indicates if the GUI should be fullscreen.
186            * render_simplified:            Indicates if the GUI should be simplified.
187            * trace_length:                 Length of the trace to display.
188            * gui_speed:                    Speed of the GUI.
189        Out:
190            * None.
191    """
192
193    # Debug
194    assert isinstance(gui_initialized_synchronizer, multiprocessing.managers.BarrierProxy) # Type check for gui_initialized_synchronizer
195    assert isinstance(gui_queue, multiprocessing.managers.BaseProxy) # Type check for gui_queue
196    assert isinstance(maze, Maze) # Type check for maze
197    assert isinstance(initial_game_state, GameState) # Type check for initial_game_state
198    assert isinstance(players, list) # Type check for players
199    assert all(isinstance(player, Player) for player in players) # Type check for players
200    assert isinstance(fullscreen, bool) # Type check for fullscreen
201    assert isinstance(render_simplified, bool) # Type check for render_simplified
202    assert isinstance(trace_length, Integral) # Type check for trace_length
203    assert isinstance(gui_speed, Number) # Type check for gui_speed
204    assert trace_length >= 0 # trace_length must be positive
205    assert gui_speed > 0.0 # gui_speed must be positive
206
207    # We catch exceptions that may happen during the game
208    try:
209
210        # Initialize PyGame
211        # Imports are done here to avoid multiple initializations in multiprocessing
212        os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide"
213        import pygame
214        import pygame.locals as pglocals
215        pygame.init()
216        pygame.mixer.init()
217
218        # Random number generator
219        rng = random.Random()
220
221        # Start screen
222        if fullscreen:
223            gui_screen = pygame.display.set_mode((0, 0), pygame.NOFRAME)
224            pygame.display.toggle_fullscreen()
225        else:
226            gui_screen = pygame.display.set_mode((int(pygame.display.Info().current_w * 0.8), int(pygame.display.Info().current_h * 0.8)), pygame.SCALED)
227
228        # We will store elements to display
229        maze_elements = []
230        avatar_elements = []
231        player_elements = {}
232        cheese_elements = {}
233        
234        # Parameters of the GUI
235        window_width, window_height = pygame.display.get_surface().get_size()
236        cell_size = int(min(window_width / maze.width, window_height / maze.height) * 0.9)
237        background_color = (0, 0, 0)
238        cell_text_color = (50, 50, 50)
239        cell_text_offset = int(cell_size * 0.1)
240        wall_size = cell_size // 7
241        mud_text_color = (185, 155, 60)
242        corner_wall_ratio = 1.2
243        flag_size = int(cell_size * 0.4)
244        flag_x_offset = int(cell_size * 0.2)
245        flag_x_next_offset = int(cell_size * 0.07)
246        flag_y_offset = int(cell_size * 0.3)
247        game_area_width = cell_size * maze.width
248        game_area_height = cell_size * maze.height
249        maze_x_offset = int((window_width - game_area_width) * 0.9)
250        maze_y_offset = (window_height - game_area_height) // 2
251        avatars_x_offset = window_width - maze_x_offset - game_area_width
252        avatars_area_width = maze_x_offset - 2 * avatars_x_offset
253        avatars_area_height = min(game_area_height // 2, (game_area_height - (len(initial_game_state.teams) - 1) * maze_y_offset) // len(initial_game_state.teams))
254        avatars_area_border = 2
255        avatars_area_angle = 10
256        avatars_area_color = (255, 255, 255)
257        teams_enabled = len(initial_game_state.teams) > 1 or len(list(initial_game_state.teams.keys())[0]) > 0
258        if teams_enabled:
259            avatars_area_padding = avatars_area_height // 13
260            team_text_size = avatars_area_padding * 3
261            colors = distinctipy.distinctipy.get_colors(len(initial_game_state.teams))
262            team_colors = {list(initial_game_state.teams.keys())[i]: tuple([int(c * 255) for c in colors[i]]) for i in range(len(initial_game_state.teams))}
263        else:
264            avatars_area_padding = avatars_area_height // 12
265            team_text_size = 0
266            avatars_area_height -= avatars_area_padding * 3
267            team_colors = {list(initial_game_state.teams.keys())[i]: avatars_area_color for i in range(len(initial_game_state.teams))}
268        player_avatar_size = avatars_area_padding * 3
269        player_avatar_horizontal_padding = avatars_area_padding * 4
270        player_name_text_size = avatars_area_padding
271        cheese_score_size = avatars_area_padding
272        text_size = int(cell_size * 0.17)
273        cheese_size = int(cell_size * 0.4)
274        player_size = int(cell_size * 0.5)
275        flag_border_color = (255, 255, 255)
276        flag_border_width = 1
277        player_border_width = 2
278        cheese_border_color = (255, 255, 0)
279        cheese_border_width = 1
280        cheese_score_border_color = (100, 100, 100)
281        cheese_score_border_width = 1
282        trace_size = wall_size // 2
283        animation_steps = int(max(cell_size / gui_speed, 1))
284        animation_time = 0.01
285        medal_size = min(avatars_x_offset, maze_y_offset) * 2
286        icon_size = 50
287        main_image_factor = 0.8
288        main_image_border_color = (0, 0, 0)
289        main_image_border_size = 1
290        go_image_duration = 0.5
291        
292        # Function to load an image with some scaling
293        # If only 2 arguments are provided, scales keeping ratio specifying the maximum size
294        # If first argument is a directory, returns a random image from it
295        already_loaded_images = {}
296        def ___surface_from_image (file_or_dir_name, target_width_or_max_size, target_height=None):
297            full_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), file_or_dir_name)
298            if os.path.isdir(full_path):
299                full_path = rng.choice(glob.glob(os.path.join(full_path, "*")))
300            loaded_image_key = str(full_path) + "_" + str(target_width_or_max_size) + "_" + str(target_height)
301            if loaded_image_key in already_loaded_images:
302                return already_loaded_images[loaded_image_key]
303            surface = pygame.image.load(full_path).convert_alpha()
304            if target_height is None:
305                max_surface_size = max(surface.get_width(), surface.get_height())
306                surface = pygame.transform.scale(surface, (surface.get_width() * target_width_or_max_size // max_surface_size, surface.get_height() * target_width_or_max_size // max_surface_size))
307            else:
308                surface = pygame.transform.scale(surface, (target_width_or_max_size, target_height))
309            already_loaded_images[loaded_image_key] = surface
310            return surface
311        
312        # Same function for text
313        def ___surface_from_text (text, target_height, text_color, original_font_size=50):
314            surface = pygame.font.SysFont(None, original_font_size).render(text, True, text_color)
315            surface = pygame.transform.scale(surface, (surface.get_width() * target_height // surface.get_height(), target_height))
316            return surface
317
318        # Function to colorize an object
319        def ___colorize (surface, color):
320            final_surface = surface.copy()
321            color_surface = pygame.Surface(final_surface.get_size()).convert_alpha()
322            color_surface.fill(color)
323            final_surface.blit(color_surface, (0, 0), special_flags=pygame.BLEND_MULT)
324            return final_surface
325            
326        # Function to add a colored border around an object
327        def ___add_color_border (surface, border_color, border_size, final_rescale=True):
328            final_surface = pygame.Surface((surface.get_width() + 2 * border_size, surface.get_height() + 2 * border_size)).convert_alpha()
329            final_surface.fill((0, 0, 0, 0))
330            mask_surface = surface.copy()
331            color_surface = pygame.Surface(mask_surface.get_size())
332            color_surface.fill((0, 0, 0, 0))
333            mask_surface.blit(color_surface, (0, 0), special_flags=pygame.BLEND_MIN)
334            color_surface.fill(border_color)
335            mask_surface.blit(color_surface, (0, 0), special_flags=pygame.BLEND_MAX)
336            for offset_x in range(-border_size, border_size + 1):
337                for offset_y in range(-border_size, border_size + 1):
338                    if math.dist([0, 0], [offset_x, offset_y]) <= border_size:
339                        final_surface.blit(mask_surface, (border_size // 2 + offset_x, border_size // 2 + offset_y))
340            final_surface.blit(surface, (border_size // 2, border_size // 2))
341            if final_rescale:
342                final_surface = pygame.transform.scale(final_surface, surface.get_size())
343            return final_surface
344
345        # Function to load the surfaces of a player
346        def ___load_player_surfaces (player_skin, scale, border_color=None, border_width=None, add_border=teams_enabled):
347            try:
348                player_neutral = ___surface_from_image(os.path.join("..", "gui", "players", player_skin.value, "neutral.png"), scale)
349                player_north = ___surface_from_image(os.path.join("..", "gui", "players", player_skin.value, "north.png"), scale)
350                player_south = ___surface_from_image(os.path.join("..", "gui", "players", player_skin.value, "south.png"), scale)
351                player_west = ___surface_from_image(os.path.join("..", "gui", "players", player_skin.value, "west.png"), scale)
352                player_east = ___surface_from_image(os.path.join("..", "gui", "players", player_skin.value, "east.png"), scale)
353                if add_border:
354                    player_neutral = ___add_color_border(player_neutral, border_color, border_width)
355                    player_north = ___add_color_border(player_north, border_color, border_width)
356                    player_south = ___add_color_border(player_south, border_color, border_width)
357                    player_west = ___add_color_border(player_west, border_color, border_width)
358                    player_east = ___add_color_border(player_east, border_color, border_width)
359                return player_neutral, player_north, player_south, player_west, player_east
360            except:
361                return ___load_player_surfaces("default", scale, border_color, border_width, add_border)
362        
363        # Function to play a sound
364        def ___play_sound (file_name, alternate_file_name=None):
365            sound_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), file_name)
366            if not os.path.exists(sound_file):
367                sound_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), alternate_file_name)
368            sound = pygame.mixer.Sound(sound_file)
369            channel = pygame.mixer.find_channel()
370            channel.play(sound)
371        
372        # Function to load the avatar of a player
373        def ___load_player_avatar (player_skin, scale):
374            try:
375                return ___surface_from_image(os.path.join("..", "gui", "players", player_skin.value, "avatar.png"), scale)
376            except:
377                return ___load_player_avatar("default", scale)
378        
379        # Function to get the main color of a surface
380        def ___get_main_color (surface):
381            colors = pygame.surfarray.array2d(surface)
382            counts = {color: 0 for color in set(colors.flatten())}
383            for color in colors.flatten():
384                counts[color] += 1
385            max_occurrences = sorted(counts, key=lambda x: counts[x], reverse=True)[:2]
386            main_color = surface.unmap_rgb(max_occurrences[0])
387            if main_color == (0, 0, 0, 0):
388                main_color = surface.unmap_rgb(max_occurrences[1])
389            return main_color
390
391        # Set window icon and title
392        icon = ___surface_from_image(os.path.join("..", "gui", "icon", "pyrat.png"), icon_size)
393        pygame.display.set_icon(icon)
394        pygame.display.set_caption("PyRat")
395        
396        # Set background color
397        pygame.draw.rect(gui_screen, background_color, pygame.Rect(0, 0, window_width, window_height))
398        
399        # Add cells
400        for row in range(maze.height):
401            for col in range(maze.width):
402                if maze.rc_exists(row, col):
403                    cell = ___surface_from_image(os.path.join("..", "gui", "ground"), cell_size, cell_size)
404                    cell = pygame.transform.rotate(cell, rng.randint(0, 3) * 90)
405                    cell = pygame.transform.flip(cell, bool(rng.randint(0, 1)), bool(rng.randint(0, 1)))
406                    cell_x = maze_x_offset + col * cell_size
407                    cell_y = maze_y_offset + row * cell_size
408                    maze_elements.append((cell_x, cell_y, cell))
409                    
410        # Add mud
411        mud = ___surface_from_image(os.path.join("..", "gui", "mud", "mud.png"), cell_size)
412        for row in range(maze.height):
413            for col in range(maze.width):
414                if maze.rc_exists(row, col):
415                    if maze.rc_exists(row, col - 1):
416                        if maze.has_edge(maze.rc_to_i(row, col), maze.rc_to_i(row, col - 1)):
417                            if maze.get_weight(maze.rc_to_i(row, col), maze.rc_to_i(row, col - 1)) > 1:
418                                mud_x = maze_x_offset + col * cell_size - mud.get_width() // 2
419                                mud_y = maze_y_offset + row * cell_size
420                                maze_elements.append((mud_x, mud_y, mud))
421                                if not render_simplified:
422                                    weight_text = ___surface_from_text(str(maze.get_weight(maze.rc_to_i(row, col), maze.rc_to_i(row, col - 1))), text_size, mud_text_color)
423                                    weight_text_x = maze_x_offset + col * cell_size - weight_text.get_width() // 2
424                                    weight_text_y = maze_y_offset + row * cell_size + (cell_size - weight_text.get_height()) // 2
425                                    maze_elements.append((weight_text_x, weight_text_y, weight_text))
426                    if maze.rc_exists(row - 1, col):
427                        if maze.has_edge(maze.rc_to_i(row, col), maze.rc_to_i(row - 1, col)):
428                            if maze.get_weight(maze.rc_to_i(row, col), maze.rc_to_i(row - 1, col)) > 1:
429                                mud_horizontal = pygame.transform.rotate(mud, 90)
430                                mud_x = maze_x_offset + col * cell_size
431                                mud_y = maze_y_offset + row * cell_size - mud.get_width() // 2
432                                maze_elements.append((mud_x, mud_y, mud_horizontal))
433                                if not render_simplified:
434                                    weight_text = ___surface_from_text(str(maze.get_weight(maze.rc_to_i(row, col), maze.rc_to_i(row - 1, col))), text_size, mud_text_color)
435                                    weight_text_x = maze_x_offset + col * cell_size + (cell_size - weight_text.get_width()) // 2
436                                    weight_text_y = maze_y_offset + row * cell_size - weight_text.get_height() // 2
437                                    maze_elements.append((weight_text_x, weight_text_y, weight_text))
438
439        # Add cell numbers
440        if not render_simplified:
441            for row in range(maze.height):
442                for col in range(maze.width):
443                    if maze.rc_exists(row, col):
444                        cell_text = ___surface_from_text(str(maze.rc_to_i(row, col)), text_size, cell_text_color)
445                        cell_text_x = maze_x_offset + col * cell_size + cell_text_offset
446                        cell_text_y = maze_y_offset + row * cell_size + cell_text_offset
447                        maze_elements.append((cell_text_x, cell_text_y, cell_text))
448        
449        # Add walls
450        walls = []
451        wall = ___surface_from_image(os.path.join("..", "gui", "wall", "wall.png"), cell_size)
452        for row in range(maze.height + 1):
453            for col in range(maze.width + 1):
454                case_outside_to_inside = not maze.rc_exists(row, col) and maze.rc_exists(row, col - 1)
455                case_inside_to_outside = maze.rc_exists(row, col) and not maze.rc_exists(row, col - 1)
456                case_inside_to_inside = maze.rc_exists(row, col) and maze.rc_exists(row, col - 1) and not maze.has_edge(maze.rc_to_i(row, col), maze.rc_to_i(row, col - 1))
457                if case_outside_to_inside or case_inside_to_outside or case_inside_to_inside:
458                    wall_x = maze_x_offset + col * cell_size - wall.get_width() // 2
459                    wall_y = maze_y_offset + row * cell_size
460                    maze_elements.append((wall_x, wall_y, wall))
461                    walls.append((row, col, row, col - 1))
462                case_outside_to_inside = not maze.rc_exists(row, col) and maze.rc_exists(row - 1, col)
463                case_inside_to_outside = maze.rc_exists(row, col) and not maze.rc_exists(row - 1, col)
464                case_inside_to_inside = maze.rc_exists(row, col) and maze.rc_exists(row - 1, col) and not maze.has_edge(maze.rc_to_i(row, col), maze.rc_to_i(row - 1, col))
465                if case_outside_to_inside or case_inside_to_outside or case_inside_to_inside:
466                    wall_horizontal = pygame.transform.rotate(wall, 90)
467                    wall_x = maze_x_offset + col * cell_size
468                    wall_y = maze_y_offset + row * cell_size - wall.get_width() // 2
469                    maze_elements.append((wall_x, wall_y, wall_horizontal))
470                    walls.append((row, col, row - 1, col))
471            
472        # Add corners
473        corner = ___surface_from_image(os.path.join("..", "gui", "wall", "corner.png"), int(wall.get_width() * corner_wall_ratio), int(wall.get_width() * corner_wall_ratio))
474        for row, col, neighbor_row, neighbor_col in walls:
475            if col != neighbor_col:
476                corner_x = maze_x_offset + col * cell_size - corner.get_width() // 2
477                if (row - 1, col, neighbor_row - 1, neighbor_col) not in walls or ((neighbor_row, neighbor_col, neighbor_row - 1, neighbor_col) in walls and (row, col, row - 1, col) in walls and (row - 1, col, neighbor_row - 1, neighbor_col) in walls):
478                    corner_y = maze_y_offset + row * cell_size - corner.get_width() // 2
479                    maze_elements.append((corner_x, corner_y, corner))
480                if (row + 1, col, neighbor_row + 1, neighbor_col) not in walls:
481                    corner_y = maze_y_offset + (row + 1) * cell_size - corner.get_width() // 2
482                    maze_elements.append((corner_x, corner_y, corner))
483            if row != neighbor_row:
484                corner_y = maze_y_offset + row * cell_size - corner.get_width() // 2
485                if (row, col - 1, neighbor_row, neighbor_col - 1) not in walls:
486                    corner_x = maze_x_offset + col * cell_size - corner.get_width() // 2
487                    maze_elements.append((corner_x, corner_y, corner))
488                if (row, col + 1, neighbor_row, neighbor_col + 1) not in walls:
489                    corner_x = maze_x_offset + (col + 1) * cell_size - corner.get_width() // 2
490                    maze_elements.append((corner_x, corner_y, corner))
491        
492        # Add flags
493        if not render_simplified:
494            cells_with_flags = {cell: {} for cell in initial_game_state.player_locations.values()}
495            for player in players:
496                team = [team for team in initial_game_state.teams if player.name in initial_game_state.teams[team]][0]
497                if team not in cells_with_flags[initial_game_state.player_locations[player.name]]:
498                    cells_with_flags[initial_game_state.player_locations[player.name]][team] = 0
499                cells_with_flags[initial_game_state.player_locations[player.name]][team] += 1
500            flag = ___surface_from_image(os.path.join("..", "gui", "flag", "flag.png"), flag_size)
501            max_teams_in_cells = max([len(team) for team in cells_with_flags.values()])
502            max_players_in_cells = max([cells_with_flags[cell][team] for cell in cells_with_flags for team in cells_with_flags[cell]])
503            for cell in cells_with_flags:
504                row, col = maze.i_to_rc(cell)
505                for i_team in range(len(cells_with_flags[cell])):
506                    team = list(cells_with_flags[cell].keys())[i_team]
507                    flag_colored = ___colorize(flag, team_colors[team])
508                    flag_colored = ___add_color_border(flag_colored, flag_border_color, flag_border_width)
509                    for i_player in range(cells_with_flags[cell][team]):
510                        flag_x = maze_x_offset + (col + 1) * cell_size - flag_x_offset - i_player * min(flag_x_next_offset, (cell_size - flag_x_offset) / (max_players_in_cells + 1))
511                        flag_y = maze_y_offset + row * cell_size - flag.get_height() + flag_y_offset + i_team * min(flag_y_offset, (cell_size - flag_y_offset) / (max_teams_in_cells + 1))
512                        maze_elements.append((flag_x, flag_y, flag_colored))
513
514        # Add cheese
515        cheese = ___surface_from_image(os.path.join("..", "gui", "cheese", "cheese.png"), cheese_size)
516        cheese = ___add_color_border(cheese, cheese_border_color, cheese_border_width)
517        for c in initial_game_state.cheese:
518            row, col = maze.i_to_rc(c)
519            cheese_x = maze_x_offset + col * cell_size + (cell_size - cheese.get_width()) // 2
520            cheese_y = maze_y_offset + row * cell_size + (cell_size - cheese.get_height()) // 2
521            cheese_elements[c] = (cheese_x, cheese_y, cheese)
522        
523        # Add players
524        for player in players:
525            team = [team for team in initial_game_state.teams if player.name in initial_game_state.teams[team]][0]
526            player_neutral, player_north, player_south, player_west, player_east = ___load_player_surfaces(player.skin, player_size, team_colors[team], player_border_width)
527            row, col = maze.i_to_rc(initial_game_state.player_locations[player.name])
528            player_x = maze_x_offset + col * cell_size + (cell_size - player_neutral.get_width()) // 2
529            player_y = maze_y_offset + row * cell_size + (cell_size - player_neutral.get_height()) // 2
530            player_elements[player.name] = (player_x, player_y, player_neutral, player_north, player_south, player_west, player_east)
531        
532        # Add avatars area
533        score_locations = {}
534        medal_locations = {}
535        for i in range(len(initial_game_state.teams)):
536        
537            # Box
538            team = list(initial_game_state.teams.keys())[i]
539            team_background = pygame.Surface((avatars_area_width, avatars_area_height))
540            pygame.draw.rect(team_background, background_color, pygame.Rect(0, 0, avatars_area_width, avatars_area_height))
541            pygame.draw.rect(team_background, team_colors[team], pygame.Rect(0, 0, avatars_area_width, avatars_area_height), avatars_area_border, avatars_area_angle)
542            team_background_x = avatars_x_offset
543            team_background_y = (1 + i) * maze_y_offset + i * avatars_area_height if len(initial_game_state.teams) > 1 else (window_height - avatars_area_height) // 2
544            avatar_elements.append((team_background_x, team_background_y, team_background))
545            medal_locations[team] = (team_background_x + avatars_area_width, team_background_y)
546            
547            # Team name
548            team_text = ___surface_from_text(team, team_text_size, team_colors[team])
549            if team_text.get_width() > avatars_area_width - 2 * avatars_area_padding:
550                ratio = (avatars_area_width - 2 * avatars_area_padding) / team_text.get_width()
551                team_text = pygame.transform.scale(team_text, (int(team_text.get_width() * ratio), int(team_text.get_height() * ratio)))
552            team_text_x = avatars_x_offset + (avatars_area_width - team_text.get_width()) // 2
553            team_text_y = team_background_y + avatars_area_padding + (team_text_size - team_text.get_height()) // 2
554            if not teams_enabled:
555                team_text_size = -avatars_area_padding
556            avatar_elements.append((team_text_x, team_text_y, team_text))
557            
558            # Players avatars
559            player_images = []
560            for j in range(len(initial_game_state.teams[team])):
561                player = [player for player in players if player.name == initial_game_state.teams[team][j]][0]
562                player_avatar = ___load_player_avatar(player.skin, player_avatar_size)
563                player_images.append(player_avatar)
564            avatar_area = pygame.Surface((2 * avatars_area_padding + sum([player_image.get_width() for player_image in player_images]) + player_avatar_horizontal_padding * (len(initial_game_state.teams[team]) - 1), player_avatar_size))
565            pygame.draw.rect(avatar_area, background_color, pygame.Rect(0, 0, avatar_area.get_width(), avatar_area.get_height()))
566            player_x = avatars_area_padding
567            centers = []
568            for player_avatar in player_images:
569                avatar_area.blit(player_avatar, (player_x, 0))
570                centers.append(player_x + player_avatar.get_width() // 2)
571                player_x += player_avatar.get_width() + player_avatar_horizontal_padding
572            if avatar_area.get_width() > avatars_area_width - 2 * avatars_area_padding:
573                ratio = (avatars_area_width - 2 * avatars_area_padding) / avatar_area.get_width()
574                centers = [center * ratio for center in centers]
575                avatar_area = pygame.transform.scale(avatar_area, (int(avatar_area.get_width() * ratio), int(avatar_area.get_height() * ratio)))
576            avatar_area_x = avatars_x_offset + (avatars_area_width - avatar_area.get_width()) // 2
577            avatar_area_y = team_background_y + 2 * avatars_area_padding + team_text_size + (player_avatar_size - avatar_area.get_height()) // 2
578            avatar_elements.append((avatar_area_x, avatar_area_y, avatar_area))
579
580            # Players names
581            for j in range(len(initial_game_state.teams[team])):
582                player_name = initial_game_state.teams[team][j]
583                while True:
584                    player_name_text = ___surface_from_text(player_name, player_name_text_size, avatars_area_color)
585                    if player_name_text.get_width() > (avatars_area_width - 2 * avatars_area_padding) / len(initial_game_state.teams[team]):
586                        player_name = player_name[:-2] + "."
587                    else:
588                        break
589                player_name_text_x = avatar_area_x + centers[j] - player_name_text.get_width() // 2
590                player_name_text_y = team_background_y + 3 * avatars_area_padding + team_text_size + player_avatar_size + (player_name_text_size - player_name_text.get_height()) // 2
591                avatar_elements.append((player_name_text_x, player_name_text_y, player_name_text))
592        
593            # Score locations
594            cheese_missing = ___surface_from_image(os.path.join("..", "gui", "cheese", "cheese_missing.png"), cheese_score_size)
595            score_x_offset = avatars_x_offset + avatars_area_padding
596            score_margin = avatars_area_width - 2 * avatars_area_padding - cheese_missing.get_width()
597            if len(initial_game_state.cheese) > 1:
598                score_margin /= (len(initial_game_state.cheese) - 1)
599            score_margin = min(score_margin, cheese_missing.get_width() * 2)
600            estimated_width = cheese_missing.get_width() + (len(initial_game_state.cheese) - 1) * score_margin
601            if estimated_width < avatars_area_width - 2 * avatars_area_padding:
602                score_x_offset += (avatars_area_width - 2 * avatars_area_padding - estimated_width) / 2
603            score_y_offset = team_background_y + 4 * avatars_area_padding + team_text_size + player_avatar_size + player_name_text_size
604            score_locations[team] = (score_x_offset, score_margin, score_y_offset)
605
606        # Show maze
607        def ___show_maze ():
608            pygame.draw.rect(gui_screen, background_color, pygame.Rect(maze_x_offset, maze_y_offset, game_area_width, game_area_height))
609            for surface_x, surface_y, surface in maze_elements:
610                gui_screen.blit(surface, (surface_x, surface_y))
611        ___show_maze()
612        
613        # Show cheese
614        def ___show_cheese (cheese):
615            for c in cheese:
616                cheese_x, cheese_y, surface = cheese_elements[c]
617                gui_screen.blit(surface, (cheese_x, cheese_y))
618        ___show_cheese(initial_game_state.cheese)
619        
620        # Show_players at initial locations
621        def ___show_initial_players ():
622            for p in player_elements:
623                player_x, player_y, player_neutral, _, _ , _, _ = player_elements[p]
624                gui_screen.blit(player_neutral, (player_x, player_y))
625        ___show_initial_players()
626        
627        # Show avatars
628        def ___show_avatars ():
629            for surface_x, surface_y, surface in avatar_elements:
630                gui_screen.blit(surface, (surface_x, surface_y))
631        ___show_avatars()
632        
633        # Show scores
634        def ___show_scores (team_scores):
635            cheese_missing = ___surface_from_image(os.path.join("..", "gui", "cheese", "cheese_missing.png"), cheese_score_size)
636            cheese_missing = ___add_color_border(cheese_missing, cheese_score_border_color, cheese_score_border_width)
637            cheese_eaten = ___surface_from_image(os.path.join("..", "gui", "cheese", "cheese_eaten.png"), cheese_score_size)
638            cheese_eaten = ___add_color_border(cheese_eaten, cheese_score_border_color, cheese_score_border_width)
639            for team in score_locations:
640                score_x_offset, score_margin, score_y_offset = score_locations[team]
641                for i in range(int(team_scores[team])):
642                    gui_screen.blit(cheese_eaten, (score_x_offset + i * score_margin, score_y_offset))
643                if int(team_scores[team]) != team_scores[team]:
644                    cheese_partial = ___surface_from_image(os.path.join("..", "gui", "cheese", "cheese_eaten.png"), cheese_score_size)
645                    cheese_partial = ___colorize(cheese_partial, [(team_scores[team] - int(team_scores[team])) * 255] * 3)
646                    cheese_partial = ___add_color_border(cheese_partial, cheese_score_border_color, cheese_score_border_width)
647                    gui_screen.blit(cheese_partial, (score_x_offset + int(team_scores[team]) * score_margin, score_y_offset))
648                for j in range(math.ceil(team_scores[team]), len(initial_game_state.cheese)):
649                    gui_screen.blit(cheese_missing, (score_x_offset + j * score_margin, score_y_offset))
650        initial_scores = {team: 0 for team in initial_game_state.teams}
651        ___show_scores(initial_scores)
652        
653        # Show preprocessing message
654        preprocessing_image = ___surface_from_image(os.path.join("..", "gui", "drawings", "pyrat_preprocessing.png"), int(min(game_area_width, game_area_height) * main_image_factor))
655        preprocessing_image = ___add_color_border(preprocessing_image, main_image_border_color, main_image_border_size)
656        go_image = ___surface_from_image(os.path.join("..", "gui", "drawings", "pyrat_go.png"), int(min(game_area_width, game_area_height) * main_image_factor))
657        go_image = ___add_color_border(go_image, main_image_border_color, main_image_border_size)
658        main_image_x = maze_x_offset + (game_area_width - preprocessing_image.get_width()) / 2
659        main_image_y = maze_y_offset + (game_area_height - preprocessing_image.get_height()) / 2
660        gui_screen.blit(preprocessing_image, (main_image_x, main_image_y))
661        
662        # Prepare useful variables
663        current_state = copy.deepcopy(initial_game_state)
664        mud_being_crossed = {player.name: 0 for player in players}
665        traces = {player.name: [(player_elements[player.name][0] + player_elements[player.name][2].get_width() / 2, player_elements[player.name][1] + player_elements[player.name][2].get_height() / 2)] for player in players}
666        trace_colors = {player.name: ___get_main_color(player_elements[player.name][2]) for player in players}
667        player_surfaces = {player.name: player_elements[player.name][2] for player in players}
668
669        # Show and indicate when ready
670        gui_running = True
671        pygame.display.flip()
672        time.sleep(0.1)
673        pygame.display.update()
674        gui_initialized_synchronizer.wait()
675        
676        # Run until the user asks to quit
677        while gui_running:
678            try:
679
680                # We check for termination
681                for event in pygame.event.get():
682                    if event.type == pygame.QUIT or (event.type == pglocals.KEYDOWN and event.key == pglocals.K_ESCAPE):
683                        gui_running = False
684                if not gui_running:
685                    break
686                
687                # Get turn info
688                new_state = gui_queue.get(False)
689                
690                # Indicate when preprocessing is over for a little time
691                if new_state.turn == 1:
692                    ___show_maze()
693                    ___show_cheese(current_state.cheese if i != animation_steps - 1 else new_state.cheese)
694                    ___show_initial_players()
695                    gui_screen.blit(go_image, (main_image_x, main_image_y))
696                    pygame.display.update((maze_x_offset, maze_y_offset, maze.width * cell_size, maze.height * cell_size))
697                    time.sleep(go_image_duration)
698
699                # Enter mud?
700                for player in players:
701                    if new_state.muds[player.name]["count"] > 0 and mud_being_crossed[player.name] == 0:
702                        mud_being_crossed[player.name] = new_state.muds[player.name]["count"] + 1
703
704                # Choose the correct player surface
705                for player in players:
706                    player_x, player_y, player_neutral, player_north, player_south, player_west, player_east = player_elements[player.name]
707                    row, col = maze.i_to_rc(current_state.player_locations[player.name])
708                    adjusted_new_location = new_state.player_locations[player.name] if not new_state.is_in_mud(player.name) else new_state.muds[player.name]["target"]
709                    new_row, new_col = maze.i_to_rc(adjusted_new_location)
710                    player_x += player_surfaces[player.name].get_width() / 2
711                    player_y += player_surfaces[player.name].get_height() / 2
712                    if new_col > col:
713                        player_surfaces[player.name] = player_east
714                    elif new_col < col:
715                        player_surfaces[player.name] = player_west
716                    elif new_row > row:
717                        player_surfaces[player.name] = player_south
718                    elif new_row < row:
719                        player_surfaces[player.name] = player_north
720                    else:
721                        player_surfaces[player.name] = player_neutral
722                    player_x -= player_surfaces[player.name].get_width() / 2
723                    player_y -= player_surfaces[player.name].get_height() / 2
724                    player_elements[player.name] = (player_x, player_y, player_neutral, player_north, player_south, player_west, player_east)
725
726                # Move players
727                for i in range(animation_steps):
728                
729                    # Reset background & cheese
730                    ___show_maze()
731                    ___show_cheese(current_state.cheese if i != animation_steps - 1 else new_state.cheese)
732                    
733                    # Move player with trace
734                    for player in players:
735                        player_x, player_y, player_neutral, player_north, player_south, player_west, player_east = player_elements[player.name]
736                        row, col = maze.i_to_rc(current_state.player_locations[player.name])
737                        adjusted_new_location = new_state.player_locations[player.name] if not new_state.is_in_mud(player.name) else new_state.muds[player.name]["target"]
738                        new_row, new_col = maze.i_to_rc(adjusted_new_location)
739                        shift = (i + 1) * cell_size / animation_steps
740                        if mud_being_crossed[player.name] > 0:
741                            shift /= mud_being_crossed[player.name]
742                            shift += (mud_being_crossed[player.name] - new_state.muds[player.name]["count"] - 1) * cell_size / mud_being_crossed[player.name]
743                        next_x = player_x if col == new_col else player_x + shift if new_col > col else player_x - shift
744                        next_y = player_y if row == new_row else player_y + shift if new_row > row else player_y - shift
745                        if i == animation_steps - 1 and new_state.muds[player.name]["count"] == 0:
746                            player_elements[player.name] = (next_x, next_y, player_neutral, player_north, player_south, player_west, player_east)
747                        if trace_length > 0:
748                            pygame.draw.line(gui_screen, trace_colors[player.name], (next_x + player_surfaces[player.name].get_width() / 2, next_y + player_surfaces[player.name].get_height() / 2), traces[player.name][-1], width=trace_size)
749                            for j in range(1, trace_length):
750                                if len(traces[player.name]) > j:
751                                    pygame.draw.line(gui_screen, trace_colors[player.name], traces[player.name][-j-1], traces[player.name][-j], width=trace_size)
752                            if len(traces[player.name]) == trace_length + 1:
753                                final_segment_length = math.sqrt((traces[player.name][-1][0] - (next_x + player_surfaces[player.name].get_width() / 2))**2 + (traces[player.name][-1][1] - (next_y + player_surfaces[player.name].get_height() / 2))**2)
754                                ratio = 1 - final_segment_length / cell_size
755                                pygame.draw.line(gui_screen, trace_colors[player.name], traces[player.name][1], (traces[player.name][1][0] + ratio * (traces[player.name][0][0] - traces[player.name][1][0]), traces[player.name][1][1] + ratio * (traces[player.name][0][1] - traces[player.name][1][1])), width=trace_size)
756                        gui_screen.blit(player_surfaces[player.name], (next_x, next_y))
757                    
758                    # Update maze & wait for animation
759                    pygame.display.update((maze_x_offset, maze_y_offset, maze.width * cell_size, maze.height * cell_size))
760                    time.sleep(animation_time / animation_steps)
761
762                # Exit mud?
763                for player in players:
764                    if new_state.muds[player.name]["count"] == 0:
765                        mud_being_crossed[player.name] = 0
766                    if mud_being_crossed[player.name] == 0:
767                        player_x, player_y, _, _, _, _, _ = player_elements[player.name]
768                        if traces[player.name][-1] != (player_x + player_surfaces[player.name].get_width() / 2, player_y + player_surfaces[player.name].get_height() / 2):
769                            traces[player.name].append((player_x + player_surfaces[player.name].get_width() / 2, player_y + player_surfaces[player.name].get_height() / 2))
770                        traces[player.name] = traces[player.name][-trace_length-1:]
771                
772                # Play a sound is a cheese is eaten
773                for player in players:
774                    if new_state.player_locations[player.name] in current_state.cheese and mud_being_crossed[player.name] == 0:
775                        ___play_sound(os.path.join("..", "gui", "players", player.skin.value, "cheese_eaten.wav"), os.path.join("..", "gui", "players", "default", "cheese_eaten.wav"))
776                
777                # Update score
778                ___show_avatars()
779                new_scores = new_state.get_score_per_team()
780                ___show_scores(new_scores)
781                current_state = new_state
782                
783                # Indicate if the game is over
784                if new_state.game_over():
785                    sorted_results = sorted([(new_scores[team], team) for team in new_scores], reverse=True)
786                    medals = [___surface_from_image(os.path.join("..", "gui", "endgame", medal_name), medal_size) for medal_name in ["first.png", "second.png", "third.png", "others.png"]]
787                    for i in range(len(sorted_results)):
788                        if i > 0 and sorted_results[i][0] != sorted_results[i-1][0] and len(medals) > 1:
789                            del medals[0]
790                        team = sorted_results[i][1]
791                        gui_screen.blit(medals[0], (medal_locations[team][0] - medals[0].get_width() / 2, medal_locations[team][1] - medals[0].get_height() / 3))
792                    ___play_sound(os.path.join("..", "gui", "endgame", "game_over.wav"))
793                pygame.display.update((0, 0, maze_x_offset, window_height))
794                
795            # Ignore exceptions raised due to emtpy queue
796            except queue.Empty:
797                pass
798            
799        # Quit PyGame
800        pygame.quit()
801        
802    # Ignore
803    except:
804        pass
805
806#####################################################################################################################################################
807#####################################################################################################################################################
class PygameRenderingEngine(pyrat.src.RenderingEngine.RenderingEngine):
 41class PygameRenderingEngine (RenderingEngine):
 42
 43    """
 44        This class inherits from the RenderingEngine class.
 45        Therefore, it has the attributes and methods defined in the RenderingEngine class in addition to the ones defined below.
 46
 47        This rendering engine uses the pygame library to render the game.
 48        It will create a window and display the game in it.
 49        The window will run in a different process than the one running the game.
 50    """
 51
 52    #############################################################################################################################################
 53    #                                                               MAGIC METHODS                                                               #
 54    #############################################################################################################################################
 55
 56    def __init__ ( self:         Self,
 57                   fullscreen:   bool = False,
 58                   trace_length: Integral = 0,
 59                   gui_speed:    Number = 1.0,
 60                   *args:        Any,
 61                   **kwargs:     Any
 62                 ) ->            Self:
 63
 64        """
 65            This function is the constructor of the class.
 66            When an object is instantiated, this method is called to initialize the object.
 67            This is where you should define the attributes of the object and set their initial values.
 68            Arguments *args and **kwargs are used to pass arguments to the parent constructor.
 69            This is useful not to declare again all the parent's attributes in the child class.
 70            In:
 71                * self:         Reference to the current object.
 72                * fullscreen:   Indicates if the GUI should be fullscreen.
 73                * trace_length: Length of the trace to display.
 74                * gui_speed:    Speed of the GUI.
 75                * args:         Arguments to pass to the parent constructor.
 76                * kwargs:       Keyword arguments to pass to the parent constructor.
 77            Out:
 78                * A new instance of the class.
 79        """
 80
 81        # Inherit from parent class
 82        super().__init__(*args, **kwargs)
 83
 84        # Debug
 85        assert isinstance(fullscreen, bool) # Type check for fullscreen
 86        assert isinstance(trace_length, Integral) # Type check for trace_length
 87        assert isinstance(gui_speed, Number) # Type check for gui_speed
 88        assert trace_length >= 0 # trace_length must be positive
 89        assert gui_speed > 0 # gui_speed must be positive
 90
 91        # Private attributes
 92        self.__fullscreen = fullscreen
 93        self.__trace_length = trace_length
 94        self.__gui_speed = gui_speed
 95        self.__gui_process = None
 96        self.__gui_queue = None
 97
 98    #############################################################################################################################################
 99    #                                                               PUBLIC METHODS                                                              #
100    #############################################################################################################################################
101
102    @override
103    def render ( self:       Self,
104                 players:    List[Player],
105                 maze:       Maze,
106                 game_state: GameState,
107               ) ->          None:
108        
109        """
110            This method redefines the method of the parent class.
111            This function renders the game to a Pyame window.
112            The window is created in a different process than the one running the game.
113            In:
114                * self:       Reference to the current object.
115                * players:    Players of the game.
116                * maze:       Maze of the game.
117                * game_state: State of the game.
118            Out:
119                * None.
120        """
121
122        # Debug
123        assert isinstance(players, list) # Type check for players
124        assert all(isinstance(player, Player) for player in players) # Type check for players
125        assert isinstance(maze, Maze) # Type check for maze
126        assert isinstance(game_state, GameState) # Type check for game_state
127
128        # Initialize the GUI in a different process at turn 0
129        if game_state.turn == 0:
130
131            # Initialize the GUI process
132            gui_initialized_synchronizer = multiprocessing.Manager().Barrier(2)
133            self.__gui_queue = multiprocessing.Manager().Queue()
134            self.__gui_process = multiprocessing.Process(target=_gui_process_function, args=(gui_initialized_synchronizer, self.__gui_queue, maze, game_state, players, self.__fullscreen, self._render_simplified, self.__trace_length, self.__gui_speed))
135            self.__gui_process.start()
136            gui_initialized_synchronizer.wait()
137        
138        # At each turn, send current info to the process
139        else:
140            self.__gui_queue.put(game_state)
141        
142    #############################################################################################################################################
143
144    @override
145    def end ( self: Self,
146            ) ->    None:
147        
148        """
149            This method redefines the method of the parent class.
150            It waits for the window to be closed before exiting.
151            In:
152                * self: Reference to the current object.
153            Out:
154                * None.
155        """
156
157        # Wait for GUI to be exited to quit if there is one
158        if self.__gui_process is not None:
159            self.__gui_process.join()

This class inherits from the RenderingEngine class. Therefore, it has the attributes and methods defined in the RenderingEngine class in addition to the ones defined below.

This rendering engine uses the pygame library to render the game. It will create a window and display the game in it. The window will run in a different process than the one running the game.

PygameRenderingEngine( fullscreen: bool = False, trace_length: numbers.Integral = 0, gui_speed: numbers.Number = 1.0, *args: typing_extensions.Any, **kwargs: typing_extensions.Any)
56    def __init__ ( self:         Self,
57                   fullscreen:   bool = False,
58                   trace_length: Integral = 0,
59                   gui_speed:    Number = 1.0,
60                   *args:        Any,
61                   **kwargs:     Any
62                 ) ->            Self:
63
64        """
65            This function is the constructor of the class.
66            When an object is instantiated, this method is called to initialize the object.
67            This is where you should define the attributes of the object and set their initial values.
68            Arguments *args and **kwargs are used to pass arguments to the parent constructor.
69            This is useful not to declare again all the parent's attributes in the child class.
70            In:
71                * self:         Reference to the current object.
72                * fullscreen:   Indicates if the GUI should be fullscreen.
73                * trace_length: Length of the trace to display.
74                * gui_speed:    Speed of the GUI.
75                * args:         Arguments to pass to the parent constructor.
76                * kwargs:       Keyword arguments to pass to the parent constructor.
77            Out:
78                * A new instance of the class.
79        """
80
81        # Inherit from parent class
82        super().__init__(*args, **kwargs)
83
84        # Debug
85        assert isinstance(fullscreen, bool) # Type check for fullscreen
86        assert isinstance(trace_length, Integral) # Type check for trace_length
87        assert isinstance(gui_speed, Number) # Type check for gui_speed
88        assert trace_length >= 0 # trace_length must be positive
89        assert gui_speed > 0 # gui_speed must be positive
90
91        # Private attributes
92        self.__fullscreen = fullscreen
93        self.__trace_length = trace_length
94        self.__gui_speed = gui_speed
95        self.__gui_process = None
96        self.__gui_queue = None

This function is the constructor of the class. When an object is instantiated, this method is called to initialize the object. This is where you should define the attributes of the object and set their initial values. Arguments args and *kwargs are used to pass arguments to the parent constructor. This is useful not to declare again all the parent's attributes in the child class.

In:
  • self: Reference to the current object.
  • fullscreen: Indicates if the GUI should be fullscreen.
  • trace_length: Length of the trace to display.
  • gui_speed: Speed of the GUI.
  • args: Arguments to pass to the parent constructor.
  • kwargs: Keyword arguments to pass to the parent constructor.
Out:
  • A new instance of the class.
@override
def render( self: typing_extensions.Self, players: List[pyrat.src.Player.Player], maze: pyrat.src.Maze.Maze, game_state: pyrat.src.GameState.GameState) -> None:
102    @override
103    def render ( self:       Self,
104                 players:    List[Player],
105                 maze:       Maze,
106                 game_state: GameState,
107               ) ->          None:
108        
109        """
110            This method redefines the method of the parent class.
111            This function renders the game to a Pyame window.
112            The window is created in a different process than the one running the game.
113            In:
114                * self:       Reference to the current object.
115                * players:    Players of the game.
116                * maze:       Maze of the game.
117                * game_state: State of the game.
118            Out:
119                * None.
120        """
121
122        # Debug
123        assert isinstance(players, list) # Type check for players
124        assert all(isinstance(player, Player) for player in players) # Type check for players
125        assert isinstance(maze, Maze) # Type check for maze
126        assert isinstance(game_state, GameState) # Type check for game_state
127
128        # Initialize the GUI in a different process at turn 0
129        if game_state.turn == 0:
130
131            # Initialize the GUI process
132            gui_initialized_synchronizer = multiprocessing.Manager().Barrier(2)
133            self.__gui_queue = multiprocessing.Manager().Queue()
134            self.__gui_process = multiprocessing.Process(target=_gui_process_function, args=(gui_initialized_synchronizer, self.__gui_queue, maze, game_state, players, self.__fullscreen, self._render_simplified, self.__trace_length, self.__gui_speed))
135            self.__gui_process.start()
136            gui_initialized_synchronizer.wait()
137        
138        # At each turn, send current info to the process
139        else:
140            self.__gui_queue.put(game_state)

This method redefines the method of the parent class. This function renders the game to a Pyame window. The window is created in a different process than the one running the game.

In:
  • self: Reference to the current object.
  • players: Players of the game.
  • maze: Maze of the game.
  • game_state: State of the game.
Out:
  • None.
@override
def end(self: typing_extensions.Self) -> None:
144    @override
145    def end ( self: Self,
146            ) ->    None:
147        
148        """
149            This method redefines the method of the parent class.
150            It waits for the window to be closed before exiting.
151            In:
152                * self: Reference to the current object.
153            Out:
154                * None.
155        """
156
157        # Wait for GUI to be exited to quit if there is one
158        if self.__gui_process is not None:
159            self.__gui_process.join()

This method redefines the method of the parent class. It waits for the window to be closed before exiting.

In:
  • self: Reference to the current object.
Out:
  • None.