62 lines
3.3 KiB
JavaScript
62 lines
3.3 KiB
JavaScript
// test/knifeedge.test.mjs -- S-3 regression: knife-edge overhang.
|
|
// A board whose COM is a hair (5px) past the support edge, with the pivot too
|
|
// high for either end to reach the ground, is a pendulum. It must tip ONCE,
|
|
// swing back, and REST (damped resolution) -- it must not re-tip in a
|
|
// livelock loop. This regression was found on real level L3 during the golden
|
|
// audit: 14 re-tips in 0.5s (gaps 0.01-0.04s) at one position; the v1
|
|
// anti-livelock flag was defeated by a velocity re-arm (the pendulum's own
|
|
// swing-back speed cleared the flag), so the board re-tipped forever.
|
|
import { createRequire } from 'node:module';
|
|
const require = createRequire(import.meta.url);
|
|
const ISLE = require('../js/physics/index.js');
|
|
|
|
const DT = 1 / 120;
|
|
let passed = 0, failed = 0;
|
|
function check(name, cond, detail) {
|
|
if (cond) { passed++; console.log(' PASS ' + name); }
|
|
else { failed++; console.log(' FAIL ' + name + (detail ? ' -- ' + detail : '')); }
|
|
}
|
|
function game(def) {
|
|
const lvl = Object.assign({ name: 'knife-test', sling: { x: 260, y: 560 }, birds: [], blocks: [], enemies: [] }, def);
|
|
return ISLE.createGame(lvl);
|
|
}
|
|
// pillar top y=700, right edge x=1100 (pillar 200x200 @1000,800).
|
|
// Board 300x30 center (1105,685): COM at x=1105, only 5px past the pivot edge
|
|
// x=1100. Far corner radius ~158 < 200 (pivot-to-ground) -> the ends can never
|
|
// reach the ground -> pendulum case (settle-back, not a ground landing).
|
|
const PILLAR = { x: 1000, y: 800, w: 200, h: 200, mat: 'stone' };
|
|
const KNIFE = { x: 1105, y: 685, w: 300, h: 30, mat: 'wood' };
|
|
|
|
const g = game({ blocks: [PILLAR, KNIFE] });
|
|
const board = g.blocks[1];
|
|
let tips = 0;
|
|
const origPush = g.events.push.bind(g.events);
|
|
g.events.push = function (ev) { if (ev.type === 'topple_start') tips++; return origPush(ev); };
|
|
|
|
// run 10s (covers ~9 pendulum periods; a livelock would re-tip every period)
|
|
const N = 10 / DT;
|
|
for (let i = 0; i < N; i++) ISLE.step(g, DT);
|
|
|
|
check('knife-edge board tips at all (COM 5px past edge)', tips >= 1, 'tips=' + tips);
|
|
check('NO livelock: a pendulum overhang tips at most ONCE in 10s', tips <= 2, 'tips=' + tips);
|
|
check('board ends at rest (not pivoting)', board.pivoting === false, 'pivoting=' + board.pivoting);
|
|
check('board rests flat after settle-back', Math.abs(board.ang || 0) < 0.02, 'ang=' + (board.ang * 180 / Math.PI).toFixed(1) + 'deg');
|
|
check('board snapped back to its rest position (no drift)', Math.abs(board.x - 1105) < 0.5 && Math.abs(board.y - 685) < 0.5, 'x=' + board.x.toFixed(2) + ' y=' + board.y.toFixed(2));
|
|
|
|
// a REAL knock (displacement >= 6px, COM pushed further past the edge) must
|
|
// re-arm the board to tip again
|
|
{
|
|
let tips2 = 0;
|
|
const g2 = game({ blocks: [PILLAR, KNIFE] });
|
|
const b2 = g2.blocks[1];
|
|
const push2 = g2.events.push.bind(g2.events);
|
|
g2.events.push = function (ev) { if (ev.type === 'topple_start') tips2++; return push2(ev); };
|
|
for (let i = 0; i < 2 / DT; i++) ISLE.step(g2, DT); // settle: tipped once, swung back, resting
|
|
// knock: shove it 20px PAST the edge (COM 1105 -> 1125, well outside span [1000,1100])
|
|
b2.x += 20; b2.vx = 60;
|
|
for (let i = 0; i < 3 / DT; i++) ISLE.step(g2, DT);
|
|
check('a real knock (past the edge) re-arms a settled board to tip again', tips2 >= 2, 'tips=' + tips2);
|
|
}
|
|
|
|
console.log('== ' + passed + ' passed, ' + failed + ' failed ==');
|
|
process.exit(failed ? 1 : 0);
|