Contents
All blogs / An introduction to AI programming with Bomberman (part 2)
September 01, 2022 • Joy Zhang • Tutorial • 6 minutes
Part 1: An introduction to AI programming with Bomberman (part 1)
So far we've covered:
game_state
In this post, we'll show you how to build a simple agent called a 'Wanderer Agent' that implements some scripted logic to explore and navigate the world. More specifically, it:
In this section, we'll walk through creating 5 helper methods that will be useful for our agent:
_is_in_bounds
: Checks if a location is valid (within the map boundaries)_get_surrounding_tiles
: Returns a list of our surrounding tiles_is_occupied
: Checks whether a tile is occupied by an object or player_get_empty_tiles
: Returns tiles that are valid for our units to move into_move_to_tile
: Returns the corresponding action the unit should take to move to a tile_is_in_bounds
and _get_surrounding_tiles
Our first helper method _is_in_bounds
will check whether a given [x,y] location is valid. Below is some skeleton code with gaps to fill out (or just skip ahead to the solution).
Note that instead of using the variable game_state
like in the previous section, we'll be replacing this with self._client._state
since we'll be calling these helper methods outside of our _on_game_tick
loop.
def _is_in_bounds(self, location):
width = self._client._state.get("world").get("width")
height = None ################ FILL THIS ###################
return (location[0] >= 0 & location[0] <= width & None) ################ COMPLETE THIS ###################
def _is_in_bounds(self, location):
width = self._client._state.get("world").get("width")
height = self._client._state.get("world").get("height")
return (location[0] >= 0 & location[0] <= width & location[1] >= 0 & location[1] <= height)
We'll use _is_in_bounds
to assist with our next helper, _get_surrounding_tiles
. This method will return us a list of tiles surrounding our unit's current location as an [x,y] coordinate.
We'll take advantage of the coordinate-representation of the map:
Below is the skeleton code for our _get_surrounding_tiles()
method. (💡 Hint: check the Game State Definitions for useful methods).
# given our current location as an (x,y) tuple, return the surrounding tiles as a list
# (i.e. [(x1,y1), (x2,y2),...])
def _get_surrounding_tiles(self, location):
# location[0] = x-index; location[1] = y-index
tile_north = (location[0], location[1]+1)
tile_south = None ################ FILL THIS ###################
tile_west = None ################ FILL THIS ###################
tile_east = (location[0]+1, location[1])
surrounding_tiles = [tile_north, tile_south, tile_west, tile_east]
for tile in surrounding_tiles:
# check if the tile is within the boundaries of the game
if None: ################ CHANGE 'NONE' ###################
# remove invalid tiles from our list
surrounding_tiles.remove(tile)
return surrounding_tiles
def _get_surrounding_tiles(self, location):
tile_north = [location[0], location[1]+1]
tile_south = [location[0], location[1]-1]
tile_west = [location[0]-1, location[1]]
tile_east = [location[0]+1, location[1]]
surrounding_tiles = [tile_north, tile_south, tile_west, tile_east]
for tile in surrounding_tiles:
if not self._is_in_bounds(tile):
surrounding_tiles.remove(tile)
return surrounding_tiles
Next, add your get_surrounding_tiles
method to your Agent
class in agent.py
.
class Agent():
...
########################
### HELPERS ###
########################
def _get_surrounding_tiles(self, location):
'''
Your code here
'''
return surrounding_tiles
async def _on_game_tick(self, tick_number, game_state):
...
_is_occupied
and _get_empty_tiles
In order for our units to move effectively, they will also need to know which of their surrounding tiles are actually empty (i.e. not containing a block or other player).
_is_occupied
is a helper method that will check whether or not a given location currently occupied.
def _is_occupied(self, location):
entities = self._client._state.get("entities")
units = self._client._state.get("unit_state")
list_of_entity_locations = [[entity[c] for c in ['x', 'y']] for entity in entities]
list_of_unit_locations = [units[u]["coordinates"] for u in ['c','d','e','f','g']]
list_of_occupied_locations = list_of_entity_locations + list_of_unit_locations
return location in list_of_occupied_locations
We'll use this in _get_empty_tiles
:
# given a list of tiles, return only those that are empty/free
def _get_empty_tiles(self, tiles):
empty_tiles = []
for tile in tiles:
if None: ################ CHANGE 'NONE' ###################
# add empty tiles to list
empty_tiles.append(tile)
return empty_tiles
def _get_empty_tiles(self, tiles):
empty_tiles = []
for tile in tiles:
if not self._is_occupied(tile):
empty_tiles.append(tile)
return empty_tiles
_move_to_tile()
Given an adjacent surrounding tile and our current location, _move_to_tile()
will return the action (i.e. up
, down
, left
, right
) that will get us there. E.g. if the tile we want to move to is directly north of us, this method will return up
.
# given an adjacent tile location, move us there
def _move_to_tile(self, tile, location):
# see where the tile is relative to our current location
diff = tuple(x-y for x, y in zip(tile, location))
# return the action that moves in the direction of the tile
if diff == (0,1):
action = 'up'
elif diff == (0,-1):
action = None ################ FILL THIS ###################
elif diff == (1,0):
action = None ################ FILL THIS ###################
elif diff == (-1,0):
action = 'left'
else:
action = ''
return action
def _move_to_tile(self, tile, location):
diff = tuple(x-y for x, y in zip(tile, location))
if diff == (0,1):
action = 'up'
elif diff == (0,-1):
action = 'down'
elif diff == (1,0):
action = 'right'
elif diff == (-1,0):
action = 'left'
else:
action = ''
return action
With our helper methods in place, we'll be able to implement some simple logic to let our units navigate the game world.
Here's some sample skeleton code to help you piece together your agent.
class Agent():
def __init__(self):
...
def _is_in_bounds(self, location):
...
def _get_surrounding_tiles(self, location):
...
def _is_occupied(self, location):
...
def get_empty_tiles(self, tiles):
...
def move_to_tile(self, tile, location):
...
async def _on_game_tick(self, tick_number, game_state):
# get my units
my_agent_id = game_state.get("connection").get("agent_id")
my_units = game_state.get("agents").get(my_agent_id).get("unit_ids")
# send each unit a random action
for unit_id in my_units:
# this unit's location
unit_location = None ###### FILL THIS ######
# get our surrounding tiles
surrounding_tiles = None ###### FILL THIS ######
# get list of empty tiles around us
empty_tiles = None ###### FILL THIS ######
if empty_tiles:
# choose an empty tile to walk to
random_tile = random.choice(empty_tiles)
action = None ###### FILL THIS ######
else:
# we're trapped
action = ''
if action in ["up", "left", "right", "down"]:
await self._client.send_move(action, unit_id)
elif action == "bomb":
await self._client.send_bomb(unit_id)
elif action == "detonate":
bomb_coordinates = self._get_bomb_to_detonate(unit_id)
if bomb_coordinates != None:
x, y = bomb_coordinates
await self._client.send_detonate(x, y, unit_id)
else:
print(f"Unhandled action: {action} for unit {unit_id}")
class Agent():
...
async def _on_game_tick(self, tick_number, game_state):
# get my units
my_agent_id = game_state.get("connection").get("agent_id")
my_units = game_state.get("agents").get(my_agent_id).get("unit_ids")
# send each unit a random action
for unit_id in my_units:
# this unit's location
unit_location = game_state["unit_state"][unit_id]["coordinates"]
# get our surrounding tiles
surrounding_tiles = self._get_surrounding_tiles(unit_location)
# get list of empty tiles around us
empty_tiles = self._get_empty_tiles(surrounding_tiles)
if empty_tiles:
# choose an empty tile to walk to
random_tile = random.choice(empty_tiles)
action = self._move_to_tile(random_tile, unit_location)
else:
# we're trapped
action = ''
if action in ["up", "left", "right", "down"]:
await self._client.send_move(action, unit_id)
elif action == "bomb":
await self._client.send_bomb(unit_id)
elif action == "detonate":
bomb_coordinates = self._get_bomb_to_detonate(unit_id)
if bomb_coordinates != None:
x, y = bomb_coordinates
await self._client.send_detonate(x, y, unit_id)
else:
print(f"Unhandled action: {action} for unit {unit_id}")
Save your agent (agent.py
) and run:
docker-compose up --abort-on-container-exit --force-recreate
Here's what you should see:
To win at Bomberland, your agent will need to do more than roam around the map. It will need to know how to place bombs strategically in order to blow up crates for powerups, dodge traps, and take down your opponent.
Below are some resources that you might find helpful in creating your agent for the competition.
Good luck!
If you have any questions, you can reach us on Discord or via email.