All blogs / An introduction to AI programming with Bomberman (part 2)

An introduction to AI programming with Bomberman (part 2)

September 01, 2022 • Joy Zhang • Tutorial • 6 minutes

An introduction to AI programming with Bomberman (part 2)

Recap

Part 1: An introduction to AI programming with Bomberman (part 1)

So far we've covered:

  • Setting up the game environment Bomberland
  • Working with a starter Python agent that makes random moves
  • Getting familiar with the environment using game_state

Bomberland

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:

  1. Looks at its immediate surroundings
  2. Checks which directions are valid ones to move in
  3. Chooses a random valid direction to take

Step 1: Helper methods

In this section, we'll walk through creating 5 helper methods that will be useful for our agent:

  1. _is_in_bounds: Checks if a location is valid (within the map boundaries)
  2. _get_surrounding_tiles: Returns a list of our surrounding tiles
  3. _is_occupied: Checks whether a tile is occupied by an object or player
  4. _get_empty_tiles: Returns tiles that are valid for our units to move into
  5. _move_to_tile: Returns the corresponding action the unit should take to move to a tile

Checking our surroundings using _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 ###################
See solution
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:

Bomberland grid

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
See solution
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):

        ...

Find valid tiles using _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
See solution
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

Return our unit's action using _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
See solution
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

Step 2: Agent logic

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}")
See solution
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:

Wanderer agents in Bomberland

Next steps

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.

Previous participant write-ups and post mortems

Additional tutorials

Good luck!

If you have any questions, you can reach us on Discord or via email.

Subscribe to get the latest posts in your inbox:
Tackle the world's most exciting artificial intelligence challenges with the community.
SitePoint LogoGeneral Assembly LogoHackathons Australia LogoDSAi Logo
Interested in sponsorship?
Sponsorship Enquiry Form
© 2022 Coder One Pty Ltd | Contact | Privacy