2026-09-29 09:27:44 -05:00
// test/knifeedge.test.mjs -- S-3 regression: knife-edge overhang.
Physics upgrade + repo modularization (RC-375D84)
Delivered by Hermies Qwen, gated and deployed by Relay.
- Board tip/topple: full continuous rotation when a board's center of
mass passes its support edge (Q1: smooth pivot swing, no discrete
snap). Falling boards deal impact damage and can kill enemies in
their fall path.
- Falling-object impact damage: blocks and birds can now kill enemies
from vertical drops and side plows (velocity-aware crush gate),
chain momentum transfers correctly through stacks (mass-weighted
contact), repeated impacts finish tougher enemies.
- Landed-bird physics: a bird resting on a board remains a real
physical body under continuous gravity (Q2: no lifetime timer). If
its platform is destroyed it falls, damages what it lands on
(chain reaction), and can kill enemies.
- Repo modularized: folders = modules, subfolders = submodules,
architecture.md + MODULES.md at root, an agents.md card in every
module and submodule (17 total).
- Test suite grown from 27 to 47 (20 new RED-watched tests for the
above behaviors), all green. Golden differential against the
original core confirmed the only behavioral changes are the
intended ones.
Live at http://10.244.136.121:8093/, independently re-verified by
Relay (full suite re-run, module structure check, headless Chromium
smoke) before this push.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 08:21:18 -05:00
// 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 ) ;