From 56a7cf597b1859d6a0820e94d3e621e3b2ef64d1 Mon Sep 17 00:00:00 2001 From: Armin Friedl Date: Wed, 10 Jan 2018 23:36:38 +0100 Subject: [PATCH] Refactoring, Bug Prediction, Shortest Paths prediction Chase bug and FarChase bug simulators implemented. Codes faster reachable by opponent get lowered values. --- farbot/src/farbot/FarbotIO.java | 1 - farbot/src/farbot/{game => }/Settings.java | 2 +- farbot/src/farbot/ai/AI.java | 44 ++-- farbot/src/farbot/ai/GameTree.java | 75 +++++-- farbot/src/farbot/ai/MoveResult.java | 40 ++++ farbot/src/farbot/ai/Parser.java | 11 +- farbot/src/farbot/ai/Simulator.java | 103 +++++++++ farbot/src/farbot/game/GameState.java | 195 +++++++++++------- farbot/src/farbot/game/Move.java | 14 ++ farbot/src/farbot/game/MoveResult.java | 27 --- farbot/src/farbot/game/Player.java | 6 +- farbot/src/farbot/game/{map => }/Point.java | 7 +- farbot/src/farbot/game/map/GameMap.java | 46 ++--- farbot/src/farbot/game/map/Move.java | 12 -- farbot/src/farbot/game/map/util/Movement.java | 53 +++++ .../farbot/game/map/util/ShortestPaths.java | 119 +++++++++++ farbot/src/farbot/game/token/Token.java | 2 +- 17 files changed, 573 insertions(+), 184 deletions(-) rename farbot/src/farbot/{game => }/Settings.java (98%) create mode 100644 farbot/src/farbot/ai/MoveResult.java create mode 100644 farbot/src/farbot/ai/Simulator.java create mode 100644 farbot/src/farbot/game/Move.java delete mode 100644 farbot/src/farbot/game/MoveResult.java rename farbot/src/farbot/game/{map => }/Point.java (70%) delete mode 100644 farbot/src/farbot/game/map/Move.java create mode 100644 farbot/src/farbot/game/map/util/Movement.java create mode 100644 farbot/src/farbot/game/map/util/ShortestPaths.java diff --git a/farbot/src/farbot/FarbotIO.java b/farbot/src/farbot/FarbotIO.java index 0a7a99d..b634368 100644 --- a/farbot/src/farbot/FarbotIO.java +++ b/farbot/src/farbot/FarbotIO.java @@ -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; diff --git a/farbot/src/farbot/game/Settings.java b/farbot/src/farbot/Settings.java similarity index 98% rename from farbot/src/farbot/game/Settings.java rename to farbot/src/farbot/Settings.java index 7fc4f43..b7a7522 100644 --- a/farbot/src/farbot/game/Settings.java +++ b/farbot/src/farbot/Settings.java @@ -1,4 +1,4 @@ -package farbot.game; +package farbot; public class Settings { private Integer timebank; diff --git a/farbot/src/farbot/ai/AI.java b/farbot/src/farbot/ai/AI.java index 09a40c8..ca34da0 100644 --- a/farbot/src/farbot/ai/AI.java +++ b/farbot/src/farbot/ai/AI.java @@ -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 validMoves = getValidMoves(); + List valuedMoves = getValuedMoves(validMoves); + MoveValue maxMove = getMaxMove(valuedMoves); + + this.lastMove = maxMove; + + logln("Max.move '"+maxMove.move+"': "+maxMove.value); + return maxMove.move.name().toLowerCase(); + } + + private Set getValidMoves(){ Set validMoves = currentState.selfMoves(); logln("Valid moves: "+validMoves.toString()); - + return validMoves; + } + + private List getValuedMoves(Set validMoves){ ArrayList 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 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) { diff --git a/farbot/src/farbot/ai/GameTree.java b/farbot/src/farbot/ai/GameTree.java index a2ad1d9..a1f39d1 100644 --- a/farbot/src/farbot/ai/GameTree.java +++ b/farbot/src/farbot/ai/GameTree.java @@ -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 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 children = makeChildren(node, this, level); + MoveResult mr = Simulator.move(initMove, node); + value += valueCodes(mr, level, node, this); + value += valueBugs(mr, level, node); + Queue 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 opponentShortestPath = ShortestPaths.shortestPath( + gtRoot.node.getOpponentPosition(), code.getPosition()); + + List 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; + } } diff --git a/farbot/src/farbot/ai/MoveResult.java b/farbot/src/farbot/ai/MoveResult.java new file mode 100644 index 0000000..4b5d300 --- /dev/null +++ b/farbot/src/farbot/ai/MoveResult.java @@ -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 killedBugs; + private List collectedCodes; + + public MoveResult(Player player, List collectedCodes, List killedBugs) { + this.player = player; + this.killedBugs = killedBugs; + this.collectedCodes = collectedCodes; + } + + public Player getPlayer() { + return player; + } + + public List getKilledBugs() { + return killedBugs; + } + + public int lenKilledBugs() { + return killedBugs.size(); + } + + public List getCollectedCodes() { + return collectedCodes; + } + + public int lenCollectedCodes() { + return collectedCodes.size(); + } + +} diff --git a/farbot/src/farbot/ai/Parser.java b/farbot/src/farbot/ai/Parser.java index 35a31e6..4fa0144 100644 --- a/farbot/src/farbot/ai/Parser.java +++ b/farbot/src/farbot/ai/Parser.java @@ -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); diff --git a/farbot/src/farbot/ai/Simulator.java b/farbot/src/farbot/ai/Simulator.java new file mode 100644 index 0000000..1e81ec1 --- /dev/null +++ b/farbot/src/farbot/ai/Simulator.java @@ -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 bugs = state.removeBugsAt(state.getSelfPosition()); + List 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 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 movePoints = new ArrayList(); + movePoints.addAll(bugMovePoints(from, state)); + Collections.shuffle(movePoints); + Point nearestPoint = movePoints.stream() + .min(Comparator.comparingDouble(to::distance)) + .get(); // .min returns an Optional + + return nearestPoint; + } + + private static Set bugMovePoints(Point from, GameState state){ + Set 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; + } +} diff --git a/farbot/src/farbot/game/GameState.java b/farbot/src/farbot/game/GameState.java index 21d70d0..e9d1906 100644 --- a/farbot/src/farbot/game/GameState.java +++ b/farbot/src/farbot/game/GameState.java @@ -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 fork(Integer times) { - ArrayList forks = new ArrayList<>(); - for (int i=0; i codes = map.removeCodesAt(position); - List 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 getPlayerClones() { + ArrayList 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 removeCodesAt(Point position){ + return map.removeCodesAt(position); + } + + public List removeBugsAt(Point position){ + return map.removeBugsAt(position); + } + + public List getBugs(){ + return map.getBugs(); + } + + public void clearBugs() { + map.clearBugs(); + } + + public void clearTokens() { + map.clearTokens(); + } + + /* + * Movements Management + */ + + public Set selfMovePoints(){ + return movePoints(self.getPosition()); + } + + public Set selfMoves(){ + return Movement.validMoves(self.getPosition()); + } + + public Point calcMovePoint(Point from, Move move) { + return Movement.calcMovePoint(from, move); + } + + public Set movePoints(Point from){ + return Movement.validMovePoints(from); + } + + /* + * Fork Managment + * + */ + + public List fork(Integer times) { + ArrayList forks = new ArrayList<>(); + for (int i=0; i selfMovePoints(){ - return map.validMovePoints(self.getPosition()); - } - - public Set selfMoves(){ - return map.validMoves(self.getPosition()); - } - - public Set opponentMovePoints(){ - return map.validMovePoints(opponent.getPosition()); - } - - public Set opponentMoves(){ - return map.validMoves(opponent.getPosition()); - } - - public Point calcMovePoint(Point from, Move move) { - return map.calcMovePoint(from, move); - } + @Override public String toString() { diff --git a/farbot/src/farbot/game/Move.java b/farbot/src/farbot/game/Move.java new file mode 100644 index 0000000..7cd9e34 --- /dev/null +++ b/farbot/src/farbot/game/Move.java @@ -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; } +} diff --git a/farbot/src/farbot/game/MoveResult.java b/farbot/src/farbot/game/MoveResult.java deleted file mode 100644 index 59821df..0000000 --- a/farbot/src/farbot/game/MoveResult.java +++ /dev/null @@ -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; - } - - -} diff --git a/farbot/src/farbot/game/Player.java b/farbot/src/farbot/game/Player.java index 42577f3..846da6a 100644 --- a/farbot/src/farbot/game/Player.java +++ b/farbot/src/farbot/game/Player.java @@ -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); diff --git a/farbot/src/farbot/game/map/Point.java b/farbot/src/farbot/game/Point.java similarity index 70% rename from farbot/src/farbot/game/map/Point.java rename to farbot/src/farbot/game/Point.java index c55ebf6..e029982 100644 --- a/farbot/src/farbot/game/map/Point.java +++ b/farbot/src/farbot/game/Point.java @@ -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() { diff --git a/farbot/src/farbot/game/map/GameMap.java b/farbot/src/farbot/game/map/GameMap.java index abdc150..ca55574 100644 --- a/farbot/src/farbot/game/map/GameMap.java +++ b/farbot/src/farbot/game/map/GameMap.java @@ -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 validMoves(Point from) { - EnumSet 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 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; + } } diff --git a/farbot/src/farbot/game/map/Move.java b/farbot/src/farbot/game/map/Move.java deleted file mode 100644 index 0f2f065..0000000 --- a/farbot/src/farbot/game/map/Move.java +++ /dev/null @@ -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; } -} diff --git a/farbot/src/farbot/game/map/util/Movement.java b/farbot/src/farbot/game/map/util/Movement.java new file mode 100644 index 0000000..3ee5106 --- /dev/null +++ b/farbot/src/farbot/game/map/util/Movement.java @@ -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 validMoves(Point from) { + EnumSet 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 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())); + } +} diff --git a/farbot/src/farbot/game/map/util/ShortestPaths.java b/farbot/src/farbot/game/map/util/ShortestPaths.java new file mode 100644 index 0000000..f8638d8 --- /dev/null +++ b/farbot/src/farbot/game/map/util/ShortestPaths.java @@ -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> memo = new HashMap<>(); + + public static List 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 path = aStarSearch(a,b); + memo.put(new Points(a,b), path); + + return path; + } + + private static List aStarSearch(Point start, Point goal){ + logln("Calculating aStar from "+start+" to "+goal); + + PriorityQueue frontier = new PriorityQueue<>(Comparator.comparingInt(a -> a.priority)); + HashMap cameFrom = new HashMap<>(); + HashMap 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 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; + } + } +} diff --git a/farbot/src/farbot/game/token/Token.java b/farbot/src/farbot/game/token/Token.java index 348e2d2..06a2698 100644 --- a/farbot/src/farbot/game/token/Token.java +++ b/farbot/src/farbot/game/token/Token.java @@ -1,6 +1,6 @@ package farbot.game.token; -import farbot.game.map.Point; +import farbot.game.Point; public abstract class Token { Point position;