Course Plotting¶
Purpose¶
Course plotting answers a single question for a player: "Lay in a course to that sector — how do I get there and what does it cost?" It computes a turn-cost-minimising route from the player's current sector to a target sector, walking only the known sector graph — the sectors the player is entitled to navigate through. It is the server side of ADR-0072 Pillar 1 ("lay in a course") and the input to the client-orchestrated autopilot.
This is the plot half of the navigator. Plotting computes the hop list; execution is the client replaying that hop list as ordinary moves through MovementService, one hop at a time. Nothing about movement semantics changes — turns charge per hop, encounter checks fire per hop, ARIA records each visit. The plot endpoint never moves the player; it only describes the route.
Scope of the known graph¶
A plot may only traverse sectors the player is allowed to navigate. The plottable set, the known graph, is the union of:
- Visited sectors — every sector in the player's own
ARIAExplorationMap(an exploration-map row keyed(player_id, sector_id)is created on each visit). - Corp-shared topology — when the player has a
team_id, the visited sectors of every other player on the same team. A corp-mate's exploration is shared topology, consistent with ADR-0072's corp-shared scan-knowledge layer. - The current sector — always included regardless of exploration state, because the player is standing in it.
The graph is therefore "visited ∪ corp-shared visits ∪ current sector". The Federation public-chart layer (pre-discovered Terran / Central Nexus topology) and the per-player chart economy (player_known_sectors) are later ADR-0072 pillars; they extend the known set with additional source layers when they land. Until then, a player in pre-discovered space learns connectivity by flying it, which the visited layer captures normally.
The objective is min-time: the route that minimises total turn cost. Turn cost is physics, not intelligence — every plottable hop is priced identically whether the player has flown it or only knows of it. The request carries an objective field for forward compatibility with the ADR-0072 consciousness tiers (Awakened MIN_RISK, Transcendent re-plot), but it is not differentiated today: every plot is Dijkstra by turn cost.
Inputs¶
What triggers this:
POST /api/v1/nav/plotwith body{target_sector_id, objective}— the cockpit NAV CHART destination request and the ARIA "plot a course to N" command both resolve here.
Authentication and request shape:
- The route depends on
get_current_player(the authenticated player) and a synchronousget_dbsession, following thetrading.pyroute pattern. target_sector_idis a positive integer (the global human-readable sector number,Sector.sector_id, not the UUID).objectivedefaults to"min_time".
State read:
Player.id,Player.team_id,Player.current_sector_id.ARIAExplorationMaprows for the player and (when teamed) for team-mates — for known-set membership and per-hopvisited/safety_ratingannotations.Sectorrows —sector_id,name,id(UUID), andx_coord/y_coord/z_coord(for the nearest-known fallback).sector_warpsassociation rows —source_sector_id,destination_sector_id,is_bidirectional,turn_cost(edges of the warp graph).WarpTunnelrows with statusACTIVE—origin_sector_id,destination_sector_id,is_bidirectional,turn_cost(tunnel edges).
Process¶
1. Resolve the known set¶
get_known_sector_ids(player) returns the set of numeric sector_ids the player may plot through, assembled from the three sources above (own exploration map, team-mate exploration maps when team_id is set, and the current sector). Exploration-map rows store the sector UUID, so each source joins ARIAExplorationMap.sector_id to Sector.id to recover the numeric sector_id.
2. Validate the target¶
- Look up the target by numeric
sector_id. If no sector with that number exists, return the unknown-target shape:reachable=Falsewith the nearest-known approach anderror="unknown sector". (For a non-existent sector there are no coordinates, so the nearest-known proxy is the known sector whose number is numerically closest to the requested number.) - If the target equals the player's current sector, return a trivial reachable route: empty
hops,total_turns=0. - If the target exists but is not in the known set, return the unreachable shape:
reachable=Falsewith the nearest-known approach computed by 3D Euclidean distance overx_coord/y_coord/z_coordto the target.
3. Build the known graph¶
_build_known_graph(known_ids) builds an adjacency list restricted to known sectors. The graph maps sector_id -> list of (neighbour_sector_id, turn_cost, via_tunnel). Both endpoints of every edge must be in the known set — an edge crossing into unknown space is excluded, so the pathfinder can never route through a sector the player is not entitled to.
Edge sources:
sector_warpsrows. For every warp whose source is a known sector, add the forward edge (when the destination is also known), withturn_costfrom the row (treated as 1 when null/zero). When the row is bidirectional and not a self-loop, add the reverse edge as well. Incoming bidirectional rows whose source lies outside the known set are intentionally not walked — both endpoints must be known.WarpTunnelrows with statusACTIVE. For every active tunnel originating in a known sector, add the forward edge (when the destination is also known), withturn_costfrom the tunnel (1 when null/zero) andvia_tunnel=True. When the tunnel is bidirectional, add the reverse edge. Non-active tunnels are excluded.
4. Dijkstra (min-time)¶
_dijkstra(graph, src, dst) runs a standard cost-ordered Dijkstra over the known graph, using a turn-cost-weighted priority queue. It returns the path (a list of sector_ids including the origin), the per-hop edge costs, and a per-hop via_tunnel flag. The per-hop cost is the cost of the edge arriving at that hop.
If the target is in the known set but Dijkstra finds no path (a disconnected component within known space), return the unreachable shape with the Euclidean nearest-known approach.
5. Runaway guard¶
A computed route longer than MAX_HOPS (200) is refused with {success: False, message: ...} advising the player to break the journey into legs. This bounds pathfinding over a very large or pathological known graph.
6. Assemble the response¶
The origin sector is dropped from the path; hops lists only the sectors travelled to. For each hop:
sector_id,name— from theSectorrow.turn_cost— the cost of the edge arriving at this hop.visited— true when the sector is in the player's own exploration map (corp-shared membership makes a sector plottable but does not mark it visited).safety_rating— the player's own visit-derivedARIAExplorationMap.safety_ratingfor visited hops;nullfor hops the player has not personally flown (charted/corp-shared-only hops never carry safety intelligence — visit-gated intelligence stays visit-gated).via_tunnel— true when the hop was reached over aWarpTunneledge rather than asector_warpsedge.
total_turns is the sum of per-hop turn_cost.
Response contract¶
The endpoint returns one of four shapes; game-logic outcomes (unreachable, unknown) are carried in the body, not signalled as HTTP errors.
Reachable:
{
"success": true,
"reachable": true,
"target_sector_id": 1001,
"hops": [
{
"sector_id": 412,
"name": "Vega Reach",
"turn_cost": 1,
"visited": true,
"safety_rating": 0.82,
"via_tunnel": false
}
],
"total_turns": 38
}
(An already-at-target plot is reachable with hops: [] and total_turns: 0.)
Unreachable (target exists but is outside the known graph, or in a disconnected known component):
{
"success": true,
"reachable": false,
"target_sector_id": 1001,
"nearest_known": { "sector_id": 889, "name": "Pollux Gate" }
}
Unknown target (no sector with that number exists):
{
"success": true,
"reachable": false,
"target_sector_id": 99999,
"nearest_known": { "sector_id": 100, "name": "Sol Hub" },
"error": "unknown sector"
}
Runaway guard (computed route exceeds the 200-hop limit):
{
"success": false,
"message": "Computed route is 214 hops, which exceeds the 200-hop safety limit. Break the journey into legs."
}
nearest_known is null only when the known set is empty.
Relationship to the canonical route engine¶
RouteOptimizer (route_optimizer.py) is the canonical pathfinding engine per sector-presence.md: it owns the warp/tunnel graph and the objective-weighted routing semantics (MAX_PROFIT / MIN_TIME / MIN_RISK / BALANCED). Course plotting follows those semantics — the min-time Dijkstra here is structurally the same shortest-path walk as RouteOptimizer._dijkstra_path weighted by turn cost. NavService is the synchronous, known-sector-filtered bridge the plot route uses: RouteOptimizer runs on AsyncSession and cannot be called from the synchronous plot route, and it walks the whole galaxy graph rather than a per-player known subset. NavService adds the two things the plot route needs that the async engine does not provide directly — synchronous execution and the known-graph filter — while delegating the routing objective to the canonical engine's semantics. New routing callers integrate through RouteOptimizer or, where a synchronous known-sector-filtered plot is required, through NavService; they do not introduce additional graph-walk or shortest-path implementations.
This plot is distinct from the ARIA trade-route discovery stub _find_profitable_paths (aria_personal_intelligence_service.py). That method is a placeholder that returns an empty list and performs no graph traversal; it belongs to ARIA's profit-cascade planning over its private market-intelligence graph, not to course plotting over the warp topology. Course plotting is a complete, contract-frozen route computation over the warp graph; trade-route discovery is a separate, unimplemented concern.
Outputs / state changes¶
Course plotting is read-only. The plot endpoint computes and returns a route; it reads the player, exploration maps, sectors, warps, and tunnels but mutates nothing and emits no events. All state change happens later, in the autopilot's per-hop moveToSector calls through MovementService (see sector-presence.md).
Invariants¶
- Every sector on a returned path is in the player's known set; the pathfinder never routes through unknown space.
- Every edge traversed has both endpoints in the known set.
hopsexcludes the origin sector; a reachable plot'stotal_turnsequals the sum of per-hopturn_cost.via_tunnelis true for a hop iff that hop was reached over anACTIVEWarpTunneledge.safety_ratingis non-null only for hops the player has personally visited; corp-shared and charted-only hops never carry safety intelligence.visitedreflects the player's own exploration map only, never team-mate membership.- Turn cost for a hop is identical whether the hop is visited or merely known — plotting prices physics, not intelligence.
- A reachable plot's hop count never exceeds
MAX_HOPS(200); over-long routes return the runaway-guard failure instead. - Unreachable and unknown-target outcomes are returned as
success: truebodies carryingreachable: false, never as HTTP error responses. - Only one of the four response shapes is returned per request.
Failure modes¶
| Mode | Handling |
|---|---|
| Target sector number does not exist | Unknown-target shape: reachable=false, error="unknown sector", nearest-known by numeric proximity. |
| Target exists but outside the known set | Unreachable shape: reachable=false, nearest-known by 3D Euclidean distance. |
| Target known but in a disconnected known component | Unreachable shape with Euclidean nearest-known approach. |
| Player already at target | Trivial reachable route: empty hops, total_turns=0. |
| Known set empty | nearest_known is null in the unreachable/unknown shape. |
| Computed route exceeds 200 hops | Runaway guard: success=false with a "break the journey into legs" message. |
| Sector disappears between graph build and hop assembly | Defensive: the missing hop is skipped and a warning logged; assembly continues. |
| Null/zero edge turn cost on a warp or tunnel | Treated as cost 1. |
| Non-active warp tunnel | Excluded from the graph; not traversable. |
Source map¶
| Concern | Path |
|---|---|
| Plot endpoint | services/gameserver/src/api/routes/nav.py (POST /nav/plot, PlotRequest) |
| Course-plotting service | services/gameserver/src/services/nav_service.py (NavService) |
| Known-set assembly | NavService.get_known_sector_ids |
| Plot orchestration | NavService.plot |
| Known-graph construction | NavService._build_known_graph |
| Min-time pathfinder | NavService._dijkstra |
| Nearest-known fallbacks | NavService._nearest_known_sector, NavService._nearest_known_sector_euclidean |
| Canonical route engine | services/gameserver/src/services/route_optimizer.py (RouteOptimizer) |
| Exploration map model | services/gameserver/src/models/aria_personal_intelligence.py (ARIAExplorationMap) |
| Sector / warp model | services/gameserver/src/models/sector.py (Sector, sector_warps) |
| Warp tunnel model | services/gameserver/src/models/warp_tunnel.py (WarpTunnel, WarpTunnelStatus) |
Related¶
sector-presence.md— the canonical route engine and the per-hop move semantics the autopilot replays.../ADR/0072-aria-course-plotting-and-charts.md— course plotting, autopilot, and the chart knowledge economy (this is Pillar 1).turn-regeneration.md— the turn pool a plotted course spends.aria-dialogue.md— the ARIA strip that resolves "plot a course to N" to this endpoint.