tower / cascadia / files / test / motion.test.ts
Branch cascadia · default branch · all branches
motion.test.ts
| 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 | }) |