Step 1: Avoiding collisions

The Plan

In order to prevent collisions, we will need to do a few things:

  • prevent ants from moving onto other ants
  • prevent 2 ants from moving to the same destination
  • track information about where all our ants are going

The Implementation

To track information about where ants are moving, we are going to use a dictionary. It is a data structure that will store locations, and then allow us to check if a location has already been stored. Each key of the dictionary will be a location we are moving to and each value will be the ant location that is moving to the new location. A location will be a tuple of values consisting of the row and column of the map location. We can then check the dictionary before making a move to ensure we don't move 2 ants to the same spot. Every time we move an ant, we need to be sure to update the list.

This check will come in handy later in the tutorial, so we will make a function to attempt moves and check to make sure the move is to an empty location. It will return a boolean (true or false) to let us know if the move worked.

To track information about where ants are moving, we are going to use a HashMap. It is a data structure that will store locations, and then allow us to check if a location has already been stored. Each key and value of the HashMap will be a Tile object. A Tile object is the row and column of a location on the map. The key will be the new location to move to and the value will be the location of the ant moving to the new location. We can then check the HashMap before making a move to ensure we don't move 2 ants to the same spot. Every time we move an ant, we need to be sure to update the HashMap.

This check will come in handy later in the tutorial, so we will make a function to attempt moves and check to make sure the move is to an empty location. It will return a boolean (true or false) to let us know if the move worked.

The Code

We'll trim down the starter bots comments and put the new code in:

    def do_turn(self, ants):
        # track all moves, prevent collisions
        orders = {}
        def do_move_direction(loc, direction):
            new_loc = ants.destination(loc, direction)
            if (ants.unoccupied(new_loc) and new_loc not in orders):
                ants.issue_order((loc, direction))
                orders[new_loc] = loc
                return True
            else:
                return False

        # default move
        for ant_loc in ants.my_ants():
            directions = ('n','e','s','w')
            for direction in directions:
                if do_move_direction(ant_loc, direction):
                    break

(Note: Make sure you get the indentation correct. In Python, indentation determines the code blocks or scope, so it has to be correct.)

The do_move_direction takes an ant location (a tuple of (row, col) ) and a direction ( 'n', 'e', 's' or 'w' ) and tries to perform the move. This function is located inside a class method (which is also a function) and is okay to do in python. We are using some predefined functions from the starter bot to help us:

  • ants.destination takes a location and a direction and returns the destination location for us. It takes care of the map wrapping around so we don't have to worry about it.

  • ants.unoccupied takes a location and let's us know if we can move there. This is better than the previous ants.passable because it will not allow us to step on food or other ants.

The orders dictionary is used to track what moves we have issued. In the if statement we have new_loc not in orders which will check the dictionary keys for us and help prevent collisions.

We'll trim down the starter bots comments and put the new code in:

    private Map<Tile, Tile> orders = new HashMap<Tile, Tile>();

    private boolean doMoveDirection(Tile antLoc, Aim direction) {
        Ants ants = getAnts();
        // Track all moves, prevent collisions
        Tile newLoc = ants.getTile(antLoc, direction);
        if (ants.getIlk(newLoc).isUnoccupied() && !orders.containsKey(newLoc)) {
            ants.issueOrder(antLoc, direction);
            orders.put(newLoc, antLoc);
            return true;
        } else {
            return false;
        }
    }

    @Override
    public void doTurn() {
        Ants ants = getAnts();
        orders.clear();

        //  default move
        for (Tile myAnt : ants.getMyAnts()) {
            for (Aim direction : Aim.values()) {
                if (doMoveDirection(myAnt, direction)) {
                    break;
                }
            }
        }
    }

The doMoveDirection function takes an ant location (a Tile object) and a direction (an Aim object of N, E, S or W) and tries to perform the move. This function is located outside the doTurn function, so our reserved tiles HashMap is at the class level and we clear it for each turn. We are using some predefined functions from the starter bot to help us:

  • ants.getTile(Tile, Aim) takes a location (Tile object) and a direction (Aim object) and returns the destination location (Tile object) for us. It takes care of the map wrapping around so we don't have to worry about it.

  • ants.getIlk(Tile) takes a location (Tile object) and returns the Ilk (a fancy word for type or kind). We then call the isUnoccupied() function of the Ilk object to see if it is free to move to.

  • Ilk.isUnoccupied takes a location and let's us know if we can move there. This is better than the previous Ilk.isPassable because it will not allow us to step on food or other ants.

The orders HashMap is used to track what moves we have issued. In the if statement we have !orders.containsKey(newLoc) which will check the HashMap for us and help prevent collisions.

The Results

Let's run the bot again and see how we do.

C:\aichallenge>tutorial.cmd
running for 60 turns
                  ant_count c_turns climb? cutoff food r_turn ranking_bots s_alive s_hills score  w_turn winning
turn    0 stats:   [1,1,0]     0    [1,1]    -     18    0        None      [1,1]   [1,1]  [1,1]   0     None
turn    1 stats:   [1,1,0]     0    [1,1]    -     16    1       [0,0]      [1,1]   [1,1]  [1,1]   1     [0,1]
turn    2 stats:   [2,1,0]     0    [1,1]    -     16    1       [0,0]      [1,1]   [1,1]  [1,1]   1     [0,1]
...
turn   60 stats:   [1,5,0]     0    [1,1]    -     12    1       [0,0]      [1,1]   [1,1]  [1,1]   1     [0,1]
score 1 1
status survived survived
playerturns 60 60

Here is the replay:

# [ { "embedded": true, "game": "1 - Avoiding collisions" }, 600, 600, { "speedFactor": 0, "speedFastest": 2, "speedSlowest": 2, "zoom": 1 }, "example_games/tutorial.1.replay" ]

Better, but still not good. One lone ant got out and got to fight with HunterBot. We didn't suicide, and that's an improvement. Plus, we created a helper function that will come in handy later.

If your bot's ants oscillated behind their barrier instead, it is probably due to the ordering of the ants in your loop. If the NW ant moves first it moves to the North of the SE ant, which can then only move East, South or West. Otherwise if the SE ant moves first it moves to the East of the NW ant, which can then only move South or West.

Next

On to Step 2: Gathering Food