ASPP / pelita_template

Default template to get started with the Pelita game.
3 stars 25 forks source link
educational-game hacktoberfest pelita python

Pelita

Table of contents


Introduction

Pelita is a Pac-Man like game. Two teams each of two characters (bots) are placed in a maze with food pellets. The blue team starts on the left of the maze, with bots named a and b. The red team starts on the right of the maze, with bots named x and y.

The maze is split into two parts, the left one belongs to the blue team, the right one belongs to the red team. When a bot is in its own homezone it is a ghost. A ghost can defend its own food pellets by killing the enemies. When a bot is in its enemy's homezone it is a pac-man and can eat the enemy's food. The game is turn-based: one bot moves, the rules of the game are applied, a new state of the game is generated, the next bot moves. The first bot of the blue team, bot a, moves first, then the first bot of the red team, bot x, moves, then the second bot, b, of the blue team, and finally the second bot, y, of the read team. This defines a round. A standard game lasts at most 300 rounds.

The rules:

Your task

Your task as a group is to write a bot implementation. You have to implement the intelligence to navigate your bots successfully through the maze, kill the enemy's pac-men, and eat the enemy's food. You can find a minimal implementation in the demo01_stopping.py file:

TEAM_NAME = 'StoppingBots'

def move(bot, state):
    # do not move at all
    return bot.position

As seen above, your implementation consists of a team name (the TEAM_NAME string) and a function move, which takes a bot object (which contains all the data that you need to make your decision) and a state dict (which you are supposed to fill) and returns the next position for current bot. Don't panic right now, in the Full API Description section you'll find all the details.

Content of this repository

In this repository you will find several demo implementations (all files named demoXX_XXX.py), that you can use as a starting point for your own implementations. The demos build upon each other, so it will be useful to follow them in order.

The files named test_demoXX_XXX.py are example unit tests for some of the demo bot implementations. You can run the tests within a clone of this repo with pytest by typing:

$ python -m pytest

In the notebooks folder you can find some examples of other ways of interacting with the pelita code. notebooks/nb1_matplotlib_and_numpy.ipynb shows how you might use numpy arrays and matplotlib to visualize game states. notebooks/nb2_enemy_noise.ipynb uses the same approach to visualize enemy positions and noisy positionsas explained below. Notebooks can be a useful way to get an intuition for the bot object properties or for visualizing game statistics, but should not be used to actually write your bot.

Running a game

The graphical interface

Pelita’s graphical interface contains a few tools that can help with debugging. For example, using the ‘slower’ and ‘faster’ buttons, one may change the maximum speed of the game. (Of course, it is not possible to speed up slow bots using this.)

When the game has been paused, the step and round buttons will execute a single step or play a complete round. Additionally, selecting a specific square in the grid will show its coordinates and if the square is a wall or contains food or bots are sitting on it.

The debug button activates the debug mode.

Keyboard shortcuts

q               Quit
f               Toggle fullscreen
#               Toggle debug mode
>               Increase speed
<               Decrease speed
[Space]         Play/pause
[Enter]         Play a single step (when in pause mode)
[Shift+Enter]   Play one round (when in pause mode)

Testing

There are several strategies to test your bot implementations.

Manual testing

You can test an implementation by playing against some of the demo implementations or against itself.

You can pass several options to the pelita command to help you with testing.

Unit testing

You should write unit tests to test your utility functions and to test your bot implementations. It is quite hard to test a full strategy, especially because you can not have a real opponent in a test game. It is useful to focus on specific situations (called layouts) and verify that your bot is doing the right thing. More info about layouts in the Layouts section.

Several examples for unit testing are available in this repo in the files named test_XXX.py. If you name your test files starting with test_ your tests will be automatically picked up by pytest when you run on the console in the directory where you cloned this repo:

$ python -m pytest

An example unit test could look like this:

from demo01_stopping import move
from pelita.utils import setup_test_game

def test_stays_there_builtin_random_layout():
    # Using a random builtin layout, verify that the bot stays on its initial position
    bot = setup_test_game(layout=None, is_blue=True)
    next_pos = move(bot, {})
    # check that we did not move
    assert next_pos == bot.position

The test first has to set up a game environment by using the utility function setup_test_game imported from the pelita.utils module before calling the move function of the loaded bot.

setup_test_game(layout, is_blue=True, round=None, score=None, seed=None, food=None, bots=None, is_noisy=None) ⟶ bot

Given a layout, it returns a Bot which you can pass to the move function.

The blue team's homezone is always on the left and the red team's homezone is always on the right. In the setup_test_game function you can pass is_blue which defines which side you are currently playing on.

The full documentation for the setup_test_game function can be read in its docstring:

$ python
>>> from pelita.utils import setup_test_game
>>> help(setup_test_game)
>>> ...

Layouts

When you play a game using the command pelita without specifying a specific layout with the --layout option, one of the built-in layouts will be picked at random. The name of the used layout is seen in the lower right corner of the GUI and it is also print to the terminal:

$ pelita
Replay this game with --seed 354761256309345545
Using layout 'normal_035'

When you run your own games or write tests, you may want to play a game on a fixed layout. You can do that by specifying the option --layout normal_035 to the pelita command, or passing layout="normal_035" to setup_test_game.

You may also want to specify very simple layouts to test some basic features of your bot that require you to know exactly where the walls, the enemies and the food are. In this case you can pass to setup_test_game a layout string. There are several examples of layout strings in the demo tests.

By inserting print(bot) within your move function, you can print the layout string corresponding to the current layout, together with other useful information. An example:

Basic Attacker Bots (you) vs Basic Defender Bots.
Playing on blue side. Current turn: 1. Bot: b. Round: 79, score: 11:15. timeouts: 0:0
################################
#     .  .   .      #    #     #
#  ##### ####  ##.  # .# # ### #
#     ..       ##   #  # #     #
#  #    .. # . ###.##  #    . .#
#  #.. ..  #. .     #. ###### ##
#. # .    .  .      #  #    b .#
# ######## # .   .  #        # #
# #    a   #  .     # ######## #
#.  .   #  #  .     x     . # .#
## ###### .#     . .#  ..   #  #
#. .    #  ##.###   #y..    #  #
#     # #  #   ##              #
# ### # #. #  .##  #### #####  #
#     #    #      .   .  .     #
################################
Bots: {'a': (7, 8), 'x': (20, 9), 'b': (28, 6), 'y': (21, 11)}
Noisy: {'a': False, 'x': True, 'b': False, 'y': True}
Food: [(4, 9), (14, 13), (10, 6), (1, 6), (1, 9), (13, 11), (4, 5), (14, 9), (5, 6),
(9, 1), (8, 5), (9, 4), (13, 4), (9, 13), (13, 1), (13, 7), (1, 11), (6, 1), (7, 3),
(12, 5), (14, 5), (3, 11), (14, 8), (5, 5), (8, 4), (10, 10), (13, 6), (7, 5),
(6, 3), (7, 8), (22, 2), (22, 11), (23, 10), (22, 14), (30, 6), (30, 9), (18, 4),
(17, 2), (24, 10), (21, 5), (23, 11), (28, 4), (17, 7), (30, 4), (17, 10), (19, 10),
(25, 14), (26, 9), (18, 14)]

The walls are identified by #, the food by ., the bots are a b for the blue team and x y for the red team. The exact coordinates of all food pellets, the bots and their noisy state are listed. More details about noise below.

You can create smaller mazes, which are easier to test with and can be typed directly into the tests. For example a maze 8x4 with the blue bots in (1, 1) and (1, 2), where the red bots are on (5,2) and (6,2) and food pellets in (2, 2) and (6,1), and an additional wall in (4,1) will look like this:

layout="""
########
#a # . #
#b.  xy#
########
"""

In case some objects are overlapping (for example you want to locate a red bot over a food pellet) you can pass a partial layout and specify the positions of the objects in a list of coordinates to setup_test_game. For example:

from pelita.utils import setup_test_game

def test_print_stuff():
    layout="""
    ########
    #a # . #
    #b.  xy#
    ########
    """
    bot = setup_test_game(layout=layout, food=[(1,1), (1,2), (5,2), (6,2)])
    print(bot)

Save this into a file test_test.py. When you run this test with python -m pytest -s test_test.py you get:

...

test_test.py blue (you) vs red.
Playing on blue side. Current turn: 0. Bot: a. Round: None, score: 0:0. timeouts: 0:0
########
#a # . #
#b.  xy#
########
Bots: {'a': (1, 1), 'x': (5, 2), 'b': (1, 2), 'y': (6, 2)}
Noisy: {'a': False, 'x': False, 'b': False, 'y': False}
Food: [(1, 1), (1, 2), (2, 2), (6, 2), (5, 1), (5, 2)]
...

Notice that we have to pass the option -s to pytest so that it shows what we print. By default pytest swallows everything that gets printed to standard output and standard error on the terminal.

Full API Description

The maze

The maze is a grid. Each square in the grid is defined by its coordinates. The default width of the maze is 32 squares, the default height is 16 squares. The coordinate system has the origin (0, 0) in the top left of the maze and its maximum value (31, 15) in the bottom right. Each square which is not a wall can be empty or contain a food pellet or one or more bots. The different mazes are called layouts. For the tournament all layouts will have the default values for width and height and will have a wall on all squares around the border.

The move function

move(bot, state) ⟶ (x, y)

The move function gets two input arguments:

The move function returns the position to move the bot to in the current turn. The position is a tuple of two integers (x, y), which are the coordinates on the game grid.

Note that the returned value must represent a legal position, i.e. it must be an adjacent position and you can not move your bot onto a wall or outside of the maze. If you return an illegal position, a legal position will be chosen at random instead and an error will be recorded for your team. After 5 errors the game is over and you lose the game.

The Bot object

Note that the Bot object is read-only, i.e. any modifications you make to that object within the move function will be discarded at the next round. Use the state dictionary for keeping track of information between rounds.

Running multiple games in the background

You may want to run multiple games in the background to gather statistics about your implementation, or to fit some parameters of your implementation. The script demo08_background_games.py is an example of this. You can run it like this:

python demo08_background_games.py

Debug using the GUI

When you push the debug button in the GUI you'll see a lot more info. You can see: