Difference between revisions of "Basilisk"

From Robowiki
Jump to navigation Jump to search
Line 90: Line 90:
 
A huge thank you to all the contributors on the RoboWiki for your awesome tutorials and open source robots!
 
A huge thank you to all the contributors on the RoboWiki for your awesome tutorials and open source robots!
  
 +
== Code ==
 +
 +
'''Version 2.13'''
 +
 +
<syntaxhighlight>
 +
package slugzilla;
 +
import robocode.*;
 +
//import robocode.util.Utils;
 +
import java.awt.geom.*;
 +
import robocode.util.*;
 +
import java.awt.*;
 +
import static robocode.util.Utils.normalRelativeAngleDegrees;
 +
 +
public class Basilisk extends AdvancedRobot {
 +
 +
private static double lateralDirection;
 +
private static double lastEnemyVelocity;
 +
static final double MAX_VELOCITY = 8;
 +
    static final double WALL_MARGIN = 25;
 +
// static double alpha;
 +
static double theta;
 +
static double enemyFirePower;
 +
static double enemyBulletVelocity;
 +
    Point2D robotLocation;
 +
    Point2D enemyLocation;
 +
    double enemyDistance;
 +
    double enemyAbsoluteBearing;
 +
double enemyVelocity;
 +
    double movementLateralAngle;
 +
double direction = 1;
 +
static int bulletCount;
 +
static double hits;
 +
static double prevEnergy = 100.0;
 +
static double enemyEnergy;
 +
static double deltaT;
 +
static double lastDeltaT;
 +
static double lastReverseTime;
 +
    public void run() {
 +
        setAdjustRadarForGunTurn(true);
 +
setAdjustGunForRobotTurn(true);
 +
setBodyColor(new Color(40, 100, 100));
 +
setGunColor(new Color(34, 50, 50));
 +
setRadarColor(new Color(0, 255, 0));
 +
setScanColor(new Color(0, 255, 0));
 +
turnRadarRightRadians(Double.POSITIVE_INFINITY);
 +
        do {
 +
            scan();
 +
        } while (true);
 +
    }
 +
 +
    public void onScannedRobot(ScannedRobotEvent e) {
 +
//radar ↓
 +
double radarTurn = getHeadingRadians() + e.getBearingRadians() - getRadarHeadingRadians();
 +
 +
//movement ↓
 +
        robotLocation = new Point2D.Double(getX(), getY());
 +
        enemyAbsoluteBearing = getHeadingRadians() + e.getBearingRadians();
 +
        enemyDistance = e.getDistance();
 +
enemyVelocity = e.getVelocity();
 +
enemyEnergy = e.getEnergy();
 +
        enemyLocation = vectorToLocation(enemyAbsoluteBearing, enemyDistance, robotLocation);
 +
enemyBulletVelocity = 20 - 3*enemyFirePower;
 +
 +
//alpha = 1/(enemyDistance/19);
 +
//we move far away when close, but stabilize and move perpendicular when far ↓
 +
movementLateralAngle = Math.max(enemyDistance/2000, 0.075);
 +
if (prevEnergy > enemyEnergy) {
 +
enemyFirePower = prevEnergy - enemyEnergy;
 +
}
 +
move();
 +
prevEnergy = e.getEnergy();
 +
//gun ↓
 +
 +
//to beat the bullet shielders ↓
 +
double wiggle;
 +
wiggle = ((Math.random()/500)-0.002);
 +
 +
if (enemyVelocity != 0) {
 +
lateralDirection = GFTUtils.sign(enemyVelocity * Math.sin(e.getHeadingRadians() - enemyAbsoluteBearing));
 +
}
 +
GFTWave wave = new GFTWave(this);
 +
wave.bulletPower = Math.min(e.getEnergy()/4, Math.min(1.5+200/enemyDistance, getEnergy()/8));
 +
wave.gunLocation = new Point2D.Double(getX(), getY());
 +
GFTWave.targetLocation = GFTUtils.project(wave.gunLocation, enemyAbsoluteBearing, enemyDistance);
 +
wave.lateralDirection = lateralDirection;
 +
wave.setSegmentations(enemyDistance, enemyVelocity, lastEnemyVelocity);
 +
lastEnemyVelocity = enemyVelocity;
 +
wave.bearing = enemyAbsoluteBearing;
 +
setTurnGunRightRadians(Utils.normalRelativeAngle(enemyAbsoluteBearing - getGunHeadingRadians() + wave.mostVisitedBearingOffset()) + wiggle);
 +
if (getEnergy() > 0.4) {
 +
//fire at will! ↓
 +
setFire(wave.bulletPower);
 +
}
 +
if (getEnergy() >= wave.bulletPower) {
 +
addCustomEvent(wave);
 +
}
 +
//radar ↓
 +
    setTurnRadarRightRadians(Utils.normalRelativeAngle(radarTurn));
 +
    }
 +
//movement ↓
 +
    void move() {
 +
if (bulletCount >= 1) {
 +
considerChangingDirection();
 +
}
 +
for (int i = 0; i < 2; i++) {
 +
Point2D robotDestination = null;
 +
double tries = 0;
 +
do {
 +
robotDestination = vectorToLocation(absoluteBearing(enemyLocation, robotLocation) + movementLateralAngle*direction,
 +
enemyDistance * (1.1 - tries / 125.0), enemyLocation);
 +
tries++;
 +
} while (tries < 100 && !fieldRectangle(WALL_MARGIN).contains(robotDestination));
 +
goTo(robotDestination);
 +
if (tries < 27 + i * 100 || movementLateralAngle > 0.5) {
 +
break;
 +
}
 +
// dive protection ↓
 +
direction *= -1;
 +
lastDeltaT = deltaT;
 +
lastReverseTime = getTime();
 +
}
 +
    }
 +
//random movement  ↓
 +
    void considerChangingDirection() {
 +
deltaT = getTime() - lastReverseTime;
 +
// if ((Math.random()*.92) > .96*Math.pow(alpha, alpha)) {
 +
// if (Math.random() < 1 - (Math.pow(alpha, alpha))) {
 +
// if (Math.random() < 2.5/(enemyDistance/enemyBulletVelocity)) {
 +
theta = 0.5952*(20D - 3D*enemyFirePower)/enemyDistance;
 +
if (Math.random() > Math.pow(theta, theta)) {
 +
if (deltaT >= lastDeltaT +2 || deltaT <= lastDeltaT -2) {
 +
direction *= -1;
 +
lastDeltaT = deltaT;
 +
lastReverseTime = getTime();
 +
}
 +
}
 +
    }
 +
    RoundRectangle2D fieldRectangle(double margin) {
 +
        return new RoundRectangle2D.Double(margin, margin,
 +
    getBattleFieldWidth() - margin * 2, getBattleFieldHeight() - margin * 2, 75, 75);
 +
    }
 +
 +
    void goTo(Point2D destination) {
 +
        double angle = Utils.normalRelativeAngle(absoluteBearing(robotLocation, destination) - getHeadingRadians());
 +
double turnAngle = Math.atan(Math.tan(angle));
 +
        setTurnRightRadians(turnAngle);
 +
 +
//random movement ↓
 +
if (bulletCount >= 1) {
 +
// setAhead(robotLocation.distance(destination) * (angle == turnAngle ? 1 : -1));
 +
setAhead(Double.POSITIVE_INFINITY * (angle == turnAngle ? 1 : -1));
 +
//stop and go ↓
 +
} else {
 +
if (prevEnergy > enemyEnergy) {
 +
//enemyFirePower = prevEnergy - enemyEnergy;
 +
setAhead(((3 + (int)(enemyFirePower * 1.999999)) << 3 ) * (angle == turnAngle ? 1 : -1));
 +
}
 +
prevEnergy = enemyEnergy;
 +
}
 +
// Hit the brake pedal hard if we need to turn sharply
 +
setMaxVelocity(Math.abs(getTurnRemaining()) > 33 ? 2 : MAX_VELOCITY);
 +
    }
 +
 +
    static Point2D vectorToLocation(double angle, double length, Point2D sourceLocation) {
 +
return vectorToLocation(angle, length, sourceLocation, new Point2D.Double());
 +
    }
 +
 +
    static Point2D vectorToLocation(double angle, double length, Point2D sourceLocation, Point2D targetLocation) {
 +
        targetLocation.setLocation(sourceLocation.getX() + Math.sin(angle) * length,
 +
            sourceLocation.getY() + Math.cos(angle) * length);
 +
return targetLocation;
 +
    }
 +
 +
    static double absoluteBearing(Point2D source, Point2D target) {
 +
        return Math.atan2(target.getX() - source.getX(), target.getY() - source.getY());
 +
    }
 +
 +
public void onHitByBullet(HitByBulletEvent e) {
 +
enemyEnergy = prevEnergy += (3*enemyFirePower);
 +
//enemyEnergy += (3*enemyFirePower);
 +
 +
if ((hits += (4.25 / enemyBulletVelocity)) > getRoundNum() + 1) {
 +
bulletCount++;
 +
}
 +
    }
 +
 +
public void onBulletHit(BulletHitEvent e) {
 +
enemyEnergy = prevEnergy -= ((4*enemyFirePower) + Math.max(2*(enemyFirePower-1), 0));
 +
// enemyEnergy -= ((4*enemyFirePower) + Math.max(2*(enemyFirePower-1), 0));
 +
}
 +
}
 +
//gun ↓
 +
class GFTWave extends Condition {
 +
static Point2D targetLocation;
 +
double bulletPower;
 +
Point2D gunLocation;
 +
double bearing;
 +
double lateralDirection;
 +
// 1000 is the maximum space away in a 800 by 600 battlefield
 +
private static final double MAX_DISTANCE = 1000;
 +
//segment into 5 sections ↓
 +
private static final int DISTANCE_INDEXES = 5;
 +
private static final int VELOCITY_INDEXES = 5;
 +
private static final int BINS = 25;
 +
private static final int MIDDLE_BIN = (BINS - 1) / 2;
 +
private static final double MAX_ESCAPE_ANGLE = 0.7;
 +
private static final double BIN_WIDTH = MAX_ESCAPE_ANGLE / (double)MIDDLE_BIN;
 +
 +
private static int[][][][] statBuffers = new int[DISTANCE_INDEXES][VELOCITY_INDEXES][VELOCITY_INDEXES][BINS];
 +
 +
private int[] buffer;
 +
private AdvancedRobot robot;
 +
private double distanceTraveled;
 +
 +
GFTWave(AdvancedRobot _robot) {
 +
this.robot = _robot;
 +
}
 +
public boolean test() {
 +
advance();
 +
//remove passed waves
 +
if (hasArrived()) {
 +
buffer[currentBin()]++;
 +
robot.removeCustomEvent(this);
 +
}
 +
return false;
 +
}
 +
double mostVisitedBearingOffset() {
 +
return (lateralDirection * BIN_WIDTH) * (mostVisitedBin() - MIDDLE_BIN);
 +
}
 +
void setSegmentations(double distance, double velocity, double lastVelocity) {
 +
int distanceIndex = (int)(distance / (MAX_DISTANCE / DISTANCE_INDEXES));
 +
int velocityIndex = (int)Math.abs(velocity / 2);
 +
int lastVelocityIndex = (int)Math.abs(lastVelocity / 2);
 +
 +
buffer = statBuffers[distanceIndex][velocityIndex][lastVelocityIndex];
 +
}
 +
private void advance() {
 +
distanceTraveled += GFTUtils.bulletVelocity(bulletPower);
 +
}
 +
private boolean hasArrived() {
 +
return distanceTraveled > gunLocation.distance(targetLocation) - 18;
 +
}
 +
private int currentBin() {
 +
int bin = (int)Math.round(((Utils.normalRelativeAngle(GFTUtils.absoluteBearing(gunLocation, targetLocation) - bearing)) /
 +
(lateralDirection * BIN_WIDTH)) + MIDDLE_BIN);
 +
return GFTUtils.minMax(bin, 0, BINS - 1);
 +
}
 +
private int mostVisitedBin() {
 +
int mostVisited = MIDDLE_BIN;
 +
for (int i = 0; i < BINS; i++) {
 +
if (buffer[i] > buffer[mostVisited]) {
 +
mostVisited = i;
 +
}
 +
}
 +
return mostVisited;
 +
}
 +
}
 +
//helpful utilities ↓
 +
class GFTUtils {
 +
static double bulletVelocity(double power) {
 +
return 20 - 3 * power;
 +
}
 +
static Point2D project(Point2D sourceLocation, double angle, double length) {
 +
return new Point2D.Double(sourceLocation.getX() + Math.sin(angle) * length,
 +
sourceLocation.getY() + Math.cos(angle) * length);
 +
}
 +
static double absoluteBearing(Point2D source, Point2D target) {
 +
return Math.atan2(target.getX() - source.getX(), target.getY() - source.getY());
 +
}
 +
static int sign(double v) {
 +
return v < 0 ? -1 : 1;
 +
}
 +
static int minMax(int v, int min, int max) {
 +
return Math.max(min, Math.min(max, v));
 +
}
 +
}
 +
</syntaxhighlight>
 
[[Category:MiniBots]]
 
[[Category:MiniBots]]
 
[[Category:Bots]]
 
[[Category:Bots]]

Revision as of 01:39, 8 April 2019

Basilisk Sub-pages:
Version History
Basilisk
Author(s) User:Slugzilla
Extends AdvancedRobot
Targeting Guessfactor Targeting
Movement Stop and go and Random Movement
Released 2019
Current Version 2.5
Code License RWPCL
[[1] Download]

Background Information

What's special about it?

It's my first robot =)

How competitive is it?

Currently #22 in the MiniRumble and 133 in the RoboRumble.

Strategy

How does it move?

It starts with stop and go, but if it gets hit too much, it will switch to random movement.

How does it fire?

It uses guessfactor targeting.

How does it dodge bullets?

Stop and go dodges the simple targeters, random movement dodges the rest =)

How does the melee strategy differ from one-on-one strategy?

It doesn't have a melee strategy.

Additional Information

Where did you get the name?

I wanted a robot that was mean, lean, and green. Basilisks are green, and I think they're pretty mean, at least according to the legends =)

Can I use your code?

Sure! It's in the .jar file Basilisk RWPCL.

What's next for your robot?

Some anti ramming code.

More segmentation on the guessfactor gun.

Tweak the movement.

Does it have any White Whales?

Numbers 1 - 21 in the MiniRumble =)

What other robot(s) is it based on?

It's based off of RandomMovementBot and the GFTargetingBot by PEZ

Uses the Turn Multiplier Lock from the RoboWiki

Dive protection is from Tityus

Deciding whether to reverse direction every tick was inspired by FloodHT and Raiko

The favorite distance idea was inspired by Raiko

Stop and go at the beginning and then swapping to random movement was inspired by Cotillion and BlackWidow

Improved mode swaps and stop and go move amount from EpeeistMicro and Cotillion

Special thanks to Dsekercioglu for giving me ideas on beating the bullet shielders

A huge thank you to all the contributors on the RoboWiki for your awesome tutorials and open source robots!

Code

Version 2.13

package slugzilla;
import robocode.*;
//import robocode.util.Utils;
import java.awt.geom.*;
import robocode.util.*;
import java.awt.*;
import static robocode.util.Utils.normalRelativeAngleDegrees;

public class Basilisk extends AdvancedRobot {

	private static double lateralDirection;
	private static double lastEnemyVelocity;
	static final double MAX_VELOCITY = 8;
    static final double WALL_MARGIN = 25;
//	static double alpha;
	static double theta;
	static double enemyFirePower;
	static double enemyBulletVelocity;
    Point2D robotLocation;
    Point2D enemyLocation;
    double enemyDistance;
    double enemyAbsoluteBearing;
	double enemyVelocity;
    double movementLateralAngle;
	double direction = 1;
	static int bulletCount;
	static double hits;
	static double prevEnergy = 100.0;
	static double enemyEnergy;
	static double deltaT;
	static double lastDeltaT;
	static double lastReverseTime;
    public void run() {
        setAdjustRadarForGunTurn(true);
 		setAdjustGunForRobotTurn(true);
		setBodyColor(new Color(40, 100, 100));
		setGunColor(new Color(34, 50, 50));
		setRadarColor(new Color(0, 255, 0));
		setScanColor(new Color(0, 255, 0));
	 	turnRadarRightRadians(Double.POSITIVE_INFINITY);
        do {
            scan();
        } while (true);
    }
 
    public void onScannedRobot(ScannedRobotEvent e) {
		//radar ↓
		double radarTurn = getHeadingRadians() + e.getBearingRadians() - getRadarHeadingRadians();
		
		//movement ↓
        robotLocation = new Point2D.Double(getX(), getY());
        enemyAbsoluteBearing = getHeadingRadians() + e.getBearingRadians();
        enemyDistance = e.getDistance();
		enemyVelocity = e.getVelocity();
		enemyEnergy = e.getEnergy();
        enemyLocation = vectorToLocation(enemyAbsoluteBearing, enemyDistance, robotLocation);
		enemyBulletVelocity = 20 - 3*enemyFirePower;
		
		//alpha = 1/(enemyDistance/19);
		//we move far away when close, but stabilize and move perpendicular when far ↓
		movementLateralAngle = Math.max(enemyDistance/2000, 0.075);
		if (prevEnergy > enemyEnergy) {
			enemyFirePower = prevEnergy - enemyEnergy;
		}
		move();
		prevEnergy = e.getEnergy();
		//gun ↓
	
		//to beat the bullet shielders ↓
		double wiggle;
		wiggle = ((Math.random()/500)-0.002);
		
		if (enemyVelocity != 0) {
			lateralDirection = GFTUtils.sign(enemyVelocity * Math.sin(e.getHeadingRadians() - enemyAbsoluteBearing));
		}
		GFTWave wave = new GFTWave(this);
		wave.bulletPower = Math.min(e.getEnergy()/4, Math.min(1.5+200/enemyDistance, getEnergy()/8));
		wave.gunLocation = new Point2D.Double(getX(), getY());
		GFTWave.targetLocation = GFTUtils.project(wave.gunLocation, enemyAbsoluteBearing, enemyDistance);
		wave.lateralDirection = lateralDirection;
		wave.setSegmentations(enemyDistance, enemyVelocity, lastEnemyVelocity);
		lastEnemyVelocity = enemyVelocity;
		wave.bearing = enemyAbsoluteBearing;
		setTurnGunRightRadians(Utils.normalRelativeAngle(enemyAbsoluteBearing - getGunHeadingRadians() + wave.mostVisitedBearingOffset()) + wiggle);
		if (getEnergy() > 0.4) {
		//fire at will! ↓
			setFire(wave.bulletPower);
		}
		if (getEnergy() >= wave.bulletPower) {
			addCustomEvent(wave);
		}
		//radar ↓
    	setTurnRadarRightRadians(Utils.normalRelativeAngle(radarTurn));
    }
	//movement ↓
    void move() {
		if (bulletCount >= 1) {
			considerChangingDirection();
		} 
		for (int i = 0; i < 2; i++) {
			Point2D robotDestination = null;
			double tries = 0;
			do {
				robotDestination = vectorToLocation(absoluteBearing(enemyLocation, robotLocation) + movementLateralAngle*direction,
					enemyDistance * (1.1 - tries / 125.0), enemyLocation);	
			tries++;
			} while (tries < 100 && !fieldRectangle(WALL_MARGIN).contains(robotDestination));	
				goTo(robotDestination);
			if (tries < 27 + i * 100 || movementLateralAngle > 0.5) {
				break;
			}
			// dive protection ↓
			direction *= -1;
			lastDeltaT = deltaT;
			lastReverseTime = getTime();
		}	
    }	
	//random movement  ↓
    void considerChangingDirection() {
		deltaT = getTime() - lastReverseTime;
//		if ((Math.random()*.92) > .96*Math.pow(alpha, alpha)) {
	//	if (Math.random() < 1 - (Math.pow(alpha, alpha))) {
	//	if (Math.random() < 2.5/(enemyDistance/enemyBulletVelocity)) {
		theta = 0.5952*(20D - 3D*enemyFirePower)/enemyDistance;
		if (Math.random() > Math.pow(theta, theta)) {
			if (deltaT >= lastDeltaT +2 || deltaT <= lastDeltaT -2) {
				direction *= -1;
				lastDeltaT = deltaT;
				lastReverseTime = getTime();
			}
		}
    }
    RoundRectangle2D fieldRectangle(double margin) {
        return new RoundRectangle2D.Double(margin, margin,
	    getBattleFieldWidth() - margin * 2, getBattleFieldHeight() - margin * 2, 75, 75);
    }
 
    void goTo(Point2D destination) {
        double angle = Utils.normalRelativeAngle(absoluteBearing(robotLocation, destination) - getHeadingRadians());
		double turnAngle = Math.atan(Math.tan(angle));
        setTurnRightRadians(turnAngle);
		
		//random movement ↓
		if (bulletCount >= 1) {
	//		setAhead(robotLocation.distance(destination) * (angle == turnAngle ? 1 : -1));
			setAhead(Double.POSITIVE_INFINITY * (angle == turnAngle ? 1 : -1));
		//stop and go ↓	
		} else { 		
			if (prevEnergy > enemyEnergy) {
				//enemyFirePower = prevEnergy - enemyEnergy;
				setAhead(((3 + (int)(enemyFirePower * 1.999999)) << 3 ) * (angle == turnAngle ? 1 : -1));
			}
			prevEnergy = enemyEnergy;
		}
		// Hit the brake pedal hard if we need to turn sharply
		setMaxVelocity(Math.abs(getTurnRemaining()) > 33 ? 2 : MAX_VELOCITY);
    }
	
    static Point2D vectorToLocation(double angle, double length, Point2D sourceLocation) {
	return vectorToLocation(angle, length, sourceLocation, new Point2D.Double());
    }
 
    static Point2D vectorToLocation(double angle, double length, Point2D sourceLocation, Point2D targetLocation) {
        targetLocation.setLocation(sourceLocation.getX() + Math.sin(angle) * length,
            sourceLocation.getY() + Math.cos(angle) * length);
	return targetLocation;
    }
 
    static double absoluteBearing(Point2D source, Point2D target) {
        return Math.atan2(target.getX() - source.getX(), target.getY() - source.getY());
    }
	
	public void onHitByBullet(HitByBulletEvent e) {
		enemyEnergy = prevEnergy += (3*enemyFirePower);	
		//enemyEnergy += (3*enemyFirePower);	
		
		if ((hits += (4.25 / enemyBulletVelocity)) > getRoundNum() + 1) {
			bulletCount++;
		}
    }

	public void onBulletHit(BulletHitEvent e) {
		enemyEnergy = prevEnergy -= ((4*enemyFirePower) + Math.max(2*(enemyFirePower-1), 0));
	//	enemyEnergy -= ((4*enemyFirePower) + Math.max(2*(enemyFirePower-1), 0));
	}
}
//gun ↓
class GFTWave extends Condition {
	static Point2D targetLocation;
	double bulletPower;
	Point2D gunLocation;
	double bearing;
	double lateralDirection;
	// 1000 is the maximum space away in a 800 by 600 battlefield
	private static final double MAX_DISTANCE = 1000;
	//segment into 5 sections ↓
	private static final int DISTANCE_INDEXES = 5;
	private static final int VELOCITY_INDEXES = 5;
	private static final int BINS = 25;
	private static final int MIDDLE_BIN = (BINS - 1) / 2;
	private static final double MAX_ESCAPE_ANGLE = 0.7;
	private static final double BIN_WIDTH = MAX_ESCAPE_ANGLE / (double)MIDDLE_BIN;
 
	private static int[][][][] statBuffers = new int[DISTANCE_INDEXES][VELOCITY_INDEXES][VELOCITY_INDEXES][BINS];
 
	private int[] buffer;
	private AdvancedRobot robot;
	private double distanceTraveled;
 
	GFTWave(AdvancedRobot _robot) {
		this.robot = _robot;
	}
	public boolean test() {
		advance();
		//remove passed waves
		if (hasArrived()) {
			buffer[currentBin()]++;
			robot.removeCustomEvent(this);
		}
		return false;
	}
	double mostVisitedBearingOffset() {
		return (lateralDirection * BIN_WIDTH) * (mostVisitedBin() - MIDDLE_BIN);
	}
	void setSegmentations(double distance, double velocity, double lastVelocity) {
		int distanceIndex = (int)(distance / (MAX_DISTANCE / DISTANCE_INDEXES));
		int velocityIndex = (int)Math.abs(velocity / 2);
		int lastVelocityIndex = (int)Math.abs(lastVelocity / 2);
		
		buffer = statBuffers[distanceIndex][velocityIndex][lastVelocityIndex];
	}
	private void advance() {
		distanceTraveled += GFTUtils.bulletVelocity(bulletPower);
	}
	private boolean hasArrived() {
		return distanceTraveled > gunLocation.distance(targetLocation) - 18;
	}
	private int currentBin() {
		int bin = (int)Math.round(((Utils.normalRelativeAngle(GFTUtils.absoluteBearing(gunLocation, targetLocation) - bearing)) /
				(lateralDirection * BIN_WIDTH)) + MIDDLE_BIN);
		return GFTUtils.minMax(bin, 0, BINS - 1);
	}
	private int mostVisitedBin() {
		int mostVisited = MIDDLE_BIN;
		for (int i = 0; i < BINS; i++) {
			if (buffer[i] > buffer[mostVisited]) {
				mostVisited = i;
			}
		}
		return mostVisited;
	}	
}
//helpful utilities ↓
class GFTUtils {
	static double bulletVelocity(double power) {
		return 20 - 3 * power;
	}
	static Point2D project(Point2D sourceLocation, double angle, double length) {
		return new Point2D.Double(sourceLocation.getX() + Math.sin(angle) * length,
				sourceLocation.getY() + Math.cos(angle) * length);
	}
	static double absoluteBearing(Point2D source, Point2D target) {
		return Math.atan2(target.getX() - source.getX(), target.getY() - source.getY());
	}
	static int sign(double v) {
		return v < 0 ? -1 : 1;
	}
	static int minMax(int v, int min, int max) {
		return Math.max(min, Math.min(max, v));
	}
}