pyrat.src.ShellRenderingEngine
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 colored 21import re 22import math 23import sys 24 25# PyRat imports 26from pyrat.src.RenderingEngine import RenderingEngine 27from pyrat.src.Player import Player 28from pyrat.src.Maze import Maze 29from pyrat.src.GameState import GameState 30 31##################################################################################################################################################### 32###################################################################### CLASSES ###################################################################### 33##################################################################################################################################################### 34 35class ShellRenderingEngine (RenderingEngine): 36 37 """ 38 This class inherits from the RenderingEngine class. 39 Therefore, it has the attributes and methods defined in the RenderingEngine class in addition to the ones defined below. 40 41 An ASCII rendering engine is a rendering engine that can render a PyRat game in ASCII. 42 It also supports ANSI escape codes to colorize the rendering. 43 """ 44 45 ############################################################################################################################################# 46 # MAGIC METHODS # 47 ############################################################################################################################################# 48 49 def __init__ ( self: Self, 50 use_colors: bool = True, 51 *args: Any, 52 **kwargs: Any 53 ) -> Self: 54 55 """ 56 This function is the constructor of the class. 57 When an object is instantiated, this method is called to initialize the object. 58 This is where you should define the attributes of the object and set their initial values. 59 Arguments *args and **kwargs are used to pass arguments to the parent constructor. 60 This is useful not to declare again all the parent's attributes in the child class. 61 In: 62 * self: Reference to the current object. 63 * use_colors: Boolean indicating whether the rendering engine should use colors or not. 64 * args: Arguments to pass to the parent constructor. 65 * kwargs: Keyword arguments to pass to the parent constructor. 66 Out: 67 * A new instance of the class. 68 """ 69 70 # Inherit from parent class 71 super().__init__(*args, **kwargs) 72 73 # Debug 74 assert isinstance(use_colors, bool) # Type check for the use of colors 75 76 # Private attributes 77 self.__use_colors = use_colors 78 79 ############################################################################################################################################# 80 # PUBLIC METHODS # 81 ############################################################################################################################################# 82 83 @override 84 def render ( self: Self, 85 players: List[Player], 86 maze: Maze, 87 game_state: GameState, 88 ) -> None: 89 90 """ 91 This method redefines the method of the parent class. 92 This function renders the game to show its current state. 93 It does so by creating a string representing the game state and printing it. 94 In: 95 * self: Reference to the current object. 96 * players: Players of the game. 97 * maze: Maze of the game. 98 * game_state: State of the game. 99 Out: 100 * None. 101 """ 102 103 # Debug 104 assert isinstance(players, list) # Type check for the players 105 assert all(isinstance(player, Player) for player in players) # Type check for the players 106 assert isinstance(maze, Maze) # Type check for the maze 107 assert isinstance(game_state, GameState) # Type check for the game state 108 109 # Dimensions 110 max_weight = max([maze.get_weight(*edge) for edge in maze.edges]) 111 max_weight_len = len(str(max_weight)) 112 max_player_name_len = max([len(player.name) for player in players]) + (max_weight_len + 5 if max_weight > 1 else 0) 113 max_cell_number_len = len(str(maze.width * maze.height - 1)) 114 cell_width = max(max_player_name_len, max_weight_len, max_cell_number_len + 1) + 2 115 116 # Game elements 117 wall = self.__colorize(" ", colored.bg("light_gray"), "#") 118 ground = self.__colorize(" ", colored.bg("grey_23")) 119 cheese = self.__colorize("▲", colored.bg("grey_23") + colored.fg("yellow_1")) 120 mud_horizontal = self.__colorize("ⴾ", colored.bg("grey_23") + colored.fg("orange_4b")) 121 mud_vertical = self.__colorize("ⵘ", colored.bg("grey_23") + colored.fg("orange_4b")) 122 mud_value = lambda number: self.__colorize(str(number), colored.bg("grey_23") + colored.fg("orange_4b")) 123 path_horizontal = self.__colorize("⋅", colored.bg("grey_23") + colored.fg("orange_4b")) 124 path_vertical = self.__colorize("ⵗ", colored.bg("grey_23") + colored.fg("orange_4b")) 125 cell_number = lambda number: self.__colorize(str(number), colored.bg("grey_23") + colored.fg("magenta")) 126 score_cheese = self.__colorize("▲ ", colored.fg("yellow_1")) 127 score_half_cheese = self.__colorize("△ ", colored.fg("yellow_1")) 128 129 # Player/team elements 130 teams = {team: self.__colorize(team, colored.fg(9 + list(game_state.teams.keys()).index(team))) for team in game_state.teams} 131 mud_indicator = lambda player_name: " (" + ("⬇" if maze.coords_difference(game_state.muds[player_name]["target"], game_state.player_locations[player_name]) == (1, 0) else "⬆" if maze.coords_difference(game_state.muds[player_name]["target"], game_state.player_locations[player_name]) == (-1, 0) else "➡" if maze.coords_difference(game_state.muds[player_name]["target"], game_state.player_locations[player_name]) == (0, 1) else "⬅") + " " + str(game_state.muds[player_name]["count"]) + ")" if game_state.muds[player_name]["count"] > 0 else "" 132 player_names = {player.name: self.__colorize(player.name + mud_indicator(player.name), colored.bg("grey_23") + colored.fg(9 + ["team" if player.name in team else 0 for team in game_state.teams.values()].index("team"))) for player in players} 133 134 # Game info 135 environment_str = "" if self.__use_colors else "\n" 136 environment_str += "Game over" if game_state.game_over() else "Starting turn %d" % game_state.turn if game_state.turn > 0 else "Initial configuration" 137 team_scores = game_state.get_score_per_team() 138 scores_str = "" 139 for team in game_state.teams: 140 scores_str += "\n" + score_cheese * int(team_scores[team]) + score_half_cheese * math.ceil(team_scores[team] - int(team_scores[team])) 141 scores_str += "[" + teams[team] + "] " if len(teams) > 1 or len(team) > 0 else "" 142 scores_str += " + ".join(["%s (%s)" % (player_in_team, str(round(game_state.score_per_player[player_in_team], 3)).rstrip('0').rstrip('.') if game_state.score_per_player[player_in_team] > 0 else "0") for player_in_team in game_state.teams[team]]) 143 environment_str += scores_str 144 145 # Consider cells in lexicographic order 146 environment_str += "\n" + wall * (maze.width * (cell_width + 1) + 1) 147 for row in range(maze.height): 148 players_in_row = [game_state.player_locations[player.name] for player in players if maze.i_to_rc(game_state.player_locations[player.name])[0] == row] 149 cell_height = max([players_in_row.count(cell) for cell in players_in_row] + [max_weight_len]) + 2 150 environment_str += "\n" 151 for subrow in range(cell_height): 152 environment_str += wall 153 for col in range(maze.width): 154 155 # Check cell contents 156 players_in_cell = [player.name for player in players if game_state.player_locations[player.name] == maze.rc_to_i(row, col)] 157 cheese_in_cell = maze.rc_to_i(row, col) in game_state.cheese 158 159 # Find subrow contents (nothing, cell number, cheese, trace, player) 160 background = wall if not maze.rc_exists(row, col) else ground 161 cell_contents = "" 162 if subrow == 0: 163 if background != wall and not self._render_simplified: 164 cell_contents += background 165 cell_contents += cell_number(maze.rc_to_i(row, col)) 166 elif cheese_in_cell: 167 if subrow == (cell_height - 1) // 2: 168 cell_contents = background * ((cell_width - self.__colored_len(cheese)) // 2) 169 cell_contents += cheese 170 else: 171 cell_contents = background * cell_width 172 else: 173 first_player_index = (cell_height - len(players_in_cell)) // 2 174 if first_player_index <= subrow < first_player_index + len(players_in_cell): 175 cell_contents = background * ((cell_width - self.__colored_len(player_names[players_in_cell[subrow - first_player_index]])) // 2) 176 cell_contents += player_names[players_in_cell[subrow - first_player_index]] 177 else: 178 cell_contents = background * cell_width 179 environment_str += cell_contents 180 environment_str += background * (cell_width - self.__colored_len(cell_contents)) 181 182 # Right separation 183 right_weight = "0" if not maze.rc_exists(row, col) or not maze.rc_exists(row, col + 1) or not maze.has_edge(maze.rc_to_i(row, col), maze.rc_to_i(row, col + 1)) else str(maze.get_weight(maze.rc_to_i(row, col), maze.rc_to_i(row, col + 1))) 184 if col == maze.width - 1 or right_weight == "0": 185 environment_str += wall 186 else: 187 if right_weight == "1": 188 environment_str += path_vertical 189 elif not self._render_simplified and math.ceil((cell_height - len(right_weight)) / 2) <= subrow < math.ceil((cell_height - len(right_weight)) / 2) + len(right_weight): 190 digit_number = subrow - math.ceil((cell_height - len(right_weight)) / 2) 191 environment_str += mud_value(right_weight[digit_number]) 192 else: 193 environment_str += mud_vertical 194 environment_str += "\n" 195 environment_str += wall 196 197 # Bottom separation 198 for col in range(maze.width): 199 bottom_weight = "0" if not maze.rc_exists(row, col) or not maze.rc_exists(row + 1, col) or not maze.has_edge(maze.rc_to_i(row, col), maze.rc_to_i(row + 1, col)) else str(maze.get_weight(maze.rc_to_i(row, col), maze.rc_to_i(row + 1, col))) 200 if bottom_weight == "0": 201 environment_str += wall * (cell_width + 1) 202 elif bottom_weight == "1": 203 environment_str += path_horizontal * cell_width + wall 204 else: 205 cell_contents = mud_horizontal * ((cell_width - self.__colored_len(bottom_weight)) // 2) + mud_value(bottom_weight) if not self._render_simplified else "" 206 environment_str += cell_contents 207 environment_str += mud_horizontal * (cell_width - self.__colored_len(cell_contents)) + wall 208 209 # Render 210 if self.__use_colors: 211 nb_rows = 1 + len(environment_str.splitlines()) 212 nb_cols = 1 + (cell_width + 1) * maze.width 213 print("\x1b[8;%d;%dt" % (nb_rows, nb_cols), file=sys.stderr) 214 print(environment_str, file=sys.stderr) 215 216 ############################################################################################################################################# 217 # PRIVATE METHODS # 218 ############################################################################################################################################# 219 220 def __colorize ( self: Self, 221 text: str, 222 colorization: str, 223 alternate_text: Optional[str] = None 224 ) -> str: 225 226 """ 227 This method colorizes a text. 228 It does so by adding the colorization to the text and resetting the colorization at the end of the text. 229 In: 230 * self: Reference to the current object. 231 * text: Text to colorize. 232 * colorization: Colorization to use. 233 * alternate_text: Alternate text to use if we don't use colors and the provided text does not fit. 234 Out: 235 * colorized_text: Colorized text. 236 """ 237 238 # Debug 239 assert isinstance(text, str) # Type check for the text 240 assert isinstance(colorization, str) # Type check for the colorization 241 assert isinstance(alternate_text, (str, type(None))) # Type check for the alternate text 242 243 # If we don't use colors, we return the correct text 244 if not self.__use_colors: 245 if alternate_text is None: 246 colorized_text = str(text) 247 else: 248 colorized_text = str(alternate_text) 249 250 # If using colors, we return the colorized text 251 else: 252 colorized_text = colorization + str(text) + colored.attr(0) 253 254 # Return the colorized (or not) text 255 return colorized_text 256 257 ############################################################################################################################################# 258 259 def __colored_len ( self: Self, 260 text: str 261 ) -> Integral: 262 263 """ 264 This method returns the true len of a color-formated string. 265 In: 266 * self: Reference to the current object. 267 * text: Text to measure. 268 Out: 269 * text_length: Length of the text. 270 """ 271 272 # Debug 273 assert isinstance(text, str) # Type check for the text 274 275 # Return the length of the text without the colorization 276 text_length = len(re.sub(r"[\u001B\u009B][\[\]()#;?]*((([a-zA-Z\d]*(;[-a-zA-Z\d\/#&.:=?%@~_]*)*)?\u0007)|((\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-ntqry=><~]))", "", text)) 277 return text_length 278 279##################################################################################################################################################### 280#####################################################################################################################################################
36class ShellRenderingEngine (RenderingEngine): 37 38 """ 39 This class inherits from the RenderingEngine class. 40 Therefore, it has the attributes and methods defined in the RenderingEngine class in addition to the ones defined below. 41 42 An ASCII rendering engine is a rendering engine that can render a PyRat game in ASCII. 43 It also supports ANSI escape codes to colorize the rendering. 44 """ 45 46 ############################################################################################################################################# 47 # MAGIC METHODS # 48 ############################################################################################################################################# 49 50 def __init__ ( self: Self, 51 use_colors: bool = True, 52 *args: Any, 53 **kwargs: Any 54 ) -> Self: 55 56 """ 57 This function is the constructor of the class. 58 When an object is instantiated, this method is called to initialize the object. 59 This is where you should define the attributes of the object and set their initial values. 60 Arguments *args and **kwargs are used to pass arguments to the parent constructor. 61 This is useful not to declare again all the parent's attributes in the child class. 62 In: 63 * self: Reference to the current object. 64 * use_colors: Boolean indicating whether the rendering engine should use colors or not. 65 * args: Arguments to pass to the parent constructor. 66 * kwargs: Keyword arguments to pass to the parent constructor. 67 Out: 68 * A new instance of the class. 69 """ 70 71 # Inherit from parent class 72 super().__init__(*args, **kwargs) 73 74 # Debug 75 assert isinstance(use_colors, bool) # Type check for the use of colors 76 77 # Private attributes 78 self.__use_colors = use_colors 79 80 ############################################################################################################################################# 81 # PUBLIC METHODS # 82 ############################################################################################################################################# 83 84 @override 85 def render ( self: Self, 86 players: List[Player], 87 maze: Maze, 88 game_state: GameState, 89 ) -> None: 90 91 """ 92 This method redefines the method of the parent class. 93 This function renders the game to show its current state. 94 It does so by creating a string representing the game state and printing it. 95 In: 96 * self: Reference to the current object. 97 * players: Players of the game. 98 * maze: Maze of the game. 99 * game_state: State of the game. 100 Out: 101 * None. 102 """ 103 104 # Debug 105 assert isinstance(players, list) # Type check for the players 106 assert all(isinstance(player, Player) for player in players) # Type check for the players 107 assert isinstance(maze, Maze) # Type check for the maze 108 assert isinstance(game_state, GameState) # Type check for the game state 109 110 # Dimensions 111 max_weight = max([maze.get_weight(*edge) for edge in maze.edges]) 112 max_weight_len = len(str(max_weight)) 113 max_player_name_len = max([len(player.name) for player in players]) + (max_weight_len + 5 if max_weight > 1 else 0) 114 max_cell_number_len = len(str(maze.width * maze.height - 1)) 115 cell_width = max(max_player_name_len, max_weight_len, max_cell_number_len + 1) + 2 116 117 # Game elements 118 wall = self.__colorize(" ", colored.bg("light_gray"), "#") 119 ground = self.__colorize(" ", colored.bg("grey_23")) 120 cheese = self.__colorize("▲", colored.bg("grey_23") + colored.fg("yellow_1")) 121 mud_horizontal = self.__colorize("ⴾ", colored.bg("grey_23") + colored.fg("orange_4b")) 122 mud_vertical = self.__colorize("ⵘ", colored.bg("grey_23") + colored.fg("orange_4b")) 123 mud_value = lambda number: self.__colorize(str(number), colored.bg("grey_23") + colored.fg("orange_4b")) 124 path_horizontal = self.__colorize("⋅", colored.bg("grey_23") + colored.fg("orange_4b")) 125 path_vertical = self.__colorize("ⵗ", colored.bg("grey_23") + colored.fg("orange_4b")) 126 cell_number = lambda number: self.__colorize(str(number), colored.bg("grey_23") + colored.fg("magenta")) 127 score_cheese = self.__colorize("▲ ", colored.fg("yellow_1")) 128 score_half_cheese = self.__colorize("△ ", colored.fg("yellow_1")) 129 130 # Player/team elements 131 teams = {team: self.__colorize(team, colored.fg(9 + list(game_state.teams.keys()).index(team))) for team in game_state.teams} 132 mud_indicator = lambda player_name: " (" + ("⬇" if maze.coords_difference(game_state.muds[player_name]["target"], game_state.player_locations[player_name]) == (1, 0) else "⬆" if maze.coords_difference(game_state.muds[player_name]["target"], game_state.player_locations[player_name]) == (-1, 0) else "➡" if maze.coords_difference(game_state.muds[player_name]["target"], game_state.player_locations[player_name]) == (0, 1) else "⬅") + " " + str(game_state.muds[player_name]["count"]) + ")" if game_state.muds[player_name]["count"] > 0 else "" 133 player_names = {player.name: self.__colorize(player.name + mud_indicator(player.name), colored.bg("grey_23") + colored.fg(9 + ["team" if player.name in team else 0 for team in game_state.teams.values()].index("team"))) for player in players} 134 135 # Game info 136 environment_str = "" if self.__use_colors else "\n" 137 environment_str += "Game over" if game_state.game_over() else "Starting turn %d" % game_state.turn if game_state.turn > 0 else "Initial configuration" 138 team_scores = game_state.get_score_per_team() 139 scores_str = "" 140 for team in game_state.teams: 141 scores_str += "\n" + score_cheese * int(team_scores[team]) + score_half_cheese * math.ceil(team_scores[team] - int(team_scores[team])) 142 scores_str += "[" + teams[team] + "] " if len(teams) > 1 or len(team) > 0 else "" 143 scores_str += " + ".join(["%s (%s)" % (player_in_team, str(round(game_state.score_per_player[player_in_team], 3)).rstrip('0').rstrip('.') if game_state.score_per_player[player_in_team] > 0 else "0") for player_in_team in game_state.teams[team]]) 144 environment_str += scores_str 145 146 # Consider cells in lexicographic order 147 environment_str += "\n" + wall * (maze.width * (cell_width + 1) + 1) 148 for row in range(maze.height): 149 players_in_row = [game_state.player_locations[player.name] for player in players if maze.i_to_rc(game_state.player_locations[player.name])[0] == row] 150 cell_height = max([players_in_row.count(cell) for cell in players_in_row] + [max_weight_len]) + 2 151 environment_str += "\n" 152 for subrow in range(cell_height): 153 environment_str += wall 154 for col in range(maze.width): 155 156 # Check cell contents 157 players_in_cell = [player.name for player in players if game_state.player_locations[player.name] == maze.rc_to_i(row, col)] 158 cheese_in_cell = maze.rc_to_i(row, col) in game_state.cheese 159 160 # Find subrow contents (nothing, cell number, cheese, trace, player) 161 background = wall if not maze.rc_exists(row, col) else ground 162 cell_contents = "" 163 if subrow == 0: 164 if background != wall and not self._render_simplified: 165 cell_contents += background 166 cell_contents += cell_number(maze.rc_to_i(row, col)) 167 elif cheese_in_cell: 168 if subrow == (cell_height - 1) // 2: 169 cell_contents = background * ((cell_width - self.__colored_len(cheese)) // 2) 170 cell_contents += cheese 171 else: 172 cell_contents = background * cell_width 173 else: 174 first_player_index = (cell_height - len(players_in_cell)) // 2 175 if first_player_index <= subrow < first_player_index + len(players_in_cell): 176 cell_contents = background * ((cell_width - self.__colored_len(player_names[players_in_cell[subrow - first_player_index]])) // 2) 177 cell_contents += player_names[players_in_cell[subrow - first_player_index]] 178 else: 179 cell_contents = background * cell_width 180 environment_str += cell_contents 181 environment_str += background * (cell_width - self.__colored_len(cell_contents)) 182 183 # Right separation 184 right_weight = "0" if not maze.rc_exists(row, col) or not maze.rc_exists(row, col + 1) or not maze.has_edge(maze.rc_to_i(row, col), maze.rc_to_i(row, col + 1)) else str(maze.get_weight(maze.rc_to_i(row, col), maze.rc_to_i(row, col + 1))) 185 if col == maze.width - 1 or right_weight == "0": 186 environment_str += wall 187 else: 188 if right_weight == "1": 189 environment_str += path_vertical 190 elif not self._render_simplified and math.ceil((cell_height - len(right_weight)) / 2) <= subrow < math.ceil((cell_height - len(right_weight)) / 2) + len(right_weight): 191 digit_number = subrow - math.ceil((cell_height - len(right_weight)) / 2) 192 environment_str += mud_value(right_weight[digit_number]) 193 else: 194 environment_str += mud_vertical 195 environment_str += "\n" 196 environment_str += wall 197 198 # Bottom separation 199 for col in range(maze.width): 200 bottom_weight = "0" if not maze.rc_exists(row, col) or not maze.rc_exists(row + 1, col) or not maze.has_edge(maze.rc_to_i(row, col), maze.rc_to_i(row + 1, col)) else str(maze.get_weight(maze.rc_to_i(row, col), maze.rc_to_i(row + 1, col))) 201 if bottom_weight == "0": 202 environment_str += wall * (cell_width + 1) 203 elif bottom_weight == "1": 204 environment_str += path_horizontal * cell_width + wall 205 else: 206 cell_contents = mud_horizontal * ((cell_width - self.__colored_len(bottom_weight)) // 2) + mud_value(bottom_weight) if not self._render_simplified else "" 207 environment_str += cell_contents 208 environment_str += mud_horizontal * (cell_width - self.__colored_len(cell_contents)) + wall 209 210 # Render 211 if self.__use_colors: 212 nb_rows = 1 + len(environment_str.splitlines()) 213 nb_cols = 1 + (cell_width + 1) * maze.width 214 print("\x1b[8;%d;%dt" % (nb_rows, nb_cols), file=sys.stderr) 215 print(environment_str, file=sys.stderr) 216 217 ############################################################################################################################################# 218 # PRIVATE METHODS # 219 ############################################################################################################################################# 220 221 def __colorize ( self: Self, 222 text: str, 223 colorization: str, 224 alternate_text: Optional[str] = None 225 ) -> str: 226 227 """ 228 This method colorizes a text. 229 It does so by adding the colorization to the text and resetting the colorization at the end of the text. 230 In: 231 * self: Reference to the current object. 232 * text: Text to colorize. 233 * colorization: Colorization to use. 234 * alternate_text: Alternate text to use if we don't use colors and the provided text does not fit. 235 Out: 236 * colorized_text: Colorized text. 237 """ 238 239 # Debug 240 assert isinstance(text, str) # Type check for the text 241 assert isinstance(colorization, str) # Type check for the colorization 242 assert isinstance(alternate_text, (str, type(None))) # Type check for the alternate text 243 244 # If we don't use colors, we return the correct text 245 if not self.__use_colors: 246 if alternate_text is None: 247 colorized_text = str(text) 248 else: 249 colorized_text = str(alternate_text) 250 251 # If using colors, we return the colorized text 252 else: 253 colorized_text = colorization + str(text) + colored.attr(0) 254 255 # Return the colorized (or not) text 256 return colorized_text 257 258 ############################################################################################################################################# 259 260 def __colored_len ( self: Self, 261 text: str 262 ) -> Integral: 263 264 """ 265 This method returns the true len of a color-formated string. 266 In: 267 * self: Reference to the current object. 268 * text: Text to measure. 269 Out: 270 * text_length: Length of the text. 271 """ 272 273 # Debug 274 assert isinstance(text, str) # Type check for the text 275 276 # Return the length of the text without the colorization 277 text_length = len(re.sub(r"[\u001B\u009B][\[\]()#;?]*((([a-zA-Z\d]*(;[-a-zA-Z\d\/#&.:=?%@~_]*)*)?\u0007)|((\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-ntqry=><~]))", "", text)) 278 return text_length
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.
An ASCII rendering engine is a rendering engine that can render a PyRat game in ASCII. It also supports ANSI escape codes to colorize the rendering.
50 def __init__ ( self: Self, 51 use_colors: bool = True, 52 *args: Any, 53 **kwargs: Any 54 ) -> Self: 55 56 """ 57 This function is the constructor of the class. 58 When an object is instantiated, this method is called to initialize the object. 59 This is where you should define the attributes of the object and set their initial values. 60 Arguments *args and **kwargs are used to pass arguments to the parent constructor. 61 This is useful not to declare again all the parent's attributes in the child class. 62 In: 63 * self: Reference to the current object. 64 * use_colors: Boolean indicating whether the rendering engine should use colors or not. 65 * args: Arguments to pass to the parent constructor. 66 * kwargs: Keyword arguments to pass to the parent constructor. 67 Out: 68 * A new instance of the class. 69 """ 70 71 # Inherit from parent class 72 super().__init__(*args, **kwargs) 73 74 # Debug 75 assert isinstance(use_colors, bool) # Type check for the use of colors 76 77 # Private attributes 78 self.__use_colors = use_colors
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.
- use_colors: Boolean indicating whether the rendering engine should use colors or not.
- args: Arguments to pass to the parent constructor.
- kwargs: Keyword arguments to pass to the parent constructor.
Out:
- A new instance of the class.
84 @override 85 def render ( self: Self, 86 players: List[Player], 87 maze: Maze, 88 game_state: GameState, 89 ) -> None: 90 91 """ 92 This method redefines the method of the parent class. 93 This function renders the game to show its current state. 94 It does so by creating a string representing the game state and printing it. 95 In: 96 * self: Reference to the current object. 97 * players: Players of the game. 98 * maze: Maze of the game. 99 * game_state: State of the game. 100 Out: 101 * None. 102 """ 103 104 # Debug 105 assert isinstance(players, list) # Type check for the players 106 assert all(isinstance(player, Player) for player in players) # Type check for the players 107 assert isinstance(maze, Maze) # Type check for the maze 108 assert isinstance(game_state, GameState) # Type check for the game state 109 110 # Dimensions 111 max_weight = max([maze.get_weight(*edge) for edge in maze.edges]) 112 max_weight_len = len(str(max_weight)) 113 max_player_name_len = max([len(player.name) for player in players]) + (max_weight_len + 5 if max_weight > 1 else 0) 114 max_cell_number_len = len(str(maze.width * maze.height - 1)) 115 cell_width = max(max_player_name_len, max_weight_len, max_cell_number_len + 1) + 2 116 117 # Game elements 118 wall = self.__colorize(" ", colored.bg("light_gray"), "#") 119 ground = self.__colorize(" ", colored.bg("grey_23")) 120 cheese = self.__colorize("▲", colored.bg("grey_23") + colored.fg("yellow_1")) 121 mud_horizontal = self.__colorize("ⴾ", colored.bg("grey_23") + colored.fg("orange_4b")) 122 mud_vertical = self.__colorize("ⵘ", colored.bg("grey_23") + colored.fg("orange_4b")) 123 mud_value = lambda number: self.__colorize(str(number), colored.bg("grey_23") + colored.fg("orange_4b")) 124 path_horizontal = self.__colorize("⋅", colored.bg("grey_23") + colored.fg("orange_4b")) 125 path_vertical = self.__colorize("ⵗ", colored.bg("grey_23") + colored.fg("orange_4b")) 126 cell_number = lambda number: self.__colorize(str(number), colored.bg("grey_23") + colored.fg("magenta")) 127 score_cheese = self.__colorize("▲ ", colored.fg("yellow_1")) 128 score_half_cheese = self.__colorize("△ ", colored.fg("yellow_1")) 129 130 # Player/team elements 131 teams = {team: self.__colorize(team, colored.fg(9 + list(game_state.teams.keys()).index(team))) for team in game_state.teams} 132 mud_indicator = lambda player_name: " (" + ("⬇" if maze.coords_difference(game_state.muds[player_name]["target"], game_state.player_locations[player_name]) == (1, 0) else "⬆" if maze.coords_difference(game_state.muds[player_name]["target"], game_state.player_locations[player_name]) == (-1, 0) else "➡" if maze.coords_difference(game_state.muds[player_name]["target"], game_state.player_locations[player_name]) == (0, 1) else "⬅") + " " + str(game_state.muds[player_name]["count"]) + ")" if game_state.muds[player_name]["count"] > 0 else "" 133 player_names = {player.name: self.__colorize(player.name + mud_indicator(player.name), colored.bg("grey_23") + colored.fg(9 + ["team" if player.name in team else 0 for team in game_state.teams.values()].index("team"))) for player in players} 134 135 # Game info 136 environment_str = "" if self.__use_colors else "\n" 137 environment_str += "Game over" if game_state.game_over() else "Starting turn %d" % game_state.turn if game_state.turn > 0 else "Initial configuration" 138 team_scores = game_state.get_score_per_team() 139 scores_str = "" 140 for team in game_state.teams: 141 scores_str += "\n" + score_cheese * int(team_scores[team]) + score_half_cheese * math.ceil(team_scores[team] - int(team_scores[team])) 142 scores_str += "[" + teams[team] + "] " if len(teams) > 1 or len(team) > 0 else "" 143 scores_str += " + ".join(["%s (%s)" % (player_in_team, str(round(game_state.score_per_player[player_in_team], 3)).rstrip('0').rstrip('.') if game_state.score_per_player[player_in_team] > 0 else "0") for player_in_team in game_state.teams[team]]) 144 environment_str += scores_str 145 146 # Consider cells in lexicographic order 147 environment_str += "\n" + wall * (maze.width * (cell_width + 1) + 1) 148 for row in range(maze.height): 149 players_in_row = [game_state.player_locations[player.name] for player in players if maze.i_to_rc(game_state.player_locations[player.name])[0] == row] 150 cell_height = max([players_in_row.count(cell) for cell in players_in_row] + [max_weight_len]) + 2 151 environment_str += "\n" 152 for subrow in range(cell_height): 153 environment_str += wall 154 for col in range(maze.width): 155 156 # Check cell contents 157 players_in_cell = [player.name for player in players if game_state.player_locations[player.name] == maze.rc_to_i(row, col)] 158 cheese_in_cell = maze.rc_to_i(row, col) in game_state.cheese 159 160 # Find subrow contents (nothing, cell number, cheese, trace, player) 161 background = wall if not maze.rc_exists(row, col) else ground 162 cell_contents = "" 163 if subrow == 0: 164 if background != wall and not self._render_simplified: 165 cell_contents += background 166 cell_contents += cell_number(maze.rc_to_i(row, col)) 167 elif cheese_in_cell: 168 if subrow == (cell_height - 1) // 2: 169 cell_contents = background * ((cell_width - self.__colored_len(cheese)) // 2) 170 cell_contents += cheese 171 else: 172 cell_contents = background * cell_width 173 else: 174 first_player_index = (cell_height - len(players_in_cell)) // 2 175 if first_player_index <= subrow < first_player_index + len(players_in_cell): 176 cell_contents = background * ((cell_width - self.__colored_len(player_names[players_in_cell[subrow - first_player_index]])) // 2) 177 cell_contents += player_names[players_in_cell[subrow - first_player_index]] 178 else: 179 cell_contents = background * cell_width 180 environment_str += cell_contents 181 environment_str += background * (cell_width - self.__colored_len(cell_contents)) 182 183 # Right separation 184 right_weight = "0" if not maze.rc_exists(row, col) or not maze.rc_exists(row, col + 1) or not maze.has_edge(maze.rc_to_i(row, col), maze.rc_to_i(row, col + 1)) else str(maze.get_weight(maze.rc_to_i(row, col), maze.rc_to_i(row, col + 1))) 185 if col == maze.width - 1 or right_weight == "0": 186 environment_str += wall 187 else: 188 if right_weight == "1": 189 environment_str += path_vertical 190 elif not self._render_simplified and math.ceil((cell_height - len(right_weight)) / 2) <= subrow < math.ceil((cell_height - len(right_weight)) / 2) + len(right_weight): 191 digit_number = subrow - math.ceil((cell_height - len(right_weight)) / 2) 192 environment_str += mud_value(right_weight[digit_number]) 193 else: 194 environment_str += mud_vertical 195 environment_str += "\n" 196 environment_str += wall 197 198 # Bottom separation 199 for col in range(maze.width): 200 bottom_weight = "0" if not maze.rc_exists(row, col) or not maze.rc_exists(row + 1, col) or not maze.has_edge(maze.rc_to_i(row, col), maze.rc_to_i(row + 1, col)) else str(maze.get_weight(maze.rc_to_i(row, col), maze.rc_to_i(row + 1, col))) 201 if bottom_weight == "0": 202 environment_str += wall * (cell_width + 1) 203 elif bottom_weight == "1": 204 environment_str += path_horizontal * cell_width + wall 205 else: 206 cell_contents = mud_horizontal * ((cell_width - self.__colored_len(bottom_weight)) // 2) + mud_value(bottom_weight) if not self._render_simplified else "" 207 environment_str += cell_contents 208 environment_str += mud_horizontal * (cell_width - self.__colored_len(cell_contents)) + wall 209 210 # Render 211 if self.__use_colors: 212 nb_rows = 1 + len(environment_str.splitlines()) 213 nb_cols = 1 + (cell_width + 1) * maze.width 214 print("\x1b[8;%d;%dt" % (nb_rows, nb_cols), file=sys.stderr) 215 print(environment_str, file=sys.stderr)
This method redefines the method of the parent class. This function renders the game to show its current state. It does so by creating a string representing the game state and printing it.
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.