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!
The specific objectives of this exercise are:
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 :)
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:
$ pytest
to dummy-test the template.$ pelita myteam demo_opponents/PolitePlayers.py
for a quick game against a demo teamFor more control options, type pelita --help
.
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.
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
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):
test_layout = (
""" ##############################################################
#0 . 1#
# . #
# . #
# . #
# . . #
# . #
# . #
# ........................#
#2 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()
Now we are ready to see what is happening. Lets see the state of the universe before and after a round:
print(game_master.universe.pretty)
game_master.play_round()
print(game_master.universe.pretty)
Our player moved one step. What exacly is our player seeing from here?
For example, the player sees the locations of the enemy food:
print(player_0.enemy_food)
The player also knows the (noisy) position of the enemy bots:
player_0.enemy_bots
player_0.enemy_bots[0].current_pos
You can verify that it is noisy by checking that on the next round the positions might change although the players didn't move:
game_master.play_round()
player_0.enemy_bots
print(game_master.universe.pretty)
What is the graph
attribute actually useful for? Let's find out:
path = player_0.graph.bfs(player_0.current_pos, player_0.enemy_food)
print(path)
This gives us the shortest path towards all enemy food! Taking the last position gives us the next position we'd like to go:
next_pos = path[-1]
move = diff_pos(player_0.current_pos, next_pos)
move
# 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
test_layout = (
""" ##############################################################
#0 ## . 1#
# ## ## . #
# # # . #
# # # . #
# . # # . #
# # # . #
# # # . #
# # # ........................#
#2 # 3#
##############################################################
"""
)
# 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?
for _ in range(30):
game_master.play_round()
print(game_master.universe.pretty)
Looks like it! Let the game run until the end:
game_master.play()
print(game_master.universe.pretty)
Let's make the opponent team a little more active:
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
# 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:
for _ in range(40):
game_master.play_round()
print(game_master.universe.pretty)
for _ in range(8):
game_master.play_round()
print(game_master.universe.pretty)
for _ in range(8):
game_master.play_round()
print(game_master.universe.pretty)
The bot will never be able to catch it! Having completely deterministic moves might be contra-productive sometimes.
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:
Back to the table of contents.