Refactoring, Bug Prediction, Shortest Paths prediction

Chase bug and FarChase bug simulators implemented. Codes faster reachable by
opponent get lowered values.
This commit is contained in:
Armin Friedl 2018-01-10 23:36:38 +01:00
parent a0ced36a12
commit 56a7cf597b
17 changed files with 573 additions and 184 deletions

View file

@ -5,7 +5,6 @@ import java.util.regex.Pattern;
import farbot.ai.AI;
import farbot.game.GameState;
import farbot.game.Settings;
public class FarbotIO {
private static Scanner in;

View file

@ -1,4 +1,4 @@
package farbot.game;
package farbot;
public class Settings {
private Integer timebank;

View file

@ -1,11 +1,12 @@
package farbot.ai;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import farbot.game.GameState;
import farbot.game.MoveResult;
import farbot.game.map.Move;
import farbot.game.Move;
import static farbot.FarbotIO.logln;
@ -21,28 +22,46 @@ public class AI {
public String calcMove(Integer time) {
logln("Calculating move in "+time+"ms");
Set<Move> validMoves = getValidMoves();
List<MoveValue> valuedMoves = getValuedMoves(validMoves);
MoveValue maxMove = getMaxMove(valuedMoves);
this.lastMove = maxMove;
logln("Max.move '"+maxMove.move+"': "+maxMove.value);
return maxMove.move.name().toLowerCase();
}
private Set<Move> getValidMoves(){
Set<Move> validMoves = currentState.selfMoves();
logln("Valid moves: "+validMoves.toString());
return validMoves;
}
private List<MoveValue> getValuedMoves(Set<Move> validMoves){
ArrayList<MoveValue> valuedMoves = new ArrayList<>();
for(Move m: validMoves) {
logln("Checking move: "+m.name());
GameState fork = currentState.fork();
MoveResult mr = fork.moveSelf(m);
GameTree gt = new GameTree(fork);
GameTree gt = new GameTree(currentState.fork(), m);
gt.build(treeDepth);
Double value = gt.getValue();
value += mr.getCollectedCodes()*Math.pow((treeDepth+1), 1.6);
value -= mr.getKilledBugs()*8*Math.pow((treeDepth+1), 1.6);
logln("Move '"+m.name()+"': "+value);
valuedMoves.add(new MoveValue(m, value));
}
valuedMoves.sort((mv1, mv2) -> -mv1.value.compareTo(mv2.value));
return valuedMoves;
}
private MoveValue getMaxMove(List<MoveValue> valuedMoves) {
valuedMoves.sort(Comparator.comparingDouble(m -> -m.value));
MoveValue maxMove = valuedMoves.get(0);
if(this.lastMove != null && this.lastMove.value <= maxMove.value) {
// Prevent fallback loop
if(this.lastMove != null && this.lastMove.value >= maxMove.value) {
switch(this.lastMove.move) {
case LEFT:
if(maxMove.move == Move.RIGHT) maxMove = valuedMoves.get(1);
@ -61,10 +80,7 @@ public class AI {
}
}
this.lastMove = maxMove;
logln("Max.move '"+maxMove.move+"': "+maxMove.value);
return maxMove.move.name().toLowerCase();
return maxMove;
}
public void parseUpdate(String[] update) {

View file

@ -2,33 +2,42 @@ package farbot.ai;
import static farbot.FarbotIO.logln;
import farbot.game.map.Point;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.function.BinaryOperator;
import farbot.game.GameState;
import farbot.game.MoveResult;
import farbot.game.Move;
import farbot.game.Point;
import farbot.game.map.util.ShortestPaths;
import farbot.game.token.Code;
public class GameTree {
private GameState node;
private Double value;
private HashSet<Point> visited;
private Integer totalLevel;
private Move initMove;
public GameTree(GameState root) {
public GameTree(GameState root, Move initMove) {
this.node = root;
this.value = 0.0;
this.totalLevel = 1;
this.visited = new HashSet<>();
this.initMove = initMove;
}
public void build(int level) {
this.totalLevel = level;
Queue<GameState> children = makeChildren(node, this, level);
MoveResult mr = Simulator.move(initMove, node);
value += valueCodes(mr, level, node, this);
value += valueBugs(mr, level, node);
Queue<GameState> children = makeChildren(node, this, --level);
while(level > 0) {
level--;
@ -62,16 +71,10 @@ public class GameTree {
for(Point selfMove: selfMoves) {
GameState child = state.fork();
MoveResult mr = child.moveSelf(selfMove);
MoveResult mr = Simulator.move(selfMove, child);
if(mr.getCollectedCodes() != 0){
logln("Got "+mr.getCollectedCodes()+" codes at "+selfMove.toString()+" in level "+level);
gtRoot.value += Math.pow(level*mr.getCollectedCodes(), 1.3);
}
if(mr.getKilledBugs() != 0) {
logln("Killed "+mr.getKilledBugs()+" bugs at "+selfMove.toString()+" in level "+level);
gtRoot.value -= Math.pow(level*4*mr.getKilledBugs(), level/totalLevel);
}
gtRoot.value += valueCodes(mr, level, child, gtRoot);
gtRoot.value += valueBugs(mr, level, child);
gtRoot.visited.add(selfMove);
@ -80,4 +83,50 @@ public class GameTree {
return children;
}
private double valueBugs(MoveResult moveResult, int level, GameState state) {
double value = 0.0;
if(moveResult.lenKilledBugs() != 0) {
value = -1 * Math.pow(level*4*moveResult.lenKilledBugs(), (double) level/totalLevel);
if (level >= totalLevel-1) value *= 2;
if (level >= 5) value /= 2;
logln("Killed "+moveResult.lenKilledBugs()+" bugs at "+state.getSelfPosition()+" in level "+level+" @ "+value);
}
return value;
}
private double valueCodes(MoveResult moveResult, int level, GameState state, GameTree gtRoot) {
double value = 0.0;
if(moveResult.lenCollectedCodes() != 0){
double codeValSum = moveResult.getCollectedCodes().stream()
.map(c -> codeVal(c, gtRoot))
.reduce(Double::sum).get();
value = Math.pow(level*codeValSum, 1.7);
logln("Got "+moveResult.lenCollectedCodes()+" codes at "+state.getSelfPosition()+" in level "+level+ " @ "+value);
}
return value;
}
private Double codeVal(Code code, GameTree gtRoot) {
List<Point> opponentShortestPath = ShortestPaths.shortestPath(
gtRoot.node.getOpponentPosition(), code.getPosition());
List<Point> selfShortestPath = ShortestPaths.shortestPath(
gtRoot.node.getSelfPosition(), code.getPosition());
logln("Shortest path for Code at "+code.getPosition()+
": Opponent="+opponentShortestPath.size()+
", Self="+selfShortestPath.size());
if(opponentShortestPath.size() < selfShortestPath.size()) {
if(opponentShortestPath.size() <= 3) return 0.0;
return 1.0/(selfShortestPath.size() - opponentShortestPath.size()+0.5);
}
return 1.0;
}
}

View file

@ -0,0 +1,40 @@
package farbot.ai;
import java.util.List;
import farbot.game.Player;
import farbot.game.token.Bug;
import farbot.game.token.Code;
public class MoveResult {
private Player player;
private List<Bug> killedBugs;
private List<Code> collectedCodes;
public MoveResult(Player player, List<Code> collectedCodes, List<Bug> killedBugs) {
this.player = player;
this.killedBugs = killedBugs;
this.collectedCodes = collectedCodes;
}
public Player getPlayer() {
return player;
}
public List<Bug> getKilledBugs() {
return killedBugs;
}
public int lenKilledBugs() {
return killedBugs.size();
}
public List<Code> getCollectedCodes() {
return collectedCodes;
}
public int lenCollectedCodes() {
return collectedCodes.size();
}
}

View file

@ -1,19 +1,24 @@
package farbot.ai;
import farbot.game.map.Point;
import farbot.Settings;
import farbot.game.GameState;
import farbot.game.Settings;
import farbot.game.Point;
import farbot.game.token.Bug;
import farbot.game.token.Bug.BugAI;
import farbot.game.token.Code;
import farbot.game.token.Mine;
import static farbot.FarbotIO.logln;
import static farbot.FarbotIO.log;
public class Parser {
public static void parse(String[] update, GameState state) {
switch(update[2]) {
case "round":
state.setRound(Integer.parseInt(update[3]));
log("\n");
logln("******* Round "+state.getRound()+" *******");
log("\n");
break;
case "field":
parseMap(update[3].split(","), state);

View file

@ -0,0 +1,103 @@
package farbot.ai;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import farbot.game.GameState;
import farbot.game.Move;
import farbot.game.Player;
import farbot.game.Point;
import farbot.game.token.Bug;
import farbot.game.token.Code;
public class Simulator {
public static MoveResult move(Move move, GameState state) {
Point movePoint = state.calcMovePoint(state.getSelfPosition(), move);
return move(movePoint, state);
}
public static MoveResult move(Point position, GameState state) {
state.moveSelf(position);
List<Bug> bugs = state.removeBugsAt(state.getSelfPosition());
List<Code> codes = state.removeCodesAt(state.getSelfPosition());
predictBugs(state);
bugs.addAll(state.removeBugsAt(state.getSelfPosition()));
state.addSelfSnippets(codes.size());
state.removeSelfSnippets(bugs.size()*4);
return new MoveResult(state.getSelfClone(), codes, bugs);
}
private static void predictBugs(GameState state) {
List<Bug> bugs = state.getBugs();
state.clearBugs();
for(Bug bug: bugs) {
Point movePoint = bug.getPosition();
switch(bug.getAiType()) {
case CHASE:
movePoint = chaseAI(bug, state);
break;
case FAR_CHASE:
movePoint = farChaseAI(bug, state);
break;
default:
break;
}
state.putToken(new Bug(movePoint.x, movePoint.y, bug.getAiType()));
}
}
private static Point chaseAI(Bug chaseBug, GameState state) {
Player nearestPlayer = state.getPlayerClones().stream()
.min(Comparator.comparingDouble(p -> p.distance(chaseBug.getPosition())))
.get();
return euclideanBugMove(chaseBug.getPosition(),
nearestPlayer.getPosition(),
state);
}
private static Point farChaseAI(Bug farChaseBug, GameState state) {
// only difference to chaseAI: uses .max
Player farthestPlayer = state.getPlayerClones().stream()
.max(Comparator.comparingDouble(p -> p.distance(farChaseBug.getPosition())))
.get();
return euclideanBugMove(farChaseBug.getPosition(),
farthestPlayer.getPosition(),
state);
}
private static Point euclideanBugMove(Point from, Point to, GameState state) {
ArrayList<Point> movePoints = new ArrayList<Point>();
movePoints.addAll(bugMovePoints(from, state));
Collections.shuffle(movePoints);
Point nearestPoint = movePoints.stream()
.min(Comparator.comparingDouble(to::distance))
.get(); // .min returns an Optional<Point>
return nearestPoint;
}
private static Set<Point> bugMovePoints(Point from, GameState state){
Set<Point> movePoints = state.movePoints(from);
// Bugs can't use Gates
// TODO: Move this to GameMap
if(from.equals(new Point(0,7))) {
movePoints.remove(new Point(18,7));
}else if(from.equals(new Point(18,7))) {
movePoints.remove(new Point(0,7));
}
return movePoints;
}
}

View file

@ -1,20 +1,32 @@
package farbot.game;
import farbot.game.map.Point;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import farbot.Settings;
import farbot.ai.MoveResult;
import farbot.game.map.GameMap;
import farbot.game.map.Move;
import farbot.game.map.util.Movement;
import farbot.game.token.Bug;
import farbot.game.token.Code;
import farbot.game.token.Mine;
/**
* Manager for the GameState.
* The public interface for managing the game state
*
* Any data retrieved from GameState belongs to the caller.
* Users may not alter player, map or token
* states directly, but only through the API provided by
* this class.
*
* Any data retrieved from GameState belongs to the caller
* and is safe to use. You cannot change the GameState by
* altering data returned from GameState methods. Use
* the appropriate mutating methods instead.
*
* GameState can create a fast, lazy clone of itself
* by calling fork(). Forking itself is as cheap as possible.
* Deep field copies are created lazily, i.e. only if needed.
*/
public class GameState {
private Integer round;
@ -44,34 +56,18 @@ public class GameState {
this.forked = forked;
}
public List<GameState> fork(Integer times) {
ArrayList<GameState> forks = new ArrayList<>();
for (int i=0; i<times; i++) forks.add(fork());
return forks;
public Integer getRound() {
return round;
}
public void setRound(Integer round) {
this.round = round;
}
public GameState fork() {
GameState fork = new GameState(self.clone(), opponent.clone(), map.fork(), round, true);
fork.map.setForked(true);
return fork;
}
public void putToken(Bug bug) {
map.putToken(bug);
}
public void putToken(Code code) {
map.putToken(code);
}
public void putToken(Mine mine) {
map.putToken(mine);
}
public void clearTokens() {
map.clearTokens();
}
/*
* Player Management
*/
public void setSnippets(String playerName, Integer snippets) {
Settings s = Settings.getInstance();
if (playerName == s.getSelfName()) self.setSnippets(snippets);
@ -82,8 +78,12 @@ public class GameState {
return self.getSnippets();
}
public Integer getOpponentSnippets() {
return opponent.getSnippets();
public void addSelfSnippets(Integer snippets) {
self.addSnippets(snippets);
}
public void removeSelfSnippets(Integer snippets) {
self.removeSnippets(snippets);
}
public void setBombs(String playerName, Integer bombs) {
@ -102,64 +102,107 @@ public class GameState {
return self.getPosition();
}
public MoveResult moveSelf(Point position) {
return makeMove(self, position);
}
public MoveResult moveSelf(Move move) {
return makeMove(self, calcMovePoint(self.getPosition(), move));
public Point getOpponentPosition() {
return opponent.getPosition();
}
public MoveResult moveOpponent(Point position) {
return makeMove(opponent, position);
public void moveSelf(Point position) {
self.setPosition(position);
}
public MoveResult moveOpponent(Move move) {
return makeMove(self, calcMovePoint(opponent.getPosition(), move));
public void moveSelf(Move move) {
self.setPosition(calcMovePoint(self.getPosition(), move));
}
private MoveResult makeMove(Player player, Point position) {
player.setPosition(position);
List<Code> codes = map.removeCodesAt(position);
List<Bug> bugs = map.removeBugsAt(position);
player.addSnippets(codes.size());
player.removeSnippets(bugs.size()*4);
return new MoveResult(player.clone(), codes.size(), bugs.size());
public Player getSelfClone() {
return self.clone();
}
public List<Player> getPlayerClones() {
ArrayList<Player> players = new ArrayList<>();
players.add(self.clone());
players.add(opponent.clone());
return players;
}
/*
* Token Management
*/
public void putToken(Bug bug) {
map.putToken(bug);
}
public Integer getRound() {
return round;
public void putToken(Code code) {
map.putToken(code);
}
public void setRound(Integer round) {
this.round = round;
public void putToken(Mine mine) {
map.putToken(mine);
}
public List<Code> removeCodesAt(Point position){
return map.removeCodesAt(position);
}
public List<Bug> removeBugsAt(Point position){
return map.removeBugsAt(position);
}
public List<Bug> getBugs(){
return map.getBugs();
}
public void clearBugs() {
map.clearBugs();
}
public void clearTokens() {
map.clearTokens();
}
/*
* Movements Management
*/
public Set<Point> selfMovePoints(){
return movePoints(self.getPosition());
}
public Set<Move> selfMoves(){
return Movement.validMoves(self.getPosition());
}
public Point calcMovePoint(Point from, Move move) {
return Movement.calcMovePoint(from, move);
}
public Set<Point> movePoints(Point from){
return Movement.validMovePoints(from);
}
/*
* Fork Managment
*
*/
public List<GameState> fork(Integer times) {
ArrayList<GameState> forks = new ArrayList<>();
for (int i=0; i<times; i++) forks.add(fork());
return forks;
}
public GameState fork() {
GameState fork = new GameState(self.clone(), opponent.clone(), map.fork(), round, true);
fork.map.setForked(true);
return fork;
}
public boolean isForked() {
return forked;
}
public Set<Point> selfMovePoints(){
return map.validMovePoints(self.getPosition());
}
public Set<Move> selfMoves(){
return map.validMoves(self.getPosition());
}
public Set<Point> opponentMovePoints(){
return map.validMovePoints(opponent.getPosition());
}
public Set<Move> opponentMoves(){
return map.validMoves(opponent.getPosition());
}
public Point calcMovePoint(Point from, Move move) {
return map.calcMovePoint(from, move);
}
@Override
public String toString() {

View file

@ -0,0 +1,14 @@
package farbot.game;
public enum Move {
UP (0,-1),
DOWN (0,1),
LEFT (-1,0),
RIGHT (1,0);
private final int x;
private final int y;
Move(int x, int y) { this.x = x; this.y = y; }
public int getX() { return x; }
public int getY() { return y; }
}

View file

@ -1,27 +0,0 @@
package farbot.game;
public class MoveResult {
private Player player;
private int killedBugs;
private int collectedCodes;
public MoveResult(Player player, int collectedCodes, int killedBugs) {
this.player = player;
this.killedBugs = killedBugs;
this.collectedCodes = collectedCodes;
}
public Player getPlayer() {
return player;
}
public int getKilledBugs() {
return killedBugs;
}
public int getCollectedCodes() {
return collectedCodes;
}
}

View file

@ -1,7 +1,5 @@
package farbot.game;
import farbot.game.map.Point;
public class Player implements Cloneable {
private Integer bombs;
private Integer snippets;
@ -53,6 +51,10 @@ public class Player implements Cloneable {
this.id = id;
}
public double distance(Point point) {
return this.position.distance(point);
}
@Override
public Player clone() {
return new Player(this.bombs, this.snippets, new Point(this.position), this.id);

View file

@ -1,4 +1,4 @@
package farbot.game.map;
package farbot.game;
public class Point extends java.awt.Point{
@ -9,6 +9,11 @@ public class Point extends java.awt.Point{
public Point(Point p) { super(p); }
public Point(int x, int y) { super(x,y); }
public Integer taxiDistance(Point to) {
return Math.abs(this.x - to.x) + Math.abs(this.y - to.y);
}
@Override
public String toString() {

View file

@ -1,6 +1,5 @@
package farbot.game.map;
import farbot.game.map.Point;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
@ -10,7 +9,9 @@ import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import farbot.game.Settings;
import farbot.game.Move;
import farbot.game.Point;
import farbot.Settings;
import farbot.game.token.Bug;
import farbot.game.token.Code;
import farbot.game.token.Mine;
@ -85,37 +86,6 @@ public final class GameMap {
return new GameMap(this);
}
public Set<Move> validMoves(Point from) {
EnumSet<Move> moves;
switch(base[from.x][from.y]) {
case GATE_L:
case GATE_R:
moves = EnumSet.of(Move.LEFT, Move.RIGHT);
break;
default:
moves = EnumSet.allOf(Move.class);
moves.removeIf( move -> from.x+move.x < 0
|| from.y+move.y < 0
|| from.x+move.x >= s.getFieldWidth()
|| from.y+move.y >= s.getFieldHeight()
|| base[from.x+move.x][from.y+move.y] == Field.WALL );
break;
}
return moves;
}
public Set<Point> validMovePoints(Point from){
return validMoves(from).stream()
.map(move -> calcMovePoint(from, move))
.collect(Collectors.toSet());
}
public Point calcMovePoint(Point from, Move move) {
return new Point(Math.floorMod(from.x+move.x, s.getFieldWidth()),
Math.floorMod(from.y+move.y, s.getFieldHeight()));
}
public void putToken(Bug bug) {
if(dirtyBugs) {
@ -147,6 +117,12 @@ public final class GameMap {
mines.add(mine);
mineMap.computeIfAbsent(mine.getPosition(), k -> new ArrayList<>()).add(mine);
}
public void clearBugs() {
this.bugs = new ArrayList<>();
this.bugMap = new HashMap<>();
this.dirtyBugs = false;
}
public void clearTokens() {
this.bugs = new ArrayList<>();
@ -300,4 +276,8 @@ public final class GameMap {
return Collections.unmodifiableList(tokens);
}
public static Field[][] getBase() {
return base;
}
}

View file

@ -1,12 +0,0 @@
package farbot.game.map;
public enum Move {
UP (0,-1),
DOWN (0,1),
LEFT (-1,0),
RIGHT (1,0);
protected final int x;
protected final int y;
Move(int x, int y) { this.x = x; this.y = y; }
}

View file

@ -0,0 +1,53 @@
package farbot.game.map.util;
import java.util.EnumSet;
import java.util.Set;
import java.util.stream.Collectors;
import farbot.Settings;
import farbot.game.Move;
import farbot.game.Point;
import farbot.game.map.Field;
import farbot.game.map.GameMap;
/**
* Utility functions working on the invariant base map (GameMap.base).
*
* Obey functional contract. Functions must not have side-effects.
*/
public class Movement {
private static Field[][] base = GameMap.getBase();
public static Set<Move> validMoves(Point from) {
EnumSet<Move> moves;
switch(base[from.x][from.y]) {
case GATE_L:
case GATE_R:
moves = EnumSet.of(Move.LEFT, Move.RIGHT);
break;
default:
moves = EnumSet.allOf(Move.class);
moves.removeIf( move ->
from.getX()+move.getX() < 0
|| from.y+move.getY() < 0
|| from.x+move.getX() >= Settings.getInstance().getFieldWidth()
|| from.y+move.getY() >= Settings.getInstance().getFieldHeight()
|| base[from.x+move.getX()][from.y+move.getY()] == Field.WALL );
break;
}
return moves;
}
public static Set<Point> validMovePoints(Point from){
return validMoves(from).stream()
.map(move -> calcMovePoint(from, move))
.collect(Collectors.toSet());
}
public static Point calcMovePoint(Point from, Move move) {
return new Point(Math.floorMod(from.x+move.getX(), Settings.getInstance().getFieldWidth()),
Math.floorMod(from.y+move.getY(), Settings.getInstance().getFieldHeight()));
}
}

View file

@ -0,0 +1,119 @@
package farbot.game.map.util;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.PriorityQueue;
import farbot.game.Point;
import static farbot.FarbotIO.logln;
/**
* Utility functions working on the invariant base map (GameMap.base).
*
* Obey functional contract. Functions must not have side-effects.
*
* Memoized shortest paths calculation.
*/
public class ShortestPaths {
private static HashMap<Points, List<Point>> memo = new HashMap<>();
public static List<Point> shortestPath(Point a, Point b){
assert(a != null); assert(b != null);
Points points = new Points(a,b);
if(memo.containsKey(points)) return memo.get(points);
List<Point> path = aStarSearch(a,b);
memo.put(new Points(a,b), path);
return path;
}
private static List<Point> aStarSearch(Point start, Point goal){
logln("Calculating aStar from "+start+" to "+goal);
PriorityQueue<ANode> frontier = new PriorityQueue<>(Comparator.comparingInt(a -> a.priority));
HashMap<Point, Point> cameFrom = new HashMap<>();
HashMap<Point, Integer> cost = new HashMap<>();
frontier.add(new ANode(start,0));
cost.put(start, 0);
while(!frontier.isEmpty()) {
ANode current = frontier.poll();
Point currentP = current.point;
if(currentP.equals(goal)) break;
for(Point nextP: Movement.validMovePoints(currentP)) {
int newCost = cost.get(currentP) + 1;
if(!cost.containsKey(nextP) || newCost < cost.get(nextP)) {
cost.put(nextP, newCost);
int priority = newCost + goal.taxiDistance(nextP);
frontier.add(new ANode(nextP, priority));
cameFrom.put(nextP, currentP);
//logln("Added "+nextP+" with cost "+newCost+" and priority "+priority);
}
}
}
LinkedList<Point> path = new LinkedList<>();
Point last = goal;
path.add(last);
Point newLast;
while( (newLast = cameFrom.get(last)) != null) {
cameFrom.remove(last);
path.add(newLast);
last = newLast;
}
Collections.reverse(path);
logln("Shortest path: "+path.toString());
return path;
}
private static class ANode {
private Point point;
private Integer priority;
private ANode(Point point, Integer priority) {
this.point = point;
this.priority = priority;
}
}
private static class Points {
private Point a;
private Point b;
private Points(Point a, Point b) {
this.a = a;
this.b = b;
}
@Override
public int hashCode() {
return 31 * (31 + a.hashCode()) + b.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
Points other = (Points) obj;
if(!a.equals(other.a) || !b.equals(other.b)) return false;
return true;
}
}
}

View file

@ -1,6 +1,6 @@
package farbot.game.token;
import farbot.game.map.Point;
import farbot.game.Point;
public abstract class Token {
Point position;