Isle-Slingers/test/knifeedge.test.mjs

63 lines
3.3 KiB
JavaScript
Raw Normal View History

// test/knifeedge.test.mjs -- RC-375D84 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);