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); }); });