Pelita: learning by gaming

The pelita contest

Pelita is an "Actor-based Toolkit for Interactive Language Education in Python". The game was created for an Advanced Scientific Python summer school, and we are going to use it as well.

Have a look at their presentation from last year to get you in the mood.

The rules are simple: you work in a team, and send me your players before the contest (the exact date will be announced via OLAT). The last day of the lecture we will see the bots compete and let the best team win!

Objectives

The specific objectives of this exercise are:

  • to learn how to introspect and use a large codebase for your purpose: writing a player
  • to apply the concepts learned during the lecture: organize your code in clear modules and packages, use object oriented concepts, and write tests!
  • to define development strategies in a team
  • to have fun!

As you are going to see, it is possible to write simple to (very) complex strategies for your players. You should not try to develop the "best" strategy from scratch but rather advance in smaller steps. Also, you should know when to stop: I don't expect you to create the best players ever, nor do I expect you to spend all your time on this. Try to avoid bugs, try to develop simple but clever players, and have fun!

Your grade (10% of the final grade) will not be based on the outcome of the contest: it will be based on code quality, clarity, and originality.

The Pelita developers write it this way:

We expect you to try out the techniques, and evaluate what is feasible and what not, what helps you being more efficient at writing reliable code, and what feels like a hindrance instead. By doing this in the group we expect you to profit from the experience of other students, and, by explaining to other students your own experiences, to become more aware of what is it that you already master and what is it that you still have to learn.

Write tests for the part of your code which are testable, decide what parts you can test, what parts you should test, what parts you must test, and also what parts can not be tested.

The idea of the group project is not to write the most powered bots! Remember that and don't get carried away by the competition :)

Getting started

Download the pelita package from the official repository. Unzip the package and install it with pip install -e /path/to/package (don't forget the --user if you are on a university computer).

run your first game with a simple call to $ pelita!

To start to write your own players, download the template available here. Unpack it and follow the instructions:

  • run $ pytest to dummy-test the template.
  • run $ pelita myteam demo_opponents/PolitePlayers.py for a quick game against a demo team

For more control options, type pelita --help.

Gaining speed

The best way to get started is to have a look at all demo players in the package template. They will guide you through the basic functionalities from which you can build upon.

The documentation about writing a player is a good second step.

All this, however, might still leave you a little confused: take your time and don't rush into coding right away. The test environment available in the template package and therefore runnable in a debugger from spyder or pycharm is probably the best way to explore the tools available to you.

Let's do a little bit of exploration ourselves

Write two simple players

In [1]:
from pelita import datamodel
from pelita.graph import Graph, NoPathException, diff_pos
from pelita.player import AbstractPlayer


class FastEatingPlayer(AbstractPlayer):
    """Like SmartEatingPlayer but without taking enemies into 
    account and seeking the closest food (always)."""

    def set_initial(self):

        # This is called only once \t the beginning of the game
        # A graph is useful to help you find your way in the maze
        self.graph = Graph(self.current_uni.reachable([self.initial_pos]))

    def goto_pos(self, pos):

        # Next move to go to the desired position(s)
        return self.graph.bfs(self.current_pos, pos)[-1]

    def get_move(self):

        # Check the move towards the closest enemy food
        try:
            next_pos = self.goto_pos(self.enemy_food)
            move = diff_pos(self.current_pos, next_pos)
        except NoPathException:
            move = datamodel.stop

        # Check that it is one of all possible moves, else random
        if move in self.legal_moves:
            return move
        else:
            return self.rnd.choice(list(self.legal_moves.keys()))


class StoppingPlayer(AbstractPlayer):
    def get_move(self):
        return datamodel.stop

Define a maze layout

Mazes can be provided by the user using a simple ascii layout. Walls are represented by #, food pellets with dots, and players by their number (0 and 2 belong to team 1, 1 and 3 to team 2). Here I defined a maze made to test the behavior of the players of team 1 (left hand side), and added one food pellet on the left hand side too (this is needed for the game to be valid):

In [2]:
test_layout = (
    """ ##############################################################
        #0                                   .                      1#
        #                                    .                       #
        #                                    .                       #
        #                                    .                       #
        #      .                             .                       #
        #                                    .                       #
        #                                    .                       #
        #                                    ........................#
        #2                                                          3#
        ##############################################################
     """
)

And a team

In [3]:
from pelita.game_master import GameMaster
from pelita.player import SimpleTeam

# Make the team
player_0 = FastEatingPlayer()
player_1 = StoppingPlayer()
player_2 = StoppingPlayer()
player_3 = StoppingPlayer()
teams = [
    SimpleTeam('One', player_0, player_2),
    SimpleTeam('Two', player_1, player_3)
]

# Start the game
game_master = GameMaster(test_layout, teams, 4, 300)
game_master.set_initial()

Explore

Now we are ready to see what is happening. Lets see the state of the universe before and after a round:

In [4]:
print(game_master.universe.pretty)
##############################################################
#0                                   .                      1#
#                                    .                       #
#                                    .                       #
#                                    .                       #
#      .                             .                       #
#                                    .                       #
#                                    .                       #
#                                    ........................#
#2                                                          3#
##############################################################
Team(index=0, zone=(0, 30), score=0)
	Bot(index=0, initial_pos=(1, 1), team_index=0, homezone=(0, 30), current_pos=(1, 1), noisy=False)
	Bot(index=2, initial_pos=(1, 9), team_index=0, homezone=(0, 30), current_pos=(1, 9), noisy=False)
Team(index=1, zone=(31, 61), score=0)
	Bot(index=1, initial_pos=(60, 1), team_index=1, homezone=(31, 61), current_pos=(60, 1), noisy=False)
	Bot(index=3, initial_pos=(60, 9), team_index=1, homezone=(31, 61), current_pos=(60, 9), noisy=False)

In [5]:
game_master.play_round()
In [6]:
print(game_master.universe.pretty)
##############################################################
# 0                                  .                      1#
#                                    .                       #
#                                    .                       #
#                                    .                       #
#      .                             .                       #
#                                    .                       #
#                                    .                       #
#                                    ........................#
#2                                                          3#
##############################################################
Team(index=0, zone=(0, 30), score=0)
	Bot(index=0, initial_pos=(1, 1), team_index=0, homezone=(0, 30), current_pos=(2, 1), noisy=False)
	Bot(index=2, initial_pos=(1, 9), team_index=0, homezone=(0, 30), current_pos=(1, 9), noisy=False)
Team(index=1, zone=(31, 61), score=0)
	Bot(index=1, initial_pos=(60, 1), team_index=1, homezone=(31, 61), current_pos=(60, 1), noisy=False)
	Bot(index=3, initial_pos=(60, 9), team_index=1, homezone=(31, 61), current_pos=(60, 9), noisy=False)

Our player moved one step. What exacly is our player seeing from here?

For example, the player sees the locations of the enemy food:

In [7]:
print(player_0.enemy_food)
[(38, 8), (43, 8), (37, 7), (48, 8), (40, 8), (54, 8), (59, 8), (39, 8), (37, 2), (46, 8), (44, 8), (51, 8), (49, 8), (56, 8), (37, 6), (41, 8), (55, 8), (60, 8), (47, 8), (45, 8), (52, 8), (50, 8), (37, 1), (57, 8), (37, 8), (42, 8), (37, 5), (53, 8), (58, 8), (37, 4), (37, 3)]

The player also knows the (noisy) position of the enemy bots:

In [8]:
player_0.enemy_bots
Out[8]:
[Bot(index=1, initial_pos=(60, 1), team_index=1, homezone=(31, 61), current_pos=(60, 3), noisy=True),
 Bot(index=3, initial_pos=(60, 9), team_index=1, homezone=(31, 61), current_pos=(59, 6), noisy=True)]
In [9]:
player_0.enemy_bots[0].current_pos
Out[9]:
(60, 3)

You can verify that it is noisy by checking that on the next round the positions might change although the players didn't move:

In [10]:
game_master.play_round()
In [11]:
player_0.enemy_bots
Out[11]:
[Bot(index=1, initial_pos=(60, 1), team_index=1, homezone=(31, 61), current_pos=(58, 2), noisy=True),
 Bot(index=3, initial_pos=(60, 9), team_index=1, homezone=(31, 61), current_pos=(57, 9), noisy=True)]
In [12]:
print(game_master.universe.pretty)
##############################################################
#  0                                 .                      1#
#                                    .                       #
#                                    .                       #
#                                    .                       #
#      .                             .                       #
#                                    .                       #
#                                    .                       #
#                                    ........................#
#2                                                          3#
##############################################################
Team(index=0, zone=(0, 30), score=0)
	Bot(index=0, initial_pos=(1, 1), team_index=0, homezone=(0, 30), current_pos=(3, 1), noisy=False)
	Bot(index=2, initial_pos=(1, 9), team_index=0, homezone=(0, 30), current_pos=(1, 9), noisy=False)
Team(index=1, zone=(31, 61), score=0)
	Bot(index=1, initial_pos=(60, 1), team_index=1, homezone=(31, 61), current_pos=(60, 1), noisy=False)
	Bot(index=3, initial_pos=(60, 9), team_index=1, homezone=(31, 61), current_pos=(60, 9), noisy=False)

What is the graph attribute actually useful for? Let's find out:

In [13]:
path = player_0.graph.bfs(player_0.current_pos, player_0.enemy_food)
print(path)
[(37, 1), (36, 1), (35, 1), (34, 1), (33, 1), (32, 1), (31, 1), (30, 1), (29, 1), (28, 1), (27, 1), (26, 1), (25, 1), (24, 1), (23, 1), (22, 1), (21, 1), (20, 1), (19, 1), (18, 1), (17, 1), (16, 1), (15, 1), (14, 1), (13, 1), (12, 1), (11, 1), (10, 1), (9, 1), (8, 1), (7, 1), (6, 1), (5, 1), (4, 1), (3, 1)]

This gives us the shortest path towards all enemy food! Taking the last position gives us the next position we'd like to go:

In [14]:
next_pos = path[-1]
move = diff_pos(player_0.current_pos, next_pos)
move
Out[14]:
(1, 0)
In [15]:
# east, west, north, south are nothing more than direction tuples
assert move == datamodel.east
# shoule be a legal move
assert move in player_0.legal_moves

A more complex maze

In [16]:
test_layout = (
    """ ##############################################################
        #0                 ##                .                      1#
        #                  ## ##             .                       #
        #                   #  #             .                       #
        #                   #  #             .                       #
        #      .            #  #             .                       #
        #                   #  #             .                       #
        #                   #  #             .                       #
        #                   #  #             ........................#
        #2                     #                                    3#
        ##############################################################
     """
)
In [17]:
# Start the game
game_master = GameMaster(test_layout, teams, 4, 300)
universe = game_master.universe
game_master.set_initial()

Will our bot find its way through it?

In [18]:
for _ in range(30):
    game_master.play_round()
In [19]:
print(game_master.universe.pretty)
##############################################################
#                  ##                .                      1#
#                  ## ##             .                       #
#                   #  #             .                       #
#                   #  #             .                       #
#      .            #  #             .                       #
#                   #  #             .                       #
#                   #0 #             .                       #
#                   #  #             ........................#
#2                     #                                    3#
##############################################################
Team(index=0, zone=(0, 30), score=0)
	Bot(index=0, initial_pos=(1, 1), team_index=0, homezone=(0, 30), current_pos=(21, 7), noisy=False)
	Bot(index=2, initial_pos=(1, 9), team_index=0, homezone=(0, 30), current_pos=(1, 9), noisy=False)
Team(index=1, zone=(31, 61), score=0)
	Bot(index=1, initial_pos=(60, 1), team_index=1, homezone=(31, 61), current_pos=(60, 1), noisy=False)
	Bot(index=3, initial_pos=(60, 9), team_index=1, homezone=(31, 61), current_pos=(60, 9), noisy=False)

Looks like it! Let the game run until the end:

In [20]:
game_master.play()
In [21]:
print(game_master.universe.pretty)
##############################################################
#                  ##                                       1#
#                  ## ##                                     #
#                   #  #                                     #
#                   #  #                                     #
#      .            #  #                                     #
#                   #  #                                     #
#                   #  #                                     #
#                   #  #                                    0#
#2                     #                                    3#
##############################################################
Team(index=0, zone=(0, 30), score=31)
	Bot(index=0, initial_pos=(1, 1), team_index=0, homezone=(0, 30), current_pos=(60, 8), noisy=False)
	Bot(index=2, initial_pos=(1, 9), team_index=0, homezone=(0, 30), current_pos=(1, 9), noisy=False)
Team(index=1, zone=(31, 61), score=0)
	Bot(index=1, initial_pos=(60, 1), team_index=1, homezone=(31, 61), current_pos=(60, 1), noisy=False)
	Bot(index=3, initial_pos=(60, 9), team_index=1, homezone=(31, 61), current_pos=(60, 9), noisy=False)

A more aggressive player

Let's make the opponent team a little more active:

In [22]:
from pelita.graph import manhattan_dist
import numpy as np

class AggressivePlayer(AbstractPlayer):

    def set_initial(self):
        self.graph = Graph(self.current_uni.reachable([self.initial_pos]))

    def goto_pos(self, pos):
        return self.graph.a_star(self.current_pos, pos)[-1]

    def get_move(self):
        
        # Take the closest enemy
        dis = []
        for enemy in self.enemy_bots:
            dis.append(manhattan_dist(self.current_pos, enemy.current_pos))
        enemy = self.enemy_bots[np.argmin(dis)]

        try:
            next_pos = self.goto_pos(enemy.current_pos)
            # Check if the next_pos is on the wrong side of the maze
            if not self.team.in_zone(next_pos):
                # whoops, better not move
                move = datamodel.stop
            else:
                move = diff_pos(self.current_pos, next_pos)
            # Check that it is one of all possible moves, else stop
            if move in self.legal_moves:
                return move
            else:
                return datamodel.stop
        except NoPathException:
            return datamodel.stop
In [23]:
# Make the team
player_0 = FastEatingPlayer()
player_1 = StoppingPlayer()
player_2 = StoppingPlayer()
player_3 = AggressivePlayer()
teams = [
    SimpleTeam('One', player_0, player_2),
    SimpleTeam('Two', player_1, player_3)
]

# Start the game
game_master = GameMaster(test_layout, teams, 4, 300)
universe = game_master.universe
game_master.set_initial()

But stalking enemies is dangerous! See what happens in this game:

In [24]:
for _ in range(40):
    game_master.play_round()
print(game_master.universe.pretty)
##############################################################
#                  ##    0           .                      1#
#                  ## ##             .                       #
#                   #  #             .                       #
#                   #  #             .                       #
#      .            #  #             .                       #
#                   #  #             .                       #
#                   #  #             .                       #
#                   #  #             ........................#
#2                     #       3                             #
##############################################################
Team(index=0, zone=(0, 30), score=0)
	Bot(index=0, initial_pos=(1, 1), team_index=0, homezone=(0, 30), current_pos=(25, 1), noisy=False)
	Bot(index=2, initial_pos=(1, 9), team_index=0, homezone=(0, 30), current_pos=(1, 9), noisy=False)
Team(index=1, zone=(31, 61), score=0)
	Bot(index=1, initial_pos=(60, 1), team_index=1, homezone=(31, 61), current_pos=(60, 1), noisy=False)
	Bot(index=3, initial_pos=(60, 9), team_index=1, homezone=(31, 61), current_pos=(31, 9), noisy=False)

In [25]:
for _ in range(8):
    game_master.play_round()
print(game_master.universe.pretty)
##############################################################
#                  ##            0   .                      1#
#                  ## ##             .                       #
#                   #  #             .                       #
#                   #  #             .                       #
#      .            #  #             .                       #
#                   #  #       3     .                       #
#                   #  #             .                       #
#                   #  #             ........................#
#2                     #                                     #
##############################################################
Team(index=0, zone=(0, 30), score=0)
	Bot(index=0, initial_pos=(1, 1), team_index=0, homezone=(0, 30), current_pos=(33, 1), noisy=False)
	Bot(index=2, initial_pos=(1, 9), team_index=0, homezone=(0, 30), current_pos=(1, 9), noisy=False)
Team(index=1, zone=(31, 61), score=0)
	Bot(index=1, initial_pos=(60, 1), team_index=1, homezone=(31, 61), current_pos=(60, 1), noisy=False)
	Bot(index=3, initial_pos=(60, 9), team_index=1, homezone=(31, 61), current_pos=(31, 6), noisy=False)

In [26]:
for _ in range(8):
    game_master.play_round()
print(game_master.universe.pretty)
##############################################################
#                  ##                                       1#
#                  ## ##                                     #
#                   #  #                                     #
#                   #  #                                     #
#      .            #  #        3    0                       #
#                   #  #             .                       #
#                   #  #             .                       #
#                   #  #             ........................#
#2                     #                                     #
##############################################################
Team(index=0, zone=(0, 30), score=5)
	Bot(index=0, initial_pos=(1, 1), team_index=0, homezone=(0, 30), current_pos=(37, 5), noisy=False)
	Bot(index=2, initial_pos=(1, 9), team_index=0, homezone=(0, 30), current_pos=(1, 9), noisy=False)
Team(index=1, zone=(31, 61), score=0)
	Bot(index=1, initial_pos=(60, 1), team_index=1, homezone=(31, 61), current_pos=(60, 1), noisy=False)
	Bot(index=3, initial_pos=(60, 9), team_index=1, homezone=(31, 61), current_pos=(32, 5), noisy=False)

The bot will never be able to catch it! Having completely deterministic moves might be contra-productive sometimes.

What's next?

Now it's your turn! Try to use these players in real games to visualize the outcome, and start to define new strategies!

Here some further tips:

  • Mazes don’t have dead-ends
  • It's hard to catch another bot which outruns you: bots can combine their powers and attack from two sides
  • Think about shortest-path algorithms
  • Keep track of opponents
  • Investigate communication between the Players
  • Re-use your code
  • Think about working in a team

Back to the table of contents.