Quick Overview

This section provides a quick overview of the PyRat library and its workspace structure.

Contents of the PyRat Library

In PyRat, we manipulate two main types of files.

Games

Python scripts that define a game using the PyRat API. They import the PyRat library and use it to create a game environment, with players, mazes, and game objectives.

Games
Players

Python classes that define the behavior of a player, and inherit from the Player class provided by the PyRat library. They implement the methods that say how the player moves and reacts to the game state.

Players

The PyRat library provides a set of modules that can be used to create and manipulate mazes, players, and games. You can import these modules in your Python scripts to use their functionalities. The class diagram of the library shows how all these elements relate to each other.

How does PyRat Work?

Game Objectives

In a PyRat game, the goal is to collect pieces of cheese in a maze. The maze is represented as a grid, and players can move through the maze by iteratively choosing directions.

The game can be played with one or more players. Depending on the number of players, the winning condition may vary:

  • Single Team: The player(s) must collect all cheese pieces in the maze.

  • Multiple Teams: The player(s) must collect more cheese pieces than the other teams.

Starting a PyRat Game

Navigate to your workspace, then run the game:

cd pyrat_workspace
uv run games/sample_game.py
  1. Open VSCode, and add your pyrat_workspace directory in your VSCode workspace.

  2. Open the file sample_game.py in directory games/.

  3. Make sure VSCode is using the interpreter located in the .venv directory of your workspace.

  4. Run sample_game.py.

You should see something like this:

https://hub.imt-atlantique.fr/ueinfo-fise1a/images/s5/project/pyrat_interface.png

Elements of the Interface

In the PyRat interface above, you can see the following elements:

  • Scores Area: On the left part of the screen, you can see the scores area, that shows which players are engaged in the game, how they are grouped in teams, and their current scores. In this example, we have a match between two teams, respectively named Team Ratz and Team Pythonz. Here, each team contains a single player, respectively Random2 (with the skin of a rat), and Random3 (with the skin of a snake).

  • Maze Area: On the right part of the screen, you will find the maze, in which the game takes place. The maze lies in a rectangle of dimensions maze_width x maze_height that may have some holes. Cells are numbered from 0 to maze_width * maze_height - 1. You can also see some walls, and some cells separated with mud. The former cannot be crossed, and the latter require the number of turns indicated to reach the cell on the other side.

  • Game Elements: Characters and pieces of cheese are rendered in the maze at their current location. The color around players is there to indicate their teams. You may also notice some small flags in the middle of the maze, which indicate the starting locations.

Warning

The interface is intentionally not synchronized with the actual game. It runs in its own process, and shows every move at the same readable pace (a third of a second by default, adjustable with the rendering_speed argument of Game), whatever the turn_time of the game. The game is never slowed down by the interface, so the interface may lag behind it, and catches up once the game is over. Therefore, if you print your current location in your code, you will not see the same cell as the one shown in the interface at the same moment. This surprises everyone once, so keep it in mind when you debug a player.

Needed Elements in a Game

A PyRat game typically needs the following elements:

  • Game Settings: A description of the game elements, such as the maze dimensions, the number of pieces of cheese, density of walls, etc. These elements are defined when instantiating the game object object in the script.

  • Players: Classes that define the behavior of players in the game. These classes inherit from the Player class and implement methods that define how the player behaves in the game.

In the game script, you create your game by instantiating a Game object with desired settings. Then, you create instances of your player classes and add them to the game. Finally, you can start the game loop where players take turns to make moves until the game ends.

Warning

The code that creates and starts the game must be placed in a block if __name__ == "__main__":, as in the example below. The window of the game, as well as each player in the MATCH and SYNCHRONOUS game modes (MATCH being the default as soon as there are several teams), runs in another process, which executes your script again on some systems (Windows, macOS). Without this block, that process would create a new game in turn, and fail. PyRat therefore raises a PyRatException when such a game is created or started outside of this block, on all systems, so that a script working on one system also works on the others.

if __name__ == "__main__":

    # First, let's customize the game elements
    # This is done by setting the arguments of the Game class when instantiating it
    # In Python, we can also create a dictionary `d` with these arguments and pass it to the Game class using `game = Game(**d)`
    # This can be convenient for code organization and readability
    game_config = {"mud_percentage": 20.0,
                   "cell_percentage": 80.0,
                   "wall_percentage": 60.0,
                   "maze_width": 13,
                   "maze_height": 10,
                   "nb_cheese": 5}

    # Instantiate a game with specified arguments
    game = Game(**game_config)

    # Instantiate players with different skins, and add them to the game in distinct teams
    player_1 = Random2(skin=PlayerSkin.RAT)
    player_2 = Random3(skin=PlayerSkin.PYTHON)
    game.add_player(player_1, team="Team Ratz")
    game.add_player(player_2, team="Team Pythonz")

    # Start the game
    stats = game.start()
    pprint.pprint(stats)

You can also build more complex game scripts, for instance by running multiple games in a loop to gather statistics. This can be helpful to compare multiple algorithms for a same objective, or to test the robustness of a player against different mazes.

Phases of a Game

A PyRat game consists in four phases, that we illustrate below using the code in TemplatePlayer.

1. Constructor

__init__()

2. Preprocessing

preprocessing()

3. Turns

turn()

4. Postprocessing

postprocessing()

  1. Before the Game Starts: When players are instantiated in the game script (see above), the constructor of the player class (__init__() method) is called. This is where you can define attributes or perform any setup that is needed before the game starts. However, you do not have access to the maze or the game state at this point.

    def __init__ ( self,
                   *args:    object,
                   **kwargs: object
                 ) ->        None:
    
        """
        This function is the constructor of the class.
        When an object is instantiated, this method is called to initialize the object.
        This is where you should define the attributes of the object and set their initial values.
        Arguments *args and **kwargs are used to pass arguments to the parent constructor.
    
        Args:
            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)
    
        # Do what you want here
    
  2. Preprocessing: When the game starts, players are given some time to optionally make computations and prepare their strategies. The duration of this phase can be set in the game settings using the preprocessing_time argument. During this phase, players can analyze the maze, plan their moves, and prepare for the game. To describe what to do during this phase, you should implement the preprocessing() method in your player class.

    @override
    def preprocessing ( self,
                        maze:       Maze,
                        game_state: GameState,
                      ) ->          None:
    
        """
        *(This method redefines the method of the parent class with the same name).*
    
        This method is called once at the beginning of the game.
        It can be used to initialize attributes or to perform any other setup that is needed before the game starts.
        It typically is given more computational resources than the ``turn()`` method.
        Therefore, it is a good place to perform any heavy computations that are needed for the player to function correctly.
    
        Args:
            maze:       An object representing the maze in which the player plays.
            game_state: An object representing the state of the game.
        """
    
        # Do what you want here
    

    Note that this function receives two arguments: the maze and the game_state:

    • The maze is a particular type of graph (in details, class Maze inherits from class Graph). It contains information about the walls, holes, and other elements of the maze. It also provides methods to access the neighbors of a cell, check for mud, etc. We advise you to read the documentation of the Maze class to understand how to use it.

    • The game state is an object that contains information about the current state of the game, such as the players’ positions, scores, and remaining cheese. We advise you to read the documentation of the GameState class to understand how to use it.

  3. Player Turns: After the preprocessing phase, the game enters the main loop where players take turns. Each player has a limited amount of time to make a move, which is defined in the game settings using the turn_time argument. During this phase, players can analyze the maze, check their current position, and decide on their next move. To define what a player does during its turn, you should implement the turn() method in your player class.

    @override
    def turn ( self,
               maze:       Maze,
               game_state: GameState,
             ) ->          Action:
    
        """
        *(This method redefines the method of the parent class with the same name).*
    
        This method is called at each turn of the game.
        It returns an action to perform among the possible actions, defined in the ``Action`` enumeration.
        It is generally given less computational resources than the ``preprocessing()`` method.
        Therefore, you should limit the amount of computations you perform in this method to those that require real-time information.
    
        Args:
            maze:       An object representing the maze in which the player plays.
            game_state: An object representing the state of the game.
    
        Returns:
            One of the possible action, defined in the ``Action`` enumeration.
        """
    
        # Do what you want here
    
        # Return an action
        return Action.NOTHING
    

    As in the preprocessing phase, this function receives two arguments: the maze and the game_state. The game state is updated at each turn, so you can use it to check the current position of the player, the scores, and the remaining cheese.

    Note that the turn() method must return an action, which is one of the possible actions defined in the Action enumeration. Check the documentation of the Action enumeration to see the available actions.

  4. Postprocessing: When the game ends, players can perform some final computations and cleanup. This phase is optional and can be used to gather statistics, save results, or perform any other final tasks. To define what a player does during this phase, you should implement the postprocessing() method in your player class.

    @override
    def postprocessing ( self,
                         maze:       Maze,
                         game_state: GameState,
                         stats:      dict[str, object],
                       ) ->          None:
    
        """
        *(This method redefines the method of the parent class with the same name).*
    
        This method is called once at the end of the game.
        It can be used to perform any cleanup that is needed after the game ends.
        It is not timed, and can be used to analyze the completed game, train models, etc.
    
        Args:
            maze:       An object representing the maze in which the player plays.
            game_state: An object representing the state of the game.
            stats:      A dictionary containing statistics about the game.
        """
    
        # Do what you want here