Published October 15, 2025 • 10 min read
Scouts in EVE Frontier face a unique challenge: visiting multiple star systems efficiently. Unlike simple point-to-point navigation, scout routes require visiting 5, 10, or even 20 systems in a single expedition. Finding the optimal order to visit them is a classic computer science problem—and one that becomes exponentially harder as you add more waypoints.
Here's how we built EF-Map's Scout Optimizer to solve this problem in real-time.
The Problem: Traveling Salesman in Space
The Traveling Salesman Problem (TSP) asks: "Given a list of cities and the distances between them, what is the shortest possible route that visits each city exactly once?"
This problem is NP-hard, meaning there's no known algorithm that can solve it perfectly in polynomial time. For 10 systems, there are 3.6 million possible routes. For 20 systems? 2.4 quintillion routes.
Brute-forcing the answer isn't an option.
Our challenge: Find a "good enough" route in under 2 seconds that users can trust for real gameplay.
The Algorithm: Genetic Optimization
We chose a genetic algorithm approach combined with spatial grid optimization to balance speed and solution quality.
Why Genetic Algorithms?
Genetic algorithms mimic natural selection:
- Generate a population of random routes
 - Evaluate each route's fitness (total distance)
 - Select the best routes to "reproduce"
 - Create offspring by combining parts of parent routes
 - Add random mutations to explore new solutions
 - Repeat for N generations
 
This approach doesn't guarantee the perfect route, but it consistently finds routes within 5-10% of optimal in a fraction of the time.
Population Size and Generations
After extensive testing, we settled on:
- Population size: 100 routes
 - Generations: 50 iterations
 - Elite preservation: Keep top 10% unchanged each generation
 - Mutation rate: 15% chance to swap two waypoints
 
These parameters balance solution quality with computation time. On average, the algorithm runs in 800-1200ms for 15 waypoints.
Spatial Grid Optimization
Before running the genetic algorithm, we pre-compute a spatial grid to accelerate neighbor lookups.
The Problem with Naive Distance Calculations
Calculating distances between all pairs of systems is expensive:
For N systems: N × (N-1) / 2 distance calculations
For 20 systems: 190 calculations
For 50 systems: 1,225 calculations
Repeated across 50 generations and 100 population members, this becomes a bottleneck.
The Solution: Spatial Hashing
We divide the map into a 3D grid of cells. Each system belongs to one cell based on its coordinates:
const CELL_SIZE = 50; // lightyears
const getCellKey = (x, y, z) => {
  const cellX = Math.floor(x / CELL_SIZE);
  const cellY = Math.floor(y / CELL_SIZE);
  const cellZ = Math.floor(z / CELL_SIZE);
  return `${cellX},${cellY},${cellZ}`;
};
When calculating the distance from system A to system B, we only check:
- Systems in A's cell
 - Systems in A's 26 neighboring cells
 
This reduces the search space dramatically. For a map with 1,000 systems, we might only check 20-30 candidates instead of all 1,000.
Performance impact: Distance lookups went from O(N) to O(1) average case.
Route Crossover Strategies
Creating "offspring" routes requires combining two parent routes without duplicating waypoints. We use Ordered Crossover (OX):
const crossover = (parent1, parent2) => {
  // Select a random segment from parent1
  const start = randomInt(0, parent1.length);
  const end = randomInt(start + 1, parent1.length);
  
  const segment = parent1.slice(start, end);
  const offspring = new Array(parent1.length);
  
  // Copy segment to offspring
  for (let i = start; i < end; i++) {
    offspring[i] = segment[i - start];
  }
  
  // Fill remaining positions from parent2 (in order, skipping duplicates)
  let p2Index = 0;
  for (let i = 0; i < offspring.length; i++) {
    if (offspring[i] === undefined) {
      while (segment.includes(parent2[p2Index])) {
        p2Index++;
      }
      offspring[i] = parent2[p2Index];
      p2Index++;
    }
  }
  
  return offspring;
};
This preserves relative ordering from both parents while avoiding duplicate waypoints.
Mutation: Escaping Local Optima
Without mutation, the algorithm can get stuck in local optima—routes that are better than their neighbors but not globally optimal.
We use swap mutation: randomly select two waypoints and exchange their positions.
const mutate = (route, mutationRate = 0.15) => {
  if (Math.random() > mutationRate) return route;
  
  const i = randomInt(0, route.length);
  const j = randomInt(0, route.length);
  
  [route[i], route[j]] = [route[j], route[i]];
  return route;
};
This simple operation occasionally produces dramatic improvements by breaking up inefficient route segments.
Handling Special Constraints
EVE Frontier's navigation has unique considerations beyond pure distance:
1. Fuel Costs vs. Jump Counts
Should we optimize for:
- Shortest total distance (minimize fuel consumption)?
 - Fewest jumps (minimize time)?
 
We let users choose via a toggle. The algorithm adapts by changing the cost function:
const calculateCost = (route, optimizeFor) => {
  if (optimizeFor === 'fuel') {
    // Sum of direct distances between waypoints
    return route.reduce((sum, system, i) => {
      if (i === 0) return 0;
      return sum + distance(route[i - 1], system);
    }, 0);
  } else {
    // Count jumps via pathfinding (respects stargates)
    return route.reduce((sum, system, i) => {
      if (i === 0) return 0;
      const path = findPath(route[i - 1], system);
      return sum + path.length;
    }, 0);
  }
};
2. Start/End Points
Users can lock specific systems as the start or end of their route. The algorithm respects these constraints by:
- Fixing those positions in all generated routes
 - Only mutating/crossing over the unlocked waypoints
 
3. Smart Gate Access
Some routes require avoiding restricted Smart Gates. We integrate with EF-Map's gate access snapshot to filter valid paths during cost calculation.
Performance Optimizations
Running genetic algorithms in the browser requires careful optimization:
Web Workers
We offload the entire optimization to a Web Worker to keep the UI responsive:
// Main thread
const worker = new Worker('/workers/scout_optimizer_worker.js');
worker.postMessage({ waypoints, settings });
worker.onmessage = (e) => {
  displayOptimizedRoute(e.data.route);
};
Caching
We cache spatial grids and neighbor lookups based on parameters:
const spatialGrids = new Map();
const getOrCreateGrid = (systems, cellSize) => {
  const key = `${systems.length}-${cellSize}`;
  if (!spatialGrids.has(key)) {
    spatialGrids.set(key, buildSpatialGrid(systems, cellSize));
  }
  return spatialGrids.get(key);
};
Incremental Updates
If a user adds a single waypoint to an existing route, we don't recompute from scratch:
const incrementalOptimize = (existingRoute, newWaypoint) => {
  // Try inserting newWaypoint at each position
  let bestRoute = null;
  let bestCost = Infinity;
  
  for (let i = 0; i <= existingRoute.length; i++) {
    const candidate = [
      ...existingRoute.slice(0, i),
      newWaypoint,
      ...existingRoute.slice(i)
    ];
    
    const cost = calculateCost(candidate);
    if (cost < bestCost) {
      bestCost = cost;
      bestRoute = candidate;
    }
  }
  
  return bestRoute;
};
Results and User Feedback
Since launching the Scout Optimizer:
- Average route improvement: 22% shorter than user-manual ordering
 - Optimization time: 950ms median for 12 waypoints
 - User satisfaction: 4.7/5 rating (from in-app feedback)
 
Most impressive result: A user planning a 28-waypoint expedition saw a 38% distance reduction compared to their manual route. That's multiple hours saved in-game.
Edge Cases and Failures
No algorithm is perfect. We've encountered several interesting edge cases:
Cluster Imbalance
If waypoints form two distant clusters, the algorithm sometimes "zig-zags" between them inefficiently. We added cluster detection to handle this:
const detectClusters = (waypoints) => {
  // Use k-means clustering (k=2) to identify groups
  const clusters = kMeans(waypoints, 2);
  
  // If clusters are well-separated, optimize each independently
  if (clusterSeparation(clusters) > THRESHOLD) {
    const route1 = optimizeRoute(clusters[0]);
    const route2 = optimizeRoute(clusters[1]);
    return [...route1, ...route2]; // Or connect with shortest bridge
  }
  
  return null; // Fall back to standard optimization
};
Premature Convergence
Early versions would occasionally converge too quickly, missing better solutions. We fixed this by:
- Increasing mutation rate from 10% to 15%
 - Adding "fitness diversity" bonus (reward routes that differ from the population average)
 
Pathfinding Timeout
For extremely dense route graphs, A* pathfinding between some waypoints could timeout. We now:
- Use bidirectional search for distant waypoints
 - Cache previously computed paths
 - Fall back to straight-line distance if pathfinding exceeds 500ms
 
Try the Scout Optimizer
Plan your next EVE Frontier expedition with intelligent route optimization. Open EF-Map's Scout Optimizer →
Future Improvements
We're exploring several enhancements:
- Multi-objective optimization: Balance fuel, time, and danger zones simultaneously
 - Dynamic waypoints: Suggest additional systems to visit based on resources or POIs
 - Fleet coordination: Optimize routes for multiple scouts splitting duties
 - Learning from history: Adapt to user preferences over time
 
Related Posts
- Three.js Rendering: Building a 3D Starfield for 200,000 Systems - The rendering engine that visualizes optimized routes in 3D space
 - Route Sharing: Building a URL Shortener for Spatial Navigation - How we compress and share optimized routes with corpmates
 - Database Architecture: From Blockchain Events to Queryable Intelligence - The spatial indexing that powers fast neighbor lookups in the genetic algorithm
 
---
EF-Map is an open-source interactive map for EVE Frontier. Optimize your scout routes at ef-map.com or explore the source code on GitHub.