This is a robot I wrote in 2001 for Robocode which was fairly new at the time.

If you're firing at a moving target with a projectile, you need to calculate the firing angle such that the projectile and the target will intersect rather than just fire in the direction of the target. I calculated the math to do this and implemented it along with some very basic motion routines.

If you're so inclined, the code can be downloaded and the javadoc is also available.

package com.oroup.robocode;

import robocode.*;

import java.util.Iterator;
import java.util.HashMap;
import java.util.Map;

import java.awt.geom.Point2D;

import java.text.NumberFormat;

/** <p>This is a tank for
 * <a href="http://robocode.alphaworks.ibm.com">Robocode</a>.</p>
 * <p>
 * The latest version should be available at
 * <a href="https://oroup.com/pub/warthog/">https://oroup.com/pub/warthog/</a></p>
 * <p>
 * The source code is available at <a href="https://oroup.com/pub/warthog/WartHog.java">https://oroup.com/pub/warthog/WartHog.java</a>
 * </p>
 * <p>The javadoc is available at <a href="https://oroup.com/pub/warthog/javadoc/com/oroup/robocode/WartHog.html">https://oroup.com/pub/warthog/javadoc/com/oroup/robocode/WartHog.html</a>
 * </p>
 * <p>This tank takes the basic strategy of always moving, trying to maintain
 * a fixed range from the best target and firing on it when it thinks
 * it can hit. In case you're wondering, the WartHog is what people call
 * the American A-10 Tank Killer airplane.</p>
 *
 * TODO:
 * <ul>
 * <li>The tank sometimes gets stuck against the walls. The movement algorithm
 * could be much smarter.</li>
 * <li>The constants are just first guesses and not tuned at all. I should
 * write statistics gathering routines to help tune those values or make
 * them dynamic.</li>
 * <li>If the tank can't detect any enemies (on a big field) the wandering
 * algorithm should be smarter.</li>
 * <li>If there are multiple enemies still present, we could go into "lurk"
 * mode to let everyone else fight it out, and then face the victor fresh.</li>
 * </ul>
 * @author Oliver Roup <a href="mailto:oroup@oroup.com">oroup@oroup.com</a> */
public class WartHog extends AdvancedRobot {

    /** A very small distance. Used for computing if two floating
     * points are "equal".
     *  */
    static final double EPSILON = 0.01;

    /** Anything closer than this, we fire full force.
     *  */
    static final double DISTANCE_MIN = 50.0;

    /** Anything further than this, we don't fire at all.
     *  */
    static final double DISTANCE_MAX = 500.0;

    /** If we're closer to a tank than this, we increase the range.
     *  */
    static final double RANGE_INNER = 200.0;

    /** If we're further from a tank than this, we decrease the range.
     *  */
    static final double RANGE_OUTER = 250.0;

    /** This is how many degrees off the tangent we use to change
     * ranges.  90 would be perpendicular to the tangent, and 0 would
     * be parallel to the tangent. (And the range would never change)
     *  */
    static final double RANGE_CHANGE_ANGLE = 60.0;

    /** A distance further than the tank can go in a turn.
     *  */
    static final double BIG_DISTANCE = 100.0;

    /** A turn larger than the rader can turn in one turn.
     *  */
    static final double BIG_TURN = 90.0;

    /** The tank is modeled as a circle around it's center with this
     * radius.
     *  */
    static final double TANK_RADIUS = 18.0;

    /** The maximum force that the gun can shoot.
     *  */
    static final double FORCE_MAX = 3.0;

    /** The minimum force that the gun should shoot.
     *  This should probably be the fun recharge rate.
     *  */
    static final double FORCE_MIN = 0.5;

    /** How many turns an enemy must be stationary to be counted as
     * stationary.
     *  */
    static final int STATIONARY_COUNT = 25;

    /** A Map of enemies, keyed off their names.
     *  */
    Map enemies = null;

    /** A NumberFormat for printing doubles.
     *  */
    NumberFormat nf = null;

    /** The current direction of the tank. true = FORWARD, false =
     * BACKWARD.
     *  */
    boolean direction;

    /** The tank execution loop:
     * <ol>
     * <li>Spin the radar.
     * <li>Find the best target.
     * <li>Move to the correct range from the enemy.
     * <li>Compute a firing solution.
     * <li>Aim the gun.
     * <li>If we can hit, fire.
     * </ol>
     *
     */
    public void run() {

        // Create a NumberFormat for printing doubles.

        nf = NumberFormat.getInstance();
        nf.setMaximumFractionDigits(2);
        nf.setMinimumFractionDigits(2);

        // Create a hashmap for tracking enemies.

        enemies = new HashMap();

        // Set the robot to move the tank, gun and radar independently.

        setAdjustGunForRobotTurn(true);
        setAdjustRadarForGunTurn(true);

        // Set the current direction to be foward.

        direction = true;

        // Keep track of whether we've executed this turn yet.

        boolean turnExecuted;

  	while (true) {

            // Mark the current turn as not executed yet.

            turnExecuted = false;

            // Set the radar to turn right as far as it'll go.

            setTurnRadarRight(BIG_TURN);

            // Move the tank as far as it can go in it's current direction.

            if (direction) {
                setAhead(BIG_DISTANCE);

            }
            else {
                setBack(BIG_DISTANCE);
            }

            // Find the best enemy to shoot at.

            Enemy enemy = getBestEnemy();

            // If there is such an enemy...

            if (enemy != null) {

                // Find out how far we are from the enemy.

                double dist = distance(enemy.getPoint());

                // Calculate a line of travel tangent to the enemy.

                double tangent =
                    normalizeAngle(bearing(enemy.getPoint()) - 90.0);

                // Find an angle that would bring us closer.

                double bearIn = normalizeAngle(tangent + RANGE_CHANGE_ANGLE);

                // Find an angle that would push us further out.

                double bearOut = normalizeAngle(tangent - RANGE_CHANGE_ANGLE);

                // If we're too far come closer..

                if (dist > RANGE_OUTER) {
                    if (direction) {
                        setHeading(bearIn);
                    }
                    else {
                        setHeading(bearOut);
                    }
                }

                // If we're too close go farther..

                else if (dist < RANGE_INNER) {
                    if (direction) {
                        setHeading(bearOut);
                    }
                    else {
                        setHeading(bearIn);
                    }
                }

                // Otherwise, move so that the range doesn't change.

                else {
                    setHeading(tangent);
                }

                // Compute the firing solution to the target..

                FiringSoln soln = computeFiringSolution(enemy);

                // Find the bearing for the solution and point the gun there.

                double solnBearing = bearing(soln.getPoint());
                setGunHeading(solnBearing);

                // If the aim of the gun is good enough so that the

                // bullet will arrive within a tank radius of the target

                // and if there is enough charge on the gun, then fire

                // and mark the turn as executed.

                if ((gunBearingTolerance(soln.getPoint(),
                                         getGunHeading()) < TANK_RADIUS) &&
                    (getGunCharge() >= soln.getPower()) &&
                    (soln.getPower() != 0.0)) {
                    fire(soln.getPower());
                    turnExecuted = true;
                }
            }

            // If the turn never executed, then execute it.

            if (!turnExecuted) {
                execute();
            }
        }
    }

    /** If we see an enemy, figure out where it is and store it's data
     * away.
     *
     * @param e ScannedRobotEvent */
    public void onScannedRobot(ScannedRobotEvent e) {
        // Find the distance and direction to the scanned robot.

        double distance = e.getRobotDistance();
        double dir = normalizeAngle(e.getRobotBearing() +
                                    getHeading());

        // Calculate an absolute enemy position and store that along

        // with other pertinent enemy data.

        updateEnemyData(e.getRobotName(),
                        getPoint().sum(getRadDelt(distance, dir)),
                        e.getRobotHeading(), e.getRobotVelocity(),
                        e.getTime());
    }


    /** If we hit a robot, change directions if that will get us away
     * from what we hit.
     *
     * @param e HitRobotEvent */
    public void onHitRobot(HitRobotEvent e) {
        double bearDif = Math.abs(normalizeAngle(e.getBearing()));
        if (((direction) && (bearDif < 90.0)) ||
            ((!direction) && (bearDif > 90.0))) {
              direction = !direction;
          }
    }

    /** If we hit a wall change directions.
     *
     * @param e HitWallEvent */
    public void onHitWall(HitWallEvent e) {
        double bearDif = Math.abs(normalizeAngle(e.getBearing()));
        if (((direction) && (bearDif < 90.0)) ||
            ((!direction) && (bearDif > 90.0))) {
            direction = !direction;
        }
    }

    /** If a robot dies, remove it from our list so we don't go after
     * dead enemies.
     *
     * @param e RobotDeathEvent */
    public void onRobotDeath(RobotDeathEvent e) {
        enemies.remove(e.getRobotName());
    }

    /** Find the juiciest target to shoot at.
     *
     * @return The best target
     */
    public Enemy getBestEnemy() {
        Iterator iter = enemies.values().iterator();
        double closest = Double.MAX_VALUE;
        Enemy result = null;

        // Iterate through all known enemies..

        while (iter.hasNext()) {
            Enemy enemy = (Enemy) iter.next();

            // For each enemy, compute the firing solution..

            FiringSoln soln = computeFiringSolution(enemy);

            // and find the distance to that point.

            double distance = distance(soln.getPoint());

            // if this is the closest enemy so far, keep a record of it.

            if (distance < closest) {
                closest = distance;
                result = enemy;
            }
        }

        // return the firing solution to the closest enemy.

        return result;
    }

    /** Takes the data about a single enemy and the firepower we
     * intend to fire at and returns the
     * distance and direction to hit that robot.
     *
     * This algorithm uses some ugly trig to compute the amount
     * of time it will take for both the bullet and the enemy
     * to collide on their current paths.
     *
     * @param enemy The enemy to compute a solution to.
     * @return The firing solution to enemy
     */
    FiringSoln computeFiringSolution(Enemy enemy) {

        double force = computeForce(enemy);

        double difDelt = Math.sin(Math.toRadians(180.0 +
                                                 bearing(enemy.getPoint()) -
                                                 enemy.getHeading()));

        double thetaRad = Math.asin((enemy.getVelocity() /
                                     bulletVelocity(force)) *
                                    difDelt) +
            Math.toRadians(bearing(enemy.getPoint()));

        double time =(distance(enemy.getPoint()) * difDelt) /
            (bulletVelocity(force) *
             Math.sin(Math.toRadians(enemy.getHeading()) -
                      thetaRad));

        // Decompose the tanks distance and heading into an x,y vector.

        Point delt = getRadDelt(time * enemy.getVelocity(), enemy.getHeading());

        //Compute where the tank will be after the delta.

        Point pos = enemy.getPoint().sum(delt);

        // Return a new firing solution to the tanks expected position.

        return  new FiringSoln(pos, force);
    }

    /** Compute how hard to fire at an enemy.
     *
     * @param enemy The enemy to calculate force for
     * @return The force we should fire at.
     */
    public double computeForce(Enemy enemy) {

        // Find the distance to the enemy.

        double distance = distance(enemy.getPoint());

        double force;

        // If the enemy hasn't moved in a while, fire full force.

        if (enemy.getCountSinceMove() > STATIONARY_COUNT) {
            force = FORCE_MAX;
        }

        // If the enemy is close, fire full force.

        else if (distance <= DISTANCE_MIN) {
            force = FORCE_MAX;
        }

        // If the enemy is not close, but still close enough to shoot

        // use a linear interpolation of how much force to use.

        else if ((distance > DISTANCE_MIN) &&
                 (distance < DISTANCE_MAX)) {
            force = FORCE_MIN + (((distance - DISTANCE_MIN) /
                  (DISTANCE_MAX - DISTANCE_MIN)) *
                 (FORCE_MAX - FORCE_MIN));
        }

        // If the enemy is too far away, don't shoot at all.

        else {
            force = 0.0;
        }
        return force;
    }

    /** Calculate how long a bullet will take to fly a certain distance.
     *
     * @param distance The distance for the bullet to travel
     * @param force The force of the bullet.
     * @return The time the bullet will take to travel.
     */
    public double bulletFlightTime(double distance, double force) {
        return distance / bulletVelocity(force);
    }

    public double bulletVelocity(double force) {
        return 20.0 - (3.0 * force);
    }

    /** Given a target point, and a gun bearing, figure out how close the bullet will come to the target.
     *
     * @param target The point we are shooting at.
     * @param gunBearing The actual bearing of our gun.
     * @return The distance the bullet will come to the target.
     */
    public double gunBearingTolerance(Point target,
                                      double gunBearing) {
        double distance = distance(target);
        Point bd = getPoint().sum(getRadDelt(distance, gunBearing));
        return target.distance(bd);
    }

    /** Save away all the data about enemies that we see.
     *
     * @param name The name of the enemy.
     * @param point The position of the enemy.
     * @param heading The enemy heading
     * @param velocity The enemy velocity
     * @param time The time we saw this enemy.
     */
    public void updateEnemyData(String name, Point point,
                                double heading, double velocity,
                                long time) {
        Enemy enemy = (Enemy) enemies.get(name);
        if (enemy == null) {
            enemy = new Enemy(name);
            enemies.put(name, enemy);
        }
        enemy.setData(point, heading, velocity, time);
    }

    /** Turns the tank to the supplied heading,
     * using whichever direction
     * is fastest.
     *
     * @param heading The heading for the tank to assume
     */
    public void setHeading(double heading) {
	double deltHeading = normalizeAngle(heading - getHeading());
        if (deltHeading < 0.0) {
            setTurnLeft(Math.abs(deltHeading));
        }
        else if (deltHeading > 0.0) {
            setTurnRight(deltHeading);
        }
    }

    /** Turn the gun to the heading supplied using whichever direction is faster.
     *
     * @param heading The heading for the gun to assume.
     */
    public void setGunHeading(double heading) {
	double deltHeading = normalizeAngle(heading - getGunHeading());
        if (deltHeading < 0.0) {
            setTurnGunLeft(Math.abs(deltHeading));
        }
        else if (deltHeading > 0.0) {
            setTurnGunRight(deltHeading);
        }
    }

    /**
     * Calculates the distance from the tank to the point.
     *
     * @param p The point
     * @return The distance
     */
    public double distance(Point p) {
	return p.distance(getPoint());
    }

    /** Calculates the bearing from the tank to the point.
     *
     * @param p The point
     * @return The bearing
     */
    public double bearing(Point p) {
        return p.bearing(getPoint());
    }

    /** Converts any angle to be between -180 and 180.
     *
     * @param angle The supplied angle.
     * @return The normalized angle
     */
    public double normalizeAngle(double angle) {
        double nAngle = angle % 360.0;
        if (nAngle < -180.0) {
            return nAngle + 360.0;
        }
        if (nAngle > 180.0) {
            return nAngle - 360.0;
        }
        return nAngle;
    }

    /** Returns the current position of the tank as a point.
     *
     * @return The current position of the tank
     */
    public Point getPoint() {
        return new Point(getX(), getY());
    }

    /** Given a distance and a direction, compute the point we will arrive at.
     *
     * @param dist The distance
     * @param heading The heading
     * @return The point
     */
    public Point getRadDelt(double dist, double heading) {
        return new Point(dist *
                         Math.cos(Math.toRadians(normalizeAngle(90.0 -
                                                                heading))),
                         dist *
                         Math.sin(Math.toRadians(normalizeAngle(90.0 -
                                                                heading))));
    }

    /**
     * There is a point we want to target, but we want to
     * keep a certain distance from it, and there's a certain
     * amount of distance we can cover this turn. This function
     * uses the law of cosines to compute which direction to head
     * relative to the target bearing so that we maintain the
     * range from the target.
     * Law of cosines: c^2 = a^2 + b^2 - 2ab cos(C);
     * Not currently used.
     *
     * @param distToTarget Distance to target
     * @param rangeToKeep Range to keep
     * @param distanceToCover Distance to cover
     * @return Angle to target
     */
    public double triangulate(double distToTarget,
                              double rangeToKeep,
                              double distanceToCover) {
        double result;
        if ((rangeToKeep + distanceToCover < distToTarget) ||
            (distToTarget == 0.0)) {
            result =  0.0;
        }
        else if(distToTarget + distanceToCover < rangeToKeep) {
            result = 180.0;
        }
        else {
            result =
                Math.toDegrees(Math.acos((Math.pow(distToTarget, 2.0) +
                                          Math.pow(distanceToCover, 2.0) -
                                          Math.pow(rangeToKeep, 2.0)) /
                                         (2.0 * distToTarget *
                                          distanceToCover)));
        }
        return result;
    }

    private class Enemy {

        /** The name of this enemy
         *
         */
        String name;
        /** The position of this enemy.
         *
         */
        Point point;
        /** The heading of this enemy.
         *
         */
        double heading;
        /** The velocity of this enemy.
         *
         */
        double velocity;
        /** The time we took these readings.
         *
         */
        long time;
        /** The number of readings we've taken.
         *
         */
        int count;
        /** How many readings it's been since this tank moved.
         *
         */
        int countSinceMove;

        /** Create a new enemy with null data.
         *
         * @param name The name of the tank.
         */
        Enemy(String name) {
            this.name = name;
            point = null;
            heading = 0.0;
            velocity = 0.0;
            time = 0;
            count = 0;
            countSinceMove = 0;
        }

        /** Update the data about an enemy.
         *
         * @param newPt The new point for the tank.
         * @param heading The new heading
         * @param velocity The new velocity
         * @param time The new time we made the readings.
         */
        void setData(Point newPt,
                     double heading, double velocity,
                     long time) {
            if (point != null) {
                // If the new point is very close to the old point,

                // consider the tank not to have moved.

                if (point.distance(newPt) < EPSILON) {
                    countSinceMove++;
                }
                else {
                    countSinceMove = 0;
                }
            }
            point = newPt;
            this.heading = heading;
            this.velocity = velocity;
            this.time = time;
            count++;
        }

        /** Return the tanks name
         *
         * @return The tanks name
         */
        String getName() {
            return name;
        }

        /** Return where the enemy should be now, assuming it has not
         * changed direction or velocity
         *
         * @return The tanks calculated position.
         */
        Point getPoint() {
            double elapsed = (double) (getTime() - time);
            return point.sum(getRadDelt(velocity * elapsed,
                                        heading));
        }

        /** The tanks heading.
         *
         * @return The tanks heading.
         */
        double getHeading() {
            return heading;
        }

        /** The tank velocity.
         *
         * @return The tank velocity.
         */
        double getVelocity() {
            return velocity;
        }

        /** The time we took this reading.
         *
         * @return The time of the reading.
         */
        long getTime() {
            return time;
        }

        /** how many readings have we taken.
         *
         * @return The number of readings taken
         */
        int getCount() {
            return count;
        }

        /** returns how many readings since this tank moved.
         *
         * @return The number of readings since this tank moved.
         */
        int getCountSinceMove() {
            return countSinceMove;
        }

        /** Return a string representation of this enemy.
         *
         */
        public String toString() {
            StringBuffer sb = new StringBuffer();
            sb.append("Enemy[name: ");
            sb.append(name);
            sb.append("; Position: ");
            sb.append(point.toString());
            sb.append("; Heading: ");
            sb.append(nf.format(heading));
            sb.append("; Velocity: ");
            sb.append(nf.format(velocity));
            sb.append("; Time: ");
            sb.append(time);
            sb.append("; ScanCount: ");
            sb.append(count);
            sb.append("; countSinceMove: ");
            sb.append(countSinceMove);
            sb.append("]");
            return sb.toString();
        }

    }

    private class FiringSoln {

        /** The position of this firing solution.
         *
         */
        Point point;
        /** The power of this firing solution.
         *
         */
        double power;

        /** Given a position and a firing power, create a new Firing Solution.
         *
         * @param point The position
         * @param power The power
         */
        FiringSoln(Point point, double power) {
            this.point = point;
            this.power = power;
        }

        /** Return the position of this firing solution.
         *
         * @return The position
         */
        Point getPoint() {
            return point;
        }

        /** Return the power of this firing solution.
         *
         * @return The poewr
         */
        double getPower() {
            return power;
        }

        /** Return a string representation of this Firing Solution
         *
         */
        public String toString() {
            StringBuffer sb = new StringBuffer();
            sb.append("FiringSoln[Pos: ");
            sb.append(point.toString());
            sb.append("; Power: ");
            sb.append(nf.format(power));
            sb.append("]");
            return sb.toString();
        }

    }

    /** Mutable point class - used for points and deltas. */
    private class Point extends Point2D.Double {

        /** Given an x and a y coordinate, return a new point.
         *
         * @param x x coord
         * @param y y coord
         */
        Point(double x, double y) {
            super(x, y);
        }

        /** Add a point to this one.
         *
         * @param point The point to add to this one.
         * @return The sum
         */
        Point sum(Point point) {
            return new Point(getX() + point.getX(),
                            getY() + point.getY());
        }

        /** Calculates the bearing from this point to a supplied one.
         *
         * @param p The supplied point
         * @return The bearing
         */
        public double bearing(Point p) {
            return Math.toDegrees(Math.atan2(getX() - p.getX(),
                                             getY() - p.getY()));
        }

        /** Returns a string representation of this point.
         *
         */
        public String toString() {
            StringBuffer sb = new StringBuffer();
            sb.append("(");
            sb.append(nf.format(getX()));
            sb.append(",");
            sb.append(nf.format(getY()));
            sb.append(")");
            return sb.toString();
        }
    }

    public static void main(String[] args) throws Throwable {
    }

}