@shipload/sdk 1.0.0-next.36 → 1.0.0-next.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipload/sdk",
3
3
  "description": "SDKs for Shipload",
4
- "version": "1.0.0-next.36",
4
+ "version": "1.0.0-next.37",
5
5
  "homepage": "https://github.com/shipload/toolkit/tree/master/packages/sdk",
6
6
  "repository": {
7
7
  "type": "git",
@@ -191,6 +191,21 @@ export type {
191
191
  HasScheduleAndLocation,
192
192
  } from './travel/travel'
193
193
 
194
+ export {planRoute, sdkSystemGraph} from './travel/route-planner'
195
+ export type {
196
+ Coord,
197
+ Neighbor,
198
+ SystemGraph,
199
+ RoutePlan,
200
+ RouteFailure,
201
+ RouteResult,
202
+ RouteFailureReason,
203
+ PlanRouteParams,
204
+ } from './travel/route-planner'
205
+
206
+ export {computePerLegReach, computeGroupPerLegReach} from './travel/reach'
207
+ export type {ReachStats} from './travel/reach'
208
+
194
209
  export * as schedule from './scheduling/schedule'
195
210
  export {LANE_MOBILITY, LANE_BARRIER} from './scheduling/schedule'
196
211
  export type {
@@ -0,0 +1,23 @@
1
+ export interface ReachStats {
2
+ generator?: {capacity: bigint}
3
+ engines?: {drain: bigint}
4
+ hauler?: {drain: bigint}
5
+ }
6
+
7
+ export function computePerLegReach(s: ReachStats, haulCount = 0): number {
8
+ const capacity = s.generator?.capacity
9
+ const drain = s.engines?.drain
10
+ if (capacity === undefined || drain === undefined || drain === 0n) {
11
+ throw new Error('entity has no usable engine/generator (cannot compute per-leg reach)')
12
+ }
13
+ const haulDrain = s.hauler && haulCount > 0 ? s.hauler.drain * BigInt(haulCount) : 0n
14
+ return Number(capacity) / Number(drain + haulDrain)
15
+ }
16
+
17
+ export function computeGroupPerLegReach(participants: ReachStats[], haulCount: number): number {
18
+ const movers = participants.filter((p) => p.engines !== undefined && p.engines.drain !== 0n)
19
+ if (movers.length === 0) {
20
+ throw new Error('group has no moving entity (cannot compute per-leg reach)')
21
+ }
22
+ return Math.min(...movers.map((p) => computePerLegReach(p, haulCount)))
23
+ }
@@ -0,0 +1,157 @@
1
+ import {distanceBetweenPoints, findNearbyPlanets} from './travel'
2
+ import {hasSystem} from '../utils/system'
3
+ import {PRECISION} from '../types'
4
+ import {Checksum256, type Checksum256Type} from '@wharfkit/antelope'
5
+
6
+ export interface Coord {
7
+ x: number
8
+ y: number
9
+ }
10
+
11
+ export interface Neighbor {
12
+ coord: Coord
13
+ dist: number
14
+ }
15
+
16
+ export interface SystemGraph {
17
+ hasSystem(c: Coord): boolean
18
+ nearby(c: Coord, reachTiles: number): Neighbor[]
19
+ }
20
+
21
+ export interface RoutePlan {
22
+ ok: true
23
+ waypoints: Coord[]
24
+ legs: number
25
+ totalDistance: number
26
+ }
27
+
28
+ export type RouteFailureReason = 'empty-destination' | 'no-path' | 'max-legs'
29
+
30
+ export interface RouteFailure {
31
+ ok: false
32
+ reason: RouteFailureReason
33
+ furthest?: Coord
34
+ legsNeeded?: number
35
+ }
36
+
37
+ export type RouteResult = RoutePlan | RouteFailure
38
+
39
+ export interface PlanRouteParams {
40
+ origin: Coord
41
+ dest: Coord
42
+ perLegReach: number
43
+ graph: SystemGraph
44
+ corridorSlack?: number
45
+ nodeBudget?: number
46
+ maxLegs?: number
47
+ }
48
+
49
+ const key = (c: Coord): string => `${c.x},${c.y}`
50
+ const sameCoord = (a: Coord, b: Coord): boolean => a.x === b.x && a.y === b.y
51
+ const dist = (a: Coord, b: Coord): number => Math.hypot(a.x - b.x, a.y - b.y)
52
+
53
+ export function planRoute(params: PlanRouteParams): RouteResult {
54
+ const {origin, dest, perLegReach, graph} = params
55
+ const corridorSlack = params.corridorSlack ?? perLegReach
56
+ const nodeBudget = params.nodeBudget ?? 5000
57
+ const maxLegs = params.maxLegs ?? 12
58
+
59
+ if (!graph.hasSystem(dest)) {
60
+ return {ok: false, reason: 'empty-destination'}
61
+ }
62
+
63
+ const straightLine = dist(origin, dest)
64
+ const heuristic = (c: Coord): number => Math.ceil(dist(c, dest) / perLegReach)
65
+
66
+ const gScore = new Map<string, number>([[key(origin), 0]])
67
+ const cameFrom = new Map<string, Coord>()
68
+ const frontier: {coord: Coord; g: number; f: number; remaining: number}[] = [
69
+ {coord: origin, g: 0, f: heuristic(origin), remaining: straightLine},
70
+ ]
71
+
72
+ let furthest = origin
73
+ let furthestRemaining = dist(origin, dest)
74
+ let expansions = 0
75
+ let cappedByMaxLegs = false
76
+
77
+ while (frontier.length > 0) {
78
+ let bestIdx = 0
79
+ for (let i = 1; i < frontier.length; i++) {
80
+ const a = frontier[i]
81
+ const b = frontier[bestIdx]
82
+ if (a.f < b.f || (a.f === b.f && a.remaining < b.remaining)) {
83
+ bestIdx = i
84
+ }
85
+ }
86
+ const current = frontier.splice(bestIdx, 1)[0]
87
+
88
+ if (sameCoord(current.coord, dest)) {
89
+ return reconstruct(cameFrom, origin, dest)
90
+ }
91
+
92
+ if (current.remaining < furthestRemaining) {
93
+ furthestRemaining = current.remaining
94
+ furthest = current.coord
95
+ }
96
+
97
+ if (++expansions > nodeBudget) break
98
+
99
+ for (const n of graph.nearby(current.coord, perLegReach)) {
100
+ const inCorridor =
101
+ dist(origin, n.coord) + dist(n.coord, dest) <= straightLine + corridorSlack
102
+ if (!inCorridor) continue
103
+
104
+ const tentativeG = current.g + 1
105
+ if (tentativeG > maxLegs) {
106
+ cappedByMaxLegs = true
107
+ continue
108
+ }
109
+ const nk = key(n.coord)
110
+ if (tentativeG < (gScore.get(nk) ?? Infinity)) {
111
+ gScore.set(nk, tentativeG)
112
+ cameFrom.set(nk, current.coord)
113
+ const remaining = dist(n.coord, dest)
114
+ frontier.push({
115
+ coord: n.coord,
116
+ g: tentativeG,
117
+ f: tentativeG + Math.ceil(remaining / perLegReach),
118
+ remaining,
119
+ })
120
+ }
121
+ }
122
+ }
123
+
124
+ if (cappedByMaxLegs) {
125
+ return {ok: false, reason: 'max-legs', furthest}
126
+ }
127
+ return {ok: false, reason: 'no-path', furthest}
128
+ }
129
+
130
+ function reconstruct(cameFrom: Map<string, Coord>, origin: Coord, dest: Coord): RoutePlan {
131
+ const path: Coord[] = [dest]
132
+ let cur = dest
133
+ let totalDistance = 0
134
+ while (!sameCoord(cur, origin)) {
135
+ const prev = cameFrom.get(key(cur))
136
+ if (!prev) break
137
+ totalDistance += dist(prev, cur)
138
+ path.unshift(prev)
139
+ cur = prev
140
+ }
141
+ const waypoints = path.slice(1)
142
+ return {ok: true, waypoints, legs: waypoints.length, totalDistance}
143
+ }
144
+
145
+ export function sdkSystemGraph(seed: Checksum256Type): SystemGraph {
146
+ const s = Checksum256.from(seed)
147
+ return {
148
+ hasSystem: (c) => hasSystem(s, {x: c.x, y: c.y}),
149
+ nearby: (c, reachTiles) =>
150
+ findNearbyPlanets(s, {x: c.x, y: c.y}, reachTiles * PRECISION)
151
+ .map((d) => ({
152
+ coord: {x: Number(d.destination.x), y: Number(d.destination.y)},
153
+ dist: Number(d.distance) / PRECISION,
154
+ }))
155
+ .filter((n) => !(n.coord.x === c.x && n.coord.y === c.y)),
156
+ }
157
+ }