Well, the Google AI Challenge is over, and I ended up as the second best Clojure bot. My skill rating was 53.23, which put me in 1,335th place. I never expected to be so high, but it turns out there weren’t a lot of Clojure entries. I was beaten, by a large margin, by thobel. He did a great job. With the contest over, I thought I’d post a little postmortem of how my bot worked.
Global State & Exploration
My bot really doesn’t keep any global state, which is one of the reasons it didn’t do better. I planned to start adding some, but I never got around to it as I got distracted by work and other things. Here is the only global state that really matters:
(def ^{:dynamic true} *current-defense* (atom nil)) (def ^{:dynamic true} *positions-to-fill* (atom #{})) (def ^{:dynamic true} *positions-unfilled* (atom {})) (def ^{:dynamic true} *ant-last-moves* (atom {}))
The first three variables keep track of bot’s defenses, which is the only form of coordination between the ants. *ants-last-moves*
keeps track of the last move each ant made so they can be prevented from backtracking.
In fact, the backtracking prevention is the only form of exploration in the code. Each ant sets out when spawned in a random direction, and ants have a 90% chance of continuing in the direction they were already traveling.
Core Logic
The core of the bot is a function called process-ants-for-moves
. It is called once for each ant and is responsible for determining what that individual ant will do. In the last set of changes I uploaded, I added some code to keep track of how long the turns were taking and abort processing if it hit the 90% mark. It runs the process-ant
function for each ant, building up a list of where the ants new positions are so they can be sent to the server at the end of processing.
The process-ant
has a list of individual functions it can apply to the world state to find the best move for the ant. Cleaning things up with some pseudo code, it looks like this:
(defn find-move-through-functions "Run each function, return the first valid non-nil direction" [ant valid-directions valid-directions-with-back ants-last-move] (when (not-empty valid-directions)) (loop [functions-to-run {move-to-defend-our-hills :defense move-to-emergency-defense :e-defense move-to-capture-hill :capture move-away-from-enemy :run-away move-towards-food-classic :food move-in-random-direction :random}] (if (not-empty functions-to-run) ; Still functions to check (let [the-function-stuff (first functions-to-run) the-function (key the-function-stuff) the-function-purpose (val the-function-stuff) result (apply the-function ...)] (if (= :none result) nil ; The answer is 'don't move' (if (empty? result) (recur (rest functions-to-run)) ; No answer, try next (random direction from result))))))))
This works quite well. It runs each function in turn for the ant. If the function returns nil
, the next function is run. Otherwise a random direction for the list of possibilities the function returns is chosen as the ant’s move.
The Move Functions
There are quite a few of these, including some from experimentation that don’t actually get run. Here is the description of those that are in use, in priority order. They all tend to follow the same pattern. I never put it into a macro or function, but that would have been smart. Here is the basic template:
(defn some-move-function [ant _ _] (cond (check that that function applies, such as if we see enemies) nil ; It doesn't :else (let [distances (calculate distances to things of interest) spots (sort distances and filter out things too far away) visible-spots (stuff in spots that's line-of-site of ant) closest-spot (first (first visible-spots))] (when closest-spot ; If something matches the criteria ; Some additional processing may be done here (utilities/direction ant closest-spot)))))
The line-of-sight code was a pretty big improvement. It meant that ants no longer got stuck against water trying to get to a piece of food on the other side. The function is basically Bresenham’s line algorithm. It’s not perfect, but it worked well enough and was pretty fast. As it walks along the line, it checks for water. If it doesn’t find any, the line-of-sight is clear.
Each function returns a set of directions the ant should move in, and the code picks one randomly. The selection is weighted so the ants tend to move in straight lines.
move-to-defend-our-hills
This is the function that uses up most of the global state. The global variable *current-defense*
is checked to see what kind of defense we should be running. This is set at the start of each turn based on the number of ants we have per hill.
This is the code that sets up the little formations around my hills. First the ant is checked against the list of defense positions. If the ant is already on one of the spots, the answer is the keyword :none
, meaning “don’t move from that spot.”
If the ant isn’t in a good position, it checks to see if it’s within visual range of a spot that does need filling. If so, it goes straight to it. This has the effect that newly spawned ants first action is to move into any holes in the defensive positions. Because of the way the defenses are setup (and since ants can’t move diagonally), attackers have to come in from a corner or two-abreast if they don’t want to lose their ants. This was remarkably effective.
move-to-emergency-defense
Once the defensive positions are filled, the next thing to do is to run emergency defenses. The code finds the closest visible hill to the ant, and then checks if any enemy ants are too close.
If they are, any ants that can see that hill rush back to try to stand on top of it. This floods the hill with defenders, hopefully allowing it to survive the onslaught. Since there is no real state on the ants, as soon as the danger is passed (the enemies are gone), they go straight back to normal behavior.
move-to-capture-hill
Whenever my ants see an enemy hill, they go straight for it. There is no hesitation, no strategy, just suicide charges. As a result of the next function (move-away-from-enemy
), my ants don’t tend to get near enemy hills unless the area is not well defended. I’ll often lose ants this way, but it’s amazingly effective at grabbing hills people leave undefended. If an enemy moves so much as moves a square or two off a hill, my ants have a good chance at taking it.
move-away-from-enemy
This function is my bot’s entire survival instinct. My ants run in the other direction from the closest enemy that’s within 8 squares. This does hamper food gathering, but it prevents my ants from getting roundly slaughtered during movement. Note that this function doesn’t apply if the ant is in one of the defense modes (since this function wouldn’t get run), or when the ant is within view distance of one of my hills (an old condition from before defense mode so my ants wouldn’t give up their home bases).
move-towards-food-classic
It’s called “classic” because I came up with a better function for finding food, but it was never turned on due to performance problems. This function simply moves the ant towards the nearest piece of food they can see (with the line-of-sight check). In practice, having multiple ants go after the same piece of food wasn’t a problem.
moves-in-random-direction
If nothing else came up with a move, the ant would go in a random direction. This caused (limited) exploration.
Things I Meant To Do
If you look through the code, you can find commented out sections from some of my experiments. One of the early ones was move-towards-food-res
. It would find the closest ant to each piece of food and record a reservation. This prevented multiple ants from going towards one piece of food. When I turned this on, my ants picked up much less food. The simpler method seemed to work better.
The better method of finding food was diffusion. There are globals and functions to take the food that the bot knows about and diffuse their influence, producing a gradient that the ants could follow to the areas with the most food (see influence mapping).
It turned out that this was much too slow. Others got it working (quite well), but I didn’t. Data structures were (I think) a big part of this. I was using a pseudo queue made out of a vector, and it wasn’t that fast. As it turns out Clojure has a queue that simply has no syntax yet which was very fast, but I didn’t get around to converting my code. The other thing that would have helped was not recalculating everything each turn. I started planning this but never got around to it either.
Many people used some form of this to explore the maps, which would have also been a smart thing to do.
Debugging
One thing I learned a fair bit about was debugging. It’s something difficult to do in Clojure (since there aren’t any native debuggers that I know about), so I had to invent some. I started debugging by sprinkling my code with statements like this:
(utilities/debug-log "Ant at " ant " doing " the-function-purpose ", going " dir)
The actual function looked like this:
(defn debug-log "Log something to the console for us to go through later" [& message] (when defines/logging-enabled (binding [*out* (if (nil? defines/*log-file*) *err* defines/*log-file*)] (apply println message) (flush))))
First the function checked to see if logging was enabled. If it was the *log-file*
global variable was checked. When defined, the code would write the log message out to a file (which I would use to follow what my bot was doing). If the file wasn’t defined, it would just spit the message to standard error (which would cause the ants server to mark my bot as crashed for producing unexpected output).
Later I started using the visualizer that was available in the forums, but I had to be careful. I found the python server wouldn’t terminate if I produced too much visualization output.
Summary
That’s basically it. I’ve put my bot’s source up on my GitHub account if you want to take a look at it. The Clojure code is in the src/
directory, and it’s pretty clear that it was under active development when I stopped. Code is still commented out in places from debugging.