Skip to main content

AStar

Dependencies​

This library does not require any dependencies.

Usage​

import { Map2D, Node2D, Vector2D } from "@arclockproject/common/library/AStar";
import Random from "@arclockproject/common/library/Random";

const worldMap = new Map2D(
// Loop to generate a Node2D[][] Matrix
new Array(5).fill(0).map((_value, y, arr) => {
return arr.map(
(_, x) =>
new Node2D(
new Vector2D(x, y), // Correctly pass coordinate reference to node
Random.rangeInt(0, 4), // Generate random cost
Math.random() > 0.1, // 10% chance to become not traversable
),
);
}),
);

const foundPath = worldMap.calculatePath(
worldMap.getNode(new Vector2D(1, 1)), // Point 1
worldMap.getNode(new Vector2D(4, 4)), // Point 2
);

if (foundPath) {
console.log("Valid path was found!");
} else {
console.log("Was unable to find path between 2 points.");
}

calculatePath returns the path from the end back to the start, including both, or null if there is no path. Use .reverse() to walk it from the start. Movement is up, down, left and right only.

The example below is live. Edit the code to try your own values.

Pathfinding playground​

Pick a tool, then click cells. Mud costs more to walk through, so the path tries to go around it.

▶ Show code (click to view and edit)
Live Editor
function PathfindingPlayground() {
  // Build the map once. Clicks change its nodes in place, then call redraw().
  const map = useMemo(() => {
    const map = new Map2D(Map2D.generateNodeMatrix((pos) => new Node2D(pos, 1), 20, 12));
    map.getNodeAreaFlat({ x: 6, y: 2 }, { x: 7, y: 10 }).forEach((node) => (node.traversable = false));
    map.getNodeAreaFlat({ x: 13, y: 0 }, { x: 14, y: 8 }).forEach((node) => (node.traversable = false));
    map.getNodeAreaFlat({ x: 8, y: 6 }, { x: 12, y: 12 }).forEach((node) => (node.cost = 5));
    return map;
  }, []);
  const [, redraw] = useReducer((v) => v + 1, 0);
  const [start, setStart] = useState({ x: 2, y: 5 });
  const [end, setEnd] = useState({ x: 17, y: 6 });
  const [tool, setTool] = useState("wall");
  const path = map.calculatePath(map.getNode(start), map.getNode(end));

  const click = (node) => {
    if (tool === "start") return setStart(node.pos);
    if (tool === "end") return setEnd(node.pos);
    if (tool === "wall") node.traversable = !node.traversable;
    if (tool === "mud") node.cost = node.cost === 5 ? 1 : 5;
    redraw();
  };

  const clear = () => {
    map.forEachNode((node) => Object.assign(node, { traversable: true, cost: 1 }));
    redraw();
  };

  const color = (node) => {
    const { x, y } = node.pos;
    if (x === start.x && y === start.y) return "#2f855a";
    if (x === end.x && y === end.y) return "#c53030";
    if (!node.traversable) return "#2d3748";
    if (path?.includes(node)) return "var(--ifm-color-primary)";
    if (node.cost === 5) return "#975a16";
    return "#e2e8f0";
  };

  return (
    <div>
      {["wall", "mud", "start", "end"].map((name) => (
        <button key={name} onClick={() => setTool(name)} style={{ fontWeight: tool === name ? "bold" : undefined, marginRight: 4 }}>
          {name}
        </button>
      ))}
      <button onClick={clear}>Clear</button>
      <div style={{ display: "grid", gridTemplateColumns: `repeat(${map.width}, 24px)`, gap: 1, margin: "8px 0", userSelect: "none" }}>
        {map.nodes.flat().map((node) => (
          <div key={`${node.pos}`} onClick={() => click(node)} style={{ width: 24, height: 24, background: color(node), cursor: "pointer" }} />
        ))}
      </div>
      <p>{path ? `Path found: ${path.length} cells.` : "No path between start and end."}</p>
      <h6>Cost for Mud = 5x</h6>
    </div>
  );
}
Result
Loading...

API​

See the AStar API reference for every prop, method and type.