Skip to content

Dodging Bullets ​

Origins

Bullet dodging was developed and documented by the RoboWiki community. The biggest step forward, Wave Surfing, was invented by ABC, and the fire-timing details below come from the Wave Surfing Tutorial by Patrick Cupka (Voidious).

A bot cannot see bullets in flight. The radar reports enemy bots, never projectiles, so a bot that wants to dodge has to work out that a shot happened from indirect evidence, then guess where that shot is heading. Both steps are easy to get subtly wrong, and a dodge built on a false alarm can steer a bot straight into a real bullet.

Hearing the shot: the energy drop ​

Firing costs energy equal to the bullet power, and bullet power ranges from 0.1 to 3.0. So when an enemy's energy falls by an amount in that range between two scans, it probably fired.

The catch is that energy moves for other reasons too:

Event since the last scanEffect on enemy energy
Your bullet hit the enemyLoses 4p4p, plus 2(p−1)2(p - 1) when p>1p > 1
The enemy's bullet hit youGains 3p3p
The two bots collidedLoses 0.6
The enemy drove into a wallLoses max⁡(0,∣v∣/2−1)\max(0, \lvert v \rvert / 2 - 1)

Here pp is the power of the bullet involved and vv is the enemy's speed at impact. A power-2 hit on the enemy, for example, removes 10 energy and hides any shot fired on the same turn. The fix is bookkeeping: record the energy changes the bot already knows about, and remove them from the observed drop before testing it.

java
public final class FireDetector {
    private double previousEnergy = Double.NaN;
    private double knownChange;

    public void onMyBulletHitEnemy(double power) {
        knownChange -= 4 * power + 2 * Math.max(power - 1, 0);
    }

    public void onEnemyBulletHitMe(double power) {
        knownChange += 3 * power;
    }

    public void onCollision() {
        knownChange -= 0.6;
    }

    public double firedPower(double enemyEnergy) {
        double drop = previousEnergy - enemyEnergy + knownChange;
        previousEnergy = enemyEnergy;
        knownChange = 0;
        return drop > 0.09 && drop < 3.01 ? drop : 0;
    }
}
python
class FireDetector:
    def __init__(self) -> None:
        self.previous_energy = float("nan")
        self.known_change = 0.0

    def on_my_bullet_hit_enemy(self, power: float) -> None:
        self.known_change -= 4 * power + 2 * max(power - 1, 0)

    def on_enemy_bullet_hit_me(self, power: float) -> None:
        self.known_change += 3 * power

    def on_collision(self) -> None:
        self.known_change -= 0.6

    def fired_power(self, enemy_energy: float) -> float:
        drop = self.previous_energy - enemy_energy + self.known_change
        self.previous_energy = enemy_energy
        self.known_change = 0.0
        return drop if 0.09 < drop < 3.01 else 0.0
java
public final class FireDetector {
    private double previousEnergy = Double.NaN;
    private double knownChange;

    public void onMyBulletHitEnemy(double power) {
        knownChange -= 4 * power + 2 * Math.max(power - 1, 0);
    }

    public void onEnemyBulletHitMe(double power) {
        knownChange += 3 * power;
    }

    public void onCollision() {
        knownChange -= 0.6;
    }

    public double firedPower(double enemyEnergy) {
        double drop = previousEnergy - enemyEnergy + knownChange;
        previousEnergy = enemyEnergy;
        knownChange = 0;
        return drop > 0.09 && drop < 3.01 ? drop : 0;
    }
}
csharp
using System;

public sealed class FireDetector
{
    private double previousEnergy = double.NaN;
    private double knownChange;

    public void OnMyBulletHitEnemy(double power) =>
        knownChange -= 4 * power + 2 * Math.Max(power - 1, 0);

    public void OnEnemyBulletHitMe(double power) => knownChange += 3 * power;

    public void OnCollision() => knownChange -= 0.6;

    public double FiredPower(double enemyEnergy)
    {
        double drop = previousEnergy - enemyEnergy + knownChange;
        previousEnergy = enemyEnergy;
        knownChange = 0;
        return drop > 0.09 && drop < 3.01 ? drop : 0;
    }
}
typescript
class FireDetector {
    private previousEnergy = Number.NaN;
    private knownChange = 0;

    onMyBulletHitEnemy(power: number) {
        this.knownChange -= 4 * power + 2 * Math.max(power - 1, 0);
    }

    onEnemyBulletHitMe(power: number) {
        this.knownChange += 3 * power;
    }

    onCollision() {
        this.knownChange -= 0.6;
    }

    firedPower(enemyEnergy: number) {
        const drop = this.previousEnergy - enemyEnergy + this.knownChange;
        this.previousEnergy = enemyEnergy;
        this.knownChange = 0;
        return drop > 0.09 && drop < 3.01 ? drop : 0;
    }
}

Call the event methods from the bot's hit and collision handlers, then call firedPower on every scan of that enemy. The first scan returns 0, because there is no earlier energy to compare. The slightly widened limits, 0.09 and 3.01, follow the Wave Surfing Tutorial and absorb floating-point noise. Wall hits are missing from the class on purpose. A bot is only told about its own wall collisions, so an enemy's wall hit has to be inferred from a sudden stop next to a wall. Gun Heat Waves add a second filter: a drop cannot be a shot while the enemy's gun is still hot.

The shot is already a turn old ​

In classic Robocode, a bot notices the drop on the turn after the shot. The Wave Surfing Tutorial spells out what that means: the bullet left from the enemy's position on the previous turn, has already traveled one turn at 20−3p20 - 3p units per turn, and was aimed using what the enemy saw two turns ago. A dodge computed from the enemy's current position starts from the wrong spot.

Getting out of the way ​

A bullet flies in a straight line, so only one thing decides a hit: the angle from the shooter to the bot when the bullet arrives. Moving toward or away from the enemy barely changes that angle. Moving perpendicular to the line between the bots changes it fastest.

The widest angle a bot can reach is the maximum escape angle, arcsin⁡(8/s)\arcsin(8 / s), where 8 is the top bot speed and ss is the bullet speed. A power-3 bullet (s=11s = 11) gives the target up to about 46.7°, while a power-0.1 bullet (s=19.7s = 19.7) leaves only about 24.0°.

Moving perpendicular to the shooter changes the bullet's required angle fastest, up to the escape angle.
Moving perpendicular to the shooter changes the bullet's required angle fastest, up to the escape angle.

Where inside that range to go is the real question, and RoboWiki describes three answers:

  • Randomize. Early dodgers changed direction and speed at random, as in Random Movement. That works against simple guns and spreads the bot's positions fairly evenly, but it never adapts to the gun it faces.
  • Predict the gun. Bots such as TheArtOfWar computed where head-on, linear, and circular targeting would aim, and avoided all three. This is cheap and strong against those guns, but it has no answer to a gun that aims anywhere else, such as a statistical one.
  • Surf the wave. Record where the enemy's bullets actually hit, then move to the angle the enemy fires at least. This is Wave Surfing, and it costs far more code and CPU time than the other two.

Surfing raises a new question: how should the bot actually steer to the safe angle? Wave Surfing Implementations compares driving toward a chosen destination with simulating every turn of the dodge.

Platform notes ​

The energy rules above are the same on both platforms, with one exception. In classic Robocode, a bot extending the basic Robot class takes no wall damage, so only AdvancedRobot wall hits change energy. The one-turn detection delay is documented for classic Robocode. A Tank Royale bot should confirm it by drawing its waves and checking where real bullets appear.

Further Reading ​

Based on RoboWiki content (CC BY-SA 3.0) for classic Robocode and the official Robocode Tank Royale documentation.