An introduction to bot programming: building a simple Bomberman bot (part 2)

This is part 2 of a series originally published on the Coder One blog. Check out Part 1 here: Building an AI game bot for Bomberman. In this tutorial series, we'll build a bot in Python that can play in a variation of the classic Bomberman game called Dungeons and Data Structures. We'll start off simple, and add advanced strategies in the later parts of the series.

Recap

Make sure you check out part 1 of this bot programming tutorial series first. So far we've covered:

  • Setting up the game environment Dungeons and Data Structures
  • Working with a starter Python agent that makes random moves
  • Getting familiar with the game environment's objects and methods

In part 2, we'll show you how to build a simple agent called the '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

If you're more interested in the topic of reinforcement learning, check out my other introductory tutorial to Reinforcement Learning With OpenAI Gym’s ‘Taxi’.

Step 1: Helper methods

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

  1. get_surrounding_tiles(): Returns a list of our surrounding tiles
  2. get_empty_tiles(): Returns tiles that are valid for our agent to move into
  3. move_to_tile(): Returns the corresponding action the agent should take to move to a tile

You should be familiar with the game_state and player_state objects from part 1 of the tutorial.

get_surrounding_tiles()

Our first helper method get_surrounding_tiles() will return us a list of tiles surrounding our agent's current location as an (x,y) tuple of the game map.

We'll take advantage of the coordinate-representation of the map:

Below is the skeleton code for our get_surrounding_tiles() method*.* We've left you some gaps to fill out. If you get stuck, check out the solution. (💡 Hint: check the game state documentation 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

Next, add your get_surrounding_tiles() method to your Agent class in my_agent.py.

class Agent:

    def __init__(self):
        pass

    def next_move(self, game_state, player_state):
        '''
        This method is called each time your Agent is required to choose an action
        '''
        pass

    ########################
    ###      HELPERS     ###
    ########################

    def get_surrounding_tiles(self, location):
        '''
        Your code here
        '''
        return surrounding_tiles

get_empty_tiles()

In order for our agent to move effectively, it will also need to know which of its surrounding tiles are actually empty (i.e. not containing a block or other player). Here's a get_empty_tiles method with some blanks for you to fill out:

# 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

move_to_tile()

Given an adjacent surrounding tile and our current location, move_to_tile() will return the action (i.e. u, d, l, r) that will get us there. E.g. if the tile we want to move to is directly north of us, this method will return u.

# given an adjacent tile location, move us there
def move_to_tile(self, location, tile):

    # see where the tile is relative to our current location
    diff = tuple(x-y for x, y in zip(tile, self.location))

    # return the action that moves in the direction of the tile
    if diff == (0,1):
        action = 'u'
    elif diff == (0,-1):
        action = None     ################ FILL THIS ###################
    elif diff == (1,0):
        action = None     ################ FILL THIS ###################
    elif diff == (-1,0):
        action = 'l'
    else:
        action = ''

    return action

Step 2: Agent logic

With our helper methods in place, we'll be able to implement some simple logic to control our agent to navigate the game world.

Here's some sample skeleton code to help you piece together your agent. Here's also a link to our own version of Wanderer Agent.

import random

class Agent:

    def __init__(self):
        '''
        Place any initialization code for your agent here (if any)
        '''
        pass

    def next_move(self, game_state, player_state):
        '''
        This method is called each time your Agent is required to choose an action
        '''
        ########################
        ###    VARIABLES     ###
        ########################

        # game map is represented in the form (x,y)
        self.cols = game_state.size[0]
        self.rows = None           ################ FILL THIS ###################

        # useful for later
        self.game_state = game_state 
        self.location = player_state.location

        ########################
        ###      AGENT       ###
        ########################

        # get our surrounding tiles
        surrounding_tiles = self.get_surrounding_tiles(self.location)

        # 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 = ''

        return action

    ########################
    ###      HELPERS     ###
    ########################

    # 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

    # 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

    # given an adjacent tile location, move us there
    def move_to_tile(self, location, tile):

        # see where the tile is relative to our current location
        diff = tuple(x-y for x, y in zip(tile, self.location))

        # return the action that moves in the direction of the tile
        if diff == (0,1):
            action = 'u'
        elif diff == (0,-1):
            action = None     ################ FILL THIS ###################
        elif diff == (1,0):
            action = None     ################ FILL THIS ###################
        elif diff == (-1,0):
            action = 'l'
        else:
            action = ''

        return action

Save your agent (my_agent.py) then run the following command in your terminal to watch your new bot go up against itself:

coderone-dungeon --watch my_agent my_agent

Step 3: Bombs away

To win at Dungeons and Data Structures, 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 points or take down your opponent.

Now that you've got a better grasp of the environment, have a go at implementing some logic around bomb placements.

If you're interested, check out our implementation of a very simple 'Flee Agent'. It uses the Manhattan Distance formula to decide whether to focus on running away from nearby bombs or placing more of them.

Next steps

In this part, we went from a random agent that selects its moves at random, to one that can interpret its surrounding environment and explore it.

In the next part of the series, we'll implement a pathfinding algorithm to help it navigate to useful objects around the map. You can follow me here on DEV or on Medium to get notified when Part 3 is out.

P.S. If you're interested in supporting this project, please follow us on Product Hunt!

Thanks for reading 🙌

24