TD-5: A* Implementation

This commit is contained in:
Snoweuph 2025-03-13 17:42:00 +01:00
parent 423fb9ec40
commit ac434b8463
Signed by: snoweuph
GPG key ID: BEFC41DA223CEC55
6 changed files with 155 additions and 15 deletions

View file

@ -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;
}

View file

@ -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<Enemy> moveEnemies() {
// List<Enemy> changedEnemies = moveEnemies();
List<Enemy> 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 {

View file

@ -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<Vector2i, PathNode> remaining = new HashMap<>();
Set<Vector2i> evaluated = new HashSet<>();
Map<Vector2i, Vector2i> 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<Vector2i, Vector2i> 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<Vector2i, PathNode> openSet) {
PathNode lowestNode = null;
for (PathNode node : openSet.values()) {
if (lowestNode == null || node.estimate < lowestNode.estimate) {
lowestNode = node;
}
}
return lowestNode;
}
private List<PathNode> getNeighbors(Vector2i pos, long nextCost, Vector2i target) {
List<PathNode> 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<PathNode> 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));
}
}

View file

@ -0,0 +1,4 @@
package de.towerdefence.server.game.pathfinding;
public class OutOfBoundsException extends RuntimeException {
}

View file

@ -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
}

View file

@ -0,0 +1,8 @@
package de.towerdefence.server.game.pathfinding;
import org.joml.Vector2i;
@FunctionalInterface
public interface WalkableCallback {
boolean call(Vector2i pos);
}