diff --git a/src/main/java/de/towerdefence/server/game/Enemy.java b/src/main/java/de/towerdefence/server/game/Enemy.java index dc21088..cbd0112 100644 --- a/src/main/java/de/towerdefence/server/game/Enemy.java +++ b/src/main/java/de/towerdefence/server/game/Enemy.java @@ -1,5 +1,7 @@ package de.towerdefence.server.game; +import org.joml.Vector2i; + import lombok.AllArgsConstructor; import lombok.Getter; @@ -7,6 +9,5 @@ import lombok.Getter; @AllArgsConstructor public class Enemy { protected int id; - protected int x; - protected int y; + protected Vector2i pos; } diff --git a/src/main/java/de/towerdefence/server/game/GameSession.java b/src/main/java/de/towerdefence/server/game/GameSession.java index fcaee35..a31e6ad 100644 --- a/src/main/java/de/towerdefence/server/game/GameSession.java +++ b/src/main/java/de/towerdefence/server/game/GameSession.java @@ -11,6 +11,9 @@ import de.towerdefence.server.game.callbacks.PlayerHitpointsCallback; import de.towerdefence.server.game.exeptions.InvalidPlacementException; import de.towerdefence.server.game.exeptions.InvalidPlacementReason; import de.towerdefence.server.game.exeptions.WaveInProgressException; +import de.towerdefence.server.game.pathfinding.AStar; +import de.towerdefence.server.game.pathfinding.NoPathException; +import de.towerdefence.server.game.pathfinding.OutOfBoundsException; import de.towerdefence.server.player.Player; import lombok.Getter; import org.joml.Vector2i; @@ -82,20 +85,37 @@ public class GameSession { } private List moveEnemies() { - // List changedEnemies = moveEnemies(); + List changedEnemies = moveEnemies(); - // TODO: Implement Moving of Enemies (possibly through A*) - throw new RuntimeException("NOT IMPLEMENTED"); - - // return changedEnemies; - - } - - /** - * @return the next position to go to - */ - private Vector2i pathfinding(Vector2i start, Vector2i target) { + for (int x = 0; x < enemies.length; x++) { + Enemy[] column = enemies[x]; + for (int y = 0; y < column.length; y++) { + Enemy enemy = column[y]; + if (changedEnemies.contains(enemy)) { + continue; + } + AStar a = new AStar((Vector2i pos) -> { + return pos.x >= 0 + && pos.x < MAP_SIZE.x + && pos.y >= 0 + && pos.y < MAP_SIZE.y + && (towers[pos.x] == null || towers[pos.x][pos.y] == null) + && (enemies[pos.x] == null || enemies[pos.x][pos.y] != null); + }); + Vector2i next; + try { + next = a.next(enemy.pos, WAVE_TARGET); + } catch (NoPathException | OutOfBoundsException exceptionO) { + throw new RuntimeException("TODO: Implement"); + } + enemy.pos = next; + enemies[x][y] = null; + enemies[next.x][next.y] = enemy; + changedEnemies.add(enemy); + } + } + return changedEnemies; } public void placeTower(Tower tower, int x, int y) throws InvalidPlacementException { diff --git a/src/main/java/de/towerdefence/server/game/pathfinding/AStar.java b/src/main/java/de/towerdefence/server/game/pathfinding/AStar.java index d4a4f00..28c37b5 100644 --- a/src/main/java/de/towerdefence/server/game/pathfinding/AStar.java +++ b/src/main/java/de/towerdefence/server/game/pathfinding/AStar.java @@ -1,5 +1,100 @@ package de.towerdefence.server.game.pathfinding; -public class AStar { +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.joml.Vector2i; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class AStar { + private final WalkableCallback walkable; + + public Vector2i next(Vector2i current, Vector2i target) throws NoPathException, OutOfBoundsException { + if (!walkable.call(current) || !walkable.call(target)) { + throw new OutOfBoundsException(); + } + + if (current.equals(target)) { + return target; + } + + Map remaining = new HashMap<>(); + Set evaluated = new HashSet<>(); + Map takenPath = new HashMap<>(); + + PathNode origin = new PathNode(current, 0, current.gridDistance(target)); + remaining.put(current, origin); + + while (!remaining.isEmpty()) { + PathNode currentNode = getLowestEstimate(remaining); + Vector2i currentPos = currentNode.pos; + + if (currentPos.equals(target)) { + return reconstructPath(takenPath, current); + } + + remaining.remove(currentPos); + evaluated.add(currentPos); + + long nextCost = currentNode.cost + 1; + for (PathNode neighbor : getNeighbors(currentPos, nextCost, target)) { + if (evaluated.contains(neighbor.pos)) { + continue; + } + if (!remaining.containsKey(neighbor.pos) || nextCost < remaining.get(neighbor.pos).cost) { + takenPath.put(neighbor.pos, currentPos); + remaining.put(neighbor.pos, neighbor); + } + } + } + + throw new NoPathException(); + } + + private Vector2i reconstructPath(Map takenPath, Vector2i origin) { + Vector2i current = takenPath.keySet().stream().reduce((first, second) -> second).orElse(null); + if (current == null) + return null; + while (takenPath.get(current) != null && !takenPath.get(current).equals(origin)) { + current = takenPath.get(current); + } + return current; + } + + private PathNode getLowestEstimate(Map openSet) { + PathNode lowestNode = null; + for (PathNode node : openSet.values()) { + if (lowestNode == null || node.estimate < lowestNode.estimate) { + lowestNode = node; + } + } + return lowestNode; + } + + private List getNeighbors(Vector2i pos, long nextCost, Vector2i target) { + List neighbors = new ArrayList<>(); + + addNeighbor(neighbors, new Vector2i(pos.x + 1, pos.y), nextCost, target); + addNeighbor(neighbors, new Vector2i(pos.x - 1, pos.y), nextCost, target); + addNeighbor(neighbors, new Vector2i(pos.x, pos.y + 1), nextCost, target); + addNeighbor(neighbors, new Vector2i(pos.x, pos.y - 1), nextCost, target); + return neighbors; + } + + private void addNeighbor(List accumulator, Vector2i pos, long nextCost, Vector2i target) { + Vector2i right = new Vector2i(pos.x + 1, pos.y); + if (!walkable.call(pos)) { + return; + } + accumulator.add(new PathNode( + right, + nextCost, + right.gridDistance(target) + nextCost)); + } } diff --git a/src/main/java/de/towerdefence/server/game/pathfinding/OutOfBoundsException.java b/src/main/java/de/towerdefence/server/game/pathfinding/OutOfBoundsException.java new file mode 100644 index 0000000..3b77118 --- /dev/null +++ b/src/main/java/de/towerdefence/server/game/pathfinding/OutOfBoundsException.java @@ -0,0 +1,4 @@ +package de.towerdefence.server.game.pathfinding; + +public class OutOfBoundsException extends RuntimeException { +} diff --git a/src/main/java/de/towerdefence/server/game/pathfinding/PathNode.java b/src/main/java/de/towerdefence/server/game/pathfinding/PathNode.java new file mode 100644 index 0000000..76893c0 --- /dev/null +++ b/src/main/java/de/towerdefence/server/game/pathfinding/PathNode.java @@ -0,0 +1,12 @@ +package de.towerdefence.server.game.pathfinding; + +import org.joml.Vector2i; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +final class PathNode { + protected final Vector2i pos; + protected final long cost; // This is the cost to this Node + protected final long estimate; // This is the estimated remaining cost to the Target +} diff --git a/src/main/java/de/towerdefence/server/game/pathfinding/WalkableCallback.java b/src/main/java/de/towerdefence/server/game/pathfinding/WalkableCallback.java new file mode 100644 index 0000000..90c737c --- /dev/null +++ b/src/main/java/de/towerdefence/server/game/pathfinding/WalkableCallback.java @@ -0,0 +1,8 @@ +package de.towerdefence.server.game.pathfinding; + +import org.joml.Vector2i; + +@FunctionalInterface +public interface WalkableCallback { + boolean call(Vector2i pos); +}