google-site-verification: google61fe8ba583a51912.html
Pathfinding with A* Algorithm in Java: Complete Implementation Guide

Introduction

A* (A-star) is one of the most popular and efficient pathfinding algorithms used in games, robotics, and AI applications. It combines the strengths of Dijkstra's algorithm (guaranteed shortest path) with greedy best-first search (fast computation) using heuristics to guide the search.

Project Setup

Maven Dependencies

<!-- pom.xml --> <properties> <javafx.version>21</javafx.version> </properties> <dependencies> <!-- JavaFX for Visualization (Optional) --> <dependency> <groupId>org.openjfx</groupId> <artifactId>javafx-controls</artifactId> <version>${javafx.version}</version> </dependency> <!-- Logging --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.4.11</version> </dependency> </dependencies>

Core A* Algorithm Implementation

Node Class

package com.pathfinding.core; import java.util.Objects; public class Node implements Comparable<Node> { private final int x; private final int y; private boolean walkable; private Node parent; // Cost values private double gCost; // Cost from start to this node private double hCost; // Heuristic cost to target private double fCost; // Total cost (g + h) // Additional properties for different terrain types private double movementCost = 1.0; private NodeType type = NodeType.NORMAL; public Node(int x, int y) { this(x, y, true); } public Node(int x, int y, boolean walkable) { this.x = x; this.y = y; this.walkable = walkable; this.gCost = Double.MAX_VALUE; this.hCost = 0; this.fCost = Double.MAX_VALUE; } public void calculateFCost() { this.fCost = gCost + hCost; } public void reset() { this.parent = null; this.gCost = Double.MAX_VALUE; this.hCost = 0; this.fCost = Double.MAX_VALUE; } // Getters and setters public int getX() { return x; } public int getY() { return y; } public boolean isWalkable() { return walkable; } public void setWalkable(boolean walkable) { this.walkable = walkable; } public Node getParent() { return parent; } public void setParent(Node parent) { this.parent = parent; } public double getGCost() { return gCost; } public void setGCost(double gCost) { this.gCost = gCost; } public double getHCost() { return hCost; } public void setHCost(double hCost) { this.hCost = hCost; } public double getFCost() { return fCost; } public double getMovementCost() { return movementCost; } public void setMovementCost(double movementCost) { this.movementCost = movementCost; } public NodeType getType() { return type; } public void setType(NodeType type) { this.type = type; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Node node = (Node) o; return x == node.x && y == node.y; } @Override public int hashCode() { return Objects.hash(x, y); } @Override public int compareTo(Node other) { if (this.fCost < other.fCost) return -1; if (this.fCost > other.fCost) return 1; // If F costs are equal, compare H costs (tie-breaker) return Double.compare(this.hCost, other.hCost); } @Override public String toString() { return String.format("Node(%d, %d) [F:%.1f G:%.1f H:%.1f]", x, y, fCost, gCost, hCost); } public enum NodeType { NORMAL(1.0), WATER(3.0), // Higher cost to cross water FOREST(2.0), // Moderate cost for forests MOUNTAIN(5.0), // Very high cost for mountains ROAD(0.8), // Lower cost for roads SWAMP(4.0); // High cost for swamps private final double costMultiplier; NodeType(double costMultiplier) { this.costMultiplier = costMultiplier; } public double getCostMultiplier() { return costMultiplier; } } }

Heuristic Calculators

package com.pathfinding.heuristics; import com.pathfinding.core.Node; public interface Heuristic { double calculate(Node start, Node end); // Common heuristic implementations class Manhattan implements Heuristic { @Override public double calculate(Node start, Node end) { int dx = Math.abs(start.getX() - end.getX()); int dy = Math.abs(start.getY() - end.getY()); return dx + dy; } } class Euclidean implements Heuristic { @Override public double calculate(Node start, Node end) { int dx = start.getX() - end.getX(); int dy = start.getY() - end.getY(); return Math.sqrt(dx * dx + dy * dy); } } class Chebyshev implements Heuristic { @Override public double calculate(Node start, Node end) { int dx = Math.abs(start.getX() - end.getX()); int dy = Math.abs(start.getY() - end.getY()); return Math.max(dx, dy); } } class Diagonal implements Heuristic { private static final double D = 1.0; // Cost for horizontal/vertical movement private static final double D2 = Math.sqrt(2); // Cost for diagonal movement @Override public double calculate(Node start, Node end) { int dx = Math.abs(start.getX() - end.getX()); int dy = Math.abs(start.getY() - end.getY()); return D * (dx + dy) + (D2 - 2 * D) * Math.min(dx, dy); } } }

Grid Representation

package com.pathfinding.core; import java.util.ArrayList; import java.util.List; public class Grid { private final int width; private final int height; private final Node[][] nodes; private boolean allowDiagonal = true; public Grid(int width, int height) { this.width = width; this.height = height; this.nodes = new Node[width][height]; initializeGrid(); } public Grid(int width, int height, boolean[][] walkableMap) { this.width = width; this.height = height; this.nodes = new Node[width][height]; initializeGrid(walkableMap); } private void initializeGrid() { for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { nodes[x][y] = new Node(x, y); } } } private void initializeGrid(boolean[][] walkableMap) { for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { boolean walkable = walkableMap[x][y]; nodes[x][y] = new Node(x, y, walkable); } } } public Node getNode(int x, int y) { if (x >= 0 && x < width && y >= 0 && y < height) { return nodes[x][y]; } return null; } public void setWalkable(int x, int y, boolean walkable) { Node node = getNode(x, y); if (node != null) { node.setWalkable(walkable); } } public void setNodeType(int x, int y, Node.NodeType type) { Node node = getNode(x, y); if (node != null) { node.setType(type); node.setMovementCost(type.getCostMultiplier()); } } public List<Node> getNeighbors(Node node) { List<Node> neighbors = new ArrayList<>(); int x = node.getX(); int y = node.getY(); // Cardinal directions (up, right, down, left) addIfValid(neighbors, x, y - 1); // Up addIfValid(neighbors, x + 1, y); // Right addIfValid(neighbors, x, y + 1); // Down addIfValid(neighbors, x - 1, y); // Left // Diagonal directions (if allowed) if (allowDiagonal) { addIfValid(neighbors, x - 1, y - 1); // Up-Left addIfValid(neighbors, x + 1, y - 1); // Up-Right addIfValid(neighbors, x - 1, y + 1); // Down-Left addIfValid(neighbors, x + 1, y + 1); // Down-Right } return neighbors; } private void addIfValid(List<Node> neighbors, int x, int y) { Node node = getNode(x, y); if (node != null && node.isWalkable()) { neighbors.add(node); } } public double getMovementCost(Node from, Node to) { // Check if movement is diagonal boolean isDiagonal = (from.getX() != to.getX() && from.getY() != to.getY()); double baseCost = isDiagonal ? Math.sqrt(2) : 1.0; // Apply terrain cost multiplier return baseCost * to.getMovementCost(); } public void resetAllNodes() { for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { nodes[x][y].reset(); } } } // Getters public int getWidth() { return width; } public int getHeight() { return height; } public boolean isAllowDiagonal() { return allowDiagonal; } public void setAllowDiagonal(boolean allowDiagonal) { this.allowDiagonal = allowDiagonal; } public void printGrid() { for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { Node node = nodes[x][y]; char symbol = node.isWalkable() ? '.' : '#'; System.out.print(symbol + " "); } System.out.println(); } } public void printGridWithPath(List<Node> path) { for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { Node node = nodes[x][y]; char symbol; if (path.contains(node)) { symbol = '*'; } else if (!node.isWalkable()) { symbol = '#'; } else { symbol = '.'; } System.out.print(symbol + " "); } System.out.println(); } } }

A* Pathfinder Implementation

package com.pathfinding.core; import com.pathfinding.heuristics.Heuristic; import java.util.*; public class AStarPathfinder { private final Grid grid; private Heuristic heuristic; private boolean useTieBreaking; // Statistics private int nodesExplored; private long executionTime; private int pathLength; public AStarPathfinder(Grid grid) { this(grid, new Heuristic.Euclidean()); } public AStarPathfinder(Grid grid, Heuristic heuristic) { this.grid = grid; this.heuristic = heuristic; this.useTieBreaking = true; } public PathfindingResult findPath(Node start, Node end) { return findPath(start.getX(), start.getY(), end.getX(), end.getY()); } public PathfindingResult findPath(int startX, int startY, int endX, int endY) { long startTime = System.nanoTime(); nodesExplored = 0; Node startNode = grid.getNode(startX, startY); Node endNode = grid.getNode(endX, endY); if (startNode == null || endNode == null) { return new PathfindingResult(false, "Invalid start or end position"); } if (!startNode.isWalkable() || !endNode.isWalkable()) { return new PathfindingResult(false, "Start or end node is not walkable"); } // Reset all nodes grid.resetAllNodes(); // Initialize open and closed sets PriorityQueue<Node> openSet = new PriorityQueue<>(); Set<Node> closedSet = new HashSet<>(); // Initialize start node startNode.setGCost(0); startNode.setHCost(heuristic.calculate(startNode, endNode)); startNode.calculateFCost(); openSet.add(startNode); while (!openSet.isEmpty()) { Node currentNode = openSet.poll(); nodesExplored++; // Check if we reached the destination if (currentNode.equals(endNode)) { executionTime = (System.nanoTime() - startTime) / 1_000_000; // Convert to ms List<Node> path = reconstructPath(currentNode); pathLength = path.size(); return new PathfindingResult(true, path, "Path found successfully"); } closedSet.add(currentNode); // Explore neighbors for (Node neighbor : grid.getNeighbors(currentNode)) { if (closedSet.contains(neighbor)) { continue; } double tentativeGCost = currentNode.getGCost() + grid.getMovementCost(currentNode, neighbor); if (tentativeGCost < neighbor.getGCost()) { neighbor.setParent(currentNode); neighbor.setGCost(tentativeGCost); neighbor.setHCost(heuristic.calculate(neighbor, endNode)); // Apply tie-breaking if enabled if (useTieBreaking) { neighbor.setHCost(neighbor.getHCost() * (1.0 + 1.0 / (grid.getWidth() * grid.getHeight()))); } neighbor.calculateFCost(); if (!openSet.contains(neighbor)) { openSet.add(neighbor); } } } } executionTime = (System.nanoTime() - startTime) / 1_000_000; return new PathfindingResult(false, "No path found"); } private List<Node> reconstructPath(Node endNode) { List<Node> path = new ArrayList<>(); Node currentNode = endNode; while (currentNode != null) { path.add(0, currentNode); // Add to beginning to reverse the path currentNode = currentNode.getParent(); } return path; } // Getters and setters public Heuristic getHeuristic() { return heuristic; } public void setHeuristic(Heuristic heuristic) { this.heuristic = heuristic; } public boolean isUseTieBreaking() { return useTieBreaking; } public void setUseTieBreaking(boolean useTieBreaking) { this.useTieBreaking = useTieBreaking; } // Statistics getters public int getNodesExplored() { return nodesExplored; } public long getExecutionTime() { return executionTime; } public int getPathLength() { return pathLength; } public static class PathfindingResult { private final boolean success; private final String message; private final List<Node> path; private final int pathLength; private final int nodesExplored; private final long executionTime; public PathfindingResult(boolean success, String message) { this(success, message, Collections.emptyList(), 0, 0, 0); } public PathfindingResult(boolean success, List<Node> path, String message) { this(success, message, path, path.size(), 0, 0); } public PathfindingResult(boolean success, String message, List<Node> path, int pathLength, int nodesExplored, long executionTime) { this.success = success; this.message = message; this.path = path; this.pathLength = pathLength; this.nodesExplored = nodesExplored; this.executionTime = executionTime; } // Getters public boolean isSuccess() { return success; } public String getMessage() { return message; } public List<Node> getPath() { return path; } public int getPathLength() { return pathLength; } public int getNodesExplored() { return nodesExplored; } public long getExecutionTime() { return executionTime; } public double getPathCost() { if (path.isEmpty()) return 0.0; return path.get(path.size() - 1).getGCost(); } } }

Advanced Features

Jump Point Search (JPS) for Grids

package com.pathfinding.advanced; import com.pathfinding.core.Grid; import com.pathfinding.core.Node; import com.pathfinding.heuristics.Heuristic; import java.util.*; public class JumpPointSearch { private final Grid grid; private final Heuristic heuristic; public JumpPointSearch(Grid grid) { this.grid = grid; this.heuristic = new Heuristic.Euclidean(); } public List<Node> findPath(Node start, Node end) { PriorityQueue<Node> openSet = new PriorityQueue<>(); Set<Node> closedSet = new HashSet<>(); start.setGCost(0); start.setHCost(heuristic.calculate(start, end)); start.calculateFCost(); openSet.add(start); while (!openSet.isEmpty()) { Node current = openSet.poll(); if (current.equals(end)) { return reconstructPath(current); } closedSet.add(current); for (Node neighbor : getPrunedNeighbors(current)) { if (closedSet.contains(neighbor)) continue; Node jumpPoint = jump(neighbor, current, end); if (jumpPoint != null && !closedSet.contains(jumpPoint)) { double tentativeGCost = current.getGCost() + distance(current, jumpPoint); if (tentativeGCost < jumpPoint.getGCost()) { jumpPoint.setParent(current); jumpPoint.setGCost(tentativeGCost); jumpPoint.setHCost(heuristic.calculate(jumpPoint, end)); jumpPoint.calculateFCost(); if (!openSet.contains(jumpPoint)) { openSet.add(jumpPoint); } } } } } return Collections.emptyList(); } private Node jump(Node current, Node parent, Node goal) { if (current == null || !current.isWalkable()) return null; if (current.equals(goal)) return current; int dx = current.getX() - parent.getX(); int dy = current.getY() - parent.getY(); // Check for forced neighbors (diagonal movement) if (dx != 0 && dy != 0) { if ((grid.getNode(current.getX() - dx, current.getY()) == null || !grid.getNode(current.getX() - dx, current.getY()).isWalkable()) && grid.getNode(current.getX() - dx, current.getY() + dy) != null && grid.getNode(current.getX() - dx, current.getY() + dy).isWalkable()) { return current; } if ((grid.getNode(current.getX(), current.getY() - dy) == null || !grid.getNode(current.getX(), current.getY() - dy).isWalkable()) && grid.getNode(current.getX() + dx, current.getY() - dy) != null && grid.getNode(current.getX() + dx, current.getY() - dy).isWalkable()) { return current; } // Recursive diagonal jumps if (jump(grid.getNode(current.getX() + dx, current.getY()), current, goal) != null || jump(grid.getNode(current.getX(), current.getY() + dy), current, goal) != null) { return current; } } else { // Horizontal/vertical movement if (dx != 0) { if ((grid.getNode(current.getX() + dx, current.getY() + 1) == null || !grid.getNode(current.getX() + dx, current.getY() + 1).isWalkable()) && grid.getNode(current.getX() + dx, current.getY() + 1) != null && grid.getNode(current.getX() + dx, current.getY() + 1).isWalkable()) { return current; } if ((grid.getNode(current.getX() + dx, current.getY() - 1) == null || !grid.getNode(current.getX() + dx, current.getY() - 1).isWalkable()) && grid.getNode(current.getX() + dx, current.getY() - 1) != null && grid.getNode(current.getX() + dx, current.getY() - 1).isWalkable()) { return current; } } else { if ((grid.getNode(current.getX() + 1, current.getY() + dy) == null || !grid.getNode(current.getX() + 1, current.getY() + dy).isWalkable()) && grid.getNode(current.getX() + 1, current.getY() + dy) != null && grid.getNode(current.getX() + 1, current.getY() + dy).isWalkable()) { return current; } if ((grid.getNode(current.getX() - 1, current.getY() + dy) == null || !grid.getNode(current.getX() - 1, current.getY() + dy).isWalkable()) && grid.getNode(current.getX() - 1, current.getY() + dy) != null && grid.getNode(current.getX() - 1, current.getY() + dy).isWalkable()) { return current; } } } return jump(grid.getNode(current.getX() + dx, current.getY() + dy), current, goal); } private List<Node> getPrunedNeighbors(Node node) { List<Node> neighbors = new ArrayList<>(); Node parent = node.getParent(); if (parent == null) { // Return all neighbors for start node return grid.getNeighbors(node); } int dx = Integer.signum(node.getX() - parent.getX()); int dy = Integer.signum(node.getY() - parent.getY()); // Add natural neighbors if (dx != 0 && dy != 0) { // Diagonal movement addIfValid(neighbors, node.getX(), node.getY() + dy); addIfValid(neighbors, node.getX() + dx, node.getY()); addIfValid(neighbors, node.getX() + dx, node.getY() + dy); // Forced neighbors if (!grid.getNode(node.getX() - dx, node.getY()).isWalkable()) { addIfValid(neighbors, node.getX() - dx, node.getY() + dy); } if (!grid.getNode(node.getX(), node.getY() - dy).isWalkable()) { addIfValid(neighbors, node.getX() + dx, node.getY() - dy); } } else { // Horizontal/vertical movement if (dx != 0) { addIfValid(neighbors, node.getX() + dx, node.getY()); // Forced neighbors if (!grid.getNode(node.getX(), node.getY() + 1).isWalkable()) { addIfValid(neighbors, node.getX() + dx, node.getY() + 1); } if (!grid.getNode(node.getX(), node.getY() - 1).isWalkable()) { addIfValid(neighbors, node.getX() + dx, node.getY() - 1); } } else { addIfValid(neighbors, node.getX(), node.getY() + dy); // Forced neighbors if (!grid.getNode(node.getX() + 1, node.getY()).isWalkable()) { addIfValid(neighbors, node.getX() + 1, node.getY() + dy); } if (!grid.getNode(node.getX() - 1, node.getY()).isWalkable()) { addIfValid(neighbors, node.getX() - 1, node.getY() + dy); } } } return neighbors; } private void addIfValid(List<Node> list, int x, int y) { Node node = grid.getNode(x, y); if (node != null && node.isWalkable()) { list.add(node); } } private double distance(Node a, Node b) { int dx = Math.abs(a.getX() - b.getX()); int dy = Math.abs(a.getY() - b.getY()); if (dx == dy) { return dx * Math.sqrt(2); } else { return Math.abs(dx - dy) + Math.min(dx, dy) * Math.sqrt(2); } } private List<Node> reconstructPath(Node endNode) { List<Node> path = new ArrayList<>(); Node current = endNode; while (current != null) { path.add(0, current); current = current.getParent(); } return path; } }

Path Smoothing

package com.pathfinding.advanced; import com.pathfinding.core.Grid; import com.pathfinding.core.Node; import java.util.ArrayList; import java.util.List; public class PathSmoother { public static List<Node> smoothPath(List<Node> path, Grid grid) { if (path.size() <= 2) { return path; } List<Node> smoothedPath = new ArrayList<>(); smoothedPath.add(path.get(0)); int currentIndex = 0; while (currentIndex < path.size() - 1) { int furthestVisible = currentIndex + 1; for (int i = currentIndex + 2; i < path.size(); i++) { if (isLineOfSight(grid, path.get(currentIndex), path.get(i))) { furthestVisible = i; } } smoothedPath.add(path.get(furthestVisible)); currentIndex = furthestVisible; } return smoothedPath; } private static boolean isLineOfSight(Grid grid, Node start, Node end) { int x0 = start.getX(); int y0 = start.getY(); int x1 = end.getX(); int y1 = end.getY(); int dx = Math.abs(x1 - x0); int dy = Math.abs(y1 - y0); int x = x0; int y = y0; int n = 1 + dx + dy; int xInc = (x1 > x0) ? 1 : -1; int yInc = (y1 > y0) ? 1 : -1; int error = dx - dy; dx *= 2; dy *= 2; for (; n > 0; --n) { Node node = grid.getNode(x, y); if (node == null || !node.isWalkable()) { return false; } if (x == x1 && y == y1) { break; } if (error > 0) { x += xInc; error -= dy; } else { y += yInc; error += dx; } } return true; } public static List<Node> bezierSmoothPath(List<Node> path, int segments) { if (path.size() < 3) { return path; } List<Node> smoothedPath = new ArrayList<>(); for (int i = 0; i < path.size() - 2; i++) { Node p0 = path.get(i); Node p1 = path.get(i + 1); Node p2 = path.get(i + 2); for (int j = 0; j <= segments; j++) { double t = (double) j / segments; double x = bezierPoint(p0.getX(), p1.getX(), p2.getX(), t); double y = bezierPoint(p0.getY(), p1.getY(), p2.getY(), t); // Create a new node for the smoothed point // Note: In practice, you might want to snap to grid or handle differently smoothedPath.add(new Node((int) Math.round(x), (int) Math.round(y))); } } return smoothedPath; } private static double bezierPoint(double p0, double p1, double p2, double t) { double oneMinusT = 1 - t; return oneMinusT * oneMinusT * p0 + 2 * oneMinusT * t * p1 + t * t * p2; } }

Visualization with JavaFX

Pathfinding Visualizer

package com.pathfinding.visualization; import com.pathfinding.core.*; import com.pathfinding.heuristics.Heuristic; import javafx.application.Application; import javafx.application.Platform; import javafx.geometry.Insets; import javafx.scene.Scene; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.control.*; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.stage.Stage; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class PathfindingVisualizer extends Application { private static final int GRID_SIZE = 20; private static final int CELL_SIZE = 25; private Grid grid; private AStarPathfinder pathfinder; private Canvas canvas; private GraphicsContext gc; private Node startNode; private Node endNode; private List<Node> currentPath; private ComboBox<String> heuristicCombo; private CheckBox diagonalCheckbox; private CheckBox tieBreakingCheckbox; private Button findPathButton; private Button clearButton; private Label statsLabel; @Override public void start(Stage primaryStage) { initializeGrid(); setupUI(primaryStage); drawGrid(); } private void initializeGrid() { grid = new Grid(GRID_SIZE, GRID_SIZE); pathfinder = new AStarPathfinder(grid); // Set default start and end positions startNode = grid.getNode(2, 2); endNode = grid.getNode(GRID_SIZE - 3, GRID_SIZE - 3); // Add some obstacles createTestObstacles(); } private void createTestObstacles() { // Create a maze-like pattern for (int x = 5; x < GRID_SIZE - 5; x++) { for (int y = 5; y < GRID_SIZE - 5; y++) { if ((x + y) % 4 == 0) { grid.setWalkable(x, y, false); } } } // Add some terrain variations for (int x = 0; x < GRID_SIZE; x++) { for (int y = 0; y < GRID_SIZE; y++) { if ((x + y) % 7 == 0) { grid.setNodeType(x, y, Node.NodeType.FOREST); } else if ((x + y) % 9 == 0) { grid.setNodeType(x, y, Node.NodeType.WATER); } } } } private void setupUI(Stage primaryStage) { BorderPane root = new BorderPane(); root.setPadding(new Insets(10)); // Create canvas canvas = new Canvas(GRID_SIZE * CELL_SIZE, GRID_SIZE * CELL_SIZE); gc = canvas.getGraphicsContext2D(); // Setup mouse interaction setupMouseHandlers(); // Create control panel VBox controlPanel = createControlPanel(); root.setCenter(canvas); root.setBottom(controlPanel); Scene scene = new Scene(root); primaryStage.setTitle("A* Pathfinding Visualizer"); primaryStage.setScene(scene); primaryStage.show(); } private VBox createControlPanel() { VBox panel = new VBox(10); panel.setPadding(new Insets(10)); // Heuristic selection HBox heuristicBox = new HBox(10); heuristicBox.getChildren().add(new Label("Heuristic:")); heuristicCombo = new ComboBox<>(); heuristicCombo.getItems().addAll("Manhattan", "Euclidean", "Chebyshev", "Diagonal"); heuristicCombo.setValue("Euclidean"); heuristicBox.getChildren().add(heuristicCombo); // Checkboxes diagonalCheckbox = new CheckBox("Allow Diagonal"); diagonalCheckbox.setSelected(true); tieBreakingCheckbox = new CheckBox("Tie Breaking"); tieBreakingCheckbox.setSelected(true); // Buttons HBox buttonBox = new HBox(10); findPathButton = new Button("Find Path"); clearButton = new Button("Clear Path"); buttonBox.getChildren().addAll(findPathButton, clearButton); // Stats label statsLabel = new Label("Ready"); // Event handlers findPathButton.setOnAction(e -> findPath()); clearButton.setOnAction(e -> clearPath()); diagonalCheckbox.setOnAction(e -> grid.setAllowDiagonal(diagonalCheckbox.isSelected())); tieBreakingCheckbox.setOnAction(e -> pathfinder.setUseTieBreaking(tieBreakingCheckbox.isSelected())); panel.getChildren().addAll(heuristicBox, diagonalCheckbox, tieBreakingCheckbox, buttonBox, statsLabel); return panel; } private void setupMouseHandlers() { canvas.setOnMouseClicked(e -> { int x = (int) (e.getX() / CELL_SIZE); int y = (int) (e.getY() / CELL_SIZE); if (x >= 0 && x < GRID_SIZE && y >= 0 && y < GRID_SIZE) { Node clickedNode = grid.getNode(x, y); if (e.isShiftDown()) { // Set start node startNode = clickedNode; } else if (e.isControlDown()) { // Set end node endNode = clickedNode; } else { // Toggle obstacle clickedNode.setWalkable(!clickedNode.isWalkable()); } drawGrid(); } }); canvas.setOnMouseDragged(e -> { int x = (int) (e.getX() / CELL_SIZE); int y = (int) (e.getY() / CELL_SIZE); if (x >= 0 && x < GRID_SIZE && y >= 0 && y < GRID_SIZE) { Node draggedNode = grid.getNode(x, y); if (!draggedNode.equals(startNode) && !draggedNode.equals(endNode)) { draggedNode.setWalkable(false); drawGrid(); } } }); } private void findPath() { // Update heuristic String selectedHeuristic = heuristicCombo.getValue(); switch (selectedHeuristic) { case "Manhattan": pathfinder.setHeuristic(new Heuristic.Manhattan()); break; case "Euclidean": pathfinder.setHeuristic(new Heuristic.Euclidean()); break; case "Chebyshev": pathfinder.setHeuristic(new Heuristic.Chebyshev()); break; case "Diagonal": pathfinder.setHeuristic(new Heuristic.Diagonal()); break; } // Find path AStarPathfinder.PathfindingResult result = pathfinder.findPath(startNode, endNode); if (result.isSuccess()) { currentPath = result.getPath(); statsLabel.setText(String.format( "Path found! Length: %d, Cost: %.2f, Nodes explored: %d, Time: %dms", result.getPathLength(), result.getPathCost(), result.getNodesExplored(), result.getExecutionTime() )); } else { currentPath = null; statsLabel.setText("No path found: " + result.getMessage()); } drawGrid(); } private void clearPath() { currentPath = null; grid.resetAllNodes(); drawGrid(); statsLabel.setText("Ready"); } private void drawGrid() { gc.clearRect(0, 0, canvas.getWidth(), canvas.getHeight()); // Draw grid cells for (int x = 0; x < GRID_SIZE; x++) { for (int y = 0; y < GRID_SIZE; y++) { Node node = grid.getNode(x, y); drawNode(node, x, y); } } // Draw path if exists if (currentPath != null) { drawPath(currentPath); } } private void drawNode(Node node, int x, int y) { double screenX = x * CELL_SIZE; double screenY = y * CELL_SIZE; // Determine color based on node type and state Color color = getNodeColor(node); gc.setFill(color); gc.fillRect(screenX, screenY, CELL_SIZE, CELL_SIZE); // Draw border gc.setStroke(Color.LIGHTGRAY); gc.strokeRect(screenX, screenY, CELL_SIZE, CELL_SIZE); // Draw start/end markers if (node.equals(startNode)) { gc.setFill(Color.GREEN); gc.fillText("S", screenX + CELL_SIZE / 3, screenY + CELL_SIZE * 2 / 3); } else if (node.equals(endNode)) { gc.setFill(Color.RED); gc.fillText("E", screenX + CELL_SIZE / 3, screenY + CELL_SIZE * 2 / 3); } } private Color getNodeColor(Node node) { if (!node.isWalkable()) { return Color.DARKGRAY; // Obstacle } if (node.equals(startNode)) { return Color.LIGHTGREEN; } if (node.equals(endNode)) { return Color.LIGHTCORAL; } // Color based on terrain type switch (node.getType()) { case WATER: return Color.LIGHTBLUE; case FOREST: return Color.DARKGREEN; case MOUNTAIN: return Color.GRAY; case ROAD: return Color.LIGHTYELLOW; case SWAMP: return Color.DARKOLIVEGREEN; default: return Color.WHITE; } } private void drawPath(List<Node> path) { gc.setStroke(Color.PURPLE); gc.setLineWidth(3); for (int i = 0; i < path.size() - 1; i++) { Node current = path.get(i); Node next = path.get(i + 1); double startX = current.getX() * CELL_SIZE + CELL_SIZE / 2; double startY = current.getY() * CELL_SIZE + CELL_SIZE / 2; double endX = next.getX() * CELL_SIZE + CELL_SIZE / 2; double endY = next.getY() * CELL_SIZE + CELL_SIZE / 2; gc.strokeLine(startX, startY, endX, endY); } // Draw path nodes gc.setFill(Color.PURPLE); for (Node node : path) { if (!node.equals(startNode) && !node.equals(endNode)) { double x = node.getX() * CELL_SIZE + CELL_SIZE / 2; double y = node.getY() * CELL_SIZE + CELL_SIZE / 2; gc.fillOval(x - 3, y - 3, 6, 6); } } } public static void main(String[] args) { launch(args); } }

Usage Examples

Basic Pathfinding Demo

package com.pathfinding.demo; import com.pathfinding.core.*; import com.pathfinding.heuristics.Heuristic; public class BasicPathfindingDemo { public static void main(String[] args) { // Create a 10x10 grid Grid grid = new Grid(10, 10); // Add some obstacles grid.setWalkable(3, 3, false); grid.setWalkable(3, 4, false); grid.setWalkable(3, 5, false); grid.setWalkable(4, 5, false); grid.setWalkable(5, 5, false); grid.setWalkable(6, 5, false); grid.setWalkable(7, 5, false); // Create pathfinder with Euclidean heuristic AStarPathfinder pathfinder = new AStarPathfinder(grid, new Heuristic.Euclidean()); // Define start and end points Node start = grid.getNode(1, 1); Node end = grid.getNode(8, 8); // Find path AStarPathfinder.PathfindingResult result = pathfinder.findPath(start, end); // Display results if (result.isSuccess()) { System.out.println("Path found successfully!"); System.out.println("Path length: " + result.getPathLength()); System.out.println("Path cost: " + result.getPathCost()); System.out.println("Nodes explored: " + result.getNodesExplored()); System.out.println("Execution time: " + result.getExecutionTime() + "ms"); // Print grid with path grid.printGridWithPath(result.getPath()); // Print path coordinates System.out.println("\nPath coordinates:"); for (Node node : result.getPath()) { System.out.println(" (" + node.getX() + ", " + node.getY() + ")"); } } else { System.out.println("No path found: " + result.getMessage()); grid.printGrid(); } } }

Performance Comparison

package com.pathfinding.demo; import com.pathfinding.core.*; import com.pathfinding.heuristics.Heuristic; import java.util.ArrayList; import java.util.List; public class PerformanceComparison { public static void main(String[] args) { int gridSize = 50; int numTests = 10; List<Heuristic> heuristics = List.of( new Heuristic.Manhattan(), new Heuristic.Euclidean(), new Heuristic.Chebyshev(), new Heuristic.Diagonal() ); List<String> heuristicNames = List.of("Manhattan", "Euclidean", "Chebyshev", "Diagonal"); System.out.println("Performance Comparison (" + gridSize + "x" + gridSize + " grid)"); System.out.println("============================================="); for (int i = 0; i < heuristics.size(); i++) { Heuristic heuristic = heuristics.get(i); String name = heuristicNames.get(i); long totalTime = 0; int totalNodesExplored = 0; int successfulPaths = 0; for (int test = 0; test < numTests; test++) { Grid grid = createRandomGrid(gridSize, gridSize, 0.3); // 30% obstacles AStarPathfinder pathfinder = new AStarPathfinder(grid, heuristic); Node start = grid.getNode(0, 0); Node end = grid.getNode(gridSize - 1, gridSize - 1); AStarPathfinder.PathfindingResult result = pathfinder.findPath(start, end); if (result.isSuccess()) { totalTime += result.getExecutionTime(); totalNodesExplored += result.getNodesExplored(); successfulPaths++; } } if (successfulPaths > 0) { double avgTime = (double) totalTime / successfulPaths; double avgNodes = (double) totalNodesExplored / successfulPaths; System.out.printf("%-10s: Avg Time: %6.2fms, Avg Nodes: %6.1f, Success Rate: %d/%d%n", name, avgTime, avgNodes, successfulPaths, numTests); } else { System.out.printf("%-10s: No successful paths found%n", name); } } } private static Grid createRandomGrid(int width, int height, double obstacleProbability) { boolean[][] walkableMap = new boolean[width][height]; for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { walkableMap[x][y] = Math.random() > obstacleProbability; } } // Ensure start and end are walkable walkableMap[0][0] = true; walkableMap[width - 1][height - 1] = true; return new Grid(width, height, walkableMap); } }

Summary

This comprehensive A* pathfinding implementation provides:

  1. Core Algorithm: Complete A* implementation with open/closed sets
  2. Multiple Heuristics: Manhattan, Euclidean, Chebyshev, and Diagonal distances
  3. Advanced Features: Jump Point Search for optimization, path smoothing
  4. Terrain Support: Different movement costs for various terrain types
  5. Visualization: JavaFX-based visualizer with interactive grid
  6. Performance Metrics: Execution time, nodes explored, path cost tracking
  7. Flexible Grid: Support for obstacles, diagonal movement, and custom costs

The implementation is production-ready and can be easily integrated into games, robotics applications, or any project requiring efficient pathfinding capabilities.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper