What is a PyRat game?

General principles

PyRat is essentially a library of Python functions that allow one to create a maze game, in which players fight for grabbing pieces of cheese. Once started, a game of PyRat looks like this:

Snapshot of a PyRat game. Players fight in teams to grab more cheese than the opponents. The screen is divided in two parts: the left pane recapitulates the scores, and the right pane shows the maze and players.

In addition to the players and pieces of cheese, the maze also contains walls (that cannot be crossed), as well as mud. When crossing a mud, a player uses a number of turns indicated on the mud. Also, some useful indications can be found, such as the cell numbers (top left of the cells), and flags representing the starting positions of the players.

During the game, players play concurrently. In more details, each player runs into a separate process (i.e., it runs in parallel of the others) that interacts with the PyRat main process (which handles the game) as follows:

Typical timeline of a PyRat game. Vertical lines represent the passing of time. The line PyRat represents the behavior of the main process. Lines labelled P1 and P2 represent the behaviors of two players. Crosses on the player axes indicate when the process has finished its computations. Blue and green bars indicate the durations allowed for the corresponding steps.

The diagram above illustrates the timeline of a PyRat game:

  1. First, the PyRat process creates the maze, GUI,etc;
  2. Then, it sends the initial game information (structure of the maze, player locations, cheese locations, etc.) to the processes P1 and P2;
  3. Processes P1 and P2 then enter in a phase called Preprocessing, during which they can make some computations before the players are allowed to move. Typically it can be used to determine some interesting paths, analyze the maze, etc.;
  4. After the Preprocessing phase, the game actually starts. The following steps (5 to 7) are repeated until the game is over;
  5. The PyRat process determines the state of the game, and sends the info to the player processes P1 and P2;
  6. The players P1 and P2 (now in Turn mode) decide which direction (north, south, east, west, or nothing) to move toward, and transmit their decisions to the PyRat process;
  7. The PyRat process updates the game state (locations, current scores, remaining cheese, etc.), and starts a new turn.

Remark: When the game is rendered as a GUI, the game window runs in a separate process too. Therefore it is desynchronized from the actual game state.

A game is over when one of the following conditions is met:

  • This is a single-team game, and all pieces of cheese have been collected;
  • This is a multi-team game, and one team cannot beat the other anymore.

Alternative case 1: Preprocessing too long

Note that the description above corresponds to the nominal PyRat situation. In practice, it may happen that a player takes too much time for some computations. In that case, by default, this is handled as follows:

In this scenario, P2 has taken too much time for Preprocessing.

In the diagram above, we see that P2 has taken too much time to make computations during the Preprocessing phase. Because of that, it has missed the start of the game and had to skipped a turn. Note that it could have missed many more turns depending on how long it took to end the Preprocessing phase.

Alternative case 2: Turn too long

Similarly, a player can take too much time during the Turn phase. In that case, we have the following situation:

In this scenario, P2 has taken too much time for Turn.

As with the previous error case, P2 will have to skip turns until it returns a decision.

Alternative case 3: synchronous game

Finally, it may be interesting to allow players to take as much time as they want, for instance to test a particular strategy. This is allowed in a PyRat game by enabling the synchronous option (see below for details). In a synchronous game, the timeline of a game becomes as follows:

Timeline of a synchronous game.

As you can see, in a synchronous PyRat game, the PyRat process takes the lead when all the following criteria are met:

  • All players have finished their computations for the current phase;
  • The allocated time for the phase is over.

If you want the games to be done as quickly as possible, it can therefore be done by setting some PyRat options (see below): synchronous=True, preprocessing_time=0 and turn_time=0. Also you can disable the GUI by setting render_mode=no_rendering to save a lot more time if you do not need to visualize the game.

Elements of a PyRat program

The PyRat workspace

At some point of the instructions on the PyRat GitHub repository, you are asked to run the following command:

python -c "import pyrat; pyrat.PyRat.setup_workspace()"

When executed, it will create a pyrat_workspace directory in the current location of your terminal. This directory provides the basic elements to get started. Sections below, as well as Episode 1, will guide through these elements.

The pyrat_workspace directory contains the following subdirectories:

  • programs: This directory contains a few working examples of PyRat programs. Across the course you will learn how to write new programs;
  • tests: When programming, it is essential to make sure that your code works properly. To do so, it is good practice to write unit tests, that will ensure your functions behave as expected, in standard and corner case scenarios;
  • stats: In this directory, we provide scripts that are made to analyze the PyRat programs. By default, you are given the following two scripts (feel free to write more!):
    • compare_needed_actions.py: Runs multiple programs and checks the number of actions required to complete a game. Results are aggregated across numerous mazes, common to all tested programs;
    • make_two_player_matches.py: Makes numerous matches between two programs, to analyze on average which one seems to be better than the other.

Writing a PyRat program

Let us detail the template.py and random_1.py files, that should appear in the programs directory of your PyRat workspace after installation.

The former is essentially a skeleton of code that does nothing. It is there to serve as a blank PyRat project, and is organized into areas (delimited with comments) to help you keep your code organized. The latter is a simple working example, that drives a rat in the maze by asking it to do random choices between "nothing", "north", "east", "south" and "west" at each turn.

Each PyRat script starts with a quick description of what it is made for:

template.py:

#####################################################################################################################
######################################################## INFO #######################################################
#####################################################################################################################

"""
    This program is an empty PyRat program file.
    It serves as a template for your own programs.
    Some [TODO] comments below are here to help you keep your code organized.
    Note that all PyRat programs must have a "turn" function.
    Functions "preprocessing" and "postprocessing" are optional.
    Please check the documentation of these functions for more info on their purpose.
    Also, the PyRat website gives more detailed explanation on how a PyRat game works.
"""

random_1.py:

#####################################################################################################################
######################################################## INFO #######################################################
#####################################################################################################################

"""
    This program controls a PyRat player by performing random actions.
    More precisely, at each turn, a random choice among all possible actions is selected.
    Note that this doesn't take into account the structure of the maze.
"""

Imports

The next areas you find in a PyRat code are made for you to write your own imports. Note that the only import that is absolutely required is library pyrat.

template.py:

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

# Import PyRat
from pyrat import *

# External imports 
# [TODO] Put all your standard imports (numpy, random, os, heapq...) here

# Previously developed functions
# [TODO] Put imports of functions you have developed in previous lessons here

random_1.py:

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

# Import PyRat
from pyrat import *

# External imports 
import random

Here, random_1.py makes use of the random library, which is a standard Python library for generating random numbers.

Functions, constants and global variables

You are then given a few dedicated area to write your own functions, as well as to declare your constants and global variables if you need such objects.

Keep in mind that all processes share a common memory. Therefore, there may be some ways of hacking an opponent’s global variables, so using them seems a bad idea. You will see later on this page how to share information safely across game phases.

template.py:

#####################################################################################################################
############################################### CONSTANTS & VARIABLES ###############################################
#####################################################################################################################

# [TODO] It is good practice to keep all your constants and global variables in an easily identifiable section

#####################################################################################################################
##################################################### FUNCTIONS #####################################################
#####################################################################################################################

# [TODO] It is good practice to keep all developed functions in an easily identifiable section

random_1.py:





As you can see, random_1.py does not define any function, constant or variable, so we just removed the corresponding areas for clarity.

Preprocessing function

As mentioned earlier, a PyRat game starts with a Preprocessing phase, where players can make some complex computations for later. This phase is typically allocated ~3s of computation time, which is a lot more than for turns, so do not hesitate to use this resource.

In order to define what a player does during this phase, you need to write a function as follows. For now, do not try to understand how this function is executed by the game, we will cover this later.

template.py:

#####################################################################################################################
##################################### EXECUTED ONCE AT THE BEGINNING OF THE GAME ####################################
#####################################################################################################################

def preprocessing ( maze:             Union[numpy.ndarray, Dict[int, Dict[int, int]]],
                    maze_width:       int,
                    maze_height:      int,
                    name:             str,
                    teams:            Dict[str, List[str]],
                    player_locations: Dict[str, int],
                    cheese:           List[int],
                    possible_actions: List[str],
                    memory:           threading.local
                  ) ->                None:

    """
        This function is called once at the beginning of the game.
        It is typically given more time than the turn function, to perform complex computations.
        Store the results of these computations in the provided memory to reuse them later during turns.
        To do so, you can crete entries in the memory dictionary as memory.my_key = my_value.
        In:
            * maze:             Map of the maze, as data type described by PyRat's "maze_representation" option.
            * maze_width:       Width of the maze in number of cells.
            * maze_height:      Height of the maze in number of cells.
            * name:             Name of the player controlled by this function.
            * teams:            Recap of the teams of players.
            * player_locations: Locations for all players in the game.
            * cheese:           List of available pieces of cheese in the maze.
            * possible_actions: List of possible actions.
            * memory:           Local memory to share information between preprocessing, turn and postprocessing.
        Out:
            * None.
    """

    # [TODO] Write your preprocessing code here
    pass

random_1.py:





The Preprocessing phase is optional. In this case, as random_1.py makes independent choices at each turn, there is no real need to define a preprocessing function. However, if you choose to associate a preprocessing function to your character, that function must have the same number of arguments as the one presented above. Let us detail these arguments:

  • maze: This is the map of the maze. It can be defined either with a numpy ndarray, where row/column i is associated to cell number i in the maze, or with a Python dictionary, that associates to each cell its neighbors. This choice can be controlled by the maze_representation option (see below). Manipulation of the maze is described more throughfully during Episode 1;
  • maze_width: This is the number of cells per row of the maze;
  • maze_height: This is the number of cells per column of the maze. A maze contains cells numbered from 0 to maze_width * maze_height - 1;
  • name: The name of the character using this function;
  • teams: This is a recap of the teams in the game;
  • player_locations: This dictionary associates to each player the index of the cell it is located to in the maze;
  • cheese: This list gives the locations of all pices of cheese in the maze at the beginning of the game;
  • possible_actions: This list contains all the actions that can be performed by the character at each turn. Note that this list is always equal to ["nothing", "north", "east", "south", "west"] and does not take into consideration the fact that one of these actions may drive you into a wall;
  • memory: When you make computations during the Preprocessing phase, you probably want to use the result of these computations later during the Turns. In Python, variables defined within a function are local to that function, which means that they will vanish after the function is resolved. To allow these variables to survive, they can be defined in a different memory space, that lasts all along the program execution. The memory variable provides such a space. To use it, just write for example memory.my_variable = 0, and a variable called my_variable, initialized at 0 will exist for the rest of the game within memory. To use it later, just write memory.my_variable to access its contents. Note that this memory is local to the player’s process. Therefore, it cannot be accessed by an opponent’s program, as could be the case for global variables.

Turn function

The only function that is compulsory is the turn function, that describes what a player does during each turn of the game. This function should always return an action among those listed in possible_actions.

As for preprocessing, do not try to understand now how this function will be called by the PyRat game, we will cover that later. Let us have a look at the two files:

template.py:

#####################################################################################################################
######################################### EXECUTED AT EACH TURN OF THE GAME #########################################
#####################################################################################################################

def turn ( maze:             Union[numpy.ndarray, Dict[int, Dict[int, int]]],
           maze_width:       int,
           maze_height:      int,
           name:             str,
           teams:            Dict[str, List[str]],
           player_locations: Dict[str, int],
           player_scores:    Dict[str, float],
           player_muds:      Dict[str, Dict[str, Union[None, int]]],
           cheese:           List[int],
           possible_actions: List[str],
           memory:           threading.local
         ) ->                str:

    """
        This function is called at every turn of the game.
        It should return an action within the set of possible actions.
        You can access the memory you stored during the preprocessing function by doing memory.my_key.
        You can also update the memory with new information, or create new entries as memory.my_key = my_value.
        In:
            * maze:             Map of the maze, as data type described by PyRat's "maze_representation" option.
            * maze_width:       Width of the maze in number of cells.
            * maze_height:      Height of the maze in number of cells.
            * name:             Name of the player controlled by this function.
            * teams:            Recap of the teams of players.
            * player_locations: Locations for all players in the game.
            * player_scores:    Scores for all players in the game.
            * player_muds:      Indicates which player is currently crossing mud.
            * cheese:           List of available pieces of cheese in the maze.
            * possible_actions: List of possible actions.
            * memory:           Local memory to share information between preprocessing, turn and postprocessing.
        Out:
            * action:           One of the possible actions, as given in possible_actions.
    """

    # [TODO] Write your turn code here and do not forget to return a possible action
    action = possible_actions[0]
    return action

random_1.py:

#####################################################################################################################
######################################### EXECUTED AT EACH TURN OF THE GAME #########################################
#####################################################################################################################

def turn ( maze:             Union[numpy.ndarray, Dict[int, Dict[int, int]]],
           maze_width:       int,
           maze_height:      int,
           name:             str,
           teams:            Dict[str, List[str]],
           player_locations: Dict[str, int],
           player_scores:    Dict[str, float],
           player_muds:      Dict[str, Dict[str, Union[None, int]]],
           cheese:           List[int],
           possible_actions: List[str],
           memory:           threading.local
         ) ->                str:

    """
        This function is called at every turn of the game.
        It should return an action within the set of possible actions.
        You can access the memory you stored during the preprocessing function by doing memory.my_key.
        You can also update the memory with new information, or create new entries as memory.my_key = my_value.
        In:
            * maze:             Map of the maze, as data type described by PyRat's "maze_representation" option.
            * maze_width:       Width of the maze in number of cells.
            * maze_height:      Height of the maze in number of cells.
            * name:             Name of the player controlled by this function.
            * teams:            Recap of the teams of players.
            * player_locations: Locations for all players in the game.
            * player_scores:    Scores for all players in the game.
            * player_muds:      Indicates which player is currently crossing mud.
            * cheese:           List of available pieces of cheese in the maze.
            * possible_actions: List of possible actions.
            * memory:           Local memory to share information between preprocessing, turn and postprocessing.
        Out:
            * action:           One of the possible actions, as given in possible_actions.
    """

    # Random possible action
    action = random.choice(possible_actions)
    return action

Here, random_1.py uses a function called choice from the random library. This function returns a random value from a provided list. You can find more information in the official documentation.

As you can see, many arguments are common with the preprocessing function. These are updated at each turn. In other words, function turn receives arguments that represent the current game situation. Argument cheese will therefore only contain the remaining pieces of cheese, and player_locations will give the current locations of the players. Also note that the memory argument is the same memory space as during the Preprocessing phase and all turns.

Let us detail the new arguments that do not appear in preprocessing:

  • player_scores: This dictionary associates to each player its current score;
  • player_muds: This dictionary gives all necessary information to understand the behavior of player crossing mud. It associates to each player a second dictionary with two elements : "target" and "count". If a player with name p is currently crossing mud from cell i to cell j, and has still X turns to wait, then we have player_muds[p] = {"target" : j, "count" : X} and player_locations[p] = i.

Postprocessing function

The PyRat game also allows to define a Postprocessing phase. When the game is over, it is possible to ask the game to call a postprocessing function once. It can be useful for instance if you needed to create some files on the computer during the game and want to do some proper cleaning at the end of the game, or for more advanced strategies like reinforcement learning.

As for the Preprocessing phase, this is an optional phase.

template.py:

#####################################################################################################################
######################################## EXECUTED ONCE AT THE END OF THE GAME #######################################
#####################################################################################################################

def postprocessing ( maze:             Union[numpy.ndarray, Dict[int, Dict[int, int]]],
                     maze_width:       int,
                     maze_height:      int,
                     name:             str,
                     teams:            Dict[str, List[str]],
                     player_locations: Dict[str, int],
                     player_scores:    Dict[str, float],
                     player_muds:      Dict[str, Dict[str, Union[None, int]]],
                     cheese:           List[int],
                     possible_actions: List[str],
                     memory:           threading.local,
                     stats:            Dict[str, Any],
                   ) ->                None:

    """
        This function is called once at the end of the game.
        It is not timed, and can be used to make some cleanup, analyses of the completed game, model training, etc.
        In:
            * maze:             Map of the maze, as data type described by PyRat's "maze_representation" option.
            * maze_width:       Width of the maze in number of cells.
            * maze_height:      Height of the maze in number of cells.
            * name:             Name of the player controlled by this function.
            * teams:            Recap of the teams of players.
            * player_locations: Locations for all players in the game.
            * player_scores:    Scores for all players in the game.
            * player_muds:      Indicates which player is currently crossing mud.
            * cheese:           List of available pieces of cheese in the maze.
            * possible_actions: List of possible actions.
            * memory:           Local memory to share information between preprocessing, turn and postprocessing.
        Out:
            * None.
    """

    # [TODO] Write your postprocessing code here
    pass

random_1.py:





Arguments of postprocessing are essentially the same as those of turn. They represent the state of the game when it is over. There is one additional argument called stats, that is given by the PyRat main process. This is a dictionary that sums up multiple aspects of the game. More details on these aspects are given in a separate section below.

Creating the game

Now that all important elements have been defined, it is time to start the game. Let us analyze how this is done in our example files:

template.py:

#####################################################################################################################
######################################################## GO ! #######################################################
#####################################################################################################################

if __name__ == "__main__" :

    # Map the functions to the character
    players = [{"name" : "Template", "preprocessing_function" : preprocessing, "turn_function" : turn, "postprocessing_function" : postprocessing}]

    # Customize the game elements
    config = {"maze_width" : 15,
              "maze_height" : 11,
              "mud_percentage" : 0.0,
              "nb_cheese" : 1}

    # Start the game
    game = PyRat(players, **config)
    stats = game.start()

    # Show statistics
    print(stats)

random_1.py:

#####################################################################################################################
######################################################## GO ! #######################################################
#####################################################################################################################

if __name__ == "__main__" :

    # Map the function to the character
    players = [{"name" : "Random 1", "skin": "rat", "turn_function" : turn}]

    # Customize the game elements
    config = {"maze_width" : 15,
              "maze_height" : 11,
              "mud_percentage" : 0.0,
              "nb_cheese" : 1,
              "trace_length": 1000}

    # Start the game
    game = PyRat(players, **config)
    stats = game.start()

    # Show statistics
    print(stats)

As you can see, there are 4 identifiable sections in this code:

  • # Map the functions to the character: The first thing to do is to link the functions that you have developed earlier with a character. To do so, we are going to create a list of players. In that list, each player is described by a dictionary, with the following keys (keys marked with a (*) are compulsory):
    • "name"(*): This is the name that you want to give to the character;
    • "skin": You can choose between "rat", "python" or "default" to load the existing skins. Choosing another name will be load the default skin. You can setup you own skins by replicating the contents of e.g., directory pyrat/gui/players/rat/ in your local installation of the PyRat software;
    • "team": If you want to put multiple players in a common team (thus sharing scores), you should give them the same team name here;
    • "location": You can specify where the player starts. This argument can be equal to "center" (default if nothing is specified), "random" (for a random valid cell), "same" (for the same location as the previous player in the list) or a cell number (if the cell does not exist, it will start at the closest cell according to Euclidean distance);
    • "preprocessing_function": Here, you can provide the preprocessing function that you have defined earlier. Note that this is not a string, but directly the function name, as you are passing the function itself to PyRat;
    • "turn_function" (*): Similarly to the preprocessing function, this is where you associate the turn function you developed earlier to the character;
    • "postprocessing_function": If you need a Postprocessing phase, you can map your postprocessing function here;
  • # Customize the game elements: There are multiple ways of customizing the game. One way is to define a dictionary of the parameters that can be used to tune the game, as in this example. Here, for instance, both programs indicate that the maze will have a width of 15 cells and a height of 15 cells. Also, there will be no mud, and only one piece of cheese. In random_1.py there is an extra parameter that allows to visualize the trajectory of the character. Details on which options can be used to tune the game are given in a section below;
  • # Start the game: Now that we know which players are involved, and what the maze will look like, we can start the game. To do so, we instanciate a PyRat object in a variable called game, with the players and game configuration, that will prepare the game, and then call game.start()to actually start the game;
  • # Show statistics: Once the game is over, it returns a dictionary of statistics. These are the same that are provided to the postprocessing function. Details on this dictionary are given below.

After the game

Once the game is over, the PyRat process creates a dictionary of statistics that sum up multiple aspects of the game. Let us run random_1.py to see what this looks like:

{'players': {'rat': {'actions': {'mud': 0, 'error': 0, 'miss': 0, 'nothing': 15, 'north': 15, 'east': 9, 'south': 15, 'west': 12, 'wall': 17}, 'score': 0, 'turn_durations': [1.1205673217773438e-05, 2.384185791015625e-06, 1.430511474609375e-06, 1.1920928955078125e-06, 9.5367431640625e-07, 1.1920928955078125e-06, 9.5367431640625e-07, 1.1920928955078125e-06, 1.1920928955078125e-06, 9.5367431640625e-07, 9.5367431640625e-07, 1.1920928955078125e-06, 9.5367431640625e-07, 9.5367431640625e-07, 9.5367431640625e-07, 1.1920928955078125e-06, 9.5367431640625e-07, 1.1920928955078125e-06, 1.1920928955078125e-06, 1.430511474609375e-06, 1.1920928955078125e-06, 1.1920928955078125e-06, 9.5367431640625e-07, 1.1920928955078125e-06, 9.5367431640625e-07, 1.1920928955078125e-06, 1.1920928955078125e-06, 9.5367431640625e-07, 9.5367431640625e-07, 1.1920928955078125e-06, 9.5367431640625e-07, 1.1920928955078125e-06, 9.5367431640625e-07, 9.5367431640625e-07, 9.5367431640625e-07, 9.5367431640625e-07, 9.5367431640625e-07, 9.5367431640625e-07, 1.1920928955078125e-06, 1.430511474609375e-06, 1.1920928955078125e-06, 1.430511474609375e-06, 9.5367431640625e-07, 9.5367431640625e-07, 1.1920928955078125e-06, 9.5367431640625e-07, 9.5367431640625e-07, 1.1920928955078125e-06, 9.5367431640625e-07, 9.5367431640625e-07, 9.5367431640625e-07, 1.430511474609375e-06, 9.5367431640625e-07, 1.1920928955078125e-06, 1.1920928955078125e-06, 9.5367431640625e-07, 1.430511474609375e-06, 1.1920928955078125e-06, 1.430511474609375e-06, 9.5367431640625e-07, 9.5367431640625e-07, 9.5367431640625e-07, 9.5367431640625e-07, 9.5367431640625e-07, 1.430511474609375e-06, 9.5367431640625e-07, 9.5367431640625e-07, 1.1920928955078125e-06, 1.1920928955078125e-06, 1.430511474609375e-06, 9.5367431640625e-07, 1.1920928955078125e-06, 1.1920928955078125e-06, 1.430511474609375e-06, 1.1920928955078125e-06, 1.1920928955078125e-06, 1.430511474609375e-06, 9.5367431640625e-07, 1.430511474609375e-06, 9.5367431640625e-07, 1.1920928955078125e-06, 9.5367431640625e-07, 1.1920928955078125e-06], 'preprocessing_duration': 1.1920928955078125e-06}}, 'turns': 83}

As we can see, there are many values that make it a bit hard to read for now. Let us simplify this a bit and format this output:

{
    'players':
    {
        'rat':
        {
            'actions':
            {
                'mud': 0,
                'error': 0,
                'miss': 0,
                'nothing': 15,
                'north': 15,
                'east': 9,
                'south': 15,
                'west': 12,
                'wall': 17
            },
            'score': 0,
            'turn_durations': [1.12e-05, ..., 1.19e-06],
            'preprocessing_duration': 1.19e-06
        }
    },
    'turns': 83
}

As you can see, stats is a dictionary that contains two elements:

  • "turns" is the total number of turns that were necessary to complete the game;
  • "players" is another dictionary summarizing the behaviors of the players.

Let us analyze the behavior of the player named "rat". It is accessible as stats["players"]["rat"] and contains four entries:

  • "actions": This dictionary sums up the counts of actions performed by the player. Here, as the program moves uniformly at random, we can see that all directions were taken approximately the same number of times. It hit a wall 17 times, and chose not to move 15 times. Also, the character did not cross any mud, and did not miss any timing. Finally, there were no errors during the game;
  • "score": This is the final score of the player;
  • "turn_durations": This is a list of times (in seconds) representing the time actually used by each of the calls to the turn function;
  • "preprocessing_duration": This is a time (in seconds) representing the time actually used by the preprocessing function.

Customizing a PyRat game

How to do it?

As mentioned a few times already, it is possible to customize the PyRat games. This can be done in three different ways:

  • As described earlier, using a dictionary, that is then passed to the PyRat() constructor function:
[...]

# Customize the game elements
config = {"maze_width" : 15,
          "maze_height" : 11,
          "mud_percentage" : 0.0,
          "nb_cheese" : 1,
          "trace_length": 1000}

# Start the game
game = PyRat(players, **config)

[...]
  • Arguments can be passed directly to the constructor as well:
[...]

# Start the game
game = PyRat(players, maze_width=15, maze_height=11, mud_percentage=0.0, nb_cheese=1, trace_length=1000)

[...]
  • Finally, arguments can be passed directly through the command line. Note that passing argument like this is compatible with the two scenarios above. However, those take precedence if a parameter is set both using the PyRat() constructor and the command line:
python my_program.py --maze_width 15 --maze_height 11 --mud_percentage 0.0 --nb_cheese 1 --trace_length 1000

What are the available arguments?

The list of arguments you can set can be obtained using the following command (assuming my_program.py is a PyRat program that imports the pyrat library:

python my_program.py -h

Here is the output you get:

usage: my_program.py [-h] [--random_seed RANDOM_SEED] [--random_seed_maze RANDOM_SEED_MAZE] [--random_seed_cheese RANDOM_SEED_CHEESE] [--random_seed_players RANDOM_SEED_PLAYERS] [--maze_width MAZE_WIDTH] [--maze_height MAZE_HEIGHT] [--cell_percentage CELL_PERCENTAGE] [--wall_percentage WALL_PERCENTAGE] [--mud_percentage MUD_PERCENTAGE] [--mud_range MUD_RANGE] [--maze_representation {dictionary,matrix}] [--fixed_maze FIXED_MAZE] [--nb_cheese NB_CHEESE] [--fixed_cheese FIXED_CHEESE] [--save_path SAVE_PATH] [--save_game] [--preprocessing_time PREPROCESSING_TIME] [--turn_time TURN_TIME] [--synchronous] [--continue_on_error] [--render_mode {ascii,ansi,gui,no_rendering}] [--render_simplified] [--gui_speed GUI_SPEED] [--fullscreen] [--trace_length TRACE_LENGTH]

optional arguments:

  -h, --help
  show this help message and exit

  --random_seed RANDOM_SEED
  Global random seed for all elements
  (default: None)

  --random_seed_maze RANDOM_SEED_MAZE
  Random seed for the maze generation
  (default: None)

  --random_seed_cheese RANDOM_SEED_CHEESE
  Random seed for the pieces of cheese distribution
  (default: None)

  --random_seed_players RANDOM_SEED_PLAYERS
  Random seed for the initial location of players
  (default: None)

  --maze_width MAZE_WIDTH
  Width of the maze in number of cells
  (default: 15)

  --maze_height MAZE_HEIGHT
  Height of the maze in number of cells
  (default: 13)

  --cell_percentage CELL_PERCENTAGE
  Percentage of cells that can be accessed in the maze, 0% being a useless maze, and 100% being a full rectangular maze
  (default: 80.0)

  --wall_percentage WALL_PERCENTAGE
  Percentage of walls in the maze, 0% being an empty maze, and 100% being the maximum number of walls that keep the maze connected
  (default: 60.0)

  --mud_percentage MUD_PERCENTAGE
  Percentage of pairs of adjacent cells that are separated by mud in the maze
  (default: 20.0)

  --mud_range MUD_RANGE
  Interval of turns needed to cross mud
  (default: [4, 9])

  --maze_representation {dictionary,matrix}
  Representation of the maze in memory as given to players
  (default: dictionary)

  --fixed_maze FIXED_MAZE
  Fixed maze in any PyRat accepted representation (takes priority over any maze description and will automatically set maze_height and maze_width)
  (default: None)

  --nb_cheese NB_CHEESE
  Number of pieces of cheese in the maze
  (default: 21)

  --fixed_cheese FIXED_CHEESE
  Fixed list of cheese (takes priority over random number of cheese)
  (default: None)

  --save_path SAVE_PATH
  Path where games are saved
  (default: .)

  --save_game
  Indicates if the game should be saved
  (default: False)

  --preprocessing_time PREPROCESSING_TIME
  Time given to the players before the game starts
  (default: 3.0)

  --turn_time TURN_TIME
  Time after which players will move in the maze, or miss a turn
  (default: 0.1)

  --synchronous
  If set, waits for all players to return a move before moving, even if turn_time is exceeded
  (default: False)

  --continue_on_error
  If a player crashes, continues the game anyway
  (default: False)

  --render_mode {ascii,ansi,gui,no_rendering}
  Method to display the game, or no_rendering to play without rendering
  (default: gui)

  --render_simplified
  If the maze is rendered, hides some elements that are not essential

  --gui_speed GUI_SPEED
  When rendering as GUI, controls the speed of the game
  (default: 1.0)

  --fullscreen
  Renders the game in fullscreen mode (GUI rendering only)
  (default: False)

  --trace_length TRACE_LENGTH
  Maximum length of the trace to display when players are moving (GUI rendering only)
  (default: 0)

Let us give a bit more details on these arguments:

  • random_seed: When fixing a random seed, it forces the random number generator to follow a deterministic sequence, initialized with that seed value. This is useful to reproduce a particular random realization. Here, setting random_seed=X is equivalent to setting random_seed_maze=X, random_seed_cheese=X and random_seed_players=X;
  • random_seed_maze: This seed controls the maze generation random process;
  • random_seed_cheese: This seed controls the cheese placement random process;
  • random_seed_players: This seed controls the players initial location random process. Note that this does not do anything to the players, if they choose to use random numbers for their decisions;
  • maze_width: Controls the width of the maze in number of cells;
  • maze_height: Controls the height of the maze in number of cells;
  • cell_percentage: The maze needs not be rectangular. There can be missing cells, like holes in the maze. The missing cells still are numbered though. As an example, if cell 2 is not accessible, then visible cells on the GUI will be 1, 3, …;
  • wall_percentage: This parameter controls the density of walls in the maze;
  • mud_percentage: This parameter controls the density of mud in the maze;
  • mud_range: This parameter allows one to set the minimum and maximum numbers of turns it takes to cross some mud;
  • maze_representation: This parameter controls the type of the maze variable in the preprocessing, turn and postprocessing functions:
    • If you set maze_representation="dictionary", then you will manipulate a Python dictionary. In that case, keys will be the accessible cells (i.e., not the missing ones) and associated values will be a dictionary of neighbors, where a key is the neighbor number, and associated value is the number of required turns to reach it. Using this structure, the neighbors of a cell i are obtained by maze[i].keys() and, if j is a neighbor of i, we obtain the number of required actions to go from i to j by maze[i][j] ;
    • If you set maze_representation="matrix", then you will manipulate a numpy ndarray object. In that case, the maze variable will be a (maze_width * maze_height) x (maze_width * maze_height) adjacency matrix, where row/column i is associated to cell i, and where maze[i, j] is the number of required actions to reach j from i (using the convention maze[i, j] = 0 if i and j are not neighbors). This means that missing cells will have a dedicated row/column in the matrix, which will be filled with zeros;
  • fixed_maze: It is possible to bypass the random generation of the maze, by providing a predefined maze. The maze should be provided as one of the types supported by the maze_representation argument, or as a string representing such a type;
  • save_path: The games can be saved to be replayed later. This argument controls the directory where the games will be saved;
  • save_game: If set, this parameter allows the game to generate an export of the game in the directory specified by save_path. The generated file will just reproduce the actions, and not the computations of the players;
  • preprocessing_time: You can change the time associated to the Preprocessing phase here. By default it is set to 3s;
  • turn_time: Similarly, you can change the time associated to each Turn phase here. By default it is set to 0.1s;
  • synchronous: Setting this option will put the game in Synchronous mode, as described earlier on this page;
  • continue_on_error: By default, PyRat stops when a player crashes. This option allows the game to continue even when a player crashes;
  • render_mode: PyRat has multiple GUIs. By default, you should see a nice window with drawings (render_mode="gui"). However, it is possible to have the game directly printed in the terminal (render_mode="ansi" or render_mode="ascii"), which can be useful if working from a distant computer or in case of a compatibility issue with the standard GUI. Finally, it is also possible to hide the rendering to speed up computations (render_mode="no_rendering");
  • render_simplified: The default rendering mode shows additional useful information, like cell numbers, mud values, etc. If you want to just visualize the game, you can remove these elements with this option;
  • fullscreen: This allows to play the game in fullscreen mode;
  • trace_length: When set to a value of X, there will be a line of length Xcells following the characters. This is useful to monitor the trajectory taken by the characters, to check for instance if they took the shortest path, visited a particular cell, etc.