Bλog

Links

My Haskell TronBot for the Google AI Challenge

My bot turned out to be the top scoring Haskell bot, so here's the code
Published on March 1, 2010 under the tag haskell

This is the code for my entry in the Google AI Challenge 2010. It turned out to be the best Belgian and the best Haskell bot (screenshot), so I thought some people might be interested in the code. Luckily, I have been writing this bot in Literate Haskell since the beginning, for a few reasons:

I always wanted to try Literate Haskell for something “more serious”.

This will force me to keep the code more or less readable and clean.

I am going to keep the code in one file, so it’s quite easy to maintain as well.

There’s a simple formula for calculating the Pythagorean distance between tiles. We use this in combination with the “real” distance (the distance when taking walls etc. into account). We can leave this distance squared, not taking the sqrt is a little faster, and we only have to compare distances, and x^2 < y^2 implies x < y, because distances are always non-negative.

Later on, we will construct “possible” next Boards. Given such a Board, we want to determine the first move our Bot made, since that would be the move our AI will choose. This might give no result, so we wrap it in a Maybe type.

Checking if a certain tile is a wall is quite simple – but we need to remember we also have to check the additional walls in the Board. We first check for boundaries to prevent errors, then we check in the walls first, because Array access is faster than Set access here. Also, we really want to inline this function, because it is called over 9000 times.

Next is a function to inspect the entire Board, to determine it’s value later on. It uses a flood fill based approach.

It starts one flood fill starting from the enemy, and one starting from our bot. This determines the space left for each combatant. Also, when the two flood meet, we have found a path between them. This double flood fill is illustrated here in this animation:

Flood fill illustration

The function returns:

The free space around the bot.

The free space around the enemy.

The distance between the bot and the enemy, or Nothing if there is no path from the bot to the enemy.

Because we have to make all decisions in under one second, we have a depth limit for our flood fill. When this limit is reached, the result will be the same as if we encountered walls in all directions.

If the enemy is in the next set of neighbours, we have found our distance. If there is no intersection at all, we haven’t reached the enemy yet. We have to do a little trickery here, because the distance could be even or odd.

Now, we enlarge our Set of already added tiles and remove them from the next tiles to add (since they are already added). We also filter out the non-accessible Tiles, and we make sure no Tiles appear in both validNext1 and validNext2.

The algorithm needs to be able to determine the “best” choice in some way or another. So we need to be able compare two games. To make this easier, we can assign a Score to a game - and we then make these Scores comparable.

In theory, these are the only possible outcomes. In reality, these values are often situated at the bottom of our game tree – and we can’t look down all the way. Therefore, we also have a Game score – describing a game in progress.

The Game constructor simply holds some fields so we can determine it’s value:

Free space for the bot, as determined by a flood fill.

Free space for the enemy, as determined by a flood fill.

Just d if d is the distance to the enemy. If the enemy cannot be reached, or is to far away to be detected, this will be Nothing.

We now have our main minimax search function. The maxDepth argument gives us a depth limit for our search, and also indicates if it’s our turn or the enemy’s turn (it’s our turn when it’s even, enemy’s turn when it’s odd).

The contact argument tells us if there is a way for our bot to reach the enemy. If the enemy cannot be reached, we do not have to consider it’s turns, sparing us some valuable resources.

This function uses a simple form of (deep) Alpha-beta pruning. I’m pretty sure botSearch and enemySearch could be written as one more abstract function, but I think it’s pretty clear now, too.

For one unfamiliar with minimax trees or Alpha-beta pruning, this function simply returns the possible Board with the best Score.

First, we want to make a quick (but stupid) decision, in case we’re on a very slow processor or if we don’t get a lot of CPU ticks. The following function does that, providing a simple “Chaser” approach.

This is the function that is executed in another thread. It simply tries to calculate a smart decision using minMaxDecision and then puts it in the MVar. We also do a simple inspectBoard to determine if there is a path between us and the enemy.

It also uses a form of iterative deepening; first, the best decision for depth 2 is calculated. Then, we try to find the best decision for depth 4, then 6, and so on. Tests seemed to show that it usually gets to depth 8 or 10 before it is killed.