Skip to content

Commit

Permalink
implementing astar A* + tests
Browse files Browse the repository at this point in the history
  • Loading branch information
svencc committed Sep 9, 2023
1 parent c214fc2 commit 75fec8d
Show file tree
Hide file tree
Showing 9 changed files with 560 additions and 1 deletion.
42 changes: 41 additions & 1 deletion src/main/java/lib/maze/Maze.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class Maze {
private final Cell[][] grid;

@Builder
public Maze(
private Maze(
final int rows,
final int columns,
@NonNull final MazeLocation start,
Expand All @@ -40,6 +40,16 @@ public Maze(
grid[goal.getRow()][goal.getColumn()] = Cell.GOAL;
}

public static Maze randomMaze(
final int rows,
final int columns,
@NonNull final MazeLocation start,
@NonNull final MazeLocation goal,
final double sparseness
) {
return new Maze(rows, columns, start, goal, sparseness);
}

private void randomlyFill(double sparseness) {
for (int row = 0; row < rows; row++) {
for (int column = 0; column < columns; column++) {
Expand Down Expand Up @@ -157,4 +167,34 @@ public List<MazeLocation> successors(@NonNull final MazeLocation location) {
return successors;
}

public void mark(@NonNull final List<MazeLocation> path) {
for (final MazeLocation mazeLocation : path) {
grid[mazeLocation.getRow()][mazeLocation.getColumn()] = Cell.PATH;
}
grid[start.getRow()][start.getColumn()] = Cell.START;
grid[goal.getRow()][goal.getColumn()] = Cell.GOAL;
}

public void clear(@NonNull final List<MazeLocation> path) {
for (final MazeLocation mazeLocation : path) {
grid[mazeLocation.getRow()][mazeLocation.getColumn()] = Cell.EMPTY;
}
grid[start.getRow()][start.getColumn()] = Cell.START;
grid[goal.getRow()][goal.getColumn()] = Cell.GOAL;
}

public double euclideanDistance(@NonNull final MazeLocation location) {
int xDistance = location.getColumn() - goal.getColumn();
int yDistance = location.getRow() - goal.getRow();

return Math.sqrt(Math.pow(xDistance, 2) + Math.pow(yDistance, 2));
}

public double manhattanDistance(@NonNull final MazeLocation location) {
int xDistance = Math.abs(location.getColumn() - goal.getColumn());
int yDistance = Math.abs(location.getRow() - goal.getRow());

return xDistance + yDistance;
}

}
86 changes: 86 additions & 0 deletions src/main/java/lib/maze/Node.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package lib.maze;

import lombok.Getter;
import lombok.NonNull;
import org.springframework.lang.Nullable;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@Getter
public class Node<T> implements Comparable<Node<T>> {

@NonNull
private final T state; // DFS only
@NonNull
private final Optional<Node<T>> parent; // DFS only
private final double cost; // used in A* not in DFS
private final double heuristic;// used in A* not in DFS

// DFS Node
private Node(@NonNull final T state, @Nullable final Node<T> parent) {
this.state = state;
this.parent = Optional.ofNullable(parent);
this.cost = 0;
this.heuristic = 0;
}

// A* Node
private Node(@NonNull final T state, @Nullable final Node<T> parent, final double cost, final double heuristic) {
this.state = state;
this.parent = Optional.ofNullable(parent);
this.cost = cost;
this.heuristic = heuristic;
}

public static <T> Node<T> astarStartNode(
@NonNull final T state,
final double heuristic
) {
return new Node<>(state, null, 0, heuristic);
}

public static <T> Node<T> astarNode(
@NonNull final T state,
@Nullable final Node<T> parent,
final double cost,
final double heuristic
) {
return new Node<>(state, parent, cost, heuristic);
}

public static <T> Node<T> dfsStartNode(@NonNull final T state) {
return new Node<>(state, null);
}

public static <T> Node<T> dfsNode(
@NonNull final T state,
@NonNull final Node<T> parent
) {
return new Node<>(state, parent);
}

@NonNull
public static <T> List<T> nodeToPath(@NonNull final Node<T> node) {
final List<T> path = new ArrayList<>();
path.add(node.getState());

// work backwards from end to front
Node<T> currentNode = node;
while (currentNode.getParent().isPresent()) {
currentNode = currentNode.getParent().get();
path.add(0, currentNode.getState());
}

return path;
}

@Override
public int compareTo(@NonNull final Node<T> other) {
final Double mine = cost + heuristic;
final Double theirs = other.cost + other.heuristic;
return mine.compareTo(theirs);
}

}
55 changes: 55 additions & 0 deletions src/main/java/lib/maze/searchstrategy/AstarStrategy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package lib.maze.searchstrategy;

import lib.maze.Node;
import lombok.NonNull;

import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.ToDoubleFunction;

/**
* A* Search
*/
public class AstarStrategy {

@NonNull
public <T> Optional<Node<T>> search(
@NonNull final T initialState,
@NonNull final Predicate<T> goalTest,
@NonNull final Function<T, List<T>> successors,
@NonNull final ToDoubleFunction<T> heuristic
) {
// frontier is where we've yet to go
final PriorityQueue<Node<T>> frontier = new PriorityQueue<>();
frontier.offer(Node.astarStartNode(initialState, heuristic.applyAsDouble(initialState)));

// explored is where we've already been
final Map<T, Double> explored = new HashMap<>();
explored.put(initialState, 0.0);

// keep going while there is more to explore
while (!frontier.isEmpty()) {
final Node<T> currentNode = frontier.poll();
final T currentState = currentNode.getState();

// if we found the goal, we're done
if (goalTest.test(currentState)) {
return Optional.of(currentNode);
}

// check where we can go next and haven't explored
for (final T child : successors.apply(currentState)) {
// cost=1 here assumes a simple grid, need a cost function for more sophisticated apps
final double newCost = currentNode.getCost() + 1;
if (!explored.containsKey(child) || explored.get(child) > newCost) {
explored.put(child, newCost);
frontier.offer(Node.astarNode(child, currentNode, newCost, heuristic.applyAsDouble(child)));
}
}
}

return Optional.empty(); // never found the goal
}

}
52 changes: 52 additions & 0 deletions src/main/java/lib/maze/searchstrategy/BSFStrategy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package lib.maze.searchstrategy;

import lib.maze.Node;
import lombok.NonNull;

import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;

/**
* Breadth First Search
*/
public class BSFStrategy implements GoalSearchableStrategy {

@NonNull
public <T> Optional<Node<T>> search(
@NonNull final T initialState,
@NonNull final Predicate<T> goalTest,
@NonNull final Function<T, List<T>> successors
) {
// frontier is where we've yet to go
final Queue<Node<T>> frontier = new LinkedList<>();
frontier.offer(Node.dfsStartNode(initialState));

// explored is where we've already been
final Set<T> explored = new HashSet<>();
explored.add(initialState);

// keep going while there is more to explore
while (!frontier.isEmpty()) {
final Node<T> currentNode = frontier.poll();
final T currentState = currentNode.getState();

// if we found the goal, we're done
if (goalTest.test(currentState)) {
return Optional.of(currentNode);
}

// check where we can go next and haven't explored
for (final T child : successors.apply(currentState)) {
if (explored.contains(child)) {
continue; // skip children we already explored
}
explored.add(child);
frontier.offer(Node.dfsNode(child, currentNode));
}
}

return Optional.empty(); // never found the goal
}

}
52 changes: 52 additions & 0 deletions src/main/java/lib/maze/searchstrategy/DSFStrategy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package lib.maze.searchstrategy;

import lib.maze.Node;
import lombok.NonNull;

import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;

/**
* Depth First Search
*/
public class DSFStrategy implements GoalSearchableStrategy {

@NonNull
public <T> Optional<Node<T>> search(
@NonNull final T initialState,
@NonNull final Predicate<T> goalTest,
@NonNull final Function<T, List<T>> successors
) {
// frontier is where we've yet to go
final Stack<Node<T>> frontier = new Stack<>();
frontier.push(Node.dfsStartNode(initialState));

// explored is where we've already been
final Set<T> explored = new HashSet<>();
explored.add(initialState);

// keep going while there is more to explore
while (!frontier.isEmpty()) {
final Node<T> currentNode = frontier.pop();
final T currentState = currentNode.getState();

// if we found the goal, we're done
if (goalTest.test(currentState)) {
return Optional.of(currentNode);
}

// check where we can go next and haven't explored
for (final T child : successors.apply(currentState)) {
if (explored.contains(child)) {
continue; // skip children we already explored
}
explored.add(child);
frontier.push(Node.dfsNode(child, currentNode));
}
}

return Optional.empty(); // never found the goal
}

}
20 changes: 20 additions & 0 deletions src/main/java/lib/maze/searchstrategy/GoalSearchableStrategy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package lib.maze.searchstrategy;

import lib.maze.Node;
import lombok.NonNull;

import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;

public interface GoalSearchableStrategy {

@NonNull
<T> Optional<Node<T>> search(
@NonNull final T initialState,
@NonNull final Predicate<T> goalTest,
@NonNull final Function<T, List<T>> successors
);

}
Loading

0 comments on commit 75fec8d

Please sign in to comment.