tower / files / test / motion.test.ts
motion.test.ts
a08492ba 2026-09-07 Pavel Safronov | 1 | import { describe, expect, it } from 'vitest' |
| 2 | import { G0, OMEGA_RAD_S } from '../src/canon/constants.js' | |
| 3 | import { | |
| 4 | RAINDROP_TERMINAL_M_S, freeFallDriftM, freeFallLeanRad, freeFallPathLeanRad, | |
| 5 | freeFallTimeS, steadyFallLeanRad, | |
| 6 | } from '../src/canon/motion.js' | |
| 7 | ||
| 8 | const deg = (r: number): number => (r * 180) / Math.PI | |
| 9 | ||
| 10 | /** | |
| 11 | * These two numbers are the entire visible evidence that Tower spins, so they are | |
| 12 | * worth pinning: the hull is sealed, there are no stars, and everything that does | |
| 13 | * not fall is co-rotating and therefore perfectly still. | |
| 14 | */ | |
| 15 | describe('falling in the rotating frame', () => { | |
| 16 | it('leaves rain very nearly vertical', () => { | |
| 17 | expect(deg(steadyFallLeanRad(RAINDROP_TERMINAL_M_S))).toBeCloseTo(1.5, 1) | |
| 18 | }) | |
| 19 | ||
| 20 | it('leans a waterfall enough to read as a compass', () => { | |
| 21 | // A 1 km drop: eight times the lean of rain, and unmistakable. | |
| 22 | expect(deg(freeFallLeanRad(1000))).toBeCloseTo(11.3, 1) | |
| 23 | }) | |
| 24 | ||
| 25 | it('drifts an object dropped from 1 km by over a hundred metres', () => { | |
| 26 | expect(freeFallDriftM(1000)).toBeCloseTo(133, 0) | |
| 27 | }) | |
| 28 | ||
| 29 | /** | |
| 30 | * design.md §1.9.2 tabulates 7.6° for a kilometre and this view reports 11.3°. | |
| 31 | * Both are correct and they are not the same angle, so pin the relation that | |
| 32 | * makes that true rather than quietly letting the two documents disagree. | |
| 33 | */ | |
| 34 | it('separates the lean of the path from the lean of the velocity', () => { | |
| 35 | expect(deg(freeFallPathLeanRad(1000))).toBeCloseTo(7.6, 1) | |
| 36 | for (const h of [100, 500, 1000, 2000]) { | |
| 37 | expect(Math.tan(freeFallLeanRad(h))).toBeCloseTo(1.5 * Math.tan(freeFallPathLeanRad(h)), 9) | |
| 38 | } | |
| 39 | }) | |
| 40 | ||
| 41 | it('grows the lean with the drop, which is why the waterfall wins', () => { | |
| 42 | const heights = [10, 100, 1000, 4000] | |
| 43 | const leans = heights.map(freeFallLeanRad) | |
| 44 | for (let i = 1; i < leans.length; i++) expect(leans[i]!).toBeGreaterThan(leans[i - 1]!) | |
| 45 | // Rain does not: at terminal velocity the angle is fixed whatever the cloud. | |
| 46 | expect(steadyFallLeanRad(RAINDROP_TERMINAL_M_S)) | |
| 47 | .toBe(steadyFallLeanRad(RAINDROP_TERMINAL_M_S)) | |
| 48 | }) | |
| 49 | ||
| 50 | it('agrees with the closed forms it is derived from', () => { | |
| 51 | const h = 2500 | |
| 52 | const t = freeFallTimeS(h) | |
| 53 | expect(t).toBeCloseTo(Math.sqrt((2 * h) / G0), 9) | |
| 54 | expect(freeFallLeanRad(h)).toBeCloseTo(Math.atan(OMEGA_RAD_S * t), 9) | |
| 55 | expect(freeFallDriftM(h)).toBeCloseTo((OMEGA_RAD_S * G0 * t ** 3) / 3, 6) | |
| 56 | }) | |
| 57 | ||
| 58 | it('leans everything the same way — antispinward, always', () => { | |
| 59 | expect(steadyFallLeanRad(1)).toBeGreaterThan(0) | |
| 60 | expect(freeFallLeanRad(1)).toBeGreaterThan(0) | |
| 61 | }) | |
| 62 | }) |