import { describe, it, expect } from 'vitest'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); const shared = require('../shared/lib/main.js'); const { vec2 } = require('../shared/node_modules/gl-matrix'); const { stepCharacterMovement, tickPlanetDetachment, resolveCircleMovement, PlanetBase, Random, headRadius, feetRadius, headOffset, leftFootOffset, rightFootOffset, } = shared; const stepSeconds = 1 / 200; const makeBody = (center, radius) => ({ center: vec2.clone(center), radius, velocity: vec2.create(), lastNormal: vec2.fromValues(0, 1), restitution: 0, }); const makeState = (start) => ({ head: makeBody(vec2.add(vec2.create(), start, headOffset), headRadius), leftFoot: makeBody(vec2.add(vec2.create(), start, leftFootOffset), feetRadius), rightFoot: makeBody(vec2.add(vec2.create(), start, rightFootOffset), feetRadius), direction: 0, currentPlanet: undefined, secondsSinceOnSurface: 1, bodyVelocity: vec2.create(), }); const makeWorld = (grounds) => ({ groundsNear: () => grounds, stepBody: (body, dt) => { const hit = resolveCircleMovement(body, dt, grounds); return hit && hit.isGround ? hit : undefined; }, }); const round = (v) => Math.round(v * 1000) / 1000; const roundVec = (v) => [round(v[0]), round(v[1])]; const runSimulation = () => { const state = makeState(vec2.fromValues(100, 100)); const world = makeWorld([]); for (let i = 0; i < 300; i++) { const angle = i * 0.1; stepCharacterMovement( state, world, vec2.fromValues(Math.cos(angle), Math.sin(angle)), stepSeconds, ); } return JSON.stringify({ head: roundVec(state.head.center), leftFoot: roundVec(state.leftFoot.center), rightFoot: roundVec(state.rightFoot.center), bodyVelocity: roundVec(state.bodyVelocity), }); }; const runPlanetSimulation = () => { Random.seed = 20; const planet = new PlanetBase( 1, PlanetBase.createPlanetVertices(vec2.fromValues(0, -900), 1700, 1900, 70), ); planet.angularVelocity = 0.09; const world = makeWorld([planet]); const state = makeState(vec2.fromValues(0, 250)); for (let i = 0; i < 900; i++) { const input = i < 200 ? vec2.create() : vec2.fromValues(i < 600 ? 1 : -1, 0); tickPlanetDetachment(state, stepSeconds); stepCharacterMovement(state, world, input, stepSeconds); planet.advanceRotation(stepSeconds); } return JSON.stringify({ head: roundVec(state.head.center), leftFoot: roundVec(state.leftFoot.center), rightFoot: roundVec(state.rightFoot.center), direction: round(state.direction), onSurface: !!state.currentPlanet, secondsSinceOnSurface: round(state.secondsSinceOnSurface), }); }; describe('shared character simulation determinism', () => { it('produces identical output across independent runs', () => { expect(runSimulation()).toBe(runSimulation()); }); it('matches the pinned reference pose (changes only with intentional physics edits)', () => { expect(runSimulation()).toMatchSnapshot(); }); it('is deterministic on a spinning planet too', () => { expect(runPlanetSimulation()).toBe(runPlanetSimulation()); }); it('matches the pinned reference pose on a spinning planet', () => { expect(runPlanetSimulation()).toMatchSnapshot(); }); });