Source code for Random4
##########################################################################################
########################################## INFO ##########################################
##########################################################################################
# This file is provided as an example by the PyRat library.
# It describes a player that can be used in a PyRat game.
# This file is meant to be imported, and not to be executed directly.
# Please import this file from a game script using the following syntax:
# from players.Random4 import Random4
"""
This module provides a player that performs random actions in a PyRat game.
It is an improvement of the ``Random3`` player.
Here, we illustrate how to use the ``preprocessing()`` method to do things at the beginning of the game.
"""
##########################################################################################
######################################### IMPORTS ########################################
##########################################################################################
# External imports
import random
# PyRat imports
from pyrat import Player, Maze, GameState, Action
##########################################################################################
######################################### CLASSES ########################################
##########################################################################################
[docs]
class Random4 (Player):
"""
*(This class inherits from* ``Player`` *).*
This player is an improvement of the ``Random3`` player.
A limitation of ``Random3`` is that it can easily enter its fallback mode when visiting a dead-end.
In this case, it may move randomly for a long time before reaching an unvisited cell
To improve our algorithm, we are going to create a new maze attribute that is the same as the original maze, but with the dead-end cells removed.
Since the maze is only provided at the beginning of the game, we will use the ``preprocessing()`` method to create this new maze.
"""
##################################################################################
# CONSTRUCTOR #
##################################################################################
[docs]
def __init__ ( self,
*args: object,
**kwargs: object
) -> None:
"""
Initializes a new instance of the class.
Here, in addition to the attributes developed in the ``Random3`` player, we also create an attribute for our reduced maze.
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)
# We create an attribute to keep track of visited cells
# We will initialize it in the ``preprocessing()`` method to allow the game to be reset
# Otherwise, the set would keep the cells visited in previous games
self.visited_cells = None
# We also create an attribute for the reduced maze
self.reduced_maze = None
##################################################################################
# PYRAT METHODS #
##################################################################################
[docs]
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.
Here, we use it to create a reduced maze that contains only the cells that are not dead-ends.
We define a dead-end as a cell that has only one neighbor and does not contain cheese or the player.
Note that this is not the best way to define a dead-end, but it is a simple one.
Args:
maze: An object representing the maze in which the player plays.
game_state: An object representing the state of the game.
"""
# Initialize visited cells
self.visited_cells = set()
# Reduce the maze
my_location = game_state.player_locations[self.get_name()]
self.reduced_maze = self.remove_dead_ends(maze, [my_location] + game_state.cheese)
##################################################################################
[docs]
def turn ( self,
maze: Maze,
game_state: GameState,
) -> Action:
"""
*(This method redefines the method of the parent class with the same name).*
It is called at each turn of the game.
It returns an action to perform among the possible actions, defined in the ``Action`` enumeration.
We also update the set of visited cells at each turn.
Now, we work with the reduced maze to find the next action.
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 actions.
"""
# Mark current cell as visited
my_location = game_state.player_locations[self.get_name()]
if my_location not in self.visited_cells:
self.visited_cells.add(my_location)
# Return an action
action = self.find_next_action(self.reduced_maze, game_state)
return action
##################################################################################
# OTHER METHODS #
##################################################################################
[docs]
def find_next_action ( self,
maze: Maze,
game_state: GameState,
) -> Action:
"""
This method returns an action to perform among the possible actions, defined in the ``Action`` enumeration.
Here, the action is chosen randomly among those that don't hit a wall, and that lead to an unvisited cell if possible.
If no such action exists, we choose randomly among all possible actions that don't hit a wall.
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 actions that leads to a valid neighbor.
"""
# Go to an unvisited neighbor in priority
my_location = game_state.player_locations[self.get_name()]
neighbors = maze.get_neighbors(my_location)
unvisited_neighbors = [neighbor for neighbor in neighbors if neighbor not in self.visited_cells]
if len(unvisited_neighbors) > 0:
neighbor = random.choice(unvisited_neighbors)
# If there is no unvisited neighbor, choose one randomly
else:
neighbor = random.choice(neighbors)
# Retrieve the corresponding action
action = maze.locations_to_action(my_location, neighbor)
return action
##################################################################################
[docs]
def remove_dead_ends ( self,
maze: Maze,
locations_to_keep: list[tuple[int, int]]
) -> Maze:
"""
This method returns a new maze that contains only the cells that are not dead-ends.
A dead-end is defined as a cell that has only one neighbor and does not contain cheese or the player.
Args:
maze: An object representing the maze in which the player plays.
locations_to_keep: A list of locations to keep in the reduced maze.
Returns:
A new maze with only the cells that are not dead-ends.
"""
# Initialize the reduced maze as the original one
# We do not need to make a copy of the maze, as the game sends a copy of the maze at each turn.
updated_maze = maze
# Iteratively remove dead-ends from the maze
# We still keep dead ends that contain locations to keep
removed_something = True
while removed_something:
removed_something = False
for vertex in updated_maze.get_vertices():
if len(updated_maze.get_neighbors(vertex)) == 1 and vertex not in locations_to_keep:
updated_maze.remove_vertex(vertex)
removed_something = True
# Return the updated maze
return updated_maze
##########################################################################################
##########################################################################################