153 lines
4.6 KiB
JavaScript
153 lines
4.6 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
|
import { createRequire } from 'node:module';
|
|
import { execFileSync } from 'node:child_process';
|
|
|
|
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('does not depend on configuring vector precision before imports', () => {
|
|
const run = (configureBeforeImport) =>
|
|
execFileSync(
|
|
process.execPath,
|
|
[
|
|
'-e',
|
|
`
|
|
const { glMatrix, vec2 } = require('./shared/node_modules/gl-matrix');
|
|
if (${configureBeforeImport}) glMatrix.setMatrixArrayType(Array);
|
|
const s = require('./shared/lib/main.js');
|
|
glMatrix.setMatrixArrayType(Array);
|
|
const body = (offset, radius) => ({
|
|
center: vec2.clone(offset), radius, velocity: vec2.create(),
|
|
lastNormal: [0, 1], restitution: 0,
|
|
});
|
|
const state = {
|
|
head: body(s.headOffset, s.headRadius),
|
|
leftFoot: body(s.leftFootOffset, s.feetRadius),
|
|
rightFoot: body(s.rightFootOffset, s.feetRadius),
|
|
direction: 0, currentPlanet: undefined,
|
|
secondsSinceOnSurface: 1, bodyVelocity: [0, 0],
|
|
};
|
|
const world = {
|
|
groundsNear: () => [],
|
|
stepBody: (body, dt) => s.resolveCircleMovement(body, dt, []),
|
|
};
|
|
for (let i = 0; i < 100; i++) s.stepCharacterMovement(state, world, [1, 0], 0.005);
|
|
process.stdout.write(JSON.stringify(state));
|
|
`,
|
|
],
|
|
{ encoding: 'utf8' },
|
|
);
|
|
expect(run(false)).toBe(run(true));
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|