2 changed files with 138 additions and 0 deletions
@ -0,0 +1,71 @@ |
|||
import type { GameGraph, NodeId } from "./types"; |
|||
|
|||
export function axialNeighbors(q: number, r: number): Array<[number, number]> { |
|||
return [ |
|||
[q + 1, r], |
|||
[q + 1, r - 1], |
|||
[q, r - 1], |
|||
[q - 1, r], |
|||
[q - 1, r + 1], |
|||
[q, r + 1], |
|||
]; |
|||
} |
|||
|
|||
export function shortestPathLength( |
|||
graph: GameGraph, |
|||
from: NodeId, |
|||
to: NodeId, |
|||
): number { |
|||
if (from === to) { |
|||
return 0; |
|||
} |
|||
|
|||
if (!graph.nodes[from] || !graph.nodes[to]) { |
|||
return Infinity; |
|||
} |
|||
|
|||
const queue: Array<[NodeId, number]> = [[from, 0]]; |
|||
const visited = new Set<NodeId>([from]); |
|||
|
|||
while (queue.length > 0) { |
|||
const [current, distance] = queue.shift()!; |
|||
const node = graph.nodes[current]; |
|||
|
|||
for (const neighbor of node.neighbors) { |
|||
if (neighbor === to) { |
|||
return distance + 1; |
|||
} |
|||
if (!visited.has(neighbor) && graph.nodes[neighbor]) { |
|||
visited.add(neighbor); |
|||
queue.push([neighbor, distance + 1]); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return Infinity; |
|||
} |
|||
|
|||
export function isConnected(graph: GameGraph): boolean { |
|||
const nodeIds = Object.keys(graph.nodes); |
|||
if (nodeIds.length <= 1) { |
|||
return true; |
|||
} |
|||
|
|||
const start = nodeIds[0]; |
|||
const visited = new Set<NodeId>([start]); |
|||
const queue: NodeId[] = [start]; |
|||
|
|||
while (queue.length > 0) { |
|||
const current = queue.shift()!; |
|||
const node = graph.nodes[current]; |
|||
|
|||
for (const neighbor of node.neighbors) { |
|||
if (!visited.has(neighbor) && graph.nodes[neighbor]) { |
|||
visited.add(neighbor); |
|||
queue.push(neighbor); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return visited.size === nodeIds.length; |
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
import { describe, expect, it } from "vitest"; |
|||
import { |
|||
axialNeighbors, |
|||
isConnected, |
|||
shortestPathLength, |
|||
} from "../../src/game/chase/hexGraph"; |
|||
import type { GameGraph } from "../../src/game/chase/types"; |
|||
|
|||
function buildGraph(nodes: GameGraph["nodes"]): GameGraph { |
|||
return { |
|||
nodes, |
|||
edgeList: [], |
|||
}; |
|||
} |
|||
|
|||
describe("hex graph utilities", () => { |
|||
it("returns 6 axial neighbors", () => { |
|||
const neighbors = axialNeighbors(0, 0); |
|||
expect(neighbors).toEqual([ |
|||
[1, 0], |
|||
[1, -1], |
|||
[0, -1], |
|||
[-1, 0], |
|||
[-1, 1], |
|||
[0, 1], |
|||
]); |
|||
}); |
|||
|
|||
it("finds bfs shortest path length", () => { |
|||
const graph = buildGraph({ |
|||
A: { id: "A", q: 0, r: 0, neighbors: ["B"] }, |
|||
B: { id: "B", q: 1, r: 0, neighbors: ["A", "C"] }, |
|||
C: { id: "C", q: 2, r: 0, neighbors: ["B", "D"] }, |
|||
D: { id: "D", q: 3, r: 0, neighbors: ["C"] }, |
|||
}); |
|||
|
|||
expect(shortestPathLength(graph, "A", "D")).toBe(3); |
|||
expect(shortestPathLength(graph, "A", "A")).toBe(0); |
|||
}); |
|||
|
|||
it("returns Infinity when target is unreachable", () => { |
|||
const graph = buildGraph({ |
|||
A: { id: "A", q: 0, r: 0, neighbors: ["B"] }, |
|||
B: { id: "B", q: 1, r: 0, neighbors: ["A"] }, |
|||
C: { id: "C", q: 5, r: 5, neighbors: [] }, |
|||
}); |
|||
|
|||
expect(shortestPathLength(graph, "A", "C")).toBe(Infinity); |
|||
}); |
|||
|
|||
it("checks graph connectivity", () => { |
|||
const connectedGraph = buildGraph({ |
|||
A: { id: "A", q: 0, r: 0, neighbors: ["B"] }, |
|||
B: { id: "B", q: 1, r: 0, neighbors: ["A", "C"] }, |
|||
C: { id: "C", q: 2, r: 0, neighbors: ["B"] }, |
|||
}); |
|||
|
|||
const disconnectedGraph = buildGraph({ |
|||
A: { id: "A", q: 0, r: 0, neighbors: ["B"] }, |
|||
B: { id: "B", q: 1, r: 0, neighbors: ["A"] }, |
|||
C: { id: "C", q: 5, r: 5, neighbors: [] }, |
|||
}); |
|||
|
|||
expect(isConnected(connectedGraph)).toBe(true); |
|||
expect(isConnected(disconnectedGraph)).toBe(false); |
|||
}); |
|||
}); |
|||
Loading…
Reference in new issue