Tell a search that gave up from a route that is not there
The route search stops after MAX_VISITED stars and returned null, which everything downstream read as "the catalogue holds no chain". On the real catalogue that was wrong for real questions: Sol to HD 120147 (136 pc) at 5 pc is 50 jumps, and the panel said there was no route. The budget also sat under what the shipped catalogue needs, so it is now 40 000 rather than 20 000: both that route and a star at 170 pc are found, and Sol to HD 2626 at 6 pc, which used to be refused after 4.7 s, plots 56 jumps in about 2 s. A search now reports whether it gave up. The range search no longer counts a give-up as proof that nothing routes below it — that is what reported ranges up to 29% too wide — and it stops after two of them, since those are the probes that cost the most and settle the least: for HD 2626 at 3 pc it offers 5.92 pc in about 4 s, against 6.13 pc in 4.7 s. At the panel's widest range the refused route and the range search are the same question, so it is asked once. Where nothing can be said, the panel says "Too many stars to search at this range." rather than claiming there is no route. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
This commit is contained in:
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { jumpLinkSegments, minimumRangeBetween, routeBetween } from './jump-links';
|
||||
import { answerRouting, indexCatalogue } from './routing';
|
||||
import { StarNeighbourhood } from './star-neighbourhood';
|
||||
import { StarNeighbourhood, StarPoint } from './star-neighbourhood';
|
||||
|
||||
/** Stars a parsec apart along x, then a gap of 5 pc to one more. */
|
||||
const POINTS = [...Array.from({ length: 5 }, (_, i) => ({ id: 10 + i, x: i, y: 0, z: 0 })), { id: 99, x: 9, y: 0, z: 0 }];
|
||||
@@ -25,6 +25,16 @@ describe('indexCatalogue', () => {
|
||||
});
|
||||
});
|
||||
|
||||
/** The same index, counting the neighbour queries a search makes through it. */
|
||||
class CountingNeighbourhood extends StarNeighbourhood {
|
||||
queries = 0;
|
||||
|
||||
override forEachWithin(id: number, radiusPc: number, visit: (neighbour: StarPoint, distancePc: number) => void): void {
|
||||
this.queries++;
|
||||
super.forEachWithin(id, radiusPc, visit);
|
||||
}
|
||||
}
|
||||
|
||||
describe('answerRouting', () => {
|
||||
const index = indexCatalogue(catalogue());
|
||||
const direct = new StarNeighbourhood(POINTS);
|
||||
@@ -32,16 +42,43 @@ describe('answerRouting', () => {
|
||||
it('answers a route the range allows, with nothing to raise it to', () => {
|
||||
const answer = answerRouting(index, { kind: 'route', requestId: 7, fromId: 10, toId: 14, rangePc: 1.5, ceilingPc: 8 });
|
||||
|
||||
expect(answer).toEqual({ kind: 'route', requestId: 7, route: routeBetween(direct, 10, 14, 1.5), neededRangePc: null });
|
||||
expect(answer).toEqual({ kind: 'route', requestId: 7, route: routeBetween(direct, 10, 14, 1.5).route, neededRangePc: null, gaveUp: false });
|
||||
});
|
||||
|
||||
it('answers a route the range does not allow with the range that would', () => {
|
||||
const answer = answerRouting(index, { kind: 'route', requestId: 8, fromId: 10, toId: 99, rangePc: 1.5, ceilingPc: 8 });
|
||||
|
||||
expect(answer).toEqual({ kind: 'route', requestId: 8, route: null, neededRangePc: minimumRangeBetween(direct, 10, 99, 8) });
|
||||
expect(answer).toEqual({ kind: 'route', requestId: 8, route: null, neededRangePc: minimumRangeBetween(direct, 10, 99, 8).rangePc, gaveUp: false });
|
||||
expect(answer.kind === 'route' && answer.neededRangePc).toBeCloseTo(5, 1);
|
||||
});
|
||||
|
||||
it('passes on that the search gave up, rather than reporting no route', () => {
|
||||
// A crowd larger than a search's budget around the departure, and a destination nothing reaches:
|
||||
// the answer is "it gave up", and the scene has to be able to tell that from "there is none".
|
||||
let seed = 5;
|
||||
const random = () => ((seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648) * 10 - 5;
|
||||
const crowd: StarPoint[] = Array.from({ length: 45000 }, (_, i) => ({ id: 1000 + i, x: random(), y: random(), z: random() }));
|
||||
const knot = new StarNeighbourhood([{ id: 0, x: 0, y: 0, z: 0 }, ...crowd, { id: 99, x: 500, y: 0, z: 0 }], 0.5);
|
||||
|
||||
const answer = answerRouting(knot, { kind: 'route', requestId: 12, fromId: 0, toId: 99, rangePc: 0.5, ceilingPc: 0.5 });
|
||||
|
||||
expect(answer).toMatchObject({ route: null, neededRangePc: null, gaveUp: true });
|
||||
});
|
||||
|
||||
it('asks the ceiling its question once, rather than searching it again to answer it', () => {
|
||||
// At the panel's widest range the refused route and the range search are the same question, run
|
||||
// with the same arguments over the same index: the second pays the whole budget for the answer
|
||||
// the first already gave.
|
||||
const counting = new CountingNeighbourhood(POINTS);
|
||||
const oneSearch = new CountingNeighbourhood(POINTS);
|
||||
routeBetween(oneSearch, 10, 99, 3);
|
||||
|
||||
const answer = answerRouting(counting, { kind: 'route', requestId: 11, fromId: 10, toId: 99, rangePc: 3, ceilingPc: 3 });
|
||||
|
||||
expect(answer).toMatchObject({ route: null, neededRangePc: null });
|
||||
expect(counting.queries).toBe(oneSearch.queries);
|
||||
});
|
||||
|
||||
it('offers nothing to raise to when even the ceiling does not reach', () => {
|
||||
const answer = answerRouting(index, { kind: 'route', requestId: 9, fromId: 10, toId: 99, rangePc: 1.5, ceilingPc: 3 });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user