# Let's have a tank war!

Partially inspired by Destroy Them With Lazers

## Objective

Your task is to control a tank. Move around and shoot other tanks and obstacles in the 2D battlefield. The last tank standing will be the winner!

## Map format

Your tank will be on a 2D field based on an n by n grid of unit squares. I will decide what n is based on the number of submissions. Each square can contain only one of:

• A tank
• A tree
• A rock
• A wall
• Nothing

All obstacles and tanks completely fill their spaces, and they block all shots that hit them from damaging things further down.

Here is an example of a field with #=tank; T=tree; R=rock; W=wall; .=nothing with n=10

.....#....
..T....R..
WWW...WWWW
W......T..
T...R...Ww
W...W.....
W....W...T
WWWWWW...R
W.........
WWWWWWRT..


Coordinates are in the format x, y where x increases left to right and y increases bottom to top. The bottom left space has the coordinate 0, 0. Each tank may move to any empty space and shoot in any direction.

## Map Dynamics

Your tank doesn't just have to shoot other tanks! If it shoots something on the map, things can happen.

• If a wall is shot at, it will be destroyed after some number of shots, ranging from 1 to 4
• If a tree is shot at, it will be destroyed immediately
• If a rock is shot at, the shot will pass over it and damage the next thing it hits

Once something is destroyed, it is no longer on the map (it will be replaced with nothing). If a shot destroys an obstacle, it will be blocked and will not damage anything further along its path.

## Tank dynamics

Each tank starts with life=100. Each shot at a tank will reduce 20-30 life based on distance. This can be calculated with delta_life=-30+(shot_distance*10/diagonal_map_length) (where diagonal_map_length is (n-1)*sqrt(2)). Additionally, each tank regenerates 1 life per turn.

## Turns

Some number of rounds will be run (I will decide once I have submissions). At the beginning of every round, a map will be generated randomly, and tanks will be placed on it in random empty locations. During every round, each tank will be given a turn, in any arbitrary order. After every tank has been given a turn, they will be given turns again in the same order. The round continues until there is only one tank left. That tank will be the winner, and they will receive 1 point. The game will then go on to the next round.

Once all the rounds have been run, I will post the scores on this question.

During a tank's turn, it may do one of the following

• Move up to 3 spaces in a single direction, either horizontally or vertically. If the tank is blocked by an obstacle or another tank, it will be moved as far as possible without going through the obstacle or tank.
• Shoot in some direction, represented by a floating point angle in degrees. The x-axis of your tank's local space (horizontally left to right, aka east or TurnAction.Direction.EAST) is 0deg, and angles increase counterclockwise. Shots are inaccurate, and the actual angle of the shot may be 5 degrees greater or less than the angle you choose.
• Do nothing.

Turns are not limited in time, but this doesn't mean you can intentionally waste time to hang everything up.

## Submissions/Protocol

Each program submitted will control one tank on the field. The control program is in Java, so your programs need to be in Java for now (I will probably write a wrapper for other languages at some point, or you could write your own).

Your programs will implement the Tank interface, which has the following methods:

public interface Tank {
// Called when the tank is placed on the battlefield.
public void onSpawn(Battlefield field, MapPoint position);
// Called to get an action for the tank on each turn.
public TurnAction onTurn(Battlefield field, MapPoint position, float health);
// Called with feedback after a turn is executed.
// newPosition and hit will be populated if applicable.
public void turnFeedback(MapPoint newPosition, FieldObjectType hit);
// Called when the tank is destroyed, either by another tank,
// or because the tank won. The won parameter indicates this.
public void onDestroyed(Battlefield field, boolean won);
// Return a unique name for your tank here.
public String getName();
}


The Battlefield class contains a 2D array of objects (Battlefield.FIELD_SIZE by Battlefield.FIELD_SIZE) which represents things on the battlefield. Battlefield.getObjectTypeAt(...) will give a FieldObjectType for the object at the specified coordinates (one of FieldObjectType.ROCK, FieldObjectType.TREE, FieldObjectType.TANK, FieldObjectType.WALL, or FieldObjectType.NOTHING). If you try to get an object out of range of the map (coordinates <0 or >=Battlefield.FIELD_SIZE) then an IllegalArgumentException will be thrown.

MapPoint is a class for specifying points on the map. Use MapPoint.getX() and MapPoint.getY() to access the coordinates.

EDIT: Some utility methods have been added: MapPoint.distanceTo(MapPoint), MapPoint.angleBetween(MapPoint), Battlefield.find(FieldObjectType), and TurnAction.createShootActionRadians(double) as suggested by Wasmoo.

All (public API) classes are under the package zove.ppcg.tankwar.

## Control Program

The full source and javadocs of the control program and tank API can be found on my GitHub repo: https://github.com/Hungary-Dude/TankWarControl

Feel free to send pull requests and/or comment if you see a bug or want an improvement.

I have written two sample tank programs, RandomMoveTank and RandomShootTank (the name says it all).

To run your tank, add your fully qualified (package name + class name) tank class to tanks.list (one class per line), edit settings as necessary in zove.ppcg.tankwar.Control (turn delay, whether or not to show a GUI representation of the field, etc), and run zove.ppcg.tankwar.Control. Make sure there are at least 2 tanks on the list, or results are undefined. (Use the sample tanks if necessary).

Your programs will be run on my machine under this control program. I'll include a link to the source once I write it. Feel free to suggest edits to the source.

## Rules

• Your programs may not access the filesystem, network, or attempt to attack my machine in any way
• Your programs may not attempt to exploit my control program to cheat
• No trolling (such as intentionally making your program waste time to hang everything up)
• You may have more than one submission
• Try to be creative with submissions!
• I reserve the right to allow or not allow programs arbitrarily

Good luck!

UPDATE: After fixing the wall teleportation bug and implementing regeneration, I ran the current submissions for 100 rounds with Battlefield.FIELD_SIZE = 30

UPDATE 2: I added the new submission, RunTank, after fooling with Groovy for a bit...

Updated results:

+-----------------+----+
| RandomMoveTank  | 0  |
| RandomShootTank | 0  |
| Bouncing Tank   | 4  |
| Richard-A Tank  | 9  |
| Shoot Closest   | 19 |
| HunterKiller 2  | 22 |
| RunTank         | 23 |
| Dodge Tank      | 24 |
+-----------------+----+


Currently tanks regenerate 1 life per turn. Should that be increased?

• Why are MapPoint's x and y floats? Shouldn't they be ints? – IchBinKeinBaum Jul 14 '14 at 17:09
• Good point. I'm not sure why I decided to make them floats. I'll change them to ints. Edit: updated them to ints, check the repo – DankMemes Jul 14 '14 at 17:39
• If you stand on point 1,1 and shoot with an angle of 0 degrees, the projectile goes in direction EAST, right? – CommonGuy Jul 15 '14 at 7:51
• @Manu Yes. I'm sorry if that wasn't clear. – DankMemes Jul 15 '14 at 14:17
• I found a few errors: Battlefield.java:88 Sometimes obj is null (I think when a tank dies when its move action is pending) Control.java:151 When tanks simultaneously kill each other, get(0) is invalid – Wasmoo Jul 15 '14 at 14:53

HunterKiller

This intelligent hunter will attempt to find a safe position where it can cleanly shoot at exactly one target. (And thus, only one target can shoot it)

Works best when there is lots of cover.

import zove.ppcg.tankwar.*;
import java.util.*;
public final class HunterKiller implements Tank {

private final int MAX_DEPTH = 2;
private Battlefield field;
private List<MapPoint> enemies;
private MapPoint me;
private HashMap<MapPoint, MoveNode> nodeMap = new HashMap();

//A description of how safe the position is from each tank in enemies
private class Safety extends java.util.ArrayList<Double> {
public int threats;
public Safety(MapPoint position) {
for (MapPoint p : enemies) {
int obstacles = countObstacles(position, p, false);
if (obstacles > 0) {
} else {
threats++;
}
}
}
};

//A description of a move
private class Move {

public TurnAction.Direction direction;
public int distance;
public MapPoint point;

public Move(TurnAction.Direction direction, int distance, MapPoint point) {
this.direction = direction;
this.distance = distance;
this.point = point;
}

public TurnAction action() {
return TurnAction.createMoveAction(direction, distance);
}

@Override
public String toString() {
return direction + " " + distance;
}
}

/**
* A MoveNode holds a point and all the moves available from that point.
* The relative safety of the node can be calculated as a function
* of its depth; ie. this position is safe because we can can move to safety
*/
private class MoveNode {

MapPoint point;
ArrayList<Move> moves;
ArrayList<Safety> safetyArray = new ArrayList();

public MoveNode(MapPoint point) {
this.point = point;
this.moves = getMoves(point);
}

public Safety getSafety(int depth) {
if (safetyArray.size() <= depth) {
Safety value;
if (depth == 0 || this.moves.isEmpty()) {
value = new Safety(point);
} else {
ArrayList<Safety> values = new ArrayList();
for (Move m : moves) {
MoveNode n = nodeMap.get(m.point);
if (n == null) {
n = new MoveNode(m.point);
nodeMap.put(n.point, n);
}
}
Collections.sort(values, cmp);
value = values.get(0);
}
}
return safetyArray.get(depth);
}
}

/**
* Find all the points between here and there, excluding those points
*/
private java.util.ArrayList<MapPoint> path(final MapPoint p1, MapPoint p2) {
java.util.ArrayList<MapPoint> ret = new ArrayList();
float tankX = p1.getX();
float tankY = p1.getY();
double maxDistance = p1.distanceTo(p2);
for (int x = 0; x < Battlefield.FIELD_SIZE; x++) {
for (int y = 0; y < Battlefield.FIELD_SIZE; y++) {
float x2 = (float) (((x - tankX) * Math.cos(-angle)) - ((y - tankY) * Math.sin(-angle)));
float y2 = (float) (((x - tankX) * Math.sin(-angle)) + ((y - tankY) * Math.cos(-angle)));
if (x2 > 0 && y2 >= -0.5 && y2 <= 0.5) {
MapPoint p = new MapPoint(x, y);
if (maxDistance > p1.distanceTo(p)) {
}
}
}
}
Collections.sort(ret, new Comparator<MapPoint>() {
@Override
public int compare(MapPoint o1, MapPoint o2) {
return (int) Math.signum(p1.distanceTo(o2) - p1.distanceTo(o1));
}
});
return ret;
}

/**
* Find the number of obstacles between here and there, excluding those
* points
*/
private int countObstacles(MapPoint p1, MapPoint p2, boolean countRocks) {
java.util.ArrayList<MapPoint> points = path(p1, p2);
int count = 0;
for (MapPoint p : points) {
Object obj = field.getObjectTypeAt(p);
if (FieldObjectType.NOTHING.equals(obj)
|| (!countRocks && FieldObjectType.ROCK.equals(obj))
|| (!countRocks && FieldObjectType.TANK.equals(obj))
|| p.equals(me)) {
count += 0;
} else {
count += 1;
}
}
return count;
}

/**
* Returns a value between 1.0 and 0.0, where 1.0 is far and 0.0 is close
*/
private double missChance(double distance) {
return distance / Battlefield.DIAGONAL_FIELD_SIZE;
}

//Returns a list of valid moves from the given position
private ArrayList<Move> getMoves(MapPoint position) {
ArrayList<Move> ret = new ArrayList();
for (TurnAction.Direction d : TurnAction.Direction.values()) {
for (int i = 1; i <= 3; i++) {
MapPoint p = d.translate(position, i);
try {
FieldObjectType t = field.getObjectTypeAt(p);
if (t != FieldObjectType.NOTHING && !p.equals(me)) {
break;
}
} catch (IllegalArgumentException ex) {
//Can't move off the map...
break;
}
}
}
return ret;
}

//Compares two safeties with a preference for exactly 1 threat
private Comparator<Safety> cmp = new Comparator<Safety>() {
@Override
public int compare(Safety o1, Safety o2) {
int tc1 = o1.threats;
int tc2 = o2.threats;
//Prefer 1 threat
if (tc2 == 1 && tc1 != 1) {
return 1;
}
if (tc2 != 1 && tc1 == 1) {
return -1;
}

//Prefer fewer threats
if (tc2 != tc1) {
return tc1 - tc2;
}

//We're splitting hairs here
//Determine the least safe option
int ret = -1;
double s = Double.MAX_VALUE;
for (Double d : o1) {
if (d < s) {
s = d;
}
}
for (Double d : o2) {
if (d < s) {
ret = 1;
break;
}
if (d == s) {
ret = 0;
}
}
if (tc1 > 1) {
//Prefer the safest
return -ret;
} else {
//Prefer the least safest
return ret;
}
}
};

@Override
public TurnAction onTurn(Battlefield field, MapPoint position, float health) {
//Find all the tanks
List<MapPoint> enemies = field.find(FieldObjectType.TANK);
enemies.remove(position);
if (enemies.isEmpty()) {
return TurnAction.createNothingAction();
}

//Set the constants needed for this turn
this.enemies = enemies;
this.field = field;
this.me = position;

//Create a new NodeMap
MoveNode n = new MoveNode(position);
this.nodeMap.clear();
this.nodeMap.put(position, n);

//Find the "best" safety within MAX_DEPTH moves
int depth = 0;
Safety safety = n.getSafety(0);
for (depth = 0; depth < MAX_DEPTH; depth++) {
int lastThreat = safety.threats;
safety = n.getSafety(depth);
int newThreat = safety.threats;
if (newThreat == 1) {
//Always prefer 1 threat
break;
}
if (depth != 0 && lastThreat - newThreat >= depth) {
//Prefer fewer threats only if we are much safer;
//  Specifically, don't move twice for only 1 less threat
break;
}
}

//Depth == 0         : Only 1 threat; best position
//Depth == MAX_DEPTH : Many or no threats, but no good moves available
if (depth > 0 && depth < MAX_DEPTH) {
//Move towards the better spot
for (Move m : n.moves) {
if (nodeMap.get(m.point).getSafety(depth - 1) == safety) {
return m.action();
}
}
}

//We're in a good position, shoot now
//Calculate tank with most threat
MapPoint threat = null;
double biggestThreat = Double.MAX_VALUE;
for (MapPoint p : enemies) {
double t = missChance(position.distanceTo(p)) + countObstacles(position, p, false);
if (t < biggestThreat) {
biggestThreat = t;
threat = p;
}
}

}
public void onDestroyed(Battlefield field, boolean won) {}
public void onSpawn(Battlefield field, MapPoint position) {}
public void turnFeedback(MapPoint newPosition, FieldObjectType hit) {}
public String getName() {
return "HunterKiller " + MAX_DEPTH;
}

}


And that's it. I'm spent.

This straight-forward tank finds the closest enemy tank and shoots at it. Would be nice if find, distance, and angle were built in, and if createShootAction accepted a double in radians (i.e. the result of angle)

Edit: Class rewritten to include new utility methods

public final class ShootClosestTank implements Tank {

@Override
public TurnAction onTurn(Battlefield field, MapPoint position, float health) {
//Find all the tanks
List<MapPoint> tanks = field.find(FieldObjectType.TANK);
tanks.remove(position);

if (tanks.isEmpty()) return TurnAction.createNothingAction();

//Calculate closest tank
MapPoint cPoint = null;
double cDist = Double.POSITIVE_INFINITY;
for (MapPoint p : tanks) {
double dist = position.distanceTo(p);
if (dist < cDist) {
cDist = dist;
cPoint = p;
}
}

//Shoot at closest tank

}

@Override
public void onDestroyed(Battlefield field, boolean won) {
//Sucks to be me
}

@Override
public void onSpawn(Battlefield field, MapPoint position) {
//No setup
}

@Override
public void turnFeedback(MapPoint newPosition, FieldObjectType hit) {
//Nothing to update
}

@Override
public String getName() {
return "Shoot Closest";
}
}

• Would be nice if find, distance, and angle were built in, and if createShootAction accepted a double in radians (i.e. the result of angle) - Great idea, I'll add it in. – DankMemes Jul 15 '14 at 14:10

I'm not very good at this but I thought I'd still give it a shot, you know, practice and stuff.

My tank will randomly decide to move or shoot. When it decides to shoot, it'll try to shoot at the nearest available target.

package com.richarda.tankwar;

import zove.ppcg.tankwar.*;

import java.util.Random;
import java.util.ArrayList;

public class RichardATank implements Tank
{
private String name;

public RichardATank()
{
this.name = "Richard-A Tank";
}

/**
*
* @param field
*            The current battlefield, complete with all obstacle and tank
*            positions
* @param position
*/
@Override
public void onSpawn(Battlefield field, MapPoint position)
{

}

/**
* The tank will randomly move around and occasionally shoot at the nearest available target.
*
* @param field
*            The current battlefield, complete with all obstacle and tank
*            positions
* @param position
*            The tank's current position
* @param health
*            The tank's current health
* @return
*/
@Override
public TurnAction onTurn(Battlefield field, MapPoint position, float health)
{
Random r = new Random();
int n = r.nextInt(2);

if(n == 1)
{
return this.tryShootAtNearestTank(field, position);
}

return TurnAction.createMoveAction(TurnAction.Direction.getRandom(), r.nextInt(2) + 1);
}

/**
*
* @param newPosition
*            The tank's new position (may not be changed)
* @param hit
*            What the tank hit, if it decided to shoot
*/
@Override
public void turnFeedback(MapPoint newPosition, FieldObjectType hit)
{

}

/**
*
* @param field
*            The battlefield
* @param won
*/
@Override
public void onDestroyed(Battlefield field, boolean won)
{

}

@Override
public String getName()
{
return this.name;
}

/**
* Try and shoot at the nearest tank
*
* @param bf The battlefield
* @param curTankLocation The current tank's location
* @return TurnAction the shoot action to the nearest tank
*/
private TurnAction tryShootAtNearestTank(Battlefield bf, MapPoint curTankLocation)
{
MapPoint nearestTankLoc = this.getNearestTankLocation(bf, curTankLocation);

double firingAngle = curTankLocation.angleBetween(nearestTankLoc);

return TurnAction.createShootAction((float) firingAngle);
}

/**
* Try to find the nearest tank's location
*
* @param bf The battlefield
* @param curTankLocation The current tank's location
* @return MapPoint The location of the nearest tank
*/
private MapPoint getNearestTankLocation(Battlefield bf, MapPoint curTankLocation)
{
ArrayList<MapPoint> enemyTankLocations = this.getEnemyTanksOnField(bf, curTankLocation);

MapPoint nearestTankLoc = null;

for(MapPoint enemyTankLoc : enemyTankLocations)
{
if(nearestTankLoc == null || curTankLocation.distanceTo(enemyTankLoc) < curTankLocation.distanceTo(nearestTankLoc))
{
nearestTankLoc = enemyTankLoc;
}
}

return nearestTankLoc;
}

/**
* Get all enemy tanks on the field
*
* @param bf The battlefield
* @param curTankLocation The current tank's location
* @return ArrayList<MapPoint> A list with all enemy tanks in it
*/
private ArrayList<MapPoint> getEnemyTanksOnField(Battlefield bf, MapPoint curTankLocation)
{
int maxSize = Battlefield.FIELD_SIZE;
ArrayList<MapPoint> tanks = new ArrayList<MapPoint>();

for(int i = 0; i < maxSize; i++)
{
for(int j = 0; j < maxSize; j++)
{
FieldObjectType objType = bf.getObjectTypeAt(i, j);

if(objType == FieldObjectType.TANK)
{
MapPoint tankLocation = new MapPoint(i, j);

if(!tankLocation.equals(curTankLocation))
{
}
}
}
}

return tanks;
}
}


The full code including the control program can be found here.

• Try Direction.getRandom() – DankMemes Jul 15 '14 at 15:48
• @ZoveGames Edited it, thanks for the tip. – MisterBla Jul 15 '14 at 15:52

## Dodge Tank

This tank will shoot at the closest tank. Every so often, depending on its health and the last time it moved, it will try to move perpendicular to the closest tank in an attempt to dodge its lasers.

public class DodgeTank implements Tank {

private int lastMove;
@Override
public void onSpawn(Battlefield field, MapPoint position){
}
@Override
public TurnAction onTurn(Battlefield field, MapPoint position, float health){
List<MapPoint> tanks = field.find(FieldObjectType.TANK);
tanks.remove(position);
MapPoint nearest= new MapPoint(0,0);
double dis=Double.POSITIVE_INFINITY;
for (MapPoint tank: tanks){
double distance=tank.distanceTo(position);
if (distance<dis){
nearest=tank;
dis=distance;
}
}
if (lastMove*Math.random()+(4-health/25)<5){//Attack
lastMove++;
}
else {
lastMove=0;
for (MapPoint tank : tanks){
}
cumulativeAngle/=tanks.size();
cumulativeAngle/=(Math.PI/2);
int dir=(int)Math.floor(cumulativeAngle);
TurnAction move;
switch (dir){
case 0:
if (position.getX()>2&&field.getObjectTypeAt(position.cloneAndTranslate(-3, 0)).equals(FieldObjectType.NOTHING)){
return TurnAction.createMoveAction(TurnAction.Direction.WEST, 3);
}
return TurnAction.createMoveAction(TurnAction.Direction.EAST, 3);
case 1:
if (position.getY()>2&&field.getObjectTypeAt(position.cloneAndTranslate(0, -3)).equals(FieldObjectType.NOTHING)){
return TurnAction.createMoveAction(TurnAction.Direction.SOUTH, 3);
}
return TurnAction.createMoveAction(TurnAction.Direction.NORTH, 3);
case -1:
if ((position.getY()<(Battlefield.FIELD_SIZE-4))&&field.getObjectTypeAt(position.cloneAndTranslate(0,3)).equals(FieldObjectType.NOTHING)){
return TurnAction.createMoveAction(TurnAction.Direction.NORTH, 3);
}
return TurnAction.createMoveAction(TurnAction.Direction.SOUTH, 3);
default:
if ((position.getX()<(Battlefield.FIELD_SIZE-4))&&field.getObjectTypeAt(position.cloneAndTranslate(3,0)).equals(FieldObjectType.NOTHING)){
return TurnAction.createMoveAction(TurnAction.Direction.EAST, 3);
}
return TurnAction.createMoveAction(TurnAction.Direction.WEST, 3);
}
}
}
@Override
public void turnFeedback(MapPoint newPosition, FieldObjectType hit){
}
@Override
public void onDestroyed(Battlefield field, boolean won){
//if (won) this.taunt();
//else this.selfDestruct();
}
@Override
public String getName(){
return "Dodge Tank";
}
}


This was way more complicated than I thought..

This is my entry in groovy, you need groovy installed and compile it with

groovyc zove\ppcg\tankwar\felsspat\RunTank.groovy


To call it you need to add $GROOVY_HOME/Groovy/Groovy-2.3.4/lib/groovy-2.3.4.jar (or whatever version) to the classpath. I could send you a compiled .class file and the library if you don't want to install it. There seems to be a situation where tanks can't see it other, I don't know if that is intended. That caused deadlocks while testing. Anyway here is RunTank: RunTank boldy advances in the opposite direction of the closest tank if it is the closest tank to the closest tank or more than one tank is within FIELD_SIZE / 3. I hope that makes sense, I'm drunk :) package zove.ppcg.tankwar.felsspat import zove.ppcg.tankwar.Battlefield import zove.ppcg.tankwar.FieldObjectType import zove.ppcg.tankwar.MapPoint import zove.ppcg.tankwar.Tank import zove.ppcg.tankwar.TurnAction import zove.ppcg.tankwar.TurnAction.Direction public class RunTank implements Tank { @Override public TurnAction onTurn(Battlefield field, MapPoint position, float health) { def targets = (field.find(FieldObjectType.TANK) - position).sort{ position.distanceTo(it) } if (targets) { def runDistance = (Battlefield.FIELD_SIZE / 3).toInteger() def closeTargets = targets.grep { position.distanceTo(it) < runDistance } if (position.distanceTo(closestEnemy(position, field)) < targets.first().distanceTo(closestEnemy(targets.first(), field))) { return run(field, position, targets) } if (closeTargets.size() > 1) { return run(field, position, targets) } else { return shootEnemy(position, targets) } } else { println "WTF! Targets:${field.find(FieldObjectType.TANK)} + Position \${position}"
return TurnAction.createMoveAction(Direction.getRandom(), 1);
}

}

def shootEnemy(position, targets) {
}

def run(field, position, targets) {
def freePositions = (field.find(FieldObjectType.NOTHING) - position).grep { position.distanceTo(it) <= 3.0 && [0d, 90d, 180d, 270d].contains(position.angleBetween(it))}.sort{position.distanceTo(it)}
def availablePositions = []
freePositions.each { targetPosition ->
def positions = getPositionsBetween(position,targetPosition)
if (! positions || positions.every { it.equals(FieldObjectType.NOTHING) }) {
}
}
availablePositions = availablePositions.sort{closestEnemy(it, field)}.reverse()

if (availablePositions) {
def targetPosition = availablePositions.first()
if (targetPosition.distanceTo(closestEnemy(targetPosition, field)) > position.distanceTo(closestEnemy(position, field))) { // Don't move closer to an enemy
return moveTo(position, targetPosition)
} else {
return shootEnemy(position, targets)
}
} else {
return shootEnemy(position, targets)
}
}

def moveTo(position, targetPosition) {
def distance = position.distanceTo(targetPosition).toInteger()
switch (position.angleBetween(targetPosition)) {
case 0d:
return TurnAction.createMoveAction(TurnAction.Direction.NORTH, distance)
break

case 90d:
return TurnAction.createMoveAction(TurnAction.Direction.EAST, distance)
break

case 180d:
return TurnAction.createMoveAction(TurnAction.Direction.SOUTH, distance)
break

case 270d:
return TurnAction.createMoveAction(TurnAction.Direction.West, distance)
break

default:
println "I'm stupid :("

}
}

def closestEnemy(position, field) {
return field.find(FieldObjectType.TANK).sort { position.distanceTo(it) }.first()
}

def getPositionsBetween(self, target) {
def positions = []
if(self.x == target.x) {
for (y in self.y..target.y) {
}
} else {
for (x in self.x..target.x) {
}
}
return positions - self - target
}

@Override
public void turnFeedback(MapPoint newPosition, FieldObjectType hit) {
}

@Override
public void onDestroyed(Battlefield field, boolean won) {
println ":("
}

@Override
public void onSpawn(Battlefield field, MapPoint position) {
println "Go!"
}

@Override
public String getName() {
return "RunTank";
}
}


I have one suggestion: Add colors to the tank, and a method to implement it. Also labels would be nice in the GUI :)

• def RandomMoveTank() {} - is that meant to be there? (I don't know groovy) – DankMemes Jul 25 '14 at 19:15
• No, I copied the RandomMoveTank and forgot to remove the constructor, thanks :) – Fels Jul 25 '14 at 20:06
• I compiled your code and added the folder containing the .class files and the groovy jar to my project classpath. Reflection worked! I've posted the updated scores. Your tank did pretty well :) – DankMemes Jul 25 '14 at 21:34
• Nice! And damn you DodgeTank! – Fels Jul 27 '14 at 8:36

This one is a variant on the Shoot-Closest in that, every other turn, it moves in a direction until it can no longer. It shoots every other turn.

It has a handy utility, path, which can be used to identify all points (and thus objects) between two points.

public final class BouncingTank implements Tank {

/**
* Find all the points between here and there, excluding those points
* @param p1 Here
* @param p2 There
* @return
*/
private java.util.ArrayList<MapPoint> path(MapPoint p1, MapPoint p2) {
double dist = p1.distanceTo(p2);
double dx = (p2.getX() - p1.getX()) / dist;
double dy = (p2.getY() - p1.getY()) / dist;

java.util.ArrayList<MapPoint> ret = new java.util.ArrayList();
MapPoint lastP = null;
for (int i = 0; i < dist; i++) {
MapPoint p = p1.cloneAndTranslate((int)(i*dx), (int)(i*dy));
if (p.equals(p1) || p.equals(lastP)) continue;
if (p.equals(p2)) break;
lastP = p;
}
return ret;
}

/**
* Find the number of legal moves in the given direction
* @param field
* @param position
* @param dir
* @return
*/
private int findMoves(Battlefield field, MapPoint position, TurnAction.Direction dir) {
if (dir == null) return -1;
int count = 0;
MapPoint dest = dir.translate(position, Battlefield.FIELD_SIZE);
java.util.ArrayList<MapPoint> obs = path(position, dest);
for (MapPoint oMP : obs) {
try {
if (FieldObjectType.NOTHING.equals(field.getObjectTypeAt(oMP))) {
count++;
} else {
break;
}
} catch (IllegalArgumentException ex) {
break;
}
}
return count;
}

/**
* Finds the direction towards which there are the fewest obstacles
* @param field
* @param position
* @return
*/
private TurnAction.Direction findDirection(Battlefield field, MapPoint position) {
TurnAction.Direction ret = null;
int mostMoves = 0;
for (TurnAction.Direction d : TurnAction.Direction.values()) {
int count = findMoves(field, position, d);
if (count > mostMoves) {
ret = d;
mostMoves = count;
}
}
return ret; //Maybe null
}

private TurnAction shootClosest(Battlefield field, MapPoint position) {
//Find all the tanks
List<MapPoint> tanks = field.find(FieldObjectType.TANK);
tanks.remove(position);

if (tanks.isEmpty()) return TurnAction.createNothingAction();

//Calculate closest tank
MapPoint cPoint = null;
double cDist = Double.POSITIVE_INFINITY;
for (MapPoint p : tanks) {
double dist = position.distanceTo(p);
if (dist < cDist) {
cDist = dist;
cPoint = p;
}
}

//Shoot at closest tank
}

private int turnsUntilShoot = 1;
private TurnAction.Direction moveToward = null;

@Override
public TurnAction onTurn(Battlefield field, MapPoint position, float health) {
//Determine if current direction is valid
int moves = findMoves(field, position, moveToward);
if (moves <= 0) {
moveToward = findDirection(field, position);
//Determine if we're stuck
if (moveToward == null) {
return shootClosest(field, position);
}
}

//Shoot if it's time
if (turnsUntilShoot == 0) {
turnsUntilShoot = 1;
return shootClosest(field, position);
} else {
turnsUntilShoot--;
return TurnAction.createMoveAction(moveToward, Math.min(3, moves));
}
}

public void onDestroyed(Battlefield field, boolean won) {}
public void onSpawn(Battlefield field, MapPoint position) {}
public void turnFeedback(MapPoint newPosition, FieldObjectType hit) {}
public String getName() {
return "Bouncing Tank";
}
}