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#####################################################################################################################################################
class ShellRenderingEngine(pyrat.src.RenderingEngine.RenderingEngine):
 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.

ShellRenderingEngine( use_colors: bool = True, *args: typing_extensions.Any, **kwargs: typing_extensions.Any)
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.
@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:
 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.