diff --git a/site/docs/09-math/07-graph.mdx b/site/docs/09-math/07-graph.mdx new file mode 100644 index 00000000..4b9aee8f --- /dev/null +++ b/site/docs/09-math/07-graph.mdx @@ -0,0 +1,143 @@ +--- +title: Graph +slug: /graph +section: Math +--- + +## Graphs + +A powerful and flexible graph data structure implementation for working with connected data. This module provides a complete set of tools for creating, manipulating, and traversing graph structures with support for both directed and undirected weighted edges. + +## Overview + +The Graph module allows you to: + +- Create and manage nodes/vertices with custom data +- Connect nodes with weighted, directed or undirected edges +- Position nodes in 2D space for spatial algorithms +- Perform common graph traversal operations like BFS and DFS +- Find optimal paths using Dijkstra's algorithm or A* search + +## Basic Usage + +### Creating a Graph and working with Nodes and Edges + +```ts +import { Graph } from 'excalibur'; + +// Create an empty graph of strings +const graph = new Graph(); + +// Add a few nodes with string data +const nodeA = graph.addNode("A"); +const nodeB = graph.addNode("B"); +const nodeC = graph.addNode("C"); + +// Connect nodes with directed edges (default) +graph.addEdge(nodeA, nodeB); +graph.addEdge(nodeB, nodeC); +graph.addEdge(nodeC, nodeD); +graph.addEdge(nodeD, nodeE); + +// Connect nodes with undirected edges +graph.addEdge(nodeA, nodeC, { directed: false }); + +// Connect nodes with weighted edges +graph.addEdge(nodeA, nodeB, { weight: 5 }); + +// Check if nodes are connected +const connected = graph.areNodesConnected(nodeA, nodeB); // true + +// Get neighbors of a node +const neighbors = graph.getNeighbors(nodeA); // [nodeB] + +// Delete a node (and its edges) +graph.deleteNode(nodeC); + +// Delete an edge +graph.deleteEdge(edges[0]); +``` + +## Core Concepts + +### Node Types + +The Graph module supports several node types: + +Node: Basic graph node with data +PositionNode: Node with 2D spatial coordinates, uses Excalibur's Native Vector type for position +Vertex: An alias for Node for more traditional graph terminology + +```ts +// Add positioned nodes, whe Vector positions are attached to nodes, it returns a PositionNode +const nodeA = graph.addNode("A", new Vector(0, 0)); +const nodeB = graph.addNode("B", new Vector(5, 10)); +const nodeC = graph.addNode("C", new Vector(10, 5)); +``` + +### Edge Properties + +Edges connect nodes and can have properties: + +weight: Numeric value representing distance or cost (default: 0) +directed: Whether the edge is one-way or bidirectional (default: true) + +Using a bidrectional edge will create two edges that are mirrored, and connected by a property. + +```ts +// Connect the nodes +spatialGraph.addEdge(nodeA, nodeB, { weight: 11.2, directed: false }); // Euclidean distance + +``` + +### Graph Traversal + +#### Breadth-First Search (BFS) + +Explore the graph layer by layer, visiting all direct neighbors before moving deeper: + +```ts +// Create and populate your graph first +const visitedNodeIds = graph.bfs(startNode); +``` + +#### Depth-First Search (DFS) + +Explore the graph by moving as far as possible along each branch before backtracking: + +```ts +// Create and populate your graph first +const visitedNodeIds = graph.dfs(startNode); +``` + +### Pathfinding Algorithms + +#### Shortest Path and Dijkstra's Algorithm + +Find the shortest path between two nodes in a weighted graph: + +```ts +// Find shortest path from A to C +const { path, distance } = graph.shortestPathDijkstra(nodeA, nodeC); + +// Get full analysis +const dijkstraAnalysis = graph.dijkstra(nodeA); +``` + +#### A* Algorithm + +Find the shortest path using spatial information for better performance: + +## Other Features + +### Building a Graph from Data Arrays + +For convenience, you can create a graph from arrays of node data: +```ts +// Create a graph with string data nodes +const cities = ["New York", "London", "Tokyo", "Sydney", "Paris"]; +const graph = Graph.createGraphFromNodes(cities); + +// Use alias for more traditional graph terminology +const graph2 = Graph.createGraphFromVertices(cities); +``` \ No newline at end of file diff --git a/src/engine/Math/Graph.ts b/src/engine/Math/Graph.ts index 77f8603b..99723854 100644 --- a/src/engine/Math/Graph.ts +++ b/src/engine/Math/Graph.ts @@ -473,12 +473,16 @@ export class Graph { } } + if (lowestDistanceIndex === -1) { + return []; + } + current = resultArray[lowestDistanceIndex].node; let currentEdgesArray = Array.from(current.edges); //remove visited from currentEdges currentEdgesArray = currentEdgesArray.filter((edge: Edge) => { - return !visited.includes(edge.source) && !visited.includes(edge.target); + return !visited.includes(edge.source) && !visited.includes(edge.target) && edge.target !== current; }); visited.push(current); @@ -505,9 +509,12 @@ export class Graph { return resultArray; } - shortestPathDijkstra(sourcenode: Node, endnode: Node): { path: Node[]; distance: number } { - const dAnalysis = this.dijkstra(sourcenode); + shortestPathDijkstra(startingnode: Node, endnode: Node): { path: Node[]; distance: number } { + const dAnalysis = this.dijkstra(startingnode); + if (dAnalysis.length === 0) { + return { path: [], distance: Infinity }; + } //iterate through dAnalysis to plot shortest path to endnode const path: Node[] = []; let current: Node | null | undefined = endnode; diff --git a/src/spec/GraphSpec.ts b/src/spec/GraphSpec.ts index 5a63821e..57d47218 100644 --- a/src/spec/GraphSpec.ts +++ b/src/spec/GraphSpec.ts @@ -1,6 +1,6 @@ import * as ex from '@excalibur'; -describe('A Graph', () => { +fdescribe('A Graph', () => { let graph: ex.Graph; beforeEach(() => { @@ -172,7 +172,7 @@ describe('A Graph', () => { }); }); - describe('Graph traversal', () => { + describe('traversal', () => { it('should perform breadth-first search (BFS)', () => { // Create a simple graph // A -> B -> D @@ -265,7 +265,7 @@ describe('A Graph', () => { }); describe('Path finding algorithms', () => { - it("should find shortest path using Dijkstra's algorithm", () => { + it('should create a Djikstra analysis of nodeA', () => { // Create a weighted graph // 5 // A --- B @@ -289,25 +289,48 @@ describe('A Graph', () => { expect(result.length).toBe(4); expect(result[0].node).toBe(nodeA); expect(result[0].distance).toBe(0); - expect(result[1].node).toBe(nodeC); - expect(result[1].distance).toBe(2); - expect(result[2].node).toBe(nodeB); - expect(result[2].distance).toBe(5); + expect(result[1].node).toBe(nodeB); + expect(result[1].distance).toBe(5); + expect(result[2].node).toBe(nodeC); + expect(result[2].distance).toBe(2); expect(result[3].node).toBe(nodeD); - expect(result[3].distance).toBe(9); + expect(result[3].distance).toBe(6); }); - it('should return Infinity distance when no path exists', () => { + it('should find shortest path between two nodes', () => { + const nodeA = graph.addNode('A'); + const nodeB = graph.addNode('B'); + const nodeC = graph.addNode('C'); + const nodeD = graph.addNode('D'); + const nodeE = graph.addNode('E'); + + //add edges + + graph.addEdge(nodeA, nodeB, { weight: 5 }); + graph.addEdge(nodeA, nodeC, { weight: 2 }); + graph.addEdge(nodeB, nodeD, { weight: 1 }); + graph.addEdge(nodeC, nodeD, { weight: 8 }); + graph.addEdge(nodeD, nodeE, { weight: 3 }); + + // Find shortest path between A and E + const result = graph.shortestPathDijkstra(nodeA, nodeE); + + expect(result.path.length).toBe(4); + expect(result.path[0]).toBe(nodeA); + expect(result.path[1]).toBe(nodeB); + expect(result.path[2]).toBe(nodeD); + expect(result.path[3]).toBe(nodeE); + expect(result.distance).toBe(9); + }); + + it('should return empty path when no path exists', () => { const nodeA = graph.addNode('A'); const nodeB = graph.addNode('B'); // No edge connecting A and B const result = graph.dijkstra(nodeA); - expect(result.length).toBe(2); - expect(result[0].node).toBe(nodeA); - expect(result[0].distance).toBe(0); - expect(result[1].node).toBe(nodeB); - expect(result[1].distance).toBe(Infinity); + + expect(result.length).toBe(0); }); it('should handle zero-distance path (same node)', () => { @@ -356,24 +379,32 @@ describe('A Graph', () => { it('should find shortest path using A* algorithm', () => { // Create a graph with positioned nodes const nodeA = graph.addNode('A', new ex.Vector(0, 0)); - const nodeB = graph.addNode('B', new ex.Vector(1, 0)); - const nodeC = graph.addNode('C', new ex.Vector(0, 1)); - const nodeD = graph.addNode('D', new ex.Vector(1, 1)); + const nodeB = graph.addNode('B', new ex.Vector(3, 0)); + const nodeC = graph.addNode('C', new ex.Vector(0, 4)); + const nodeD = graph.addNode('D', new ex.Vector(3, 4)); // Add edges with weights - graph.addEdge(nodeA, nodeB, { weight: 5 }); - graph.addEdge(nodeA, nodeC, { weight: 2 }); - graph.addEdge(nodeB, nodeD, { weight: 1 }); - graph.addEdge(nodeC, nodeD, { weight: 8 }); + graph.addEdge(nodeA, nodeB); + graph.addEdge(nodeA, nodeC); + graph.addEdge(nodeB, nodeD); + graph.addEdge(nodeC, nodeD); + + /* + A -> B + | | + v v + C -> D + */ const result = graph.aStar(nodeA as ex.PositionNode, nodeD as ex.PositionNode); expect(result.path).toBeDefined(); expect(result.path?.length).toBe(3); + expect(result.pathSteps).toBe(2); expect(result.path?.[0]).toBe(nodeA as ex.PositionNode); expect(result.path?.[1]).toBe(nodeB as ex.PositionNode); expect(result.path?.[2]).toBe(nodeD as ex.PositionNode); - expect(result.distance).toBe(6); // 5 + 1 = 6 + expect(result.distance).toBe(5); }); it('should throw error when A* is used with non-PositionNodes', () => { @@ -383,7 +414,7 @@ describe('A Graph', () => { // Type assertion to test error condition expect(() => { graph.aStar(nodeA as unknown as ex.PositionNode, nodeB as unknown as ex.PositionNode); - }).toThrow(/requires PositionNode/); + }).toThrow(new Error('A* algorithm requires PositionNode with position vectors')); }); it('should return null path when no path exists in A*', () => { @@ -393,7 +424,9 @@ describe('A Graph', () => { // No edge connecting A and B const result = graph.aStar(nodeA as ex.PositionNode, nodeB as ex.PositionNode); - expect(result.path).toBeNull(); + //path will be empty array and distance will be Infinity + expect(result.path.length).toBe(0); + expect(result.pathSteps).toBe(0); expect(result.distance).toBe(Infinity); }); });