Monday 18 May 2009

Clojure and Robocode

Robocode is an educational programming game originally provided by IBM. Users write code to control miniature tanks in battles with other user written robots. Each tank has the same components but different programming. Users write code to control the movement, targeting and radar features of their robot.

Robocode battle!

Robocode uses Java as the programming language, and robots are shared by packaging them into Jar files. This means we should, with very little effort, be able to use Clojure to write a robot. Start by downloading the latest version of Robocode, available here.

The first fly in the ointment is that Robocode restricts the use of third-party JARs. If you try to just add clojure.jar to the class path you'll get an error message like this:


Preventing uk.co.fatvat.robot.FatRobot (1) from access: (java.io.FilePermission clojure.jar read):
You may only read files in your own root package directory.
SYSTEM: An error occurred during initialization of uk.co.fatvat.robot.FatRobot (1)
SYSTEM: java.lang.ExceptionInInitializerError


To fix this, edit the start up script (robocode.bat or robocode.sh depending on whether you are running Windows or not) and make sure you disable the security manager by adding -DNOSECURITY=true to the startup line. This disables the security manager meaning that there are no restrictions on what the robot can do. The security manager is in place in case you are a sentient robot killing machine. Be warned.

Without further ado, here's the most basic robot imaginable, borrowed entirely from My First Robot and converted over to Clojure. This robot moves back and forth, turns the gun around and fires at anything that looks at him slightly funny.


(ns uk.co.fatvat.robot.FatRobot
(:gen-class :extends robocode.Robot))

(defn -run
[robot]
"Infinite loop whilst robot is alive"
(doto robot
(.ahead 500)
(.turnGunRight 360)
(.back 500))
(recur robot))

(defn -onScannedRobot
[robot event]
(doto robot
(.fire 1)))


It's not exactly the most exciting robot in the world and even loses against a robot that sits still and turns the turret around! How can we make it a little bit cleverer?

The robot below extends from AdvancedRobot which allows non-blocking calls, writing to the file system and custom events.


(ns uk.co.fatvat.robot.NotQuiteAsBad
(import (java.awt Color))
(import (robocode Rules))
(import (robocode.util Utils))
(:gen-class :extends robocode.AdvancedRobot :init create-robot :state state))


This shows two bits of the :gen-class directive I haven't used before. :init allows you to specify a constructor routine. The return value for this function is unusual in that it is always a vector with two elements. The first represents any arguments to the superclass (in this case empty) and the second represents any state that the object needs. The :state keyword allows you to name the method of accessing the state.


(defstruct target-details :distance :bearing :energy :velocity)

(defn -create-robot
[]
"Robot records a list of events and performs actions based on these observations"
[[] (ref [])])


The constructor function simply returns a reference to the state which is initially empty. We also define a structure representing a sighting of the enemy. These sightings will be logged in our state and will be used to determine the strategy.


(defn -run
[robot]
"Infinite loop whilst robot is alive"
(setup-robot robot)
(loop [x 1] ;; TODO is there a better idiom for an infinite loop?
(walk robot)
(recur 1)))

(defn -onScannedRobot
[robot event]
(let [distance (.getDistance event)
name (.getName event)
energy (.getEnergy event)
velocity (.getVelocity event)
bearing (.getBearing event)]
(dosync
(alter (.state robot) conj (struct target-details distance bearing energy velocity)))
(attack robot)))


onScannedRobot now records the details of the last observation (dosync sets up the transaction, alter applies the function given (conj) with the given arguments.


(defn- setup-robot
[robot]
"Ensure robot looks pretty"
(doto robot
(.setAdjustRadarForGunTurn true)
(.setColors Color/RED Color/BLACK Color/RED)))

(defn- attack
[robot]
"Based on the accrued events, hurt robots"
(let [latest (last @(.state robot))]
(.turnRight robot (get latest :bearing))
(when (zero? (get latest :velocity))
(.fire robot 3))
(.setTurnRadarRight robot 360)))

(defn- walk
[robot]
"Go for a walk around the outside of the building"
(let [x (mod (.getHeading robot) 90)]
(.ahead robot 50)
(when (not (zero? x))
(.turnLeft robot x))
(when (zero? (.getVelocity robot))
(.turnRight robot 90))))


walk simply makes the robot run around the outside of the arena. This is heavily based on the example code here. attack just waits for a stationary robot, turns and fires!

This at least allows me to beat the Fire default robot 10-0 (most of the time!).

There are a load more strategies and patterns available on the Robocode Wiki.

This was quite a useful learning exercise because I found out about init and state. One problem I ran into was that init doesn't allow you to call methods on the object that is about to be constructed. This was fixed recently, and now there is a corresponding post-init hook.

You can find all the code on GitHub.

8 comments:

  1. It would likely be more idiomatic to use (while true (..)).

    ReplyDelete
  2. Thanks, that makes a lot more sense!

    ReplyDelete
  3. You are looking for 'repeatedly'.

    From the clojure api:

    (repeatedly f)
    Takes a function of no args, presumably with side effects, and returns an infinite lazy sequence of calls to it

    I'll have to try out these robots soon :).

    ReplyDelete
  4. Thanks boR_ - repeatedly looks almost like what I needed - I think the laziness would probably bite me though! Maybe needs a doall somewhere in there too!

    ReplyDelete
  5. This was what I was looking for!
    I am trying to learn Clojure and it might be a fun way to do it, except for the added complexity of fussing with Java Methods.

    In my early Java days I wrote some reasonable robots, let's see if I can beat myself.
    I was about in the middle of all standard bots(usually the crazy or the wall following won), using some dodging and predictive targeting.

    ReplyDelete
  6. Can you add an explanation how to compile and package your bot so it's found by Robocode?

    I added that line to my .sh file, but I don't know how to package my bot into a jar file and how to make Robocode find it.

    ReplyDelete
  7. Check out the github project, there's an ant build script in there somewhere that makes the JAR file for Robocode.

    ReplyDelete