Skip to content

Instantly share code, notes, and snippets.

@subnomo
Last active November 18, 2016 19:30
Show Gist options
  • Save subnomo/da6ae7ae8705fea5fa61 to your computer and use it in GitHub Desktop.
Save subnomo/da6ae7ae8705fea5fa61 to your computer and use it in GitHub Desktop.
A robocode robot I wrote in high school.
package subnomo;
import robocode.AdvancedRobot;
import robocode.HitByBulletEvent;
import robocode.RobotDeathEvent;
import robocode.ScannedRobotEvent;
import robocode.util.Utils;
import java.awt.*;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
class WaveBullet {
private double startX, startY, startBearing, power;
private long fireTime;
private int direction;
private int[] returnSegment;
public WaveBullet(double x, double y, double bearing, double power, int direction, long time, int[] segment) {
startX = x;
startY = y;
startBearing = bearing;
this.power = power;
this.direction = direction;
fireTime = time;
returnSegment = segment;
}
public double getBulletSpeed() {
return 20 - power * 3;
}
public double maxEscapeAngle() {
return Math.asin(8 / getBulletSpeed());
}
public boolean checkHit(double enemyX, double enemyY, long currentTime) {
// if the distance from the wave origin to our enemy has passed
// the distance the bullet would have traveled...
if (Point2D.distance(startX, startY, enemyX, enemyY) <=
(currentTime - fireTime) * getBulletSpeed()) {
double desiredDirection = Math.atan2(enemyX - startX, enemyY - startY);
double angleOffset = Utils.normalRelativeAngle(desiredDirection - startBearing);
double guessFactor = Math.max(-1, Math.min(1, angleOffset / maxEscapeAngle())) * direction;
int index = (int) Math.round((returnSegment.length - 1) / 2 * (guessFactor + 1));
returnSegment[index]++;
return true;
}
return false;
}
} // end WaveBullet class
public class Nanite extends AdvancedRobot {
ArrayList<WaveBullet> waves = new ArrayList<WaveBullet>();
static int[] stats = new int[31]; // 31 is the number of unique GuessFactors we're using
// Note: this must be odd number so we can get
// GuessFactor 0 at middle.
int direction = 1;
// This class makes it easy to keep information about the enemy robots
class Enemy {
public Point2D.Double pos;
public double energy;
public double bearing;
public double heading;
public double distance;
public double velocity;
public boolean live;
}
// This class makes it easy to keep information about enemy "waves" to dodge bullets
class EnemyWave {
long fireTime;
int direction;
double bulletVelocity, directAngle, distanceTraveled;
Point2D.Double fireLocation;
}
// Some globals
// Using hashtable because it's less strict than an array (also faster!)
private static Hashtable<String, Enemy> enemies = new Hashtable<String, Enemy>();
private static Enemy target;
// Point2D represents points in an (x, y) coordinate space
private static Point2D.Double myPos;
private static Point2D.Double nextDest;
private static Point2D.Double lastPos;
private static double myEnergy;
private static double bulletPower;
// Wavesurfing (bullet dodging) globals
public static double oppEnergy = 100d;
public static int BINS = 47;
public static double surfStats[] = new double[BINS];
public ArrayList<EnemyWave> enemyWaves;
public ArrayList<Integer> surfDirections;
public ArrayList<Double> surfAbsBearings;
public Point2D.Double enemyLocation;
public Point2D.Double myLocation;
// Wallsmoothing (keeping distance from walls) globals
public static Rectangle2D.Double fieldRect;
public static final double WALL_STICK = 160;
// Math.PI / 2 would be perpendicular movement, less will keep us moving away slightly
public static final double NOT_PI = 1.25;
public void run() {
setColors(Color.darkGray, Color.black, Color.darkGray);
fieldRect = new Rectangle2D.Double(18, 18, getBattleFieldWidth() - 36, getBattleFieldHeight() - 36);
enemyWaves = new ArrayList<EnemyWave>();
surfDirections = new ArrayList<Integer>();
surfAbsBearings = new ArrayList<Double>();
// This allows for the gun to move independent of robot
setAdjustGunForRobotTurn(true);
// This allows for the radar to move independent of the gun
setAdjustRadarForGunTurn(true);
// Tells robocode to spin my radar infinity times
setTurnRadarRightRadians(Double.POSITIVE_INFINITY);
// Creates a new Enemy instance, this will be our target robot
target = new Enemy();
nextDest = lastPos = myPos = new Point2D.Double(getX(), getY());
while (true) {
// Updates position
myPos = new Point2D.Double(getX(), getY());
// Updates energy
myEnergy = getEnergy();
// Waits until scanning is complete, which takes at most 9 ticks apparently
if (getTime() > 9 && target.live) {
// Updates bulletPower, this calculation is important is takes into account my energy,
// the enemy's energy, as well as the distance to the enemy
bulletPower = Math.min(Math.min(myEnergy / 6d, 1300d / target.distance), target.energy / 3d);
if (getOthers() > 1)
movement();
}
execute();
}
}
// Movement thanks to bot HawkOnFire, I used then commented some of its code
public void movement() {
double destDist = myPos.distance(nextDest);
// Find new destination if current one reached
if (destDist < 15) {
double addLast = 1 - Math.rint(Math.pow(Math.random(), getOthers()));
Rectangle2D.Double battleField = new Rectangle2D.Double(30, 30, getBattleFieldWidth() - 60, getBattleFieldHeight() - 60);
Point2D.Double testPoint;
// Calculate test points and test them for feasibility
// If feasible, make point the new destination
for (int i = 0; i < 200; i++) {
testPoint = getPoint(myPos, Math.min(target.distance * 0.8, 100 + 200 * Math.random()), 2 * Math.PI * Math.random());
if (battleField.contains(testPoint) && evaluate(testPoint, addLast) < evaluate(nextDest, addLast))
nextDest = testPoint;
}
lastPos = myPos;
} else {
// Get angle of turn relative to current heading
double angle = getAngle(nextDest, myPos) - getHeadingRadians();
double direction = 1;
// If cos(angle) is less than zero,
// We know we will be going backwards
if (Math.cos(angle) < 0) {
angle += Math.PI;
direction = -1;
}
setAhead(destDist * direction);
setTurnRightRadians(angle = Utils.normalRelativeAngle(angle));
// If the angle to turn is larger than one, stop (so we can turn faster)
// Else, set the max velocity to 8.0
setMaxVelocity(Math.abs(angle) > 1 ? 0 : 8d);
}
}
public static double evaluate(Point2D.Double p, double addLast) {
// Adds more movement, staying still = bad
double eval = addLast * 0.08 / p.distanceSq(lastPos);
Enumeration enums = enemies.elements();
while (enums.hasMoreElements()) {
Enemy en = (Enemy) enums.nextElement();
// Math.min(en.energy/myEnergy,2) is multiplied because en.energy/myEnergy is an indicator how dangerous an enemy is
// Math.abs(Math.cos(calcAngle(myPos, p) - calcAngle(en.pos, p))) is bigger if the moving direction isn't good in relation
// to a certain bot. it would be more natural to use Math.abs(Math.cos(calcAngle(p, myPos) - calcAngle(en.pos, myPos)))
// 1 / p.distanceSq(en.pos) is anti-gravity (as it's known)
// http://robowiki.net/wiki/Anti-Gravity_Tutorial
if (en.live) {
eval += Math.min(en.energy / myEnergy, 2) * (1 + Math.abs(Math.cos(getAngle(myPos, p) - getAngle(en.pos, p)))) / p.distanceSq(en.pos);
}
}
return eval;
}
// Shoots, turning gun with values given in onScannedRobot method
public void shoot() {
/*If in a 1v1, use predictive firing, else shoot ignore physics and just shoot at current position.
In an arena fight, this works a lot better than you might think
Also, if my energy is low and the enemy's isn't, then I must not be making progress with predictive firing,
so I switch back to caveman shooting. DISCLAIMER: Disabled as I find regular shooting more effective and adaptive.
Predictive firing IS better, but needs much more code to implement correctly and is not worth it.*/
if (getGunTurnRemaining() == 0 && myEnergy > 1)
setFire(bulletPower);
setTurnGunRightRadians(Utils.normalRelativeAngle(getAngle(target.pos, myPos) - getGunHeadingRadians()));
}
public void onScannedRobot(ScannedRobotEvent e) {
myLocation = new Point2D.Double(getX(), getY());
Enemy baddie = enemies.get(e.getName());
// If the detected enemy isn't already in our "database", add it
if (baddie == null) {
baddie = new Enemy();
enemies.put(e.getName(), baddie);
}
// Collects some info about the enemy
baddie.energy = e.getEnergy();
baddie.live = true;
// Calculates enemy's position, passing the TOTAL angle
// we add getHeadingRadians() because we want the total angle
// and e.getBearingRadians() only gives the angle relative to us
baddie.pos = getPoint(myLocation, e.getDistance(), getHeadingRadians() + e.getBearingRadians());
baddie.bearing = e.getBearingRadians();
baddie.heading = e.getHeadingRadians();
baddie.velocity = e.getVelocity();
baddie.distance = e.getDistance();
// If the current target is dead, or this newly scanned enemy is closer,
// or if weak, then the newly scanned enemy becomes the new target
if (!target.live || e.getDistance() < target.distance || e.getEnergy() <= 16d) {
target = baddie;
}
double absoluteBearing = getHeadingRadians() + e.getBearingRadians();
// If there's only one enemy left, then the radar is locked
if (getOthers() == 1)
setTurnRadarLeftRadians(getRadarTurnRemainingRadians());
//shoot();
// The last section here is for wave surfing (dodging bullets)
// My velocity sideways
double lateralVelocity = getVelocity() * Math.sin(e.getBearingRadians());
// If robot's sideways velocity is positive (or 0), direction is 1. Else, it's -1
surfDirections.add(0, lateralVelocity >= 0 ? 1 : -1);
surfAbsBearings.add(0, absoluteBearing + Math.PI);
// This DOES NOT refer to the global called bulletPower
double bulletPower = oppEnergy - e.getEnergy();
if (bulletPower < 3.01 && bulletPower > 0.09 && surfDirections.size() > 2) {
EnemyWave ew = new EnemyWave();
ew.fireTime = getTime() - 1;
ew.bulletVelocity = bulletVelocity(bulletPower);
ew.distanceTraveled = bulletVelocity(bulletPower);
ew.direction = surfDirections.get(2);
ew.directAngle = surfAbsBearings.get(2);
ew.fireLocation = (Point2D.Double) enemyLocation.clone();
enemyWaves.add(ew);
}
oppEnergy = e.getEnergy();
// Update after, as prediction needs previous, not current, position as wave source
enemyLocation = project(myLocation, absoluteBearing, e.getDistance());
if (getOthers() == 1) {
updateWaves();
doSurfing();
}
// Enemy absolute bearing, you can use your one if you already declare it.
double absBearing = getHeadingRadians() + e.getBearingRadians();
// find our enemy's location:
double ex = getX() + Math.sin(absBearing) * e.getDistance();
double ey = getY() + Math.cos(absBearing) * e.getDistance();
// Let's process the waves now:
for (int i = 0; i < waves.size(); i++) {
WaveBullet currentWave = (WaveBullet) waves.get(i);
if (currentWave.checkHit(ex, ey, getTime())) {
waves.remove(currentWave);
i--;
}
}
// don't try to figure out the direction they're moving
// they're not moving, just use the direction we had before
if (e.getVelocity() != 0) {
if (Math.sin(e.getHeadingRadians() - absBearing) * e.getVelocity() < 0)
direction = -1;
else
direction = 1;
}
int[] currentStats = stats; // This seems silly, but I'm using it to
// show something else later
WaveBullet newWave = new WaveBullet(getX(), getY(), absBearing, bulletPower, direction, getTime(), currentStats);
int bestindex = 15; // initialize it to be in the middle, guessfactor 0.
for (int i = 0; i < 31; i++)
if (currentStats[bestindex] < currentStats[i])
bestindex = i;
// this should do the opposite of the math in the WaveBullet:
double guessfactor = (double) (bestindex - (stats.length - 1) / 2) / ((stats.length - 1) / 2);
double angleOffset = direction * guessfactor * newWave.maxEscapeAngle();
double gunAdjust = Utils.normalRelativeAngle(absBearing - getGunHeadingRadians() + angleOffset);
setTurnGunRightRadians(gunAdjust);
if (getGunHeat() == 0 && gunAdjust < Math.atan2(9, e.getDistance()) && setFireBullet(bulletPower) != null)
waves.add(newWave);
}
public void onRobotDeath(RobotDeathEvent e) {
// When an enemy robot dies, update the hashtable
// so that we can stop shooting at him
enemies.get(e.getName()).live = false;
}
// Calculations
public static double limit(double value, double min, double max) {
// Finds the smallest of either: 1. the maximum possible value
// 2. the larger of either: the minimum possible value and the value
// Useful for the annoying predictive firing above
return Math.min(max, Math.max(min, value));
}
private static double getAngle(Point2D.Double p2, Point2D.Double p1) {
/**
* This gets the inverse tangent of x/y
* In this instance x = p2.x - p1.x
* And y = p2.y - p1.y
*
* This method helps by getting the angle (in radians)
* between the current heading and the heading that is
* necessary to reach the next destination.
*/
return Math.atan2(p2.x - p1.x, p2.y - p1.y);
}
private static Point2D.Double getPoint(Point2D.Double p, double dist, double angle) {
/**
* Returns a new instance of Point2D.Double that contains the enemy position
*
* How this works: it takes my robot's current position on the x axis,
* then it takes the distance multiplied by the sin of the angle
* (the angle being the distance in radians of my robot's current bearing
* to the enemy robot), which gives us the distance from my robot to the enemy
* robot just on the x axis, and then it adds my robot's position on the x axis
* to give the enemy's total position on the x axis.
*
* It does the same thing for the y axis and voila! We have the enemy's coordinates.
* */
return new Point2D.Double(p.x + dist * Math.sin(angle), p.y + dist * Math.cos(angle));
}
// The rest of the file is dedicated to dodging bullets aka "Wave Surfing"
public void updateWaves() {
for (int i = 0; i < enemyWaves.size(); i++) {
EnemyWave ew = enemyWaves.get(i);
ew.distanceTraveled = (getTime() - ew.fireTime) * ew.bulletVelocity;
// If the bullet has safely passed me, remove it from the list
if (ew.distanceTraveled > myLocation.distance(ew.fireLocation) + 50) {
enemyWaves.remove(i);
i--;
}
}
}
// Assesses enemy waves and returns the closest one
public EnemyWave getClosestSurfableWave() {
double closestDistance = 50000d; // just some big number at first
EnemyWave surfWave = null;
// For each EnemyWave in enemyWaves, find the closest wave
for (EnemyWave ew : enemyWaves) {
double distance = myLocation.distance(ew.fireLocation) - ew.distanceTraveled;
if (distance > ew.bulletVelocity && distance < closestDistance) {
surfWave = ew;
closestDistance = distance;
}
}
return surfWave;
}
// Returns the index of the GuessFactor... hard to explain
// http://robowiki.net/wiki/GuessFactor
public static int getFactorIndex(EnemyWave ew, Point2D.Double targetLocation) {
double offsetAngle = (getAngle(ew.fireLocation, targetLocation) - ew.directAngle);
double factor = Utils.normalRelativeAngle(offsetAngle) / maxEscapeAngle(ew.bulletVelocity) * ew.direction;
return (int) limit((factor * ((BINS - 1) / 2)) + ((BINS - 1) / 2), 0, BINS - 1);
}
// Given the EnemyWave that the bullet was on, and the point where we
// were hit, update our stat array to reflect the danger in that area.
public void logHit(EnemyWave ew, Point2D.Double targetLocation) {
int index = getFactorIndex(ew, targetLocation);
for (int i = 0; i < BINS; i++) {
// for the spot bin that we were hit on, add 1;
// for the bins next to it, add 1 / 2;
// the next one, add 1 / 5; and so on...
surfStats[i] += 1d / (Math.pow(index - i, 2) + 1);
}
}
public void onHitByBullet(HitByBulletEvent e) {
// If enemyWaves is empty, and still we were hit by a bullet,
// then we somehow missed detecting the wave.
if (!enemyWaves.isEmpty()) {
Point2D.Double hitBulletLocation = new Point2D.Double(e.getBullet().getX(), e.getBullet().getY());
EnemyWave hitWave = null;
// Check all EnemyWaves, try to find the one that hit us
for (EnemyWave ew : enemyWaves) {
// If the enemy wave is within 50 pixels and the bullet details (velocity) match, then we found it
if (Math.abs(ew.distanceTraveled - myLocation.distance(ew.fireLocation)) < 50
&& Math.abs(bulletVelocity(e.getBullet().getPower()) - ew.bulletVelocity) < 0.001) {
hitWave = ew;
break;
}
}
// If we did indeed find the correct wave for the bullet that hit us...
if (hitWave != null) {
// Then log the hit to update surfing stats
logHit(hitWave, hitBulletLocation);
// And remove the wave
enemyWaves.remove(enemyWaves.lastIndexOf(hitWave));
}
}
}
public Point2D.Double predictPosition(EnemyWave surfWave, int direction) {
Point2D.Double predictedPos = (Point2D.Double) myLocation.clone();
double predictedV = getVelocity();
double predictedH = getHeadingRadians();
double maxTurning, moveAngle, moveDir;
int counter = 0; // # of ticks in future
boolean intercepted = false;
do {
moveAngle = wallSmoothing(predictedPos, getAngle(surfWave.fireLocation, predictedPos) + (direction * NOT_PI), direction) - predictedH;
moveDir = 1;
// Checks for direction, flips if negative
if (Math.cos(moveAngle) < 0) {
moveAngle += Math.PI;
moveDir = -1;
}
moveAngle = Utils.normalRelativeAngle(moveAngle);
// This is the most you can turn in one tick, robocode rules
maxTurning = Math.PI / 720d * (40d - 3d * Math.abs(predictedV));
predictedH = Utils.normalRelativeAngle(predictedH + limit(-maxTurning, moveAngle, maxTurning));
// If the predicted velocity and the moveDir have different signs, we want to decelerate
// Else, we want to accelerate so we multiply moveDir by 2 and add that to the current predictedV
predictedV += (predictedV * moveDir < 0 ? 2 * moveDir : moveDir);
predictedV = limit(predictedV, -8d, 8d);
// Calculates new predicted position
predictedPos = project(predictedPos, predictedH, predictedV);
++counter;
// Checks if the wave has been intercepted
if (predictedPos.distance(surfWave.fireLocation) < surfWave.distanceTraveled
+ (counter * surfWave.bulletVelocity) + surfWave.bulletVelocity) {
intercepted = true;
}
} while (!intercepted && counter < 500);
return predictedPos;
}
// By getting the GuessFactor we can search the surfStats and return the safest direction to surf in
public double checkDanger(EnemyWave surfWave, int direction) {
int index = getFactorIndex(surfWave, predictPosition(surfWave, direction));
return surfStats[index];
}
public void doSurfing() {
EnemyWave surfWave = getClosestSurfableWave();
if (surfWave == null) return;
double dangerLeft = checkDanger(surfWave, -1);
double dangerRight = checkDanger(surfWave, 1);
double goAngle = getAngle(surfWave.fireLocation, myLocation);
if (dangerLeft < dangerRight) {
goAngle = wallSmoothing(myLocation, goAngle - (Math.PI / 2), -1);
} else {
goAngle = wallSmoothing(myLocation, goAngle + (Math.PI / 2), 1);
}
setBackAsFront(this, goAngle);
}
// Calculations for wave surfing
// Calculates the largest angle that could cause the bullet to hit me
public static double maxEscapeAngle(double v) {
return Math.asin(8d / v);
}
// Calculates velocity of a bullet based on the power
public static double bulletVelocity(double power) {
return 20d - (3d * power);
}
// Changes my angle as I approach walls and stuff, hitting walls is bad
public static double wallSmoothing(Point2D.Double botLocation, double angle, int orientation) {
while (!fieldRect.contains(project(botLocation, angle, WALL_STICK))) {
angle += orientation * 0.05;
}
return angle;
}
// Dictates movement
public static void setBackAsFront(AdvancedRobot robot, double goAngle) {
double angle = Utils.normalRelativeAngle(goAngle - robot.getHeadingRadians());
if (Math.abs(angle) > (Math.PI / 2)) {
if (angle < 0) {
robot.setTurnRightRadians(Math.PI + angle);
} else {
robot.setTurnLeftRadians(Math.PI - angle);
}
robot.setBack(100);
} else {
if (angle < 0) {
robot.setTurnLeftRadians(-angle);
} else {
robot.setTurnRightRadians(angle);
}
robot.setAhead(100);
}
}
// Takes my position, enemy's heading angle, and distance to enemy to predict enemy bullet pos
public static Point2D.Double project(Point2D.Double source, double angle, double length) {
return new Point2D.Double(source.x + Math.sin(angle) * length, source.y + Math.cos(angle) * length);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment