Tutorial: Your First Agent

In this tutorial, we'll walk you through the process of creating your first bot in Python.

This tutorial is out of date! It will be updated once the game releases are more stable.

🤖 Level 0: Random Agent

Step 1: Starter Kit

Start by downloading the Python3 Starter Kit from:

Inside the python3 folder you'll see a file called agent.py. Open this up and you should see something similar to the below:

Python
Python
agent.py
from game_state import GameState
import asyncio
import random
import os
uri = os.environ.get(
'GAME_CONNECTION_STRING') or "ws://127.0.0.1:3000/?role=agent&agentId=agentId&name=defaultName"
actions = ["up", "down", "left", "right", "bomb", "detonate"]
class Agent():
def __init__(self):
self._client = GameState(uri)
self._client.set_game_tick_callback(self._on_game_tick)
loop = asyncio.get_event_loop()
connection = loop.run_until_complete(self._client.connect())
tasks = [
asyncio.ensure_future(self._client._handle_messages(connection)),
]
loop.run_until_complete(asyncio.wait(tasks))
# returns location of a placed bomb
def _get_bomb_to_detonate(self, game_state) -> [int, int] or None:
agent_number = game_state.get("connection").get("agent_number")
entities = self._client._state.get("entities")
bombs = list(filter(lambda entity: entity.get(
"owner") == agent_number and entity.get("type") == "b", entities))
bomb = next(iter(bombs or []), None)
if bomb != None:
return [bomb.get("x"), bomb.get("y")]
else:
return None
# sends a packet containing your agent's move to the game server
async def _on_game_tick(self, tick_number, game_state):
random_action = self.generate_random_action()
if random_action in ["up", "left", "right", "down"]:
await self._client.send_move(random_action)
elif random_action == "bomb":
await self._client.send_bomb()
elif random_action == "detonate":
bomb_coordinates = self._get_bomb_to_detonate(game_state)
if bomb_coordinates != None:
x, y = bomb_coordinates
await self._client.send_detonate(x, y)
else:
print(f"Unhandled action: {random_action}")
def generate_random_action(self):
actions_length = len(actions)
return actions[random.randint(0, actions_length - 1)]
def main():
Agent()
if __name__ == "__main__":
main()

Step 2: Sending actions

By default, this starter kit contains an agent that will make its moves at random.

At a basic level, the function _on_game_tick controls the behaviour of our agent and returns a packet to the game server containing its action. In our starter kit, you can use the calls send_move, send_bomb or send_detonate to send valid actions to the game server as shown below:

# sends a random action to the game server
async def _on_game_tick(self, tick_number, game_state):
random_action = self.generate_random_action()
if random_action in ["up", "left", "right", "down"]:
await self._client.send_move(random_action)
elif random_action == "bomb":
await self._client.send_bomb()
elif random_action == "detonate":
bomb_coordinates = self._get_bomb_to_detonate(game_state)
if bomb_coordinates != None:
x, y = bomb_coordinates
await self._client.send_detonate(x, y)
else:
print(f"Unhandled action: {random_action}")

Feel free to try and play around with this agent (e.g. try modifying _on_game_tick so that your agent only moves up, down, left or right).

Step 3: Build and run

Open docker-compose.yml. We have included a second agent in here, which can automatically connect as Player 1. To do this, you will need to un-comment this section:

# agent-a:
# extends:
# file: base-compose.yml
# # update next line with a service in base-compose.yml to change agent
# service: python3-agent-dev
# environment:
# - GAME_CONNECTION_STRING=ws://game-server:3000/?role=agent&agentId=agentA&name=python3-agent
# - FWD_MODEL_CONNECTION_STRING=ws://fwd-server-a:6969/?role=admin
# depends_on:
# - game-server
# - fwd-server-a
# networks:
# - coderone-tournament

By default, docker-compose.yml is pointing to a TypeScript starter agent. Change the line service: typescript-agent-dev to service: python3-agent-dev. This will connect your random agent as Player 1.

Now just as you did in the Quick Start Guide, head over to your terminal and run:

docker-compose up --abort-on-container-exit

This will build and run the game server, and connect your agent as both Player 1 and Player 2.

If this doesn't work, try instead: docker-compose up --abort-on-container-exit --build

Step 4: Spectate!

Head over to the GUI using the link below:

From the menu, select the 'Spectator' role and click connect.

You are now watching your Agent play against itself! 🚀

🥉 Level 1: Wandering Agent

Estimated time: 1 hr

To get a better understanding of how to interact with the game environment, we're going to create a simple bot called wanderer that walks aimlessly around the map. To be more specific, it:

  1. Looks at the tiles around it

  2. Checks which ones are empty

  3. Chooses a random empty tile to move to

Step 1: Understand the game state

In the previous section, you may have noticed that the _on_game_tick function received two inputs: tick_number and game_state.

tick_number refers to the progression of the game. We'll only worry about game_state for now, which contains information in a JSON format about the game environment. Below is a sample game_state:

{
"agent_state":{
"0":{
"coordinates":[
6,
7
],
"hp":3,
"inventory":{
"bombs":3
},
"blast_diameter":3,
"number":0,
"invulnerability":0
},
"1":{
"coordinates":[
6,
6
],
"hp":3,
"inventory":{
"bombs":3
},
"blast_diameter":3,
"number":1,
"invulnerability":0
}
},
"entities":[
{
"x":8,
"y":4,
"type":"m"
},
{
"x":2,
"y":5,
"type":"m"
},
...
{
"x":4,
"y":7,
"type":"o"
}
],
"world":{
"width":9,
"height":9
},
"connection":{
"id":7,
"role":"agent",
"agent_number":0
},
"tick":0,
"config":{
"tick_rate_hz":20,
"game_duration_ticks":10,
"fire_spawn_interval_ticks":5
}
}

A full description of all the properties is available here.

Below are some examples of use cases given the sample game_state above. Have a go at filling in the code yourself, and checking them against the answers.

Example 1
⚡ Answer
Example 1
# get your Agent's current location assuming you are Player 1
# note: Player 1 has id=0, and Player 2 has id=1
my_agents_location = pass ### CHANGE THIS
⚡ Answer
# get your Agent's current location assuming you are Player 1
# note: Player 1 has id=0, and Player 2 has id=1
my_agents_location = game_state["agent_state"]["0"]["coordinates"]
# returns [6, 7], i.e. [x,y] coordinates
Example 2
⚡ Answer
Example 2
# get the width and height of the Game Map
width = pass ### CHANGE THIS
height = pass ### CHANGE THIS
⚡ Answer
# get the width and height of the Game Map
width = game_state["world"]["width"] # returns 9
height = game_state["world"]["height"] # returns 9
Example 3
⚡ Answer
Example 3
# this function returns the object tag at a given an x,y location
def entity_at(x,y,game_state):
for i in game_state["entities"]:
if i["x"] == x and i["y"] == y:
return i["type"]
# return the entity located at coordinate (8,4)
entity_at_8_4 = pass ### CHANGE THIS
⚡ Answer
# this function returns the object tag at a given an x,y location
def entity_at(x,y,game_state):
for i in game_state["entities"]:
if i["x"] == x and i["y"] == y:
return i["type"]
# return the entity located at coordinate (8,4)
entity_at_8_4 = entity_at(8,4) # returns "m" for "metal block"

Step 2: Understand surrounding environment

We're going to define some helper functions that will help our Wanderer Agent process information from the game environment.

Start by creating a new file in the same working directory as agent.py called helpers.py. We'll add all our helper functions here.

Next, in order for our agent to know where it can go, it will need to know what its immediate surroundings are (to the north, south, east and west of it). Our first helper function called get_surrounding_tiles will return us a list of these surrounding tile positions, as an (x,y) tuple of the game map.

To do this, we'll take advantage of the coordinate-representation of the map:

Below is the skeleton code for our get_surrounding_tiles helper function. We've left you some gaps to fill out. If you get stuck, check the 🙈 Spoiler code.

Skeleton
🙈 Spoiler
Skeleton
# given a tile location as an (x,y) tuple
# return the surrounding tiles as a list
def get_surrounding_tiles(location):
# find all the surrounding tiles relative to us
# location[0] = x-index; location[1] = y-index
tile_up = (location[0], location[1]+1)
tile_down = None ###### CHANGE THIS ######
tile_left = None ###### CHANGE THIS ######
tile_right = (location[0]+1, location[1])
# combine these into a list
all_surrounding_tiles = [tile_up, tile_down, tile_left, tile_right]
# include only the tiles that are within the game boundary
# empty list to store our valid surrounding tiles
valid_surrounding_tiles = []
# loop through tiles
for tile in all_surrounding_tiles:
# check if the tile is within the boundaries of the game
# note: the map is size 9x9
if None: ####### CHANGE NONE ############
# add valid tiles to our list
valid_surrounding_tiles.append(tile)
return valid_surrounding_tiles
🙈 Spoiler
# given a tile location as an (x,y) tuple
# return the surrounding tiles as a list
def get_surrounding_tiles(location):
# find all the surrounding tiles relative to us
# location[0] = x-index; location[1] = y-index
tile_up = (location[0], location[1]+1)
tile_down = (location[0], location[1]-1)
tile_left = (location[0]-1, location[1])
tile_right = (location[0]+1, location[1])
# combine these into a list
all_surrounding_tiles = [tile_up, tile_down, tile_left, tile_right]
# include only the tiles that are within the game boundary
# empty list to store our valid surrounding tiles
valid_surrounding_tiles = []
# loop through tiles
for tile in all_surrounding_tiles:
# check if the tile is within the boundaries of the game
# note: the game is size 9x9
if tile[0] >= 0 and tile[0] < 9 and \
tile[1] >= 0 and tile[1] < 9:
# add valid tiles to our list
valid_surrounding_tiles.append(tile)
return valid_surrounding_tiles

Nice work! 🙌

In order for our bot to move effectively, it will also need to know which of these tiles are actually empty (i.e. not containing a block, or other player). The get_empty_tiles function will return us the locations of tiles near us where our agent is freely able to move. We've provided you with the entity_at function.

Skeleton
🙈 Spoiler
Skeleton
helpers.py
# this function returns the object tag at a location
def entity_at(x,y,game_state):
for entity in game_state["entities"]:
if entity["x"] == x and entity["y"] == y:
return entity["type"]
# given a list of tiles
# return the ones which are actually empty
def get_empty_tiles(tiles,game_state):
# empty list to store our empty tiles
empty_tiles = []
for tile in tiles:
if None: ################ CHANGE 'NONE' ###################
# the tile isn't occupied, so we'll add it to the list
empty_tiles.append(tile)
return empty_tiles
🙈 Spoiler
helpers.py
# this function returns the object tag at a location
def entity_at(x,y,game_state):
for entity in game_state["entities"]:
if entity["x"] == x and entity["y"] == y:
return entity["type"]
# given a list of tiles
# return the ones which are actually empty
def get_empty_tiles(tiles,game_state):
# empty list to store our empty tiles
empty_tiles = []
for tile in tiles:
if entity_at(tile[0],tile[1],game_state) is None:
# the tile isn't occupied, so we'll add it to the list
empty_tiles.append(tile)
return empty_tiles

Our final helper function is move_to_tile which given an adjacent surrounding tile and our current location, will return the action (i.e. up, down, etc) that will get us there. E.g. if the tile we want to move to is directly ABOVE us, this function will return up ⬆️.

Skeleton
🙈 Spoiler
Skeleton
helpers.py
# given an adjacent tile location, move us there
def move_to_tile(location, tile):
# see where the tile is relative to our current location
diff = tuple(x-y for x, y in zip(location,tile))
# return the action that moves in the direction of the tile
if diff == (0,1):
action = 'down'
elif diff == (0,-1):
action = None ################ FILL THIS ###################
elif diff == (1,0):
action = None ################ FILL THIS ###################
elif diff == (-1,0):
action = 'right'
else:
action = ''
return action
🙈 Spoiler
helpers.py
# given an adjacent tile location, move us there
def move_to_tile(location, tile):
# see where the tile is relative to our current location
diff = tuple(x-y for x, y in zip(location, tile))
# return the action that moves in the direction of the tile
if diff == (0,1):
action = 'down'
elif diff == (0,-1):
action = 'up'
elif diff == (1,0):
action = 'left'
elif diff == (-1,0):
action = 'right'
else:
action = ''
return action

Step 3: Bring it all together!

Hang in there - we're almost ready to watch our new bot play!

We've provided the skeleton code below to help you piece together your new bot.

Skeleton - agent.py
🙈 Spoiler - agent.py
🙈 Spoiler - helpers.py
Skeleton - agent.py
from game_state import GameState
import asyncio
import random
import os
import helpers # import our helper functions
uri = os.environ.get(
'GAME_CONNECTION_STRING') or "ws://127.0.0.1:3000/?role=agent&agentId=agentId&name=myAgent"
actions = ["up", "down", "left", "right"]
class Agent():
def __init__(self):
self._client = GameState(uri)
self._client.set_game_tick_callback(self._on_game_tick)
loop = asyncio.get_event_loop()
connection = loop.run_until_complete(self._client.connect())
tasks = [
asyncio.ensure_future(self._client._handle_messages(connection)),
]
loop.run_until_complete(asyncio.wait(tasks))
def _get_bomb_to_detonate(self, game_state) -> [int, int] or None:
agent_number = game_state.get("connection").get("agent_number")
entities = self._client._state.get("entities")
bombs = list(filter(lambda entity: entity.get(
"owner") == agent_number and entity.get("type") == "b", entities))
bomb = next(iter(bombs or []), None)
if bomb != None:
return [bomb.get("x"), bomb.get("y")]
else:
return None
async def _on_game_tick(self, tick_number, game_state):
########################
### VARIABLES ###
########################
my_id = str(game_state["connection"]["agent_number"])
my_location = game_state["agent_state"][my_id]["coordinates"]
ammo = game_state["agent_state"][my_id]["inventory"]["bombs"]
########################
### AGENT ###
########################
# get our surrounding tiles
surrounding_tiles = helpers.get_surrounding_tiles(my_location)
# get list of empty tiles around us
empty_tiles = None ######### CHANGE THIS #########
if empty_tiles:
# choose an empty tile to walk to
random_tile = random.choice(empty_tiles)
action = None ######### CHANGE THIS #########
else:
# we're trapped, do nothing
action = random.choice(actions)
# logic to send valid action packet to game server
if action in ["up", "left", "right", "down"]:
await self._client.send_move(action)
elif action == "bomb":
await self._client.send_bomb()
elif action == "detonate":
bomb_coordinates = self._get_bomb_to_detonate(game_state)
if bomb_coordinates != None:
x, y = bomb_coordinates
await self._client.send_detonate(x, y)
else:
print(f"Unhandled action: {action}")
def main():
Agent()
if __name__ == "__main__":
main()
🙈 Spoiler - agent.py
from game_state import GameState
import asyncio
import random
import os
import helpers # import our helper functions
uri = os.environ.get(
'GAME_CONNECTION_STRING') or "ws://127.0.0.1:3000/?role=agent&agentId=agentId&name=myAgent"
actions = ["up", "down", "left", "right"]
class Agent():
def __init__(self):
self._client = GameState(uri)
self._client.set_game_tick_callback(self._on_game_tick)
loop = asyncio.get_event_loop()
connection = loop.run_until_complete(self._client.connect())
tasks = [
asyncio.ensure_future(self._client._handle_messages(connection)),
]
loop.run_until_complete(asyncio.wait(tasks))
def _get_bomb_to_detonate(self, game_state) -> [int, int] or None:
agent_number = game_state.get("connection").get("agent_number")
entities = self._client._state.get("entities")
bombs = list(filter(lambda entity: entity.get(
"owner") == agent_number and entity.get("type") == "b", entities))
bomb = next(iter(bombs or []), None)
if bomb != None:
return [bomb.get("x"), bomb.get("y")]
else:
return None
async def _on_game_tick(self, tick_number, game_state):
########################
### VARIABLES ###
########################
my_id = str(game_state["connection"]["agent_number"])
my_location = game_state["agent_state"][my_id]["coordinates"]
ammo = game_state["agent_state"][my_id]["inventory"]["bombs"]
########################
### AGENT ###
########################
# get our surrounding tiles
surrounding_tiles = helpers.get_surrounding_tiles(my_location)
# get list of empty tiles around us
empty_tiles = helpers.get_empty_tiles(surrounding_tiles,game_state)
if empty_tiles:
# choose an empty tile to walk to
random_tile = random.choice(empty_tiles)
action = helpers.move_to_tile(my_location,random_tile)
else:
# we're trapped, do nothing
action = random.choice(actions)
# logic to send valid action packet to game server
if action in ["up", "left", "right", "down"]:
await self._client.send_move(action)
elif action == "bomb":
await self._client.send_bomb()
elif action == "detonate":
bomb_coordinates = self._get_bomb_to_detonate(game_state)
if bomb_coordinates != None:
x, y = bomb_coordinates
await self._client.send_detonate(x, y)
else:
print(f"Unhandled action: {action}")
def main():
Agent()
if __name__ == "__main__":
main()
🙈 Spoiler - helpers.py
# given a tile location as an (x,y) tuple
# return the surrounding tiles as a list
def get_surrounding_tiles(location):
# find all the surrounding tiles relative to us
# location[0] = x-index; location[1] = y-index
tile_up = (location[0], location[1]+1)
tile_down = (location[0], location[1]-1)
tile_left = (location[0]-1, location[1])
tile_right = (location[0]+1, location[1])
# combine these into a list
all_surrounding_tiles = [tile_up, tile_down, tile_left, tile_right]
# include only the tiles that are within the game boundary
# empty list to store our valid surrounding tiles
valid_surrounding_tiles = []
# loop through tiles
for tile in all_surrounding_tiles:
# check if the tile is within the boundaries of the game
# note: the map is size 9x9
if tile[0] >= 0 and tile[0] < 9 and \
tile[1] >= 0 and tile[1] < 9:
# add valid tiles to our list
valid_surrounding_tiles.append(tile)
return valid_surrounding_tiles
# this function returns the object tag at a location
def entity_at(x,y,game_state):
for entity in game_state["entities"]:
if entity["x"] == x and entity["y"] == y:
return entity["type"]
# given a list of tiles
# return the ones which are actually empty
def get_empty_tiles(tiles,game_state):
# empty list to store our empty tiles
empty_tiles = []
for tile in tiles:
if entity_at(tile[0],tile[1],game_state) is None:
# the tile isn't occupied, so we'll add it to the list
empty_tiles.append(tile)
return empty_tiles
# given an adjacent tile location, move us there
def move_to_tile(location, tile):
# see where the tile is relative to our current location
diff = tuple(x-y for x, y in zip(location, tile))
# return the action that moves in the direction of the tile
if diff == (0,1):
action = 'down'
elif diff == (0,-1):
action = 'up'
elif diff == (1,0):
action = 'left'
elif diff == (-1,0):
action = 'right'
else:
action = ''
return action

Save your file and repeat the steps from earlier to run and play test your agent.

What a thrilling match.

If you hit errors and your agent disconnects from the server, you can restart your agent (without needing to rebuild it) from Docker Desktop. Use CTRL+C or ⌘+C to abort the game early.

🥈 Level 2: Flee Bot Strikes Back

Estimated time: 1 hr

Here we'll update wanderer to create our agent flee_bot, which is a crude agent that:

  1. Checks if its near a bomb. If so, will try to find a safe place to hide.

  2. If not, will place a bomb 💣

Step 1: The Manhattan Distance

We'll need to give our Agent some concept of 'distance' from itself to other objects in the game. To do that, we'll introduce the Manhattan Distance which is useful in our grid-based game map:

Source: https://iq.opengenus.org/euclidean-vs-manhattan-vs-chebyshev-distance/

Here's our implementation of the Manhattan Distance:

helpers.py
# returns the manhattan distance between two tiles, calculated as:
# |x1 - x2| + |y1 - y2|
def manhattan_distance(start, end):
distance = abs(start[0] - end[0]) + abs(start[1] - end[1])
return distance

Step 2: Using the Manhattan Distance to check our surroundings

The first part of defining our Flee Bot's behaviour is to tell it when to hide versus when to plant bombs. To do this, we'll first create a function called get_bombs which will return us a list of bombs currently on the map. We've provided the code for you below:

helpers.py
# return a list of bombs on the map
def get_bombs(game_state):
list_of_bombs = []
for i in game_state["entities"]:
if i["type"] == "b":
x = i["x"]
y = i["y"]
list_of_bombs.append((x,y))
return list_of_bombs

We'll now create a function called get_bombs_in_range that tells us if it's within some radius of a bomb on the map. If we're close to a bomb, our flee bot will focus on running away, rather than trying to place more bombs.

Skeleton
🙈 Spoilers
Skeleton
helpers.py
# given a location as an (x,y) tuple and the game state
# return a list of the bomb positions that are nearby
def get_bombs_in_range(location, bombs):
# empty list to store our bombs that are in range of us
bombs_in_range = []
# loop through all the bombs placed in the game
for bomb in bombs:
# get our manhattan distance to a bomb
distance = None ################ FILL THIS ###################
# set to some arbitrary threshold for distance
# if we are below this threshold, we want flee bot to runaway
if distance <= 5:
bombs_in_range.append(bomb)
return bombs_in_range
🙈 Spoilers
helpers.py
# given a location as an (x,y) tuple and the game state
# return a list of the bomb positions that are nearby
def get_bombs_in_range(location, bombs):
# empty list to store our bombs that are in range of us
bombs_in_range = []
# loop through all the bombs placed in the game
for bomb in bombs:
# get our manhattan distance to a bomb
distance = manhattan_distance(location,bomb)
# set to some arbitrary threshold for distance
# if we are below this threshold, we want flee bot to runaway
if distance <= 5:
bombs_in_range.append(bomb)
return bombs_in_range

Now we want to build some 'smarts' into our bot. We don't want our bot to be moving into any tile that's within range of a bomb, so let's define a function get_safest_tile that returns us the 'safest' tile to move to.

For our get_safest_tile formula, we'll build a dictionary that stores the Manhattan distance to a bomb for each tile that surrounds our Agent. I.e.:

manhattan_distances = {
(x1,y1): distance1
(x2,y2): distance2
...
}

Note that there can be multiple bombs present, but in the simplest case we're just going to look at the bomb that's currently closest to us.

Skeleton
🙈 Spoilers
Skeleton
helpers.py
# given a list of tiles and bombs
# find the tile that's safest to move to
def get_safest_tile(tiles, bombs, location):
# which bomb is closest to us?
bomb_distance = 10 # some arbitrary high distance
closest_bomb = bombs[0] # set this to the first bomb in the list for now
# loop through the list of bombs
for bomb in bombs:
# calculate the manhattan distance
new_bomb_distance = manhattan_distance(bomb,location)
if new_bomb_distance < bomb_distance:
# this bomb is the closest one to us so far, let's store it
bomb_distance = new_bomb_distance
closest_bomb = bomb
# start with an empty dictionary
manhattan_distances = {}
# now we'll figure out which tile is furthest away from that bomb
for tile in tiles:
# get the manhattan distance from this tile to the closest bomb
distance = None ################ FILL THIS ###################
# store this in a dictionary
manhattan_distances[tile] = distance
# return the tile with the furthest distance from any bomb
safest_tile = max(manhattan_distances, key=manhattan_distances.get)
return safest_tile
🙈 Spoilers
helpers.py
# given a list of tiles and bombs
# find the tile that's safest to move to
def get_safest_tile(tiles, bombs, location):
# which bomb is closest to us?
bomb_distance = 10 # some arbitrary high distance
closest_bomb = bombs[0] # set this to the first bomb in the list for now
# loop through the list of bombs
for bomb in bombs:
# calculate the manhattan distance
new_bomb_distance = manhattan_distance(bomb,location)
if new_bomb_distance < bomb_distance:
# this bomb is the closest one to us so far, let's store it
bomb_distance = new_bomb_distance
closest_bomb = bomb
# start with an empty dictionary
manhattan_distances = {}
# now we'll figure out which tile is furthest away from that bomb
for tile in tiles:
# get the manhattan distance from this tile to the closest bomb
distance = manhattan_distance(tile, closest_bomb)
# store this in a dictionary
manhattan_distances[tile] = distance
# return the tile with the furthest distance from any bomb
safest_tile = max(manhattan_distances, key=manhattan_distances.get)
return safest_tile

Step 3: Bring it all together!

Here we go - it's time to bring Flee Bot to life:

Skeleton
🙈 Spoilers - agent.py
🙈 Spoilers - helpers.py
Skeleton
agent.py
from game_state import GameState
import asyncio
import random
import os
import helpers # import our helper functions
uri = os.environ.get(
'GAME_CONNECTION_STRING') or "ws://127.0.0.1:3000/?role=agent&agentId=agentId&name=myAgent"
actions = ["up", "down", "left", "right"]
class Agent():
def __init__(self):
self._client = GameState(uri)
self._client.set_game_tick_callback(self._on_game_tick)
loop = asyncio.get_event_loop()
connection = loop.run_until_complete(self._client.connect())
tasks = [
asyncio.ensure_future(self._client._handle_messages(connection)),
]
loop.run_until_complete(asyncio.wait(tasks))
def _get_bomb_to_detonate(self, game_state) -> [int, int] or None:
agent_number = game_state.get("connection").get("agent_number")
entities = self._client._state.get("entities")
bombs = list(filter(lambda entity: entity.get(
"owner") == agent_number and entity.get("type") == "b", entities))
bomb = next(iter(bombs or []), None)
if bomb != None:
return [bomb.get("x"), bomb.get("y")]
else:
return None
async def _on_game_tick(self, tick_number, game_state):
########################
### VARIABLES ###
########################
my_id = str(game_state["connection"]["agent_number"])
my_location = game_state["agent_state"][my_id]["coordinates"]
ammo = game_state["agent_state"][my_id]["inventory"]["bombs"]
########################
### AGENT ###
########################
# get a list of bombs on the map
bombs = None ######### CHANGE THIS #########
# check if we're within range of a bomb
# get list of bombs within range
bombs_in_range = None ######### CHANGE THIS #########
# get our surrounding tiles
surrounding_tiles = None ######### CHANGE THIS #########
# get list of empty tiles around us
empty_tiles = None ######### CHANGE THIS #########
# if I'm on a bomb, I should probably move
if helpers.entity_at(my_location[0],my_location[1],game_state) == 'b':
if empty_tiles:
random_tile = random.choice(empty_tiles)
action = None ######### CHANGE THIS #########
else:
# if there isn't a free spot to move to, we're probably stuck here
action = ''
# if we're near a bomb, we should also probably move
elif bombs_in_range:
if empty_tiles:
# get the safest tile for us to move to
safest_tile = None ######### CHANGE THIS #########
action = None ######### CHANGE THIS #########
else:
action = random.choice(actions)
# if there are no bombs in range
else:
# but first, let's check if we have any ammo
if ammo > 0:
# we've got ammo, let's place a bomb
action = "bomb"
else:
# no ammo, we'll make random moves until we have ammo
action = random.choice(actions)
# logic to send valid action packet to game server
if action in ["up", "left", "right", "down"]:
await self._client.send_move(action)
elif action == "bomb":
await self._client.send_bomb()
elif action == "detonate":
bomb_coordinates = self._get_bomb_to_detonate(game_state)
if bomb_coordinates != None:
x, y = bomb_coordinates
await self._client.send_detonate(x, y)
else:
print(f"Unhandled action: {action}")
def main():
Agent()
if __name__ == "__main__":
main()
🙈 Spoilers - agent.py
from game_state import GameState
import asyncio
import random
import os
import helpers # import our helper functions
uri = os.environ.get(
'GAME_CONNECTION_STRING') or "ws://127.0.0.1:3000/?role=agent&agentId=agentId&name=myAgent"
actions = ["up", "down", "left", "right"]
class Agent():
def __init__(self):
self._client = GameState(uri)
self._client.set_game_tick_callback(self._on_game_tick)
loop = asyncio.get_event_loop()
connection = loop.run_until_complete(self._client.connect())
tasks = [
asyncio.ensure_future(self._client._handle_messages(connection)),
]
loop.run_until_complete(asyncio.wait(tasks))
def _get_bomb_to_detonate(self, game_state) -> [int, int] or None:
agent_number = game_state.get("connection").get("agent_number")
entities = self._client._state.get("entities")
bombs = list(filter(lambda entity: entity.get(
"owner") == agent_number and entity.get("type") == "b", entities))
bomb = next(iter(bombs or []), None)
if bomb != None:
return [bomb.get("x"), bomb.get("y")]
else:
return None
async def _on_game_tick(self, tick_number, game_state):
########################
### VARIABLES ###
########################
my_id = str(game_state["connection"]["agent_number"])
my_location = game_state["agent_state"][my_id]["coordinates"]
ammo = game_state["agent_state"][my_id]["inventory"]["bombs"]
########################
### AGENT ###
########################
# get a list of bombs on the map
bombs = helpers.get_bombs(game_state)
# check if we're within range of a bomb
# get list of bombs within range
bombs_in_range = helpers.get_bombs_in_range(my_location,bombs)
# get our surrounding tiles
surrounding_tiles = helpers.get_surrounding_tiles(my_location)
# get list of empty tiles around us
empty_tiles = helpers.get_empty_tiles(surrounding_tiles,game_state)
# if I'm on a bomb, I should probably move
if helpers.entity_at(my_location[0],my_location[1],game_state) == 'b':
if empty_tiles:
random_tile = random.choice(empty_tiles)
action = helpers.move_to_tile(my_location,random_tile)
else:
# if there isn't a free spot to move to, we're probably stuck here
action = ''
# if we're near a bomb, we should also probably move
elif bombs_in_range:
if empty_tiles:
# get the safest tile for us to move to
safest_tile = helpers.get_safest_tile(empty_tiles,bombs,my_location)
action = helpers.move_to_tile(my_location,safest_tile)
else:
action = random.choice(actions)
# if there are no bombs in range
else:
# but first, let's check if we have any ammo
if ammo > 0:
# we've got ammo, let's place a bomb
action = "bomb"
else:
# no ammo, we'll make random moves until we have ammo
action = random.choice(actions)
# logic to send valid action packet to game server
if action in ["up", "left", "right", "down"]:
await self._client.send_move(action)
elif action == "bomb":
await self._client.send_bomb()
elif action == "detonate":
bomb_coordinates = self._get_bomb_to_detonate(game_state)
if bomb_coordinates != None:
x, y = bomb_coordinates
await self._client.send_detonate(x, y)
else:
print(f"Unhandled action: {action}")
def main():
Agent()
if __name__ == "__main__":
main()
🙈 Spoilers - helpers.py
# given a tile location as an (x,y) tuple
# return the surrounding tiles as a list
def get_surrounding_tiles(location):
# find all the surrounding tiles relative to us
# location[0] = x-index; location[1] = y-index
tile_up = (location[0], location[1]+1)
tile_down = (location[0], location[1]-1)
tile_left = (location[0]-1, location[1])
tile_right = (location[0]+1, location[1])
# combine these into a list
all_surrounding_tiles = [tile_up, tile_down, tile_left, tile_right]
# include only the tiles that are within the game boundary
# empty list to store our valid surrounding tiles
valid_surrounding_tiles = []
# loop through tiles
for tile in all_surrounding_tiles:
# check if the tile is within the boundaries of the game
# note: the map is size 9x9
if tile[0] >= 0 and tile[0] < 9 and \
tile[1] >= 0 and tile[1] < 9:
# add valid tiles to our list
valid_surrounding_tiles.append(tile)
return valid_surrounding_tiles
# this function returns the object tag at a location
def entity_at(x,y,game_state):
for entity in game_state["entities"]:
if entity["x"] == x and entity["y"] == y:
return entity["type"]
# given a list of tiles
# return the ones which are actually empty
def get_empty_tiles(tiles,game_state):
# empty list to store our empty tiles
empty_tiles = []
for tile in tiles:
if entity_at(tile[0],tile[1],game_state) is None:
# the tile isn't occupied, so we'll add it to the list
empty_tiles.append(tile)
return empty_tiles
# given an adjacent tile location, move us there
def move_to_tile(location, tile):
# see where the tile is relative to our current location
diff = tuple(x-y for x, y in zip(location, tile))
# return the action that moves in the direction of the tile
if diff == (0,1):
action = 'down'
elif diff == (0,-1):
action = 'up'
elif diff == (1,0):
action = 'left'
elif diff == (-1,0):
action = 'right'
else:
action = ''
return action
# returns the manhattan distance between two tiles, calculated as:
# |x1 - x2| + |y1 - y2|
def manhattan_distance(start, end):
distance = abs(start[0] - end[0]) + abs(start[1] - end[1])
return distance
# return a list of bombs on the map
def get_bombs(game_state):
list_of_bombs = []
for i in game_state["entities"]:
if i["type"] == "b":
x = i["x"]
y = i["y"]
list_of_bombs.append((x,y))
return list_of_bombs
# return a list of the bomb positions that are nearby
def get_bombs_in_range(location, bombs):
# empty list to store our bombs that are in range of us
bombs_in_range = []
# loop through all the bombs placed in the game
for bomb in bombs:
# get our manhattan distance to a bomb
distance = manhattan_distance(location, bomb)
# set to some arbitrary threshold for distance
# if we are below this threshold, we want flee bot to runaway
if distance <= 5:
bombs_in_range.append(bomb)
return bombs_in_range
# given a list of tiles and bombs
# find the tile that's safest to move to
def get_safest_tile(tiles, bombs, location):
# which bomb is closest to us?
bomb_distance = 10 # some arbitrary high distance
closest_bomb = bombs[0] # set this to the first bomb in the list for now
# loop through the list of bombs
for bomb in bombs:
# calculate the manhattan distance
new_bomb_distance = manhattan_distance(bomb,location)
if new_bomb_distance < bomb_distance:
# this bomb is the closest one to us so far, let's store it
bomb_distance = new_bomb_distance
closest_bomb = bomb
# start with an empty dictionary
manhattan_distances = {}
# now we'll figure out which tile is furthest away from that bomb
for tile in tiles:
# get the manhattan distance from this tile to the closest bomb
distance = manhattan_distance(tile,closest_bomb)
# store this in a dictionary
manhattan_distances[tile] = distance
# return the tile with the furthest distance from any bomb
safest_tile = max(manhattan_distances, key=manhattan_distances.get)
return safest_tile

Great job! 🥳

🥇 Level 3: Go for Gold

You're ready now to create your own agent!

You can either improve upon the agent you've created, or come up with an entirely new strategy. Check out more information on the game environment here:

We've also provided some sample functions and resources that might be helpful:

Good luck!

When you're ready to submit your Agent, check out:

You'll find details on all submission dates here:

💭 Need Help?

Check the FAQ to see if your question has already been answered. Otherwise feel free to ask questions on our Discord community (message ModMail to contact the Coder One team directly).

For any other questions, enquiries or concerns, you can always reach us at [email protected].