@shipload/sdk 1.0.0-next.7 → 1.0.0-next.71
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/lib/scan.d.ts +52 -0
- package/lib/scan.js +162 -0
- package/lib/scan.js.map +1 -0
- package/lib/scan.m.js +152 -0
- package/lib/scan.m.js.map +1 -0
- package/lib/shipload.d.ts +5822 -1484
- package/lib/shipload.js +20838 -5419
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +20674 -5669
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +1691 -0
- package/lib/testing.js +7109 -0
- package/lib/testing.js.map +1 -0
- package/lib/testing.m.js +7103 -0
- package/lib/testing.m.js.map +1 -0
- package/package.json +20 -2
- package/src/capabilities/craftable.test.ts +82 -0
- package/src/capabilities/craftable.ts +82 -0
- package/src/capabilities/crafting.test.ts +80 -0
- package/src/capabilities/crafting.ts +50 -8
- package/src/capabilities/gathering.test.ts +29 -0
- package/src/capabilities/gathering.ts +38 -20
- package/src/capabilities/hauling.ts +0 -5
- package/src/capabilities/index.ts +0 -1
- package/src/capabilities/modules.ts +27 -30
- package/src/capabilities/movement.ts +1 -1
- package/src/capabilities/storage.test.ts +26 -0
- package/src/capabilities/storage.ts +22 -7
- package/src/contracts/fund.ts +306 -0
- package/src/contracts/platform.ts +312 -3
- package/src/contracts/server.ts +2793 -478
- package/src/contracts/token.ts +170 -0
- package/src/contracts.ts +2 -0
- package/src/coordinates/address.ts +88 -0
- package/src/coordinates/constants.test.ts +15 -0
- package/src/coordinates/constants.ts +23 -0
- package/src/coordinates/index.ts +15 -0
- package/src/coordinates/memo.test.ts +47 -0
- package/src/coordinates/memo.ts +20 -0
- package/src/coordinates/permutation.ts +77 -0
- package/src/coordinates/regions.ts +48 -0
- package/src/coordinates/sectors.ts +115 -0
- package/src/data/capabilities.ts +49 -18
- package/src/data/capability-formulas.ts +26 -14
- package/src/data/catalog.test.ts +19 -0
- package/src/data/catalog.ts +13 -3
- package/src/data/charters.json +396 -0
- package/src/data/citizenry-adjectives.json +42 -0
- package/src/data/citizenry-nouns.json +50 -0
- package/src/data/colors.ts +14 -32
- package/src/data/entities.json +421 -15
- package/src/data/item-ids.ts +53 -13
- package/src/data/items.json +330 -77
- package/src/data/kind-registry.json +212 -0
- package/src/data/kind-registry.ts +192 -0
- package/src/data/metadata.ts +312 -51
- package/src/data/recipes-runtime.ts +6 -23
- package/src/data/recipes.json +2173 -173
- package/src/data/tiers.ts +9 -6
- package/src/derivation/build-methods.test.ts +71 -0
- package/src/derivation/build-methods.ts +50 -0
- package/src/derivation/capabilities.test.ts +348 -0
- package/src/derivation/capabilities.ts +707 -0
- package/src/derivation/capability-mappings.ts +60 -16
- package/src/derivation/crafting.test.ts +17 -0
- package/src/derivation/crafting.ts +57 -34
- package/src/derivation/index.ts +27 -2
- package/src/derivation/recipe-usage.test.ts +88 -0
- package/src/derivation/recipe-usage.ts +141 -0
- package/src/derivation/reserve-regen.ts +34 -0
- package/src/derivation/resources.ts +9 -1
- package/src/derivation/rollups.test.ts +55 -0
- package/src/derivation/rollups.ts +72 -0
- package/src/derivation/stars.test.ts +67 -0
- package/src/derivation/stars.ts +25 -0
- package/src/derivation/stat-scaling.ts +12 -0
- package/src/derivation/stats.ts +6 -6
- package/src/derivation/strata.ts +5 -3
- package/src/derivation/stratum.ts +26 -22
- package/src/derivation/tiers.ts +40 -7
- package/src/derivation/upgrades.ts +14 -0
- package/src/derivation/wormhole.ts +154 -0
- package/src/entities/entity.ts +102 -0
- package/src/entities/gamestate.ts +3 -28
- package/src/entities/makers.ts +144 -136
- package/src/entities/slot-multiplier.ts +43 -0
- package/src/entity/hydrate.test.ts +156 -0
- package/src/entity/hydrate.ts +86 -0
- package/src/errors.ts +21 -16
- package/src/format.ts +42 -9
- package/src/index-module.ts +425 -32
- package/src/influence/ballot.test.ts +103 -0
- package/src/influence/ballot.ts +98 -0
- package/src/influence/categories.ts +32 -0
- package/src/influence/charter-metadata.test.ts +136 -0
- package/src/influence/charter-metadata.ts +236 -0
- package/src/influence/charters.test.ts +112 -0
- package/src/influence/charters.ts +150 -0
- package/src/influence/citizenry.ts +38 -0
- package/src/influence/civic-shuttle.test.ts +202 -0
- package/src/influence/civic-shuttle.ts +142 -0
- package/src/influence/civic.test.ts +287 -0
- package/src/influence/civic.ts +269 -0
- package/src/influence/constants.ts +84 -0
- package/src/influence/decay.ts +20 -0
- package/src/influence/decomp.ts +95 -0
- package/src/influence/demand.ts +106 -0
- package/src/influence/duration.ts +101 -0
- package/src/influence/index.ts +118 -0
- package/src/influence/influence.test.ts +450 -0
- package/src/influence/quality.ts +25 -0
- package/src/influence/valuation.ts +154 -0
- package/src/managers/actions.ts +950 -93
- package/src/managers/balances.test.ts +161 -0
- package/src/managers/balances.ts +93 -0
- package/src/managers/base.ts +10 -2
- package/src/managers/cluster.test.ts +39 -0
- package/src/managers/cluster.ts +53 -0
- package/src/managers/construction-types.ts +80 -0
- package/src/managers/construction.ts +453 -0
- package/src/managers/context.ts +58 -1
- package/src/managers/coordinates.ts +14 -0
- package/src/managers/entities.ts +35 -63
- package/src/managers/epochs.ts +72 -1
- package/src/managers/index.ts +38 -1
- package/src/managers/influence.test.ts +56 -0
- package/src/managers/influence.ts +425 -0
- package/src/managers/jobs.test.ts +96 -0
- package/src/managers/jobs.ts +58 -0
- package/src/managers/locations.ts +25 -29
- package/src/managers/nft.test.ts +21 -0
- package/src/managers/nft.ts +110 -0
- package/src/managers/players.ts +25 -0
- package/src/managers/plot.ts +122 -0
- package/src/nft/atomicassets.abi.json +1342 -0
- package/src/nft/atomicassets.ts +237 -0
- package/src/nft/atomicdata.ts +130 -0
- package/src/nft/buildImmutableData.ts +388 -0
- package/src/nft/description.ts +222 -100
- package/src/nft/index.ts +3 -0
- package/src/planner/index.ts +296 -0
- package/src/planner/planner.test.ts +459 -0
- package/src/receipts.ts +22 -0
- package/src/resolution/describe-module.ts +43 -25
- package/src/resolution/display-name.ts +39 -11
- package/src/resolution/resolve-item.test.ts +48 -0
- package/src/resolution/resolve-item.ts +112 -58
- package/src/scan/index.ts +244 -0
- package/src/scan/scan-wasm.base64.ts +2 -0
- package/src/scheduling/accessor.ts +65 -23
- package/src/scheduling/availability.test.ts +320 -0
- package/src/scheduling/availability.ts +251 -0
- package/src/scheduling/cancel.test.ts +550 -0
- package/src/scheduling/cancel.ts +254 -0
- package/src/scheduling/cluster-stock.test.ts +31 -0
- package/src/scheduling/cluster-stock.ts +15 -0
- package/src/scheduling/energy.ts +47 -0
- package/src/scheduling/epoch.ts +1 -0
- package/src/scheduling/idle-resolve.ts +45 -0
- package/src/scheduling/jobs.test.ts +448 -0
- package/src/scheduling/jobs.ts +343 -0
- package/src/scheduling/lane-core.ts +128 -0
- package/src/scheduling/lanes.test.ts +250 -0
- package/src/scheduling/lanes.ts +279 -0
- package/src/scheduling/projection.ts +265 -149
- package/src/scheduling/schedule.test.ts +163 -0
- package/src/scheduling/schedule.ts +282 -116
- package/src/scheduling/task-cargo.test.ts +110 -0
- package/src/scheduling/task-cargo.ts +62 -0
- package/src/scheduling/task-effects.test.ts +35 -0
- package/src/scheduling/task-effects.ts +34 -0
- package/src/scheduling/unwrap.test.ts +86 -0
- package/src/scheduling/unwrap.ts +190 -0
- package/src/shipload.ts +63 -4
- package/src/subscriptions/manager.cluster.test.ts +51 -0
- package/src/subscriptions/manager.ts +245 -172
- package/src/subscriptions/mappers.ts +5 -8
- package/src/subscriptions/types.ts +28 -3
- package/src/testing/catalog-hash.ts +19 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/projection-parity.ts +167 -0
- package/src/travel/reach.test.ts +86 -0
- package/src/travel/reach.ts +34 -0
- package/src/travel/route-build.test.ts +63 -0
- package/src/travel/route-build.ts +60 -0
- package/src/travel/route-planner.ts +288 -0
- package/src/travel/route-simulator.ts +170 -0
- package/src/travel/travel.ts +190 -123
- package/src/types/capabilities.ts +37 -8
- package/src/types/entity.ts +3 -3
- package/src/types/index.ts +0 -1
- package/src/types.ts +81 -23
- package/src/utils/cargo.test.ts +14 -0
- package/src/utils/cargo.ts +2 -0
- package/src/utils/display-name.ts +70 -0
- package/src/utils/hash.ts +22 -2
- package/src/utils/system.ts +24 -5
- package/src/capabilities/loading.ts +0 -8
- package/src/entities/container.ts +0 -123
- package/src/entities/ship-deploy.ts +0 -295
- package/src/entities/ship.ts +0 -221
- package/src/entities/warehouse.ts +0 -127
- 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
|
+
}
|