@shipload/sdk 1.0.0-next.7 → 1.0.0-next.70

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.
Files changed (203) hide show
  1. package/lib/scan.d.ts +52 -0
  2. package/lib/scan.js +162 -0
  3. package/lib/scan.js.map +1 -0
  4. package/lib/scan.m.js +152 -0
  5. package/lib/scan.m.js.map +1 -0
  6. package/lib/shipload.d.ts +5696 -1503
  7. package/lib/shipload.js +20294 -5200
  8. package/lib/shipload.js.map +1 -1
  9. package/lib/shipload.m.js +20371 -5683
  10. package/lib/shipload.m.js.map +1 -1
  11. package/lib/testing.d.ts +1648 -0
  12. package/lib/testing.js +6980 -0
  13. package/lib/testing.js.map +1 -0
  14. package/lib/testing.m.js +6974 -0
  15. package/lib/testing.m.js.map +1 -0
  16. package/package.json +20 -2
  17. package/src/capabilities/craftable.test.ts +82 -0
  18. package/src/capabilities/craftable.ts +82 -0
  19. package/src/capabilities/crafting.test.ts +80 -0
  20. package/src/capabilities/crafting.ts +50 -8
  21. package/src/capabilities/gathering.test.ts +29 -0
  22. package/src/capabilities/gathering.ts +38 -20
  23. package/src/capabilities/hauling.ts +0 -5
  24. package/src/capabilities/index.ts +0 -1
  25. package/src/capabilities/modules.ts +27 -30
  26. package/src/capabilities/movement.ts +1 -1
  27. package/src/capabilities/storage.test.ts +26 -0
  28. package/src/capabilities/storage.ts +22 -7
  29. package/src/contracts/fund.ts +306 -0
  30. package/src/contracts/platform.ts +312 -3
  31. package/src/contracts/server.ts +2661 -466
  32. package/src/contracts/token.ts +170 -0
  33. package/src/contracts.ts +2 -0
  34. package/src/coordinates/address.ts +88 -0
  35. package/src/coordinates/constants.test.ts +15 -0
  36. package/src/coordinates/constants.ts +23 -0
  37. package/src/coordinates/index.ts +15 -0
  38. package/src/coordinates/memo.test.ts +47 -0
  39. package/src/coordinates/memo.ts +20 -0
  40. package/src/coordinates/permutation.ts +77 -0
  41. package/src/coordinates/regions.ts +48 -0
  42. package/src/coordinates/sectors.ts +115 -0
  43. package/src/data/capabilities.ts +49 -18
  44. package/src/data/capability-formulas.ts +26 -14
  45. package/src/data/catalog.test.ts +19 -0
  46. package/src/data/catalog.ts +13 -3
  47. package/src/data/charters.json +396 -0
  48. package/src/data/citizenry-adjectives.json +42 -0
  49. package/src/data/citizenry-nouns.json +50 -0
  50. package/src/data/colors.ts +14 -32
  51. package/src/data/entities.json +421 -15
  52. package/src/data/item-ids.ts +53 -13
  53. package/src/data/items.json +330 -77
  54. package/src/data/kind-registry.json +212 -0
  55. package/src/data/kind-registry.ts +192 -0
  56. package/src/data/metadata.ts +312 -51
  57. package/src/data/recipes-runtime.ts +6 -23
  58. package/src/data/recipes.json +2173 -173
  59. package/src/data/tiers.ts +9 -6
  60. package/src/derivation/build-methods.test.ts +71 -0
  61. package/src/derivation/build-methods.ts +50 -0
  62. package/src/derivation/capabilities.test.ts +348 -0
  63. package/src/derivation/capabilities.ts +707 -0
  64. package/src/derivation/capability-mappings.ts +60 -16
  65. package/src/derivation/crafting.test.ts +17 -0
  66. package/src/derivation/crafting.ts +57 -34
  67. package/src/derivation/index.ts +27 -2
  68. package/src/derivation/recipe-usage.test.ts +88 -0
  69. package/src/derivation/recipe-usage.ts +141 -0
  70. package/src/derivation/reserve-regen.ts +34 -0
  71. package/src/derivation/resources.ts +9 -1
  72. package/src/derivation/rollups.test.ts +55 -0
  73. package/src/derivation/rollups.ts +72 -0
  74. package/src/derivation/stars.test.ts +67 -0
  75. package/src/derivation/stars.ts +25 -0
  76. package/src/derivation/stat-scaling.ts +12 -0
  77. package/src/derivation/stats.ts +6 -6
  78. package/src/derivation/strata.ts +5 -3
  79. package/src/derivation/stratum.ts +26 -22
  80. package/src/derivation/tiers.ts +40 -7
  81. package/src/derivation/upgrades.ts +14 -0
  82. package/src/derivation/wormhole.ts +154 -0
  83. package/src/entities/entity.ts +102 -0
  84. package/src/entities/gamestate.ts +3 -28
  85. package/src/entities/makers.ts +144 -136
  86. package/src/entities/slot-multiplier.ts +43 -0
  87. package/src/entity/hydrate.test.ts +156 -0
  88. package/src/entity/hydrate.ts +86 -0
  89. package/src/errors.ts +21 -16
  90. package/src/format.ts +42 -9
  91. package/src/index-module.ts +411 -32
  92. package/src/influence/ballot.test.ts +103 -0
  93. package/src/influence/ballot.ts +98 -0
  94. package/src/influence/categories.ts +32 -0
  95. package/src/influence/charter-metadata.test.ts +136 -0
  96. package/src/influence/charter-metadata.ts +236 -0
  97. package/src/influence/charters.test.ts +112 -0
  98. package/src/influence/charters.ts +150 -0
  99. package/src/influence/citizenry.ts +38 -0
  100. package/src/influence/civic-shuttle.test.ts +202 -0
  101. package/src/influence/civic-shuttle.ts +142 -0
  102. package/src/influence/civic.test.ts +287 -0
  103. package/src/influence/civic.ts +269 -0
  104. package/src/influence/constants.ts +84 -0
  105. package/src/influence/decay.ts +20 -0
  106. package/src/influence/decomp.ts +95 -0
  107. package/src/influence/demand.ts +106 -0
  108. package/src/influence/duration.ts +101 -0
  109. package/src/influence/index.ts +118 -0
  110. package/src/influence/influence.test.ts +450 -0
  111. package/src/influence/quality.ts +25 -0
  112. package/src/influence/valuation.ts +154 -0
  113. package/src/managers/actions.ts +940 -93
  114. package/src/managers/balances.test.ts +161 -0
  115. package/src/managers/balances.ts +93 -0
  116. package/src/managers/base.ts +10 -2
  117. package/src/managers/cluster.test.ts +39 -0
  118. package/src/managers/cluster.ts +53 -0
  119. package/src/managers/construction-types.ts +80 -0
  120. package/src/managers/construction.ts +453 -0
  121. package/src/managers/context.ts +58 -1
  122. package/src/managers/coordinates.ts +14 -0
  123. package/src/managers/entities.ts +35 -63
  124. package/src/managers/epochs.ts +72 -1
  125. package/src/managers/index.ts +38 -1
  126. package/src/managers/influence.test.ts +56 -0
  127. package/src/managers/influence.ts +425 -0
  128. package/src/managers/jobs.test.ts +80 -0
  129. package/src/managers/jobs.ts +54 -0
  130. package/src/managers/locations.ts +25 -29
  131. package/src/managers/nft.test.ts +21 -0
  132. package/src/managers/nft.ts +110 -0
  133. package/src/managers/players.ts +25 -0
  134. package/src/managers/plot.ts +122 -0
  135. package/src/nft/atomicassets.abi.json +1342 -0
  136. package/src/nft/atomicassets.ts +237 -0
  137. package/src/nft/atomicdata.ts +130 -0
  138. package/src/nft/buildImmutableData.ts +388 -0
  139. package/src/nft/description.ts +222 -100
  140. package/src/nft/index.ts +3 -0
  141. package/src/planner/index.ts +296 -0
  142. package/src/planner/planner.test.ts +459 -0
  143. package/src/receipts.ts +22 -0
  144. package/src/resolution/describe-module.ts +43 -25
  145. package/src/resolution/display-name.ts +39 -11
  146. package/src/resolution/resolve-item.test.ts +48 -0
  147. package/src/resolution/resolve-item.ts +112 -58
  148. package/src/scan/index.ts +244 -0
  149. package/src/scan/scan-wasm.base64.ts +2 -0
  150. package/src/scheduling/accessor.ts +65 -23
  151. package/src/scheduling/availability.test.ts +320 -0
  152. package/src/scheduling/availability.ts +251 -0
  153. package/src/scheduling/cancel.test.ts +550 -0
  154. package/src/scheduling/cancel.ts +254 -0
  155. package/src/scheduling/cluster-stock.test.ts +31 -0
  156. package/src/scheduling/cluster-stock.ts +15 -0
  157. package/src/scheduling/energy.ts +47 -0
  158. package/src/scheduling/epoch.ts +1 -0
  159. package/src/scheduling/idle-resolve.ts +45 -0
  160. package/src/scheduling/jobs.test.ts +49 -0
  161. package/src/scheduling/jobs.ts +137 -0
  162. package/src/scheduling/lane-core.ts +128 -0
  163. package/src/scheduling/lanes.test.ts +250 -0
  164. package/src/scheduling/lanes.ts +227 -0
  165. package/src/scheduling/projection.ts +265 -149
  166. package/src/scheduling/schedule.test.ts +163 -0
  167. package/src/scheduling/schedule.ts +282 -116
  168. package/src/scheduling/task-cargo.test.ts +110 -0
  169. package/src/scheduling/task-cargo.ts +62 -0
  170. package/src/scheduling/task-effects.test.ts +35 -0
  171. package/src/scheduling/task-effects.ts +34 -0
  172. package/src/scheduling/unwrap.test.ts +86 -0
  173. package/src/scheduling/unwrap.ts +190 -0
  174. package/src/shipload.ts +63 -4
  175. package/src/subscriptions/manager.cluster.test.ts +51 -0
  176. package/src/subscriptions/manager.ts +245 -172
  177. package/src/subscriptions/mappers.ts +5 -8
  178. package/src/subscriptions/types.ts +28 -3
  179. package/src/testing/catalog-hash.ts +19 -0
  180. package/src/testing/index.ts +2 -0
  181. package/src/testing/projection-parity.ts +167 -0
  182. package/src/travel/reach.test.ts +86 -0
  183. package/src/travel/reach.ts +34 -0
  184. package/src/travel/route-build.test.ts +63 -0
  185. package/src/travel/route-build.ts +60 -0
  186. package/src/travel/route-planner.ts +288 -0
  187. package/src/travel/route-simulator.ts +170 -0
  188. package/src/travel/travel.ts +190 -123
  189. package/src/types/capabilities.ts +37 -8
  190. package/src/types/entity.ts +3 -3
  191. package/src/types/index.ts +0 -1
  192. package/src/types.ts +81 -23
  193. package/src/utils/cargo.test.ts +14 -0
  194. package/src/utils/cargo.ts +2 -0
  195. package/src/utils/display-name.ts +70 -0
  196. package/src/utils/hash.ts +22 -2
  197. package/src/utils/system.ts +24 -5
  198. package/src/capabilities/loading.ts +0 -8
  199. package/src/entities/container.ts +0 -123
  200. package/src/entities/ship-deploy.ts +0 -295
  201. package/src/entities/ship.ts +0 -221
  202. package/src/entities/warehouse.ts +0 -127
  203. package/src/types/entity-traits.ts +0 -69
@@ -0,0 +1,167 @@
1
+ import type {UInt32} from '@wharfkit/antelope'
2
+ import type {ServerContract} from '../contracts'
3
+ import type {ProjectedEntity} from '../scheduling/projection'
4
+ import {type CargoStack, cargoItemToStack, mergeStacks} from '../capabilities/storage'
5
+
6
+ export interface ContractProjectedState {
7
+ owner: {toString(): string}
8
+ coordinates: ServerContract.Types.coordinates
9
+ energy?: UInt32
10
+ cargomass: UInt32
11
+ cargo: ServerContract.Types.cargo_view[]
12
+ hullmass?: UInt32
13
+ capacity?: UInt32
14
+ engines?: ServerContract.Types.movement_stats
15
+ generator?: ServerContract.Types.energy_stats
16
+ hauler?: ServerContract.Types.hauler_stats
17
+ gatherer_lanes: ServerContract.Types.gatherer_lane[]
18
+ crafter_lanes: ServerContract.Types.crafter_lane[]
19
+ loader_lanes: ServerContract.Types.loader_lane[]
20
+ }
21
+
22
+ export interface ProjectionComparisonOptions {
23
+ step?: number
24
+ }
25
+
26
+ export function assertProjectionEquals(
27
+ contract: ContractProjectedState,
28
+ sdk: ProjectedEntity,
29
+ options: ProjectionComparisonOptions = {}
30
+ ): void {
31
+ const mismatches: string[] = []
32
+
33
+ const record = (name: string, c: unknown, s: unknown) => {
34
+ if (c !== s) mismatches.push(` ${name}: contract=${fmt(c)} sdk=${fmt(s)}`)
35
+ }
36
+
37
+ const recordStatBlock = (name: string, c: unknown, s: unknown) => {
38
+ const cPresent = c !== undefined && c !== null
39
+ const sPresent = s !== undefined && s !== null
40
+ if (cPresent !== sPresent) {
41
+ mismatches.push(
42
+ ` ${name}: contract=${cPresent ? 'present' : 'absent'} sdk=${sPresent ? 'present' : 'absent'}`
43
+ )
44
+ return
45
+ }
46
+ if (!cPresent) return
47
+ const cn = JSON.stringify(normaliseStatBlock(c))
48
+ const sn = JSON.stringify(normaliseStatBlock(s))
49
+ if (cn !== sn) mismatches.push(` ${name}: contract=${cn} sdk=${sn}`)
50
+ }
51
+
52
+ record('coordinates.x', toNum(contract.coordinates.x), Number(sdk.location.x))
53
+ record('coordinates.y', toNum(contract.coordinates.y), Number(sdk.location.y))
54
+ record('energy', toNum(contract.energy), Number(sdk.energy))
55
+ record('cargomass', toNum(contract.cargomass), Number(sdk.cargoMass))
56
+ record('hullmass', toNum(contract.hullmass), Number(sdk.shipMass))
57
+ record('capacity', toNum(contract.capacity), sdk.capacity ? Number(sdk.capacity) : undefined)
58
+
59
+ recordStatBlock('engines', contract.engines, sdk.engines)
60
+ recordStatBlock('generator', contract.generator, sdk.generator)
61
+ recordStatBlock('hauler', contract.hauler, sdk.hauler)
62
+
63
+ const normLane = (l: {slot_index?: unknown; [k: string]: unknown}) =>
64
+ Object.fromEntries(
65
+ Object.entries(l)
66
+ .filter(([k]) => k !== 'slot_index')
67
+ .map(([k, v]) => [k, toNum(v) ?? 0])
68
+ )
69
+
70
+ const compareLanes = (name: string, cLanes: unknown[], sLanes: unknown[]) => {
71
+ if (cLanes.length !== sLanes.length) {
72
+ mismatches.push(` ${name}.length: contract=${cLanes.length} sdk=${sLanes.length}`)
73
+ return
74
+ }
75
+ for (let i = 0; i < cLanes.length; i++) {
76
+ const cn = JSON.stringify(normLane(cLanes[i] as Record<string, unknown>))
77
+ const sn = JSON.stringify(normLane(sLanes[i] as Record<string, unknown>))
78
+ if (cn !== sn) mismatches.push(` ${name}[${i}]: contract=${cn} sdk=${sn}`)
79
+ }
80
+ }
81
+
82
+ compareLanes('gatherer_lanes', contract.gatherer_lanes as unknown[], sdk.gathererLanes ?? [])
83
+ compareLanes('crafter_lanes', contract.crafter_lanes as unknown[], sdk.crafterLanes ?? [])
84
+ compareLanes('loader_lanes', contract.loader_lanes as unknown[], sdk.loaderLanes ?? [])
85
+
86
+ if (contract.cargo.length > 0 || sdk.cargo.length > 0) {
87
+ const contractCargo = normaliseCargo(mergeContractCargo(contract.cargo))
88
+ const sdkCargo = normaliseCargo(sdk.cargo)
89
+ if (contractCargo.length !== sdkCargo.length) {
90
+ mismatches.push(
91
+ ` cargo.length: contract=${contractCargo.length} sdk=${sdkCargo.length}`
92
+ )
93
+ } else {
94
+ for (let i = 0; i < contractCargo.length; i++) {
95
+ const c = contractCargo[i]
96
+ const s = sdkCargo[i]
97
+ if (c.itemId !== s.itemId || c.stats !== s.stats || c.quantity !== s.quantity) {
98
+ mismatches.push(
99
+ ` cargo[${i}]: contract={item:${c.itemId},stats:${c.stats},qty:${c.quantity}} sdk={item:${s.itemId},stats:${s.stats},qty:${s.quantity}}`
100
+ )
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ if (mismatches.length > 0) {
107
+ const header =
108
+ options.step !== undefined
109
+ ? `projection divergence at step ${options.step}:`
110
+ : 'projection divergence:'
111
+ throw new Error([header, ...mismatches].join('\n'))
112
+ }
113
+ }
114
+
115
+ interface NormalisedStack {
116
+ itemId: number
117
+ stats: string
118
+ quantity: string
119
+ }
120
+
121
+ function mergeContractCargo(cargo: ServerContract.Types.cargo_view[]): CargoStack[] {
122
+ return cargo.reduce<CargoStack[]>(
123
+ (acc, row) =>
124
+ mergeStacks(acc, cargoItemToStack(row as unknown as ServerContract.Types.cargo_item)),
125
+ []
126
+ )
127
+ }
128
+
129
+ function normaliseCargo(cargo: CargoStack[]): NormalisedStack[] {
130
+ return cargo
131
+ .map((s) => ({
132
+ itemId: Number(s.item_id),
133
+ stats: BigInt(s.stats.toString()).toString(),
134
+ quantity: BigInt(s.quantity.toString()).toString(),
135
+ }))
136
+ .sort(stackSort)
137
+ }
138
+
139
+ function stackSort(a: NormalisedStack, b: NormalisedStack): number {
140
+ if (a.itemId !== b.itemId) return a.itemId - b.itemId
141
+ return a.stats < b.stats ? -1 : a.stats > b.stats ? 1 : 0
142
+ }
143
+
144
+ function toNum(v: unknown): number | undefined {
145
+ if (v === undefined || v === null) return undefined
146
+ if (typeof v === 'number') return v
147
+ if (typeof v === 'bigint') return Number(v)
148
+ if (typeof (v as {toNumber?: unknown}).toNumber === 'function') {
149
+ return (v as {toNumber(): number}).toNumber()
150
+ }
151
+ return Number(v as number)
152
+ }
153
+
154
+ function fmt(v: unknown): string {
155
+ if (v === undefined) return 'undefined'
156
+ if (v === null) return 'null'
157
+ return String(v)
158
+ }
159
+
160
+ function normaliseStatBlock(block: unknown): Record<string, number> {
161
+ const out: Record<string, number> = {}
162
+ const obj = block as Record<string, unknown>
163
+ for (const k of Object.keys(obj).sort()) {
164
+ out[k] = toNum(obj[k]) ?? 0
165
+ }
166
+ return out
167
+ }
@@ -0,0 +1,86 @@
1
+ import {expect, test} from 'bun:test'
2
+ import type {RouteMoverInput} from './route-simulator'
3
+ import {computeGroupPerLegReach, computePerLegReach, type ReachStats} from './reach'
4
+
5
+ // Type-level: a RouteMoverInput (webapp's mover shape) must satisfy ReachStats with no cast.
6
+ function acceptsReachStats(_s: ReachStats): void {}
7
+ function checkRouteMoverAssignable(m: RouteMoverInput) {
8
+ acceptsReachStats(m)
9
+ }
10
+ void checkRouteMoverAssignable
11
+
12
+ test('capacity basis: reach is generator capacity over engine drain, unrounded', () => {
13
+ const reach = computePerLegReach({generator: {capacity: 550n}, engines: {drain: 100n}})
14
+ expect(reach).toBe(5.5)
15
+ })
16
+
17
+ test('capacity basis is the default when opts is omitted', () => {
18
+ const reach = computePerLegReach({generator: {capacity: 1000n}, engines: {drain: 250n}})
19
+ expect(reach).toBe(4)
20
+ })
21
+
22
+ test('current basis prefers generator capacity over current energy, matching the webapp fallback', () => {
23
+ const reach = computePerLegReach(
24
+ {generator: {capacity: 1000n}, engines: {drain: 100n}, energy: 250n},
25
+ {basis: 'current'}
26
+ )
27
+ expect(reach).toBe(10)
28
+ })
29
+
30
+ test('current basis falls back to current energy when there is no generator', () => {
31
+ const reach = computePerLegReach({engines: {drain: 100n}, energy: 350n}, {basis: 'current'})
32
+ expect(reach).toBe(3.5)
33
+ })
34
+
35
+ test('current basis with a lower current energy still returns the capacity-derived reach', () => {
36
+ const reach = computePerLegReach(
37
+ {generator: {capacity: 900n}, engines: {drain: 90n}, energy: 90n},
38
+ {basis: 'current'}
39
+ )
40
+ expect(reach).toBe(10)
41
+ expect(reach).not.toBe(1)
42
+ })
43
+
44
+ test('capacity basis throws for a generator-less mover', () => {
45
+ expect(() => computePerLegReach({engines: {drain: 100n}})).toThrow()
46
+ })
47
+
48
+ test('current basis throws when there is no current energy figure either', () => {
49
+ expect(() => computePerLegReach({engines: {drain: 100n}}, {basis: 'current'})).toThrow()
50
+ })
51
+
52
+ test('an empty mover list has zero reach rather than throwing', () => {
53
+ expect(computeGroupPerLegReach([])).toBe(0)
54
+ })
55
+
56
+ test('a group with no moving entity has zero reach', () => {
57
+ const reach = computeGroupPerLegReach([{generator: {capacity: 1000n}}])
58
+ expect(reach).toBe(0)
59
+ })
60
+
61
+ test('a RouteMoverInput-shaped mover with hasMovement: false is excluded from the group', () => {
62
+ const mover: ReachStats = {
63
+ hasMovement: false,
64
+ engines: {drain: 100},
65
+ generator: {capacity: 1000},
66
+ }
67
+ expect(computeGroupPerLegReach([mover])).toBe(0)
68
+ })
69
+
70
+ test('hasMovement defaults to true when absent', () => {
71
+ const mover: ReachStats = {engines: {drain: 100}, generator: {capacity: 1000}}
72
+ expect(computeGroupPerLegReach([mover])).toBe(10)
73
+ })
74
+
75
+ test('group reach is the minimum across movers', () => {
76
+ const fast: ReachStats = {engines: {drain: 100n}, generator: {capacity: 1000n}}
77
+ const slow: ReachStats = {engines: {drain: 100n}, generator: {capacity: 550n}}
78
+ expect(computeGroupPerLegReach([fast, slow])).toBe(5.5)
79
+ })
80
+
81
+ // Settled by contracts/test/server/reach-parity.test.ts: the contract truncates, so ceiling here would overclaim reach.
82
+ test('rounding: a fractional reach is left unrounded, not ceiled', () => {
83
+ const reach = computePerLegReach({generator: {capacity: 550n}, engines: {drain: 100n}})
84
+ expect(reach).not.toBe(Math.ceil(reach))
85
+ expect(Math.floor(reach)).toBe(5)
86
+ })
@@ -0,0 +1,34 @@
1
+ export interface ReachStats {
2
+ generator?: {capacity: bigint | number}
3
+ engines?: {drain: bigint | number}
4
+ energy?: bigint | number
5
+ hasMovement?: boolean
6
+ }
7
+
8
+ export interface ComputePerLegReachOptions {
9
+ basis?: 'capacity' | 'current'
10
+ }
11
+
12
+ export function computePerLegReach(s: ReachStats, opts: ComputePerLegReachOptions = {}): number {
13
+ const basis = opts.basis ?? 'capacity'
14
+ const energy =
15
+ basis === 'capacity' ? s.generator?.capacity : (s.generator?.capacity ?? s.energy)
16
+ const drain = s.engines?.drain
17
+ if (energy === undefined || drain === undefined || Number(drain) === 0) {
18
+ throw new Error('entity has no usable engine/generator (cannot compute per-leg reach)')
19
+ }
20
+ return Number(energy) / Number(drain)
21
+ }
22
+
23
+ export function computeGroupPerLegReach(
24
+ participants: ReachStats[],
25
+ opts: ComputePerLegReachOptions = {}
26
+ ): number {
27
+ const movers = participants.filter(
28
+ (p) => (p.hasMovement ?? true) && p.engines !== undefined && Number(p.engines.drain) !== 0
29
+ )
30
+ if (movers.length === 0) {
31
+ return 0
32
+ }
33
+ return Math.min(...movers.map((m) => computePerLegReach(m, opts)))
34
+ }
@@ -0,0 +1,63 @@
1
+ import {expect, test} from 'bun:test'
2
+ import type {Coord, SystemGraph} from './route-planner'
3
+ import {planRouteWithRetry} from './route-build'
4
+
5
+ function countingGraph(opts: {
6
+ reachable: boolean
7
+ nearby: (c: Coord) => {coord: Coord; dist: number}[]
8
+ }): {graph: SystemGraph; hasSystemCalls: () => number} {
9
+ let calls = 0
10
+ const graph: SystemGraph = {
11
+ hasSystem: () => {
12
+ calls++
13
+ return opts.reachable
14
+ },
15
+ nearby: opts.nearby,
16
+ }
17
+ return {graph, hasSystemCalls: () => calls}
18
+ }
19
+
20
+ test('a route found on the first attempt does not retry', () => {
21
+ const origin: Coord = {x: 0, y: 0}
22
+ const dest: Coord = {x: 3, y: 0}
23
+ const {graph, hasSystemCalls} = countingGraph({reachable: true, nearby: () => []})
24
+
25
+ const result = planRouteWithRetry({origin, dest, perLegReach: 5, graph})
26
+
27
+ expect(result.ok).toBe(true)
28
+ expect(hasSystemCalls()).toBe(1)
29
+ })
30
+
31
+ test('a no-path result widens the corridor and retries until a route is found', () => {
32
+ const origin: Coord = {x: 0, y: 0}
33
+ const dest: Coord = {x: 100, y: 0}
34
+ const mid: Coord = {x: 50, y: 28.4}
35
+ const perLegReach = 6.4
36
+
37
+ const {graph, hasSystemCalls} = countingGraph({
38
+ reachable: true,
39
+ nearby: (c) => {
40
+ if (c.x === origin.x && c.y === origin.y) return [{coord: mid, dist: 0}]
41
+ if (c.x === mid.x && c.y === mid.y) return [{coord: dest, dist: 0}]
42
+ return []
43
+ },
44
+ })
45
+
46
+ const result = planRouteWithRetry({origin, dest, perLegReach, graph})
47
+
48
+ expect(result.ok).toBe(true)
49
+ // Excluded from the default corridor, found once corridorSlack widens to perLegReach * 3.
50
+ expect(hasSystemCalls()).toBe(2)
51
+ })
52
+
53
+ test('a non-no-path failure returns immediately without retrying', () => {
54
+ const origin: Coord = {x: 0, y: 0}
55
+ const dest: Coord = {x: 100, y: 0}
56
+ const {graph, hasSystemCalls} = countingGraph({reachable: false, nearby: () => []})
57
+
58
+ const result = planRouteWithRetry({origin, dest, perLegReach: 5, graph})
59
+
60
+ expect(result.ok).toBe(false)
61
+ if (!result.ok) expect(result.reason).toBe('empty-destination')
62
+ expect(hasSystemCalls()).toBe(1)
63
+ })
@@ -0,0 +1,60 @@
1
+ import {calc_rechargetime} from './travel'
2
+ import {MAX_LEGS, planRoute} from './route-planner'
3
+ import type {
4
+ Coord,
5
+ RouteHeuristicCost,
6
+ RouteLegCost,
7
+ RouteResult,
8
+ SystemGraph,
9
+ } from './route-planner'
10
+ import {simulateRoute} from './route-simulator'
11
+ import type {RouteMoverInput} from './route-simulator'
12
+
13
+ export function planRouteWithRetry(input: {
14
+ origin: Coord
15
+ dest: Coord
16
+ perLegReach: number
17
+ graph: SystemGraph
18
+ maxLegs?: number
19
+ legCost?: RouteLegCost
20
+ heuristicCost?: RouteHeuristicCost
21
+ }): RouteResult {
22
+ const args = {...input, maxLegs: input.maxLegs ?? MAX_LEGS}
23
+ const first = planRoute(args)
24
+ if (first.ok || first.reason !== 'no-path') return first
25
+ const widened = planRoute({...args, corridorSlack: input.perLegReach * 3})
26
+ if (widened.ok || widened.reason !== 'no-path') return widened
27
+ // The chain imposes no corridor, only per-hop reach, so exhaust the graph (nodeBudget-bounded) before declaring no-path.
28
+ return planRoute({...args, corridorSlack: Number.POSITIVE_INFINITY})
29
+ }
30
+
31
+ export function contractRouteLegCost(movers: readonly RouteMoverInput[]): RouteLegCost {
32
+ return ({from, to, isDestination}) => {
33
+ const sim = simulateRoute([...movers], [to], from, true)
34
+ const leg = sim.legs[0]
35
+ if (!sim.reachable || !leg) return null
36
+
37
+ let rechargeAfter = 0
38
+ for (const mover of movers) {
39
+ if (!mover.hasMovement || !mover.generator) continue
40
+ const cost = leg.energyCostByMover[String(mover.ref.entityId)] ?? 0
41
+ rechargeAfter = Math.max(
42
+ rechargeAfter,
43
+ Number(
44
+ calc_rechargetime(
45
+ mover.generator.capacity,
46
+ Math.max(0, mover.generator.capacity - cost),
47
+ mover.generator.recharge
48
+ )
49
+ )
50
+ )
51
+ }
52
+
53
+ return leg.flightSeconds + (isDestination ? 0 : rechargeAfter)
54
+ }
55
+ }
56
+
57
+ export function contractRouteHeuristic(movers: readonly RouteMoverInput[]): RouteHeuristicCost {
58
+ return (from, dest) =>
59
+ simulateRoute([...movers], [dest], from, true).legs[0]?.flightSeconds ?? 0
60
+ }
@@ -0,0 +1,288 @@
1
+ import {nearbyWormholes, wormholeAt} from '../derivation/wormhole'
2
+ import {Checksum256, type Checksum256Type} from '@wharfkit/antelope'
3
+
4
+ export interface Coord {
5
+ x: number
6
+ y: number
7
+ }
8
+
9
+ export interface Neighbor {
10
+ coord: Coord
11
+ dist: number
12
+ }
13
+
14
+ export interface SystemGraph {
15
+ hasSystem(c: Coord): boolean
16
+ nearby(c: Coord, reachTiles: number): Neighbor[]
17
+ }
18
+
19
+ export interface RoutePlan {
20
+ ok: true
21
+ waypoints: Coord[]
22
+ legs: number
23
+ totalDistance: number
24
+ }
25
+
26
+ export interface RouteLegInput {
27
+ from: Coord
28
+ to: Coord
29
+ distance: number
30
+ isDestination: boolean
31
+ }
32
+
33
+ /**
34
+ * Returns the elapsed-time search cost for a leg, or null when the leg is not contract-feasible.
35
+ */
36
+ export type RouteLegCost = (leg: RouteLegInput) => number | null
37
+ export type RouteHeuristicCost = (from: Coord, dest: Coord) => number
38
+
39
+ export type RouteFailureReason = 'empty-destination' | 'no-path' | 'max-legs'
40
+
41
+ export interface RouteFailure {
42
+ ok: false
43
+ reason: RouteFailureReason
44
+ furthest?: Coord
45
+ legsNeeded?: number
46
+ partialWaypoints?: Coord[]
47
+ }
48
+
49
+ export type RouteResult = RoutePlan | RouteFailure
50
+
51
+ export interface PlanRouteParams {
52
+ origin: Coord
53
+ dest: Coord
54
+ perLegReach: number
55
+ graph: SystemGraph
56
+ corridorSlack?: number
57
+ nodeBudget?: number
58
+ maxLegs?: number
59
+ legCost?: RouteLegCost
60
+ heuristicCost?: RouteHeuristicCost
61
+ }
62
+
63
+ const key = (c: Coord): string => `${c.x},${c.y}`
64
+ const sameCoord = (a: Coord, b: Coord): boolean => a.x === b.x && a.y === b.y
65
+ const dist = (a: Coord, b: Coord): number => Math.hypot(a.x - b.x, a.y - b.y)
66
+
67
+ export const MAX_LEGS = 12
68
+
69
+ export function planRoute(params: PlanRouteParams): RouteResult {
70
+ const {origin, dest, perLegReach, graph} = params
71
+ const corridorSlack = params.corridorSlack ?? perLegReach
72
+ const nodeBudget = params.nodeBudget ?? 5000
73
+ const maxLegs = params.maxLegs ?? MAX_LEGS
74
+
75
+ if (!graph.hasSystem(dest)) {
76
+ return {ok: false, reason: 'empty-destination'}
77
+ }
78
+
79
+ const straightLine = dist(origin, dest)
80
+ if (straightLine <= perLegReach) {
81
+ const directCost = params.legCost?.({
82
+ from: origin,
83
+ to: dest,
84
+ distance: straightLine,
85
+ isDestination: true,
86
+ })
87
+ if (directCost !== null) {
88
+ return {ok: true, waypoints: [dest], legs: 1, totalDistance: straightLine}
89
+ }
90
+ }
91
+ const heuristic = (c: Coord): number =>
92
+ params.heuristicCost?.(c, dest) ??
93
+ (params.legCost ? 0 : Math.ceil(dist(c, dest) / perLegReach))
94
+
95
+ const gScore = new Map<string, number>([[key(origin), 0]])
96
+ const cameFrom = new Map<string, Coord>()
97
+ const frontier: {coord: Coord; legs: number; cost: number; f: number; remaining: number}[] = [
98
+ {coord: origin, legs: 0, cost: 0, f: heuristic(origin), remaining: straightLine},
99
+ ]
100
+
101
+ let furthest = origin
102
+ let furthestRemaining = dist(origin, dest)
103
+ let expansions = 0
104
+ let cappedByMaxLegs = false
105
+
106
+ while (frontier.length > 0) {
107
+ let bestIdx = 0
108
+ for (let i = 1; i < frontier.length; i++) {
109
+ const a = frontier[i]
110
+ const b = frontier[bestIdx]
111
+ if (a.f < b.f || (a.f === b.f && a.remaining < b.remaining)) {
112
+ bestIdx = i
113
+ }
114
+ }
115
+ const current = frontier.splice(bestIdx, 1)[0]
116
+ if (current.cost !== gScore.get(key(current.coord))) continue
117
+
118
+ if (sameCoord(current.coord, dest)) {
119
+ return reconstruct(cameFrom, origin, dest)
120
+ }
121
+
122
+ if (current.remaining < furthestRemaining) {
123
+ furthestRemaining = current.remaining
124
+ furthest = current.coord
125
+ }
126
+
127
+ if (++expansions > nodeBudget) break
128
+
129
+ for (const n of graph.nearby(current.coord, perLegReach)) {
130
+ const inCorridor =
131
+ dist(origin, n.coord) + dist(n.coord, dest) <= straightLine + corridorSlack
132
+ if (!inCorridor) continue
133
+
134
+ const tentativeLegs = current.legs + 1
135
+ if (tentativeLegs > maxLegs) {
136
+ cappedByMaxLegs = true
137
+ continue
138
+ }
139
+ const evaluatedCost = params.legCost?.({
140
+ from: current.coord,
141
+ to: n.coord,
142
+ distance: n.dist,
143
+ isDestination: sameCoord(n.coord, dest),
144
+ })
145
+ if (evaluatedCost === null) continue
146
+ const legCost = evaluatedCost ?? 1
147
+ if (!(legCost >= 0) || !Number.isFinite(legCost)) continue
148
+ const nk = key(n.coord)
149
+ const nextCost = current.cost + legCost
150
+ if (nextCost < (gScore.get(nk) ?? Infinity)) {
151
+ gScore.set(nk, nextCost)
152
+ cameFrom.set(nk, current.coord)
153
+ const remaining = dist(n.coord, dest)
154
+ frontier.push({
155
+ coord: n.coord,
156
+ legs: tentativeLegs,
157
+ cost: nextCost,
158
+ f: nextCost + heuristic(n.coord),
159
+ remaining,
160
+ })
161
+ }
162
+ }
163
+ }
164
+
165
+ if (cappedByMaxLegs) {
166
+ return {
167
+ ok: false,
168
+ reason: 'max-legs',
169
+ furthest,
170
+ partialWaypoints: reconstructWaypoints(cameFrom, origin, furthest),
171
+ }
172
+ }
173
+ return {
174
+ ok: false,
175
+ reason: 'no-path',
176
+ furthest,
177
+ partialWaypoints: reconstructWaypoints(cameFrom, origin, furthest),
178
+ }
179
+ }
180
+
181
+ function reconstructWaypoints(cameFrom: Map<string, Coord>, origin: Coord, target: Coord): Coord[] {
182
+ if (sameCoord(target, origin)) return []
183
+ const path: Coord[] = [target]
184
+ let cur = target
185
+ while (!sameCoord(cur, origin)) {
186
+ const prev = cameFrom.get(key(cur))
187
+ if (!prev) break
188
+ path.unshift(prev)
189
+ cur = prev
190
+ }
191
+ return path.slice(1)
192
+ }
193
+
194
+ function reconstruct(cameFrom: Map<string, Coord>, origin: Coord, dest: Coord): RoutePlan {
195
+ const path: Coord[] = [dest]
196
+ let cur = dest
197
+ let totalDistance = 0
198
+ while (!sameCoord(cur, origin)) {
199
+ const prev = cameFrom.get(key(cur))
200
+ if (!prev) break
201
+ totalDistance += dist(prev, cur)
202
+ path.unshift(prev)
203
+ cur = prev
204
+ }
205
+ const waypoints = path.slice(1)
206
+ return {ok: true, waypoints, legs: waypoints.length, totalDistance}
207
+ }
208
+
209
+ export interface ScanProvider {
210
+ getLocationType(seedHex: string, x: number, y: number): number
211
+ systemsInBox(
212
+ seedHex: string,
213
+ xMin: number,
214
+ yMin: number,
215
+ xMax: number,
216
+ yMax: number
217
+ ): {x: number; y: number; locType: number}[]
218
+ }
219
+
220
+ // Keyed by provider identity, so pass a stable one (the `@shipload/sdk/scan` namespace).
221
+ const graphCache = new WeakMap<ScanProvider, Map<string, SystemGraph>>()
222
+
223
+ export function sdkSystemGraph(seed: Checksum256Type, scan: ScanProvider): SystemGraph {
224
+ const s = Checksum256.from(seed)
225
+ const seedHex = s.toString()
226
+ let bySeed = graphCache.get(scan)
227
+ if (!bySeed) {
228
+ bySeed = new Map()
229
+ graphCache.set(scan, bySeed)
230
+ }
231
+ const cached = bySeed.get(seedHex)
232
+ if (cached) return cached
233
+ const graph = wasmSystemGraph(s, seedHex, scan)
234
+ bySeed.set(seedHex, graph)
235
+ return graph
236
+ }
237
+
238
+ const SCAN_BUCKET = 48
239
+
240
+ function wasmSystemGraph(s: Checksum256, seedHex: string, scan: ScanProvider): SystemGraph {
241
+ // Per-bucket system cache: reused across the overlapping node queries A* makes (and across routes).
242
+ const bucketCache = new Map<string, {x: number; y: number}[]>()
243
+ const bucketSystems = (bx: number, by: number): {x: number; y: number}[] => {
244
+ const k = `${bx},${by}`
245
+ let v = bucketCache.get(k)
246
+ if (v === undefined) {
247
+ const xMin = bx * SCAN_BUCKET
248
+ const yMin = by * SCAN_BUCKET
249
+ v = scan
250
+ .systemsInBox(seedHex, xMin, yMin, xMin + SCAN_BUCKET - 1, yMin + SCAN_BUCKET - 1)
251
+ .map((cell) => ({x: cell.x, y: cell.y}))
252
+ bucketCache.set(k, v)
253
+ }
254
+ return v
255
+ }
256
+ return {
257
+ hasSystem: (c) =>
258
+ scan.getLocationType(seedHex, c.x, c.y) !== 0 || wormholeAt(s, c.x, c.y) !== null,
259
+ nearby: (c, reachTiles) => {
260
+ const r = Math.floor(reachTiles)
261
+ const seen = new Set<string>([`${c.x},${c.y}`])
262
+ const out: Neighbor[] = []
263
+ const bx0 = Math.floor((c.x - r) / SCAN_BUCKET)
264
+ const bx1 = Math.floor((c.x + r) / SCAN_BUCKET)
265
+ const by0 = Math.floor((c.y - r) / SCAN_BUCKET)
266
+ const by1 = Math.floor((c.y + r) / SCAN_BUCKET)
267
+ for (let bx = bx0; bx <= bx1; bx++) {
268
+ for (let by = by0; by <= by1; by++) {
269
+ for (const cell of bucketSystems(bx, by)) {
270
+ const dist = Math.hypot(cell.x - c.x, cell.y - c.y)
271
+ if (dist > reachTiles) continue
272
+ const k = `${cell.x},${cell.y}`
273
+ if (seen.has(k)) continue
274
+ seen.add(k)
275
+ out.push({coord: {x: cell.x, y: cell.y}, dist})
276
+ }
277
+ }
278
+ }
279
+ for (const coord of nearbyWormholes(s, c.x, c.y, reachTiles)) {
280
+ const k = `${coord.x},${coord.y}`
281
+ if (seen.has(k)) continue
282
+ seen.add(k)
283
+ out.push({coord, dist: Math.hypot(coord.x - c.x, coord.y - c.y)})
284
+ }
285
+ return out
286
+ },
287
+ }
288
+ }