User:Chase-san/NewTech/KakeruCode

From RoboWiki
< User:Chase-san | NewTech
Revision as of 14:35, 5 August 2011 by Chase-san (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, 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;
	}
}
Personal tools