Source code for PygameRenderingEngine

#####################################################################################################################################################
######################################################################## 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 will create a window and display the game in it.
"""

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

# External imports
import copy
import multiprocessing
import multiprocessing.managers as mpmanagers
import os
import glob
import distinctipy
import math
import random
import time
import queue

# PyRat imports
from pyrat.src.RenderingEngine import RenderingEngine
from pyrat.src.Player import Player
from pyrat.src.Maze import Maze
from pyrat.src.GameState import GameState

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

[docs] class PygameRenderingEngine (RenderingEngine): """ *(This class inherits from* ``RenderingEngine`` *).* 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. """ ############################################################################################################################################# # MAGIC METHODS # #############################################################################################################################################
[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) # Debug assert isinstance(fullscreen, bool), "Argument 'fullscreen' must be a boolean" assert isinstance(trace_length, int), "Argument 'trace_length' must be an integer" assert trace_length >= 0, "Argument 'trace_length' must be positive" # Private attributes self.__fullscreen = fullscreen self.__trace_length = trace_length self.__gui_process = None self.__gui_queue = 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. """ # Debug assert isinstance(players, list), "Argument 'players' must be a list" assert all(isinstance(player, Player) for player in players), "All elements of 'players' must be of type 'pyrat.Player'" assert isinstance(maze, Maze), "Argument 'maze' must be of type 'pyrat.Maze'" assert isinstance(game_state, GameState), "Argument 'game_state' must be of type 'pyrat.GameState'" # Initialize the GUI in a different process at turn 0 if game_state.turn == 0: # Initialize the GUI process gui_initialized_synchronizer = multiprocessing.Manager().Barrier(2) self.__gui_queue = multiprocessing.Manager().Queue() 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._rendering_speed)) self.__gui_process.start() gui_initialized_synchronizer.wait() # At each turn, send current info to the process else: self.__gui_queue.put(game_state)
##################################################################################################################################################### ##################################################################### FUNCTIONS ##################################################################### ##################################################################################################################################################### def _gui_process_function ( gui_initialized_synchronizer: mpmanagers.BarrierProxy, gui_queue: mpmanagers.BaseProxy, maze: Maze, initial_game_state: GameState, players: list[Player], fullscreen: bool, render_simplified: bool, trace_length: int, rendering_speed: float ) -> None: """ This function is executed in a separate process for the GUI. It handles rendering in a ``pygame`` environment. It is defined outside of the class due to multiprocessing limitations. Args: gui_queue: Queue to receive the game state. maze: Maze of the game. initial_game_state: Initial game state. players: Players of the game. fullscreen: Indicates if the GUI should be fullscreen. render_simplified: Indicates if the GUI should be simplified. trace_length: Length of the trace to display. rendering_speed: Speed at which the game should be rendered. """ # Debug assert isinstance(gui_initialized_synchronizer, mpmanagers.BarrierProxy), "Argument 'gui_initialized_synchronizer' must be a multiprocessing.Barrier" assert isinstance(gui_queue, mpmanagers.BaseProxy), "Argument 'gui_queue' must be a multiprocessing.Queue" assert isinstance(maze, Maze), "Argument 'maze' must be of type 'pyrat.Maze'" assert isinstance(initial_game_state, GameState), "Argument 'initial_game_state' must be of type 'pyrat.GameState'" assert isinstance(players, list), "Argument 'players' must be a list" assert all(isinstance(player, Player) for player in players), "All elements of 'players' must be of type 'pyrat.Player'" assert isinstance(fullscreen, bool), "Argument 'fullscreen' must be a boolean" assert isinstance(render_simplified, bool), "Argument 'render_simplified' must be a boolean" assert isinstance(trace_length, int), "Argument 'trace_length' must be an integer" assert isinstance(rendering_speed, float), "Argument 'rendering_speed' must be a real number" assert trace_length >= 0, "Argument 'trace_length' must be positive" assert rendering_speed > 0.0, "Argument 'rendering_speed' must be positive" # We catch exceptions that may happen during the game try: # Initialize PyGame # Imports are done here to avoid multiple initializations in multiprocessing os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide" import pygame import pygame.locals as pglocals pygame.init() pygame.mixer.init() # Random number generator rng = random.Random() # Start screen if fullscreen: gui_screen = pygame.display.set_mode((0, 0), pygame.NOFRAME) pygame.display.toggle_fullscreen() else: gui_screen = pygame.display.set_mode((int(pygame.display.Info().current_w * 0.8), int(pygame.display.Info().current_h * 0.8)), pygame.SCALED) # We will store elements to display maze_elements = [] avatar_elements = [] player_elements = {} cheese_elements = {} # Parameters of the GUI window_width, window_height = pygame.display.get_surface().get_size() cell_size = int(min(window_width / maze.get_width(), window_height / maze.get_height()) * 0.9) background_color = (0, 0, 0) cell_text_color = (50, 50, 50) cell_text_offset = int(cell_size * 0.1) wall_size = cell_size // 7 mud_text_color = (185, 155, 60) corner_wall_ratio = 1.2 flag_size = int(cell_size * 0.4) flag_x_offset = int(cell_size * 0.2) flag_x_next_offset = int(cell_size * 0.07) flag_y_offset = int(cell_size * 0.3) game_area_width = cell_size * maze.get_width() game_area_height = cell_size * maze.get_height() maze_x_offset = int((window_width - game_area_width) * 0.9) maze_y_offset = (window_height - game_area_height) // 2 avatars_x_offset = window_width - maze_x_offset - game_area_width avatars_area_width = maze_x_offset - 2 * avatars_x_offset 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)) avatars_area_border = 2 avatars_area_angle = 10 avatars_area_color = (255, 255, 255) teams_enabled = len(initial_game_state.teams) > 1 or len(list(initial_game_state.teams.keys())[0]) > 0 if teams_enabled: avatars_area_padding = avatars_area_height // 13 team_text_size = avatars_area_padding * 3 colors = distinctipy.distinctipy.get_colors(len(initial_game_state.teams)) 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))} else: avatars_area_padding = avatars_area_height // 12 team_text_size = 0 avatars_area_height -= avatars_area_padding * 3 team_colors = {list(initial_game_state.teams.keys())[i]: avatars_area_color for i in range(len(initial_game_state.teams))} player_avatar_size = avatars_area_padding * 3 player_avatar_horizontal_padding = avatars_area_padding * 4 player_name_text_size = avatars_area_padding cheese_score_size = avatars_area_padding text_size = int(cell_size * 0.17) cheese_size = int(cell_size * 0.4) player_size = int(cell_size * 0.5) flag_border_color = (255, 255, 255) flag_border_width = 1 player_border_width = 2 cheese_border_color = (255, 255, 0) cheese_border_width = 1 cheese_score_border_color = (100, 100, 100) cheese_score_border_width = 1 trace_size = wall_size // 2 animation_steps = int(max(cell_size / rendering_speed, 1)) animation_time = 0.01 medal_size = min(avatars_x_offset, maze_y_offset) * 2 icon_size = 50 main_image_factor = 0.8 main_image_border_color = (0, 0, 0) main_image_border_size = 1 go_image_duration = 0.5 # Function to load an image with some scaling # If only 2 arguments are provided, scales keeping ratio specifying the maximum size # If first argument is a directory, returns a random image from it already_loaded_images = {} def ___surface_from_image (file_or_dir_name, target_width_or_max_size, target_height=None): full_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), file_or_dir_name) if os.path.isdir(full_path): full_path = rng.choice(glob.glob(os.path.join(full_path, "*"))) loaded_image_key = str(full_path) + "_" + str(target_width_or_max_size) + "_" + str(target_height) if loaded_image_key in already_loaded_images: return already_loaded_images[loaded_image_key] surface = pygame.image.load(full_path).convert_alpha() if target_height is None: max_surface_size = max(surface.get_width(), surface.get_height()) 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)) else: surface = pygame.transform.scale(surface, (target_width_or_max_size, target_height)) already_loaded_images[loaded_image_key] = surface return surface # Same function for text def ___surface_from_text (text, target_height, text_color, original_font_size=50): surface = pygame.font.SysFont(None, original_font_size).render(text, True, text_color) surface = pygame.transform.scale(surface, (surface.get_width() * target_height // surface.get_height(), target_height)) return surface # Function to colorize an object def ___colorize (surface, color): final_surface = surface.copy() color_surface = pygame.Surface(final_surface.get_size()).convert_alpha() color_surface.fill(color) final_surface.blit(color_surface, (0, 0), special_flags=pygame.BLEND_MULT) return final_surface # Function to add a colored border around an object def ___add_color_border (surface, border_color, border_size, final_rescale=True): final_surface = pygame.Surface((surface.get_width() + 2 * border_size, surface.get_height() + 2 * border_size)).convert_alpha() final_surface.fill((0, 0, 0, 0)) mask_surface = surface.copy() color_surface = pygame.Surface(mask_surface.get_size()) color_surface.fill((0, 0, 0, 0)) mask_surface.blit(color_surface, (0, 0), special_flags=pygame.BLEND_MIN) color_surface.fill(border_color) mask_surface.blit(color_surface, (0, 0), special_flags=pygame.BLEND_MAX) for offset_x in range(-border_size, border_size + 1): for offset_y in range(-border_size, border_size + 1): if math.dist([0, 0], [offset_x, offset_y]) <= border_size: final_surface.blit(mask_surface, (border_size // 2 + offset_x, border_size // 2 + offset_y)) final_surface.blit(surface, (border_size // 2, border_size // 2)) if final_rescale: final_surface = pygame.transform.scale(final_surface, surface.get_size()) return final_surface # Function to load the surfaces of a player def ___load_player_surfaces (player_skin, scale, border_color=None, border_width=None, add_border=teams_enabled): try: player_neutral = ___surface_from_image(os.path.join("..", "gui", "players", player_skin.value, "neutral.png"), scale) player_north = ___surface_from_image(os.path.join("..", "gui", "players", player_skin.value, "north.png"), scale) player_south = ___surface_from_image(os.path.join("..", "gui", "players", player_skin.value, "south.png"), scale) player_west = ___surface_from_image(os.path.join("..", "gui", "players", player_skin.value, "west.png"), scale) player_east = ___surface_from_image(os.path.join("..", "gui", "players", player_skin.value, "east.png"), scale) if add_border: player_neutral = ___add_color_border(player_neutral, border_color, border_width) player_north = ___add_color_border(player_north, border_color, border_width) player_south = ___add_color_border(player_south, border_color, border_width) player_west = ___add_color_border(player_west, border_color, border_width) player_east = ___add_color_border(player_east, border_color, border_width) return player_neutral, player_north, player_south, player_west, player_east except: return ___load_player_surfaces("default", scale, border_color, border_width, add_border) # Function to play a sound def ___play_sound (file_name, alternate_file_name=None): sound_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), file_name) if not os.path.exists(sound_file): sound_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), alternate_file_name) sound = pygame.mixer.Sound(sound_file) channel = pygame.mixer.find_channel() channel.play(sound) # Function to load the avatar of a player def ___load_player_avatar (player_skin, scale): try: return ___surface_from_image(os.path.join("..", "gui", "players", player_skin.value, "avatar.png"), scale) except: return ___load_player_avatar("default", scale) # Function to get the main color of a surface def ___get_main_color (surface): colors = pygame.surfarray.array2d(surface) counts = {color: 0 for color in set(colors.flatten())} for color in colors.flatten(): counts[color] += 1 max_occurrences = sorted(counts, key=lambda x: counts[x], reverse=True)[:2] main_color = surface.unmap_rgb(max_occurrences[0]) if main_color == (0, 0, 0, 0): main_color = surface.unmap_rgb(max_occurrences[1]) return main_color # Set window icon and title icon = ___surface_from_image(os.path.join("..", "gui", "icon", "pyrat.png"), icon_size) pygame.display.set_icon(icon) pygame.display.set_caption("PyRat") # Set background color pygame.draw.rect(gui_screen, background_color, pygame.Rect(0, 0, window_width, window_height)) # Add cells for row in range(maze.get_height()): for col in range(maze.get_width()): if maze.rc_exists(row, col): cell = ___surface_from_image(os.path.join("..", "gui", "ground"), cell_size, cell_size) cell = pygame.transform.rotate(cell, rng.randint(0, 3) * 90) cell = pygame.transform.flip(cell, bool(rng.randint(0, 1)), bool(rng.randint(0, 1))) cell_x = maze_x_offset + col * cell_size cell_y = maze_y_offset + row * cell_size maze_elements.append((cell_x, cell_y, cell)) # Add mud mud = ___surface_from_image(os.path.join("..", "gui", "mud", "mud.png"), cell_size) for row in range(maze.get_height()): for col in range(maze.get_width()): if maze.rc_exists(row, col): if maze.rc_exists(row, col - 1): if maze.has_edge(maze.rc_to_i(row, col), maze.rc_to_i(row, col - 1)): if maze.get_weight(maze.rc_to_i(row, col), maze.rc_to_i(row, col - 1)) > 1: mud_x = maze_x_offset + col * cell_size - mud.get_width() // 2 mud_y = maze_y_offset + row * cell_size maze_elements.append((mud_x, mud_y, mud)) if not render_simplified: 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) weight_text_x = maze_x_offset + col * cell_size - weight_text.get_width() // 2 weight_text_y = maze_y_offset + row * cell_size + (cell_size - weight_text.get_height()) // 2 maze_elements.append((weight_text_x, weight_text_y, weight_text)) if maze.rc_exists(row - 1, col): if maze.has_edge(maze.rc_to_i(row, col), maze.rc_to_i(row - 1, col)): if maze.get_weight(maze.rc_to_i(row, col), maze.rc_to_i(row - 1, col)) > 1: mud_horizontal = pygame.transform.rotate(mud, 90) mud_x = maze_x_offset + col * cell_size mud_y = maze_y_offset + row * cell_size - mud.get_width() // 2 maze_elements.append((mud_x, mud_y, mud_horizontal)) if not render_simplified: 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) weight_text_x = maze_x_offset + col * cell_size + (cell_size - weight_text.get_width()) // 2 weight_text_y = maze_y_offset + row * cell_size - weight_text.get_height() // 2 maze_elements.append((weight_text_x, weight_text_y, weight_text)) # Add cell numbers if not render_simplified: for row in range(maze.get_height()): for col in range(maze.get_width()): if maze.rc_exists(row, col): cell_text = ___surface_from_text(str(maze.rc_to_i(row, col)), text_size, cell_text_color) cell_text_x = maze_x_offset + col * cell_size + cell_text_offset cell_text_y = maze_y_offset + row * cell_size + cell_text_offset maze_elements.append((cell_text_x, cell_text_y, cell_text)) # Add walls walls = [] wall = ___surface_from_image(os.path.join("..", "gui", "wall", "wall.png"), cell_size) for row in range(maze.get_height() + 1): for col in range(maze.get_width() + 1): case_outside_to_inside = not maze.rc_exists(row, col) and maze.rc_exists(row, col - 1) case_inside_to_outside = maze.rc_exists(row, col) and not maze.rc_exists(row, col - 1) 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)) if case_outside_to_inside or case_inside_to_outside or case_inside_to_inside: wall_x = maze_x_offset + col * cell_size - wall.get_width() // 2 wall_y = maze_y_offset + row * cell_size maze_elements.append((wall_x, wall_y, wall)) walls.append((row, col, row, col - 1)) case_outside_to_inside = not maze.rc_exists(row, col) and maze.rc_exists(row - 1, col) case_inside_to_outside = maze.rc_exists(row, col) and not maze.rc_exists(row - 1, col) 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)) if case_outside_to_inside or case_inside_to_outside or case_inside_to_inside: wall_horizontal = pygame.transform.rotate(wall, 90) wall_x = maze_x_offset + col * cell_size wall_y = maze_y_offset + row * cell_size - wall.get_width() // 2 maze_elements.append((wall_x, wall_y, wall_horizontal)) walls.append((row, col, row - 1, col)) # Add corners 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)) for row, col, neighbor_row, neighbor_col in walls: if col != neighbor_col: corner_x = maze_x_offset + col * cell_size - corner.get_width() // 2 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): corner_y = maze_y_offset + row * cell_size - corner.get_width() // 2 maze_elements.append((corner_x, corner_y, corner)) if (row + 1, col, neighbor_row + 1, neighbor_col) not in walls: corner_y = maze_y_offset + (row + 1) * cell_size - corner.get_width() // 2 maze_elements.append((corner_x, corner_y, corner)) if row != neighbor_row: corner_y = maze_y_offset + row * cell_size - corner.get_width() // 2 if (row, col - 1, neighbor_row, neighbor_col - 1) not in walls: corner_x = maze_x_offset + col * cell_size - corner.get_width() // 2 maze_elements.append((corner_x, corner_y, corner)) if (row, col + 1, neighbor_row, neighbor_col + 1) not in walls: corner_x = maze_x_offset + (col + 1) * cell_size - corner.get_width() // 2 maze_elements.append((corner_x, corner_y, corner)) # Add flags if not render_simplified: cells_with_flags = {cell: {} for cell in initial_game_state.player_locations.values()} for player in players: team = [team for team in initial_game_state.teams if player.get_name() in initial_game_state.teams[team]][0] if team not in cells_with_flags[initial_game_state.player_locations[player.get_name()]]: cells_with_flags[initial_game_state.player_locations[player.get_name()]][team] = 0 cells_with_flags[initial_game_state.player_locations[player.get_name()]][team] += 1 flag = ___surface_from_image(os.path.join("..", "gui", "flag", "flag.png"), flag_size) max_teams_in_cells = max([len(team) for team in cells_with_flags.values()]) max_players_in_cells = max([cells_with_flags[cell][team] for cell in cells_with_flags for team in cells_with_flags[cell]]) for cell in cells_with_flags: row, col = maze.i_to_rc(cell) for i_team in range(len(cells_with_flags[cell])): team = list(cells_with_flags[cell].keys())[i_team] flag_colored = ___colorize(flag, team_colors[team]) flag_colored = ___add_color_border(flag_colored, flag_border_color, flag_border_width) for i_player in range(cells_with_flags[cell][team]): 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)) 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)) maze_elements.append((flag_x, flag_y, flag_colored)) # Add cheese cheese = ___surface_from_image(os.path.join("..", "gui", "cheese", "cheese.png"), cheese_size) cheese = ___add_color_border(cheese, cheese_border_color, cheese_border_width) for c in initial_game_state.cheese: row, col = maze.i_to_rc(c) cheese_x = maze_x_offset + col * cell_size + (cell_size - cheese.get_width()) // 2 cheese_y = maze_y_offset + row * cell_size + (cell_size - cheese.get_height()) // 2 cheese_elements[c] = (cheese_x, cheese_y, cheese) # Add players for player in players: team = [team for team in initial_game_state.teams if player.get_name() in initial_game_state.teams[team]][0] player_neutral, player_north, player_south, player_west, player_east = ___load_player_surfaces(player.get_skin(), player_size, team_colors[team], player_border_width) row, col = maze.i_to_rc(initial_game_state.player_locations[player.get_name()]) player_x = maze_x_offset + col * cell_size + (cell_size - player_neutral.get_width()) // 2 player_y = maze_y_offset + row * cell_size + (cell_size - player_neutral.get_height()) // 2 player_elements[player.get_name()] = (player_x, player_y, player_neutral, player_north, player_south, player_west, player_east) # Add avatars area score_locations = {} medal_locations = {} for i in range(len(initial_game_state.teams)): # Box team = list(initial_game_state.teams.keys())[i] team_background = pygame.Surface((avatars_area_width, avatars_area_height)) pygame.draw.rect(team_background, background_color, pygame.Rect(0, 0, avatars_area_width, avatars_area_height)) pygame.draw.rect(team_background, team_colors[team], pygame.Rect(0, 0, avatars_area_width, avatars_area_height), avatars_area_border, avatars_area_angle) team_background_x = avatars_x_offset 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 avatar_elements.append((team_background_x, team_background_y, team_background)) medal_locations[team] = (team_background_x + avatars_area_width, team_background_y) # Team name team_text = ___surface_from_text(team, team_text_size, team_colors[team]) if team_text.get_width() > avatars_area_width - 2 * avatars_area_padding: ratio = (avatars_area_width - 2 * avatars_area_padding) / team_text.get_width() team_text = pygame.transform.scale(team_text, (int(team_text.get_width() * ratio), int(team_text.get_height() * ratio))) team_text_x = avatars_x_offset + (avatars_area_width - team_text.get_width()) // 2 team_text_y = team_background_y + avatars_area_padding + (team_text_size - team_text.get_height()) // 2 if not teams_enabled: team_text_size = -avatars_area_padding avatar_elements.append((team_text_x, team_text_y, team_text)) # Players avatars player_images = [] for j in range(len(initial_game_state.teams[team])): player = [player for player in players if player.get_name() == initial_game_state.teams[team][j]][0] player_avatar = ___load_player_avatar(player.get_skin(), player_avatar_size) player_images.append(player_avatar) 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)) pygame.draw.rect(avatar_area, background_color, pygame.Rect(0, 0, avatar_area.get_width(), avatar_area.get_height())) player_x = avatars_area_padding centers = [] for player_avatar in player_images: avatar_area.blit(player_avatar, (player_x, 0)) centers.append(player_x + player_avatar.get_width() // 2) player_x += player_avatar.get_width() + player_avatar_horizontal_padding if avatar_area.get_width() > avatars_area_width - 2 * avatars_area_padding: ratio = (avatars_area_width - 2 * avatars_area_padding) / avatar_area.get_width() centers = [center * ratio for center in centers] avatar_area = pygame.transform.scale(avatar_area, (int(avatar_area.get_width() * ratio), int(avatar_area.get_height() * ratio))) avatar_area_x = avatars_x_offset + (avatars_area_width - avatar_area.get_width()) // 2 avatar_area_y = team_background_y + 2 * avatars_area_padding + team_text_size + (player_avatar_size - avatar_area.get_height()) // 2 avatar_elements.append((avatar_area_x, avatar_area_y, avatar_area)) # Players names for j in range(len(initial_game_state.teams[team])): player_name = initial_game_state.teams[team][j] while True: player_name_text = ___surface_from_text(player_name, player_name_text_size, avatars_area_color) if player_name_text.get_width() > (avatars_area_width - 2 * avatars_area_padding) / len(initial_game_state.teams[team]): player_name = player_name[:-2] + "." else: break player_name_text_x = avatar_area_x + centers[j] - player_name_text.get_width() // 2 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 avatar_elements.append((player_name_text_x, player_name_text_y, player_name_text)) # Score locations cheese_missing = ___surface_from_image(os.path.join("..", "gui", "cheese", "cheese_missing.png"), cheese_score_size) score_x_offset = avatars_x_offset + avatars_area_padding score_margin = avatars_area_width - 2 * avatars_area_padding - cheese_missing.get_width() if len(initial_game_state.cheese) > 1: score_margin /= (len(initial_game_state.cheese) - 1) score_margin = min(score_margin, cheese_missing.get_width() * 2) estimated_width = cheese_missing.get_width() + (len(initial_game_state.cheese) - 1) * score_margin if estimated_width < avatars_area_width - 2 * avatars_area_padding: score_x_offset += (avatars_area_width - 2 * avatars_area_padding - estimated_width) / 2 score_y_offset = team_background_y + 4 * avatars_area_padding + team_text_size + player_avatar_size + player_name_text_size score_locations[team] = (score_x_offset, score_margin, score_y_offset) # Show maze def ___show_maze (): pygame.draw.rect(gui_screen, background_color, pygame.Rect(maze_x_offset, maze_y_offset, game_area_width, game_area_height)) for surface_x, surface_y, surface in maze_elements: gui_screen.blit(surface, (surface_x, surface_y)) ___show_maze() # Show cheese def ___show_cheese (cheese): for c in cheese: cheese_x, cheese_y, surface = cheese_elements[c] gui_screen.blit(surface, (cheese_x, cheese_y)) ___show_cheese(initial_game_state.cheese) # Show_players at initial locations def ___show_initial_players (): for p in player_elements: player_x, player_y, player_neutral, _, _ , _, _ = player_elements[p] gui_screen.blit(player_neutral, (player_x, player_y)) ___show_initial_players() # Show avatars def ___show_avatars (): for surface_x, surface_y, surface in avatar_elements: gui_screen.blit(surface, (surface_x, surface_y)) ___show_avatars() # Show scores def ___show_scores (team_scores): cheese_missing = ___surface_from_image(os.path.join("..", "gui", "cheese", "cheese_missing.png"), cheese_score_size) cheese_missing = ___add_color_border(cheese_missing, cheese_score_border_color, cheese_score_border_width) cheese_eaten = ___surface_from_image(os.path.join("..", "gui", "cheese", "cheese_eaten.png"), cheese_score_size) cheese_eaten = ___add_color_border(cheese_eaten, cheese_score_border_color, cheese_score_border_width) for team in score_locations: score_x_offset, score_margin, score_y_offset = score_locations[team] for i in range(int(team_scores[team])): gui_screen.blit(cheese_eaten, (score_x_offset + i * score_margin, score_y_offset)) if int(team_scores[team]) != team_scores[team]: cheese_partial = ___surface_from_image(os.path.join("..", "gui", "cheese", "cheese_eaten.png"), cheese_score_size) cheese_partial = ___colorize(cheese_partial, [(team_scores[team] - int(team_scores[team])) * 255] * 3) cheese_partial = ___add_color_border(cheese_partial, cheese_score_border_color, cheese_score_border_width) gui_screen.blit(cheese_partial, (score_x_offset + int(team_scores[team]) * score_margin, score_y_offset)) for j in range(math.ceil(team_scores[team]), len(initial_game_state.cheese)): gui_screen.blit(cheese_missing, (score_x_offset + j * score_margin, score_y_offset)) initial_scores = {team: 0 for team in initial_game_state.teams} ___show_scores(initial_scores) # Show preprocessing message preprocessing_image = ___surface_from_image(os.path.join("..", "gui", "drawings", "pyrat_preprocessing.png"), int(min(game_area_width, game_area_height) * main_image_factor)) preprocessing_image = ___add_color_border(preprocessing_image, main_image_border_color, main_image_border_size) go_image = ___surface_from_image(os.path.join("..", "gui", "drawings", "pyrat_go.png"), int(min(game_area_width, game_area_height) * main_image_factor)) go_image = ___add_color_border(go_image, main_image_border_color, main_image_border_size) main_image_x = maze_x_offset + (game_area_width - preprocessing_image.get_width()) / 2 main_image_y = maze_y_offset + (game_area_height - preprocessing_image.get_height()) / 2 gui_screen.blit(preprocessing_image, (main_image_x, main_image_y)) # Prepare useful variables current_state = copy.deepcopy(initial_game_state) mud_being_crossed = {player.get_name(): 0 for player in players} traces = {player.get_name(): [(player_elements[player.get_name()][0] + player_elements[player.get_name()][2].get_width() / 2, player_elements[player.get_name()][1] + player_elements[player.get_name()][2].get_height() / 2)] for player in players} trace_colors = {player.get_name(): ___get_main_color(player_elements[player.get_name()][2]) for player in players} player_surfaces = {player.get_name(): player_elements[player.get_name()][2] for player in players} # Show and indicate when ready gui_running = True pygame.display.flip() time.sleep(0.1) pygame.display.update() gui_initialized_synchronizer.wait() # Run until the user asks to quit while gui_running: try: # We check for termination for event in pygame.event.get(): if event.type == pygame.QUIT or (event.type == pglocals.KEYDOWN and event.key == pglocals.K_ESCAPE): gui_running = False if not gui_running: break # Get turn info new_state = gui_queue.get(False) # Indicate when preprocessing is over for a little time if new_state.turn == 1: ___show_maze() ___show_cheese(current_state.cheese if i != animation_steps - 1 else new_state.cheese) ___show_initial_players() gui_screen.blit(go_image, (main_image_x, main_image_y)) pygame.display.update((maze_x_offset, maze_y_offset, maze.get_width() * cell_size, maze.get_height() * cell_size)) time.sleep(go_image_duration) # Enter mud? for player in players: if new_state.muds[player.get_name()]["count"] > 0 and mud_being_crossed[player.get_name()] == 0: mud_being_crossed[player.get_name()] = new_state.muds[player.get_name()]["count"] + 1 # Choose the correct player surface for player in players: player_x, player_y, player_neutral, player_north, player_south, player_west, player_east = player_elements[player.get_name()] row, col = maze.i_to_rc(current_state.player_locations[player.get_name()]) adjusted_new_location = new_state.player_locations[player.get_name()] if not new_state.is_in_mud(player.get_name()) else new_state.muds[player.get_name()]["target"] new_row, new_col = maze.i_to_rc(adjusted_new_location) player_x += player_surfaces[player.get_name()].get_width() / 2 player_y += player_surfaces[player.get_name()].get_height() / 2 if new_col > col: player_surfaces[player.get_name()] = player_east elif new_col < col: player_surfaces[player.get_name()] = player_west elif new_row > row: player_surfaces[player.get_name()] = player_south elif new_row < row: player_surfaces[player.get_name()] = player_north else: player_surfaces[player.get_name()] = player_neutral player_x -= player_surfaces[player.get_name()].get_width() / 2 player_y -= player_surfaces[player.get_name()].get_height() / 2 player_elements[player.get_name()] = (player_x, player_y, player_neutral, player_north, player_south, player_west, player_east) # Move players for i in range(animation_steps): # Reset background & cheese ___show_maze() ___show_cheese(current_state.cheese if i != animation_steps - 1 else new_state.cheese) # Move player with trace for player in players: player_x, player_y, player_neutral, player_north, player_south, player_west, player_east = player_elements[player.get_name()] row, col = maze.i_to_rc(current_state.player_locations[player.get_name()]) adjusted_new_location = new_state.player_locations[player.get_name()] if not new_state.is_in_mud(player.get_name()) else new_state.muds[player.get_name()]["target"] new_row, new_col = maze.i_to_rc(adjusted_new_location) shift = (i + 1) * cell_size / animation_steps if mud_being_crossed[player.get_name()] > 0: shift /= mud_being_crossed[player.get_name()] shift += (mud_being_crossed[player.get_name()] - new_state.muds[player.get_name()]["count"] - 1) * cell_size / mud_being_crossed[player.get_name()] next_x = player_x if col == new_col else player_x + shift if new_col > col else player_x - shift next_y = player_y if row == new_row else player_y + shift if new_row > row else player_y - shift if i == animation_steps - 1 and new_state.muds[player.get_name()]["count"] == 0: player_elements[player.get_name()] = (next_x, next_y, player_neutral, player_north, player_south, player_west, player_east) if trace_length > 0: pygame.draw.line(gui_screen, trace_colors[player.get_name()], (next_x + player_surfaces[player.get_name()].get_width() / 2, next_y + player_surfaces[player.get_name()].get_height() / 2), traces[player.get_name()][-1], width=trace_size) for j in range(1, trace_length): if len(traces[player.get_name()]) > j: pygame.draw.line(gui_screen, trace_colors[player.get_name()], traces[player.get_name()][-j-1], traces[player.get_name()][-j], width=trace_size) if len(traces[player.get_name()]) == trace_length + 1: final_segment_length = math.sqrt((traces[player.get_name()][-1][0] - (next_x + player_surfaces[player.get_name()].get_width() / 2))**2 + (traces[player.get_name()][-1][1] - (next_y + player_surfaces[player.get_name()].get_height() / 2))**2) ratio = 1 - final_segment_length / cell_size pygame.draw.line(gui_screen, trace_colors[player.get_name()], traces[player.get_name()][1], (traces[player.get_name()][1][0] + ratio * (traces[player.get_name()][0][0] - traces[player.get_name()][1][0]), traces[player.get_name()][1][1] + ratio * (traces[player.get_name()][0][1] - traces[player.get_name()][1][1])), width=trace_size) gui_screen.blit(player_surfaces[player.get_name()], (next_x, next_y)) # Update maze & wait for animation pygame.display.update((maze_x_offset, maze_y_offset, maze.get_width() * cell_size, maze.get_height() * cell_size)) time.sleep(animation_time / animation_steps) # Exit mud? for player in players: if new_state.muds[player.get_name()]["count"] == 0: mud_being_crossed[player.get_name()] = 0 if mud_being_crossed[player.get_name()] == 0: player_x, player_y, _, _, _, _, _ = player_elements[player.get_name()] if traces[player.get_name()][-1] != (player_x + player_surfaces[player.get_name()].get_width() / 2, player_y + player_surfaces[player.get_name()].get_height() / 2): traces[player.get_name()].append((player_x + player_surfaces[player.get_name()].get_width() / 2, player_y + player_surfaces[player.get_name()].get_height() / 2)) traces[player.get_name()] = traces[player.get_name()][-trace_length-1:] # Play a sound is a cheese is eaten for player in players: if new_state.player_locations[player.get_name()] in current_state.cheese and mud_being_crossed[player.get_name()] == 0: ___play_sound(os.path.join("..", "gui", "players", player.get_skin().value, "cheese_eaten.wav"), os.path.join("..", "gui", "players", "default", "cheese_eaten.wav")) # Update score ___show_avatars() new_scores = new_state.get_score_per_team() ___show_scores(new_scores) current_state = new_state # Indicate if the game is over if new_state.game_over(): sorted_results = sorted([(new_scores[team], team) for team in new_scores], reverse=True) 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"]] for i in range(len(sorted_results)): if i > 0 and sorted_results[i][0] != sorted_results[i-1][0] and len(medals) > 1: del medals[0] team = sorted_results[i][1] gui_screen.blit(medals[0], (medal_locations[team][0] - medals[0].get_width() / 2, medal_locations[team][1] - medals[0].get_height() / 3)) ___play_sound(os.path.join("..", "gui", "endgame", "game_over.wav")) pygame.display.update((0, 0, maze_x_offset, window_height)) # Ignore exceptions raised due to emtpy queue except queue.Empty: pass # Quit PyGame pygame.display.quit() pygame.quit() # Ignore except: pass ##################################################################################################################################################### #####################################################################################################################################################