User:Chase-san/NewTech/KakeruCode

From Robowiki
< User:Chase-san‎ | NewTech
Revision as of 15:35, 5 August 2011 by Chase-san (talk | contribs) (update)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Code for version MC25

/**
 * Copyright (c) 2011 Chase
 * 
 * This software is provided 'as-is', without any express or implied
 * warranty. In no event will the authors be held liable for any damages
 * arising from the use of this software.
 * 
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 * 
 *    1. The origin of this software must not be misrepresented; you must not
 *    claim that you wrote the original software. If you use this software
 *    in a product, an acknowledgment in the product documentation would be
 *    appreciated but is not required.
 * 
 *    2. Altered source versions must be plainly marked as such, and must not be
 *    misrepresented as being the original software.
 * 
 *    3. This notice may not be removed or altered from any source
 *    distribution.
 */
/*
 * Dear Future Chase,
 * 
 * I cannot even begin to express how deeply sorry I am.
 * But I assure you, at the time, I (probably) knew what I was doing.
 * 
 * -Chase
 * 
 * P.S. I am trying to leave comments,
 *      but you know how bad I am at those.
 */

package cs.move;

import java.awt.Color;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import cs.Chikaku;
import cs.geom.*;
import cs.utils.*;

import ags.utils.KdTree;
import ags.utils.KdTree.Entry;
import robocode.*;
import robocode.util.Utils;

/**
 * Movement
 * <br/><br/>
 * In Japanese, Kakeru means "Dash" (in some cases)
 * @author Chase
 */
public abstract class Kakeru extends Chikaku {
	private static final int MaxTurnHistoryLength = 20;
	private static final int MaxHeatWaveQueueLength = 4;
	private static final int AverageSingleRoundTime = 1200;
	private static final int NearestNeighborsToUseInSurfing = 16;

	/**
	 * Keep in mind with heat waves we will have waves 2 rounds earlier
	 * the the gun heat would otherwise indicate
	 */
	private static final int TimeBeforeFirstWaveToStartTurning = 4;

	private static final MaximumDeque<Double> enemyBulletPower;
	private static final KdTree<Data> tree;

	static {
		enemyBulletPower = new MaximumDeque<Double>(MaxHeatWaveQueueLength);
		enemyBulletPower.addLast(3.0);

		int dimensions = new Data().getDataArray().length;
		tree = new KdTree.SqrEuclid<Data>(dimensions, 0);

		Data data = new Data();
		data.weight = 0.1;
		tree.addPoint(data.getDataArray(), data);
	}

	private final LinkedList<Wave<Data>> enemyWaves;
	private final MaximumDeque<State> history;
	private Vector lastEnemyPosition;
	private double lastEnemyEnergy;
	private double lastEnemyVelocity;
	private double enemyGunHeat;

	private int orbitDirection = 1;
	private boolean haveFiredImaginaryWave = false;

	public Kakeru() {
		enemyWaves = new LinkedList<Wave<Data>>();
		history = new MaximumDeque<State>(MaxTurnHistoryLength);
		lastEnemyPosition = new Vector(-1,-1);
		lastEnemyEnergy = 100;
	}

	@Override
	public void onRoundStarted(Event e) {
		super.onRoundStarted(e);

		/* Set it to be the same as our gun heat on round start! */
		enemyGunHeat = status.getGunHeat();
	}

	@Override
	public void onScannedRobot(ScannedRobotEvent e) {
		super.onScannedRobot(e);

		double angleToEnemy = e.getBearingRadians() + status.getHeadingRadians();
		double enemyVelocity = e.getVelocity();

		State state = new State();

		state.myPosition = myPosition;
		state.enemyPosition = myPosition.projectNew(angleToEnemy, e.getDistance());
		state.data.lateralVelocity = status.getVelocity()*Math.sin(e.getBearingRadians());
		state.data.advancingVelcity = status.getVelocity()*Math.cos(e.getBearingRadians());
		state.data.globalTime = globalTime;

		history.addFirst(state);

		int direction = Tools.sign(state.data.lateralVelocity);

		orbitDirection = direction;

		if(Math.abs(enemyVelocity) == 0 && lastEnemyVelocity > 2) {

			//Hit the wall!
			lastEnemyEnergy -= Rules.getWallHitDamage(lastEnemyVelocity);
		}

		/*
		 * Fire new imaginary wave if we can, heat waves
		 * are always 2 ticks previous to a real one 'possibly'
		 * being fired
		 */
		if(!haveFiredImaginaryWave) {
			if(enemyGunHeat <= 2.0*peer.getGunCoolingRate()) {
				haveFiredImaginaryWave = true;

				double power = 0;
				for(Double d : enemyBulletPower)
					power += d;
				power /= enemyBulletPower.size();

				/* Create the Wave */
				Wave<Data> wave = createMovementWave(power,direction);
				wave.set(simulatedEnemyPosition);
				wave.imaginary = true;

				/*
				 * Only +1 because real waves are -1 in time,
				 * meaning that by the time we detect them, it has
				 * already been 1 turn since it was fired, meaning
				 * heat waves +2 makes it only +1 here. :-)
				 */
				wave.fireTime = time + 1;
				wave.directAngle = wave.angleTo(state.myPosition);

				wave.data = state.data;

				enemyWaves.add(wave);
			}
		}

		/* Check for new enemy wave */
		double enemyEnergy = e.getEnergy();
		double energyDelta = lastEnemyEnergy - enemyEnergy;
		if(energyDelta > 0 && energyDelta <= 3.0) {

			enemyGunHeat += Rules.getGunHeat(energyDelta);
			haveFiredImaginaryWave = false;

			State old = history.get(2);
			direction = Tools.sign(old.data.lateralVelocity);

			/* Create the Wave */
			Wave<Data> wave = createMovementWave(energyDelta,direction);
			wave.set(lastEnemyPosition);

			wave.fireTime = time - 1;
			wave.directAngle = wave.angleTo(old.myPosition);

			wave.data = old.data;

			enemyBulletPower.addLast(wave.power);
			enemyWaves.add(wave);
		}

		lastEnemyPosition = state.enemyPosition;
		lastEnemyVelocity = enemyVelocity;
		lastEnemyEnergy = enemyEnergy;

	}

	private Wave<Data> createMovementWave(double power, int direction) {
		Wave<Data> wave = new Wave<Data>();
		wave.power = power;
		wave.speed = Rules.getBulletSpeed(wave.power);
		wave.escapeAngle = Math.asin(8.0/wave.speed) * direction;
		return wave;
	}

	@Override
	public void onTurnEnded(Event e) {
		super.onTurnEnded(e);

		/* Don't move if we are scanning, this is mostly start
		 * of round fast radar finding protection
		 */
		if(initialTick != 0)
			return;

		/* We track the gun heat because it is important
		 * for our heat/imaginary waves
		 */
		enemyGunHeat -= coolingRate;
		if(enemyGunHeat <= 0)
			enemyGunHeat = 0;

		/* Take are of all waves that cannot possibly even conceivably hit us anymore */
		checkWaves();

		/* Run our 'actual' movement */
		doMovement();
	}

	private void checkWaves() {
		Iterator<Wave<Data>> waveIterator = enemyWaves.iterator();
		while(waveIterator.hasNext()) {
			Wave<Data> wave = waveIterator.next();

			double radius = wave.getRadius(time);

			/* /////////////////////// */
			/* DRAW WAVE */
			g.setColor(new Color(128,128,128));
			double escape = Tools.abs(wave.escapeAngle);
			g.drawArc(
					(int)(wave.x - radius), (int)(wave.y - radius),
					(int)(radius*2), (int)(radius*2),
					(int)Math.toDegrees(wave.directAngle-escape),
					(int)Math.toDegrees(escape*2));
			/* END DRAW WAVE */
			/* /////////////////////// */

			if(wave.imaginary) {
				/*
				 * if the wave is imaginary (a heat wave)
				 * we should remove it automatically after
				 * 2 ticks which is when we will detect their
				 * actual wave, if they fire
				 */
				if(time - wave.fireTime > 2) {
					waveIterator.remove();
				}
			} else
				/* Clean up waves that can no longer hit us */
				if(wave.didIntersect(myPosition, time)) {
					waveIterator.remove();
				}
		}
	}

	private Wave<Data> findBestSurfableWave() {
		double halfBotWidth = 18 + Math.sin(lastEnemyPosition.angleTo(myPosition))*7.4558441;
		//find the wave soonest to hit us
		Wave<Data> wave = null;
		double bestETA = Double.POSITIVE_INFINITY;
		for(Wave<Data> check : enemyWaves) {
			double distance = myPosition.distance(check) - check.getRadius(time);
			double ETA = distance / check.speed;

			if(distance < 0 && Math.abs(distance) + check.speed > halfBotWidth)
				continue;

			if(ETA < bestETA) {
				//If we are already surfing a strong wave
				//that will hit at almost the same time
				//do not switch to this wave, more powerful
				//waves are slower and thus will always be
				//shot first for this to occur, which is why
				//we don't need an else on the ETA < bestETA

				//This is mostly just a hack for some score
				if(wave != null && ETA > bestETA - 3) {
					if(check.power > wave.power) {
						wave = check;
						bestETA = ETA;
					}
				} else {
					wave = check;
					bestETA = ETA;
				}
			}
		}

		return wave;
	}


	private void checkMove(Simulate sim, Wave<Data> wave, int orbitDirection) {
		Move move = predictMove(sim.position,wave,
				sim.heading,sim.velocity,orbitDirection);

		sim.angleToTurn = move.angleToTurn;
		sim.direction = move.direction;
		sim.maxVelocity = Tools.min(sim.maxVelocity, move.maxVelocity);
	}

	/**
	 * Calculate the driving
	 */
	protected Move predictMove(Vector myPosition, Vector orbitCenter,
			double myHeading, double myVelocity, int orbitDirection) {
		Move data = new Move();

		/* Better safe then very very sorry */
		if(orbitDirection == 0)
			orbitDirection = 1;

		double angleToRobot = orbitCenter.angleTo(myPosition);

		/*
		 * if the orbit direction is clockwise/counter, we want to try and
		 * point our robot in that direction, which is why we multiply by it
		 */
		double travelAngle = angleToRobot + (Math.PI/2.0) * orbitDirection;

		/* DONE add distancing to drive method */
		/* TODO add a better distancing method */
		double distance = myPosition.distance(orbitCenter);
		final double best = 500.0;
		double distancing = ((distance-best)/best);

		travelAngle += distancing*orbitDirection;

		/* DONE add a wall smoothing method */
		/* TODO add a better wall smoothing method */
		while(!field.contains(myPosition.projectNew(travelAngle, 140))) {
			travelAngle += 0.08*orbitDirection;
		}

		data.angleToTurn = Utils.normalRelativeAngle(travelAngle - myHeading);
		data.direction = 1;

		/*
		 * If our backend is closer to direction, use that instead, and
		 * inform the caller that we are going to be going in reverse instead
		 */
		if(Tools.abs(data.angleToTurn) > Math.PI/2.0) {
			data.angleToTurn = Utils.normalRelativeAngle(data.angleToTurn - Math.PI);
			data.direction = -1;
		}

		/*
		 * Slow down so we do not ram head long into the walls and can instead turn to avoid them
		 * FIXME We only have to do this because of the wall smoothing, fix that to get rid of this!
		 */
		if(!field.contains(myPosition.projectNew(myHeading, myVelocity*3.25)))
			data.maxVelocity = 0;

		if(!field.contains(myPosition.projectNew(myHeading, myVelocity*5)))
			data.maxVelocity = 4;

		return data;
	}

	private void doMovement() {
		Wave<Data> wave = findBestSurfableWave();
		if(wave == null) {
			doWavelessMovement();
			return;
		}

		g.setColor(Color.WHITE);
		g.draw(field.toRectangle2D());

		doSurfingMovement(wave);
	}

	private void doSurfingMovement(Wave<Data> wave) {
		/* Do the actual surfing and such */
		g.setColor(Color.ORANGE);

		peer.setMaxVelocity(Rules.MAX_VELOCITY);

		Risk[] directions = new Risk[] {
				checkWaveRisk(wave,orbitDirection,0),
				checkWaveRisk(wave,orbitDirection,Rules.MAX_VELOCITY),
				checkWaveRisk(wave,-orbitDirection,Rules.MAX_VELOCITY),
		};

		int bestIndex = 0;
		double minRisk = Double.POSITIVE_INFINITY;
		for(int i = 0; i < directions.length; ++i) {
			if(directions[i].risk < minRisk) {
				bestIndex = i;
				minRisk = directions[i].risk;
			}
		}

		Move move = predictMove(myPosition,lastEnemyPosition,
				status.getHeadingRadians(),status.getVelocity(),
				directions[bestIndex].orbitDirection );

		peer.setMaxVelocity(Tools.min(directions[bestIndex].maxVelocity,move.maxVelocity));
		peer.setTurnBody(move.angleToTurn);
		peer.setMove(1000*move.direction);
		peer.setCall();
	}

	private Risk checkWaveRisk(Wave<Data> wave, int orbitDirection, double maxVelocity) {
		/*
		 * Simplify the output to our direction chooser
		 */
		Risk waveRisk = new Risk();
		waveRisk.orbitDirection = orbitDirection;
		waveRisk.maxVelocity = maxVelocity;

		/*
		 * Create a simulator for our movement
		 */
		Simulate sim = createSimulator();
		sim.maxVelocity = maxVelocity;
		sim.direction = orbitDirection;

		/*
		 * Used for our distance danger
		 */
		double currentDistance = wave.distance(sim.position);
		double predictedDistance = 0;
		int intersected = 0;

		long timeOffset = 0;

		/*
		 * Reset the factors, so we do not get cross risk check contamination
		 */
		wave.resetFactors();

		/*
		 * Since our radar is limited to 1200 and the slowest bullet moves at
		 * 11 per second, calculating past 110 is pointlessly impossible, so
		 * it is a safe number to use as our break point.
		 */
		while(timeOffset < 110) {
			//DRAW DANGER MOVEMENT
			g.drawOval((int)sim.position.x-3, (int)sim.position.y-3, 6, 6);

			if(wave.doesIntersect(sim.position, time+timeOffset)) {
				++intersected;
				predictedDistance += wave.distance(sim.position);
			} else if(intersected > 0) {
				predictedDistance /= intersected;

				waveRisk.risk += checkIntersectionRisk(wave,predictedDistance);
				break;
			}

			checkMove(sim,wave,orbitDirection);
			sim.step();
			sim.maxVelocity = maxVelocity;

			++timeOffset;
		}

		if(intersected > 0) {
			double distanceRisk = currentDistance / predictedDistance;
			distanceRisk *= distanceRisk;

			//distanceRisk = (distanceRisk + 1.0) / 2.0;
			//System.out.println(distanceRisk);

			waveRisk.risk *= distanceRisk;
		}


		return waveRisk;
	}

	private double checkIntersectionRisk(Wave<Data> wave, double predicted) {
		double risk = 0;
		double factorCenter = (wave.minFactor + wave.maxFactor) / 2.0;

		double factorRange = Tools.abs(wave.maxFactor - wave.minFactor);
		double bandwidth = factorRange / Tools.abs(wave.escapeAngle);

		List<Entry<Data>> list = tree.nearestNeighbor(wave.data.getWeightedDataArray(),
				NearestNeighborsToUseInSurfing, false);

		for(Entry<Data> e : list) {
			Data data = e.value;

			double entryRisk = 0.1/(1.0+Tools.abs(data.guessfactor-factorCenter));

			if(wave.minFactor < data.guessfactor && wave.maxFactor > data.guessfactor) {
				entryRisk += 0.9;
			}

			double timeWeight = 1.0+AverageSingleRoundTime/(double)(globalTime-data.globalTime);

			//kernel calculation neh?

			double density = 0;

			for(Entry<Data> ex : list) {
				double ux = (e.value.guessfactor-data.guessfactor) / bandwidth;
				double sw = ex.value.weight / (1.0+ex.distance);
				density += Math.exp(-0.5*ux*ux)*sw;
			}

			//System.out.println(density);

			double weight = (data.weight / (1.0+e.distance)) * timeWeight * density;

			//System.out.println(weight);
			//System.out.println(entryRisk);

			risk += 1.0-(1.0/(1.0+weight*entryRisk));
		}

		return risk/NearestNeighborsToUseInSurfing;
	}

	private void doWavelessMovement() {
		final int initialTurns = (int)Math.ceil(3.0/coolingRate+4.0);
		double safeTurns = status.getGunHeat()/coolingRate;

		if(time < initialTurns) {
			/* Do we have enough time to move around before they can start firing? */
			if(safeTurns > TimeBeforeFirstWaveToStartTurning) {
				doMinimalRiskMovement();
			} else {
				Move data = predictMove(myPosition,lastEnemyPosition,
						status.getHeadingRadians(),status.getVelocity(),orbitDirection);
				peer.setTurnBody(data.angleToTurn);
				peer.setMaxVelocity(0);

				peer.setMove(0);
				//peer.setTurnBody(0);
				peer.setMaxVelocity(0);
			}
		} else {
			/*
			 * Things are no longer safe, set our movement to `sane`
			 * 
			 * TODO: RAM if enemy is disabled
			 */
			if(status.getOthers() == 0) {
				/*
				 * No bullets in the air, enemy dead, we can do our victory DANCE (YAY!)
				 */
				doVictoryDance();
			} else {
				//Some other thing to do when nothing is in the air??
				doMinimalRiskMovement();
			}
		}
	}

	protected void doMinimalRiskMovement() {
		double heading = status.getHeadingRadians();
		double velocity = status.getVelocity();

		Rectangle escapeField = new Rectangle(30,30,fieldWidth-60,fieldHeight-60);
		g.setColor(Color.WHITE);
		g.draw(escapeField.toRectangle2D());

		//Do minimal risk movement
		Vector target = myPosition.copy();
		Vector bestTarget = myPosition;
		double angle = 0;
		double bestRisk = checkWavelessRisk(bestTarget);
		double enemyDistance = myPosition.distance(lastEnemyPosition)+18;
		while(angle < Math.PI*2) {
			double targetDistance = Tools.min(200,enemyDistance);

			target.setProject(myPosition, angle, targetDistance);
			if(escapeField.contains(target)) {
				double risk = checkWavelessRisk(target);
				if(risk < bestRisk) {
					bestRisk = risk;
					bestTarget = target.copy();
				}
				g.setColor(Color.BLUE);
				g.drawRect((int)target.x-2, (int)target.y-2, 4, 4);

			}
			angle += Math.PI/32.0;
		}
		g.setColor(bodyColor);
		g.drawRect((int)bestTarget.x-2, (int)bestTarget.y-2, 4, 4);

		double travelAngle = myPosition.angleTo(bestTarget);

		double forward = myPosition.distance(bestTarget);

		double angleToTurn = Utils.normalRelativeAngle(travelAngle - status.getHeadingRadians());
		int direction = 1;

		if(Tools.abs(angleToTurn) > Math.PI/2.0) {
			angleToTurn = Utils.normalRelativeAngle(angleToTurn - Math.PI);
			direction = -1;
		}

		//Slow down so we do not ram head long into the walls and can instead turn to avoid them
		double maxVelocity = Rules.MAX_VELOCITY;

		if(!field.contains(myPosition.projectNew(heading, velocity*3.25)))
			maxVelocity = 0;

		if(!field.contains(myPosition.projectNew(heading, velocity*5)))
			maxVelocity = 4;

		if(angleToTurn > 1.0) {
			maxVelocity = 0;
		}

		peer.setMaxVelocity(maxVelocity);
		peer.setTurnBody(angleToTurn);
		peer.setMove(forward*direction);
	}

	private double checkWavelessRisk(Vector pos) {
		double risk = lastEnemyEnergy/pos.distanceSq(lastEnemyPosition);

		for(Line edge : field.getLines()) {
			risk += 5.0/(1.0+edge.ptSegDistSq(pos));
		}

		g.setColor(Color.RED);
		/*
		 * get points between enemy location and corner and add risk!!!!
		 * these are bad places to be! Our hitbox is larger here if nothing else!
		 */
		for(Vector corner : field.getCorners()) {
			corner.add(lastEnemyPosition);
			corner.scale(0.5);
			if(corner.distanceSq(lastEnemyPosition) < 22500 /* 150 */) {
				g.drawRect((int)corner.x-2, (int)corner.y-2, 4, 4);
			}
			risk += 5.0/(1.0+corner.distanceSq(pos));
		}

		return risk;
	}

	/**
	 * Account for our bullets hitting the enemy
	 */
	@Override
	public void onBulletHit(BulletHitEvent e) {
		super.onBulletHit(e);

		/* Get the power of the bullet of ours that hit */
		double bulletPower = e.getBullet().getPower();

		/* Determine how much damage our bullet does to the enemy
		 * and adjust our stored copy to reflect the amount lost
		 */
		lastEnemyEnergy -= Rules.getBulletDamage(bulletPower);
	}

	/**
	 * Account for one of our bullets hitting an enemy bullet.
	 */

	@Override
	public void onBulletHitBullet(BulletHitBulletEvent e) {
		super.onBulletHitBullet(e);

		handleBullet(e.getHitBullet());
	}

	/**
	 * Account for one of the enemies bullets hitting us.
	 */
	@Override
	public void onHitByBullet(HitByBulletEvent e) {
		super.onHitByBullet(e);

		/*
		 * Increase the enemy energy based on how powerful the bullet that
		 * hit us was this is so we can get reliable energy drop detection
		 */
		lastEnemyEnergy += Rules.getBulletHitBonus(e.getPower());

		handleBullet(e.getBullet());
	}

	/**
	 * Handle an actual enemy bullet
	 */
	private void handleBullet(Bullet b) {
		Vector bulletPosition = new Vector(b.getX(),b.getY());

		/* Find the matching wave */
		Iterator<Wave<Data>> waveIterator = enemyWaves.iterator();
		while(waveIterator.hasNext()) {
			Wave<Data> wave = waveIterator.next();

			/* the current distance of the wave */
			double radius = wave.speed*(time - wave.fireTime);

			/* check if the power is close enough to be our wave. This
			 * margin is small, only to allow room for rounding errors
			 */
			if(Tools.abs(b.getPower()-wave.power) < 0.001) {

				/* check if the bullets distance from the center of
				 * the wave is close to our waves radius. This margin
				 * is small, only to allow room for rounding errors
				 */
				if(Tools.abs(wave.distanceSq(bulletPosition)-radius*radius) < 0.1) {
					/* FIXME extensively test this detection method */

					/* Alternate method, but why recalculate something? Could be used as an extra check. */
					/* updateTree(wave,wave.angleTo(bulletPosition)); */
					updateTree(wave,b.getHeadingRadians());

					waveIterator.remove();

					return;
				}
			}
		}

		/* If not found say so, cause someone (me) SCREWED UP!!! */
		println("Error: Unknown Bullet Collision");
		println("\tBullet:["+b.getX()+","+b.getY()+"] @ h"+b.getHeadingRadians()+" @ p" + b.getPower());
	}

	private void updateTree(Wave<Data> wave, double angleToBullet) {
		double angleOffset = Utils.normalRelativeAngle(angleToBullet - wave.directAngle);

		wave.data.guessfactor = angleOffset / wave.escapeAngle;

		//TODO
		//tree.addPoint(wave.data.getDataArray(), wave.data);
		tree.addPoint(wave.data.getWeightedDataArray(), wave.data);
	}
}


/**
 * Data gained from movement prediction
 */
class Move {
	double angleToTurn = 0;
	double maxVelocity = Rules.MAX_VELOCITY;
	int direction = 1;
}

class Risk {
	double risk;
	double maxVelocity;
	int orbitDirection;
}

public class State {
	Vector myPosition;
	Vector enemyPosition;
	Data data = new Data();
}

public class Data {
	long globalTime;

	double lateralVelocity;
	double advancingVelcity;
	double guessfactor;

	double weight = 1.0;

	public double[] getDataArray() {
		return new double[] {
				Math.abs(lateralVelocity)/8.0,
				(advancingVelcity+8.0)/16.0,
		};
	}

	public double[] getDataWeights() {
		return new double[] {
				1,
				1,
		};
	}

	public double[] getWeightedDataArray() {
		double[] data = getDataArray();
		double[] weights = getDataWeights();
		for(int i=0;i<data.length;++i) {
			data[i] *= weights[i];
		}
		return data;
	}
}

public class Wave<T> extends Vector {
	public long fireTime;
	public double escapeAngle;
	public double directAngle;
	public double speed;
	public double power;
	public boolean imaginary = false;
	
	public T data;
	
	public double getRadius(long time) {
		return speed*(time - fireTime);
	}
	
	
	public int intersected = 0;
	/**
	 * Used to determine if we can safely remove this wave
	 */
	public final boolean didIntersect(Vector target, long time) {
		hitbox.set(target, 36, 36);
		
		double radius = getRadius(time);
		double nextRadius = getRadius(time+1);
		Vector[] current = Tools.intersectRectCircle(hitbox, this, radius);
		Vector[] next = Tools.intersectRectCircle(hitbox, this, nextRadius);
		
		if(current.length != 0 || next.length != 0) {
			++intersected;
		} else {
			if(intersected > 0) {
				return true;
			}
		}
		
		return false;
	}
	
	public static Rectangle hitbox = new Rectangle();
	/**
	 * Used for calculating where a bullet will intersect a target
	 */
	public final boolean doesIntersect(Vector target, long time) {
		hitbox.set(target, 36, 36);
		
		double radius = getRadius(time);
		double nextRadius = getRadius(time+1);
		Vector[] current = Tools.intersectRectCircle(hitbox, this, radius);
		Vector[] next = Tools.intersectRectCircle(hitbox, this, nextRadius);
		
		if(current.length != 0 || next.length != 0) {
			for(Vector v : current)
				expandFactors(v);
			
			for(Vector v : next)
				expandFactors(v);
			
			Vector[] corners = hitbox.getCorners();
			for(Vector v : corners) {
				double distance = distanceSq(v); 
				if(distance < nextRadius*nextRadius
				&& distance > radius*radius) {
					expandFactors(v);
				}
			}
			
			return true;
		}
		
		return false;
	}
	
	public double minFactor = Double.POSITIVE_INFINITY;
	public double maxFactor = Double.NEGATIVE_INFINITY;
	
	/**
	 * Expand the guessfactors based on target location
	 */
	private void expandFactors(Vector pos) {
		double angle = Utils.normalRelativeAngle(angleTo(pos) - directAngle) / escapeAngle;
		if(angle < minFactor) minFactor = angle;
		if(angle > maxFactor) maxFactor = angle;
	}
	
	/**
	 * Reset our factors for calculating position (important!)
	 */
	public void resetFactors() {
		minFactor = Double.POSITIVE_INFINITY;
		maxFactor = Double.NEGATIVE_INFINITY;
	}
}