Files
Epic-The-Musical-Timeline/tests/route.test.ts
T
Lucas WintherandClaude Opus 5.5 ac017787b5 feat(app): 3D world, landings and friezes
Nine stylised places on one route, ship and ribbons, the cave, arena and
hall landings, nine saga friezes and the curtain.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 15:08:11 +01:00

67 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, test } from "bun:test";
import { sagas, songs } from "../src/data/album";
import { PLACES, pointAt, sIndex, stops } from "../src/app/world/route";
describe("route geometry", () => {
test("nine places in saga order", () => {
expect(PLACES.length).toBe(9);
expect(PLACES.map((p) => p.id)).toEqual(sagas.map((s) => s.id));
});
test("one stop per song, in album order, s strictly increasing from 0 to 1", () => {
const st = stops();
expect(st.length).toBe(40);
expect(st.map((s) => s.songId)).toEqual(songs.map((s) => s.id));
expect(st[0]!.s).toBe(0);
expect(st.at(-1)!.s).toBe(1);
for (let i = 1; i < st.length; i++) expect(st[i]!.s).toBeGreaterThan(st[i - 1]!.s);
});
test("each stop lies at its saga's place, within 1.5× its radius", () => {
for (const st of stops()) {
const song = songs.find((s) => s.id === st.songId)!;
expect(st.placeId).toBe(song.sagaId);
const place = PLACES.find((p) => p.id === st.placeId)!;
expect(Math.hypot(st.x - place.x, st.z - place.z)).toBeLessThanOrEqual(1.5 * place.radius);
}
});
test("the ship never anchors inside an island: stops keep clear of the place's centre", () => {
for (const st of stops()) {
const place = PLACES.find((p) => p.id === st.placeId)!;
expect(Math.hypot(st.x - place.x, st.z - place.z)).toBeGreaterThan(0.75 * place.radius);
}
});
test("stops are close within a saga and farther apart between sagas", () => {
const st = stops();
const d = (a: number, b: number) => Math.hypot(st[a]!.x - st[b]!.x, st[a]!.z - st[b]!.z);
let maxWithin = 0, minBetween = Infinity;
for (let i = 1; i < st.length; i++) {
if (st[i]!.placeId === st[i - 1]!.placeId) maxWithin = Math.max(maxWithin, d(i, i - 1));
else minBetween = Math.min(minBetween, d(i, i - 1));
}
expect(minBetween).toBeGreaterThan(3 * maxWithin);
});
test("pointAt follows the route through every stop, with headings along the way", () => {
for (const st of stops()) {
const p = pointAt(st.s);
expect(p.x).toBeCloseTo(st.x, 6);
expect(p.z).toBeCloseTo(st.z, 6);
expect(p.heading).toBeCloseTo(st.heading, 6);
}
const mid = pointAt(0.5);
expect(Number.isFinite(mid.x) && Number.isFinite(mid.z) && Number.isFinite(mid.heading)).toBe(true);
expect(pointAt(-1)).toEqual(pointAt(0));
expect(pointAt(2)).toEqual(pointAt(1));
});
test("sIndex maps route position to a fractional song index and back", () => {
const st = stops();
st.forEach((stop, i) => expect(sIndex(stop.s)).toBeCloseTo(i, 6));
const half = (st[5]!.s + st[6]!.s) / 2;
expect(sIndex(half)).toBeCloseTo(5.5, 6);
});
});