When you calculate a route in EF-Map, you're triggering one of two pathfinding algorithms under the hood: A* (A-star) or Dijkstra's algorithm. Both find the shortest path between star systems, but they do it in fundamentally different ways—and understanding the difference can help you choose the right tool for your navigation needs.
This post breaks down how these algorithms work, when to use each one, and why we implemented both in EF-Map instead of picking a single "best" option.
The Problem: Finding Paths in a Graph
EVE Frontier's star map is a graph: systems are nodes, stargates and wormholes are edges. When you want to travel from System A to System Z, the pathfinding algorithm explores this graph to find the optimal route.
But "optimal" can mean different things:
- Fewest jumps (minimize gate transits)
- Shortest distance (minimize light-years traveled)
- Avoid danger (skip high-PvP systems)
- Fastest travel time (balance jumps vs. ship speed)
Both A* and Dijkstra can handle these scenarios, but their performance characteristics differ dramatically.
Dijkstra's Algorithm: The Exhaustive Explorer
Dijkstra's algorithm is the older, more straightforward approach. It explores the graph systematically, expanding outward from the starting system like a wavefront:
function dijkstra(start: System, goal: System): Path {
const distances = new Map<System, number>();
const previous = new Map<System, System>();
const unvisited = new Set<System>(allSystems);
distances.set(start, 0);
while (unvisited.size > 0) {
// Find unvisited system with smallest distance
const current = findMin(unvisited, distances);
if (current === goal) {
return reconstructPath(previous, goal);
}
unvisited.delete(current);
// Update distances to neighbors
for (const neighbor of getNeighbors(current)) {
const altDistance = distances.get(current) + cost(current, neighbor);
if (altDistance < distances.get(neighbor)) {
distances.set(neighbor, altDistance);
previous.set(neighbor, current);
}
}
}
}
How Dijkstra Works
- Start at the source system with distance 0
- Explore all neighboring systems, calculating their distance from source
- Always expand the closest unexplored system next
- Repeat until you reach the goal (or explore the entire graph)
The key insight: Dijkstra guarantees the shortest path by exploring systems in order of their distance from the start. Once you reach the goal, you know you've found the optimal route—there's no shorter path left to discover.
Dijkstra Strengths
- Guaranteed optimal: Always finds the absolute shortest path
- No heuristic needed: Works on any graph without domain knowledge
- Explores uniformly: Discovers all reachable systems at each distance tier
Dijkstra Weaknesses
- Slow for distant goals: Explores many irrelevant systems before reaching the target
- High memory usage: Maintains distance data for thousands of systems
- Wasteful exploration: Doesn't use goal direction to guide search
For a route from one side of New Eden to the other (200+ jumps), Dijkstra might explore 50,000+ systems before finding the goal—most of them completely irrelevant to the final path.
A* (A-Star): The Informed Navigator
A* algorithm is Dijkstra's smarter cousin. It uses a heuristic (educated guess) to bias exploration toward the goal, dramatically reducing wasted computation:
function aStar(start: System, goal: System): Path {
const gScore = new Map<System, number>(); // Distance from start
const fScore = new Map<System, number>(); // gScore + heuristic to goal
const previous = new Map<System, System>();
const openSet = new PriorityQueue<System>();
gScore.set(start, 0);
fScore.set(start, heuristic(start, goal));
openSet.enqueue(start, fScore.get(start));
while (!openSet.isEmpty()) {
const current = openSet.dequeue();
if (current === goal) {
return reconstructPath(previous, goal);
}
for (const neighbor of getNeighbors(current)) {
const tentativeGScore = gScore.get(current) + cost(current, neighbor);
if (tentativeGScore < gScore.get(neighbor)) {
previous.set(neighbor, current);
gScore.set(neighbor, tentativeGScore);
fScore.set(neighbor, tentativeGScore + heuristic(neighbor, goal));
if (!openSet.contains(neighbor)) {
openSet.enqueue(neighbor, fScore.get(neighbor));
}
}
}
}
}
The Magic: The Heuristic Function
A* works because of its heuristic function h(n), which estimates the cost from any system to the goal:
function heuristic(system: System, goal: System): number {
// Straight-line distance (Euclidean distance in 3D space)
const dx = system.x - goal.x;
const dy = system.y - goal.y;
const dz = system.z - goal.z;
return Math.sqrt(dx * dx + dy * dy + dz * dz);
}
This heuristic tells A: "You're currently 150 light-years from the goal as the crow flies." A uses this to prioritize systems that move you closer to the goal, ignoring systems in the wrong direction.
A* Strengths
- Much faster: Explores 10-100x fewer systems than Dijkstra for long routes
- Still optimal: If the heuristic is admissible (never overestimates), A* guarantees the shortest path
- Direction-aware: Naturally biases toward the goal
A* Weaknesses
- Requires good heuristic: Performance depends on heuristic quality
- More complex: Harder to implement correctly than Dijkstra
- Can struggle with obstacles: If the straight-line path is blocked, A* might explore more than necessary
Real-World Performance Comparison
We benchmarked both algorithms on typical EF-Map routes:
Short Routes (5-10 jumps)
- Dijkstra: 15ms, explores 500 systems
- A*: 12ms, explores 80 systems
- Winner: A* (20% faster)
Medium Routes (20-40 jumps)
- Dijkstra: 180ms, explores 8,000 systems
- A*: 25ms, explores 350 systems
- Winner: A* (7x faster)
Long Routes (100+ jumps)
- Dijkstra: 2,500ms, explores 45,000 systems
- A*: 90ms, explores 1,200 systems
- Winner: A* (28x faster!)
For the routes most players calculate (20-50 jumps), A* is dramatically faster—finishing in the time it takes Dijkstra to warm up.
When to Use Each Algorithm
Use Dijkstra When:
- Finding all paths from a source: If you want distances to every system from a starting point, Dijkstra is perfect. We use it for the "Show systems within X jumps" feature.
- The graph is small: For tiny subgraphs (<100 systems), Dijkstra's simplicity beats A*'s overhead.
- No good heuristic exists: If you can't estimate distance to the goal, Dijkstra is your only option.
- Debugging: Dijkstra's exhaustive exploration makes it easier to verify correctness.
Use A* When:
- Point-to-point routing: Single source to single destination—A*'s bread and butter.
- Large graphs: The bigger the graph, the more A shines. EVE Frontier's 200k systems? A all day.
- You have a good heuristic: Straight-line distance works great for spatial graphs like star maps.
- Performance matters: If users expect sub-100ms response times, A* is essential.
Implementation Details: EF-Map's Routing Engine
In EF-Map, we default to A* for all point-to-point routing but expose Dijkstra as an option for power users:
interface RoutingOptions {
algorithm: 'astar' | 'dijkstra';
optimizeFor: 'jumps' | 'distance' | 'safety';
maxJumps?: number;
avoidSystems?: Set<string>;
}
export function findPath(
start: System,
goal: System,
options: RoutingOptions
): Path {
if (options.algorithm === 'dijkstra') {
return dijkstraSearch(start, goal, options);
} else {
return aStarSearch(start, goal, options);
}
}
Optimizing A* for EVE Frontier
We made several enhancements to vanilla A*:
1. Spatial Grid Acceleration
Instead of searching all neighbors linearly, we use a spatial grid:
const CELL_SIZE = 50; // light-years
const spatialGrid = new Map<string, System[]>();
function getCellKey(system: System): string {
const cx = Math.floor(system.x / CELL_SIZE);
const cy = Math.floor(system.y / CELL_SIZE);
const cz = Math.floor(system.z / CELL_SIZE);
return `${cx},${cy},${cz}`;
}
function getNeighbors(system: System): System[] {
const cellKey = getCellKey(system);
return spatialGrid.get(cellKey) || [];
}
This reduces neighbor lookup from O(n) to O(1) for typical cases.
2. Jump Range Constraints
Many ships can't jump more than 6-8 light-years. We pre-filter neighbors:
function getReachableNeighbors(system: System, maxJumpRange: number): System[] {
return getNeighbors(system).filter(neighbor => {
const distance = calculateDistance(system, neighbor);
return distance <= maxJumpRange;
});
}
3. Bidirectional Search
For very long routes, we search from both ends simultaneously:
function bidirectionalAStar(start: System, goal: System): Path {
const forwardSearch = initAStar(start, goal);
const backwardSearch = initAStar(goal, start);
while (true) {
forwardSearch.step();
backwardSearch.step();
// Check if searches met
const meeting = findIntersection(forwardSearch, backwardSearch);
if (meeting) {
return mergePaths(forwardSearch.pathTo(meeting), backwardSearch.pathTo(meeting));
}
}
}
This can cut exploration by another 50% for ultra-long routes.
Edge Cases and Gotchas
Heuristic Admissibility
A*'s optimality guarantee requires an admissible heuristic—one that never overestimates. For EVE Frontier:
// ✅ Admissible: straight-line distance is always ≤ actual path distance
function goodHeuristic(a: System, b: System): number {
return euclideanDistance(a, b);
}
// ❌ Not admissible: could overestimate if we multiply
function badHeuristic(a: System, b: System): number {
return euclideanDistance(a, b) * 1.5; // Breaks A* optimality!
}
Negative Edge Weights
Both algorithms assume non-negative edge weights. If you model "danger" as negative cost (rewards), they break. Use positive costs instead:
// ❌ Broken
const cost = baseDistance - (dangerLevel * 10); // Can go negative!
// ✅ Correct
const cost = baseDistance + (dangerLevel * 10); // Always positive
Dynamic Graphs
When Smart Gates open/close or wormholes shift, the graph changes. We invalidate cached paths:
let pathCache = new Map<string, Path>();
function onGateStatusChange(gateId: string) {
pathCache.clear(); // Invalidate all cached paths
}
Visualizing the Difference
If you enable "Debug Mode" in EF-Map, you can see the algorithms in action:
- Dijkstra: Explores in concentric circles around the start (uniform wavefront)
- A*: Explores in an elongated ellipse toward the goal (directional bias)
The difference is striking on long routes—Dijkstra floods the entire graph, while A* carves a narrow corridor straight to the destination.
Future: Beyond A* and Dijkstra
We're exploring even faster algorithms for special cases:
- Contraction Hierarchies: Preprocess the graph for 1000x speedup on repeated queries
- ALT (A*, Landmarks, Triangle inequality): Use precomputed distances to landmark systems for better heuristics
- Jump Point Search: Exploit grid structure (if we discretize the map)
But for now, A* and Dijkstra cover 99% of use cases beautifully.
Try It Yourself
Open EF-Map's Routing panel and toggle "Algorithm: A" vs "Algorithm: Dijkstra" while calculating a long route. Watch the exploration counter—A will explore far fewer systems. For most navigation, A* is the clear winner, but understanding both algorithms makes you a better navigator in New Eden.
Related Posts
- Scout Optimizer: Solving the Traveling Salesman Problem in Space - How genetic algorithms build on top of A* for multi-waypoint routes
- Three.js Rendering: Building a 3D Starfield - Visualizing the paths that A* and Dijkstra discover
- Database Architecture: Spatial Indexing - The PostGIS spatial queries that power fast neighbor lookups