The PyRat software

General principles

The PyRat software works essentially as follows. The core of the program is managed by the pyrat.py file, which takes care of all the fundamental elements: maze creation, player management, display management, statistics generation…

When executed, this program starts two processes, one for each player. These programs allow to manage both players almost independently. Communication between the main program and the processes managing the players is done through communication queues. Again, everything is done to ensure that one player’s program does not affect the other’s.

Important: It is the PyRat main program that handles everything. As a consequence, running your AI directly from the terminal will result in nothing interesting.

Important: Similarly, pressing the play button in your favorite code editor (say, Spyder) to run PyRat will not work, as pyrat.py needs some arguments, such as which program to associate with the rat, the maze size…

A game of PyRat

When the game starts, the main program calls the preprocessing function of the players’ files (more details are given below). The PyRat core program then waits for some time (argument time_allowed of function preprocessing), in order to let the players make some computations.

After the proprocessing step is over, the PyRat core program calls the turn functions of both players to find out their movements. Again, it then waits for some time (argument time_allowed of function turn), in order to let the players decide which move to return.

After this time is over, the PyRat program applies the players’ decisions and updates the graphical interface. If one player is too late, they don’t move for the turn.

Note: It is possible to configure a game so that the PyRat core program waits for both players to return a decision, instead of using a fixed turn duration. This is done through PyRat’s option --synchronous.

Turns then continue until the game is over.

Start a game

Once PyRat is installed, the following command starts a game where the rat tries to pick up all the pieces of cheese by moving randomly:

python pyrat.py --rat AIs/random.py

After the game

Once the game is finished, a string of characters appears in the terminal, summarizing the game:

{
	"miss_python": 0.0
	"miss_rat": 114.0
	"moves_python": 123.0
	"moves_rat": 21.0
	"prep_time_python": 3.0994415283203125e-06
	"prep_time_rat": 0.0017397403717041016
	"score_python": 21.0
	"score_rat": 5.0
	"stucks_python": 17.0
	"stucks_rat": 5.0
	"turn_time_python": 0.0019042918352576775
	"turn_time_rat": 4.34830075218564e-06
	"win_python": 1.0
	"win_rat": 0.0
}

It contains a certain amount of information for each player, among which :

  • miss_xxx: The number of movements missed due to a too long calculation or moving toward a wall;
  • moves_xxx: The number of moves performed (and not missed);
  • prep_time_xxx: The time effectively taken by function preprocessing before completing;
  • score_xxx: The number of pieces of cheese collected;
  • stucks_xxx: The number of additional movements caused by mud;
  • turn_time_xxx: The time effectively taken by function turn before returning a move;
  • win_xxx: 1 if the game is won, 0 if lost, 0.5 if tied.

To obtain average statistics on several games (which is interesting, especially if they contain some random factor), use the --tests parameter. The PyRat output will indicate the average obtained for each of the criteria mentioned above.

You should use it in combination with --nodrawing --synchronous for faster computations.

PyRat options

There are many options in PyRat that allow you to customize the dimensions of the maze, add a second player, allow more time for each turn, disable graphical interface, etc. The complete list can be accessed via this command:

python pyrat.py -h

Details on PyRat player programs

The template contents

To make it easier to learn the PyRat game, we provide the teachers and the students with a Python source file skeleton. This is located in the AIs folder, and is called template.py.

Equivalently, you can use a Jupyter notebook (either on your computer or on your Google Drive using Google Colab) to program and use it as an AI for PyRat. Here is the link to the template as a notebook.

Reading the code contained in this file, you will realize that it is subdivided into subsections:

MOVE_DOWN = 'D'
MOVE_LEFT = 'L'
MOVE_RIGHT = 'R'
MOVE_UP = 'U'

The code above defines four constants that correspond to what the turn function should return, i.e., a decision of a move to perform in the maze.

Then, there is a blank area, where you should write your functions, imports, and basically everything you need when developing your AI. For sure it would work if putting them farther in the code, but keeping things organized is always better, especially when asking help to your teacher 🙂

def preprocessing (maze_map, maze_width, maze_height, player_location, opponent_location, pieces_of_cheese, time_allowed) :
    
    # ...

The preprocessing function is called exactly once before the PyRat game start. It gives you the possibility to make some time-consuming computations, and use their results in the turn function later.

def turn (maze_map, maze_width, maze_height, player_location, opponent_location, player_score, opponent_score, pieces_of_cheese, time_allowed) :

    # ...
    return MOVE_XXX

The final function of this program is turn, and is supposed to return a move among those we have defined at the beginning of the file.

The data structures used

This section presents the implementation of PyRat main structures. For more general information on data structures in Python, you can check this page.

Locations in the maze

Locations in the maze are encoded as pairs (i.e., tuples of 2 elements) (x, y) of integers, representing their horizontal and vertical coordinates. In this coordinates system, (0, 0) is at the bottom left.

In PyRat, player locations, as well as the cheese locations, are represented like that. Indeed, the comments above functions preprocessing and turn indicate:

# player_location : pair(int, int)
# opponent_location : pair(int,int)
# pieces_of_cheese : list(pair(int, int))

The maze map

In PyRat, the maze map is encoded as a dictionary, that associates to each location (x, y) another dictionary of neighbors of (x, y). This second dictionary represents the locations accessible (i.e., not separated by a wall) from (x, y) in the maze, as well as the weight to reach them.

The comments above functions preprocessing and turn indicate:

# maze_map : dict(pair(int, int), dict(pair(int, int), int))

In Python, you can access the element of a list l at index i with the notation l[i]. For this code to work, i must be an integer between 0 and len(l)-1. In short, you can understand dictionaries as a generalization of lists, where the index is not necessarily an integer.

So let’s come back to maze_map. It is a dictionary in which keys (analogous to the indices in lists) are the possible locations in the maze, and values (i.e., what we associate with the keys) are the adjacent locations which can be reached, along with the number of moves to reach it.

As an example, let us consider the following small maze:

maze_map = {(0, 0): {(1, 0): 1}, (0, 1): {(1, 1): 1}, (1, 0): {(1, 1): 6, (0, 0): 1}, (1, 1): {(1, 0): 6, (0, 1): 1}}

In this maze, location (0, 0) is at the bottom left. When writing maze_map[(0, 0)], we obtain {(1, 0): 1}, indicating that (0, 0) can access (1, 0) with a weight of maze_map[(0, 0)][(1, 0)] = 1.

Similarly, when writing maze_map[(1, 0)], we obtain {(1, 1): 6, (0, 0): 1}, indicating that (1, 0) can come back to (0, 0) with a weight of maze_map[(1, 0)][(0, 0)] = 1, and that (1, 0) can reach (1, 1) with a weight of maze_map[(1, 0)][(1, 1)] = 6 due to the presence of mud in the maze.