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.

Download and have a look at the 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

Your final grade will not be based on the outcome of the contest: it will be based on code quality, clarity, and originality. 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!

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 write your own players, download the template available here. Unpack it and follow the instructions:

  • run $ make test to test the template.
  • run $ make for a quick game against a random team

For more control, use pelita as indicated by its documentation. For example, to run a game against a predefined team:

$ pelita team/ SmartEatingPlayer

To run a game against yourself:

$ pelita team/ team/

Gaining speed

The best way to get started is to have a look at the documentation about writing a player and running & debugging.

You can test different simple strategies by having a look at the demo players that you will find in the pelita/pelita/player/ folder.

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 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 add 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]:
player_0.enemy_food
Out[7]:
[(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=(57, 1), noisy=True),
 Bot(index=3, initial_pos=(60, 9), team_index=1, homezone=(31, 61), current_pos=(60, 9), noisy=True)]

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 [9]:
game_master.play_round()
In [10]:
player_0.enemy_bots
Out[10]:
[Bot(index=1, initial_pos=(60, 1), team_index=1, homezone=(31, 61), current_pos=(58, 1), noisy=True),
 Bot(index=3, initial_pos=(60, 9), team_index=1, homezone=(31, 61), current_pos=(59, 8), noisy=True)]
In [11]:
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 actually useful for? Let's find out:

In [12]:
path = player_0.graph.bfs(player_0.current_pos, player_0.enemy_food)
path
Out[12]:
[(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 one gives us the next position we'd like to go:

In [13]:
next_pos = path[-1]
move = diff_pos(player_0.current_pos, next_pos)
move
Out[13]:
(1, 0)
In [14]:
# 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 [15]:
test_layout = (
    """ ##############################################################
        #0                 ##                .                      1#
        #                  ## ##             .                       #
        #                   #  #             .                       #
        #                   #  #             .                       #
        #      .            #  #             .                       #
        #                   #  #             .                       #
        #                   #  #             .                       #
        #                   #  #             ........................#
        #2                     #                                    3#
        ##############################################################
     """
)
In [16]:
# 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 [17]:
for _ in range(30):
    game_master.play_round()
In [18]:
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 [19]:
game_master.play()
In [20]:
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 [21]:
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 [22]:
# 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 [23]:
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 [24]:
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 [25]:
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=(31, 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! The game designers write in their presentation:

  • 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.