Files
Toolbox/dev_test_scripts/unit/test_retro_pinball_physics.js
yangxiangyuan 4492b4106a feat: 新增3个思想实验室项目,完善私有剪贴板授权与上传功能,添加配套测试脚本
1. 新增怀旧弹珠台、人性与投资、穿越三体、四维空间四个思想实验室项目
2. 为私有剪贴板添加业务层授权校验,优化上传进度显示与鉴权失败处理
3. 新增多个调试与单元测试脚本,覆盖三体积分、弹珠台物理、集成测试等场景
4. 补全穿越三体项目的完整HTML页面资源
2026-08-07 17:49:24 +08:00

153 lines
6.0 KiB
JavaScript

// ============================================================
// dev_test_scripts/unit/test_retro_pinball_physics.js
// 怀旧弹珠台 · 物理核心单元测试
// 从 public/tools/thought_lab/labs/retro_pinball/index.html 中
// 提取 ==PHYSICS_CORE_START== / ==PHYSICS_CORE_END== 之间的纯函数核心,
// 在 node vm 中运行并断言物理正确性。
// 运行:node dev_test_scripts/unit/test_retro_pinball_physics.js
// ============================================================
'use strict';
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const HTML_PATH = path.join(__dirname, '..', '..', 'public', 'tools', 'thought_lab', 'labs', 'retro_pinball', 'index.html');
function loadCore() {
const html = fs.readFileSync(HTML_PATH, 'utf-8');
const s = html.indexOf('/* ==PHYSICS_CORE_START== */');
const e = html.indexOf('/* ==PHYSICS_CORE_END== */');
if (s < 0 || e < 0 || e <= s) throw new Error('未找到物理核心标记块');
const code = html.slice(s, e);
const sandbox = { Math, console };
vm.createContext(sandbox);
vm.runInContext(code, sandbox);
if (!sandbox.PhysicsCore) throw new Error('PhysicsCore 未导出');
return sandbox.PhysicsCore;
}
const PC = loadCore();
let passed = 0, failed = 0;
function check(name, cond, detail) {
if (cond) { passed++; console.log(` [PASS] ${name}`); }
else { failed++; console.error(` [FAIL] ${name}${detail ? ' -> ' + detail : ''}`); }
}
const DT = 1 / 360;
function baseParams(over) {
return Object.assign({
g: 2000, e: 0.55, mu: 0.12, airK: 0, rollR: 0,
restV: 1, iterations: 8, slop: 0.3, correction: 0.8, maxV: 6000
}, over || {});
}
const FLOOR = { type: 'seg', x1: -500, y1: 500, x2: 500, y2: 500 };
// ---------- 1. 恢复系数:回弹高度 ≈ e²·h ----------
(function testRestitution() {
console.log('1) 恢复系数回弹高度');
const e = 0.5, h = 300;
const p = baseParams({ e, mu: 0, restV: 1 });
const b = PC.createBall(0, 500 - 11 - h, 11);
const statics = [FLOOR];
let bounced = false, apex = 1e9, done = false, t = 0;
while (t < 3 && !done) {
PC.stepWorld([b], statics, p, DT);
t += DT;
if (!bounced && b.vy < 0) bounced = true; // 回弹开始(y 向下,vy<0 为向上)
if (bounced) {
apex = Math.min(apex, b.y);
if (b.vy > 0) done = true; // 到达顶点
}
}
const rebound = (500 - 11) - apex;
const expect = e * e * h;
check(`回弹高度 ${rebound.toFixed(1)} ≈ e²h=${expect.toFixed(1)} (±10%)`, Math.abs(rebound - expect) <= expect * 0.1, `got=${rebound}`);
})();
// ---------- 2. 能量不增 ----------
(function testEnergy() {
console.log('2) 机械能不自发增加');
const p = baseParams({ e: 0.5, mu: 0 });
const b = PC.createBall(0, 100, 11);
const statics = [FLOOR];
const E0 = 0.5 * b.m * (b.vx * b.vx + b.vy * b.vy) + b.m * p.g * (500 - b.y);
let maxE = 0;
for (let t = 0; t < 2; t += DT) {
PC.stepWorld([b], statics, p, DT);
const E = 0.5 * b.m * (b.vx * b.vx + b.vy * b.vy) + b.m * p.g * (500 - b.y);
maxE = Math.max(maxE, E);
}
check(`maxE=${maxE.toFixed(0)} <= E0*1.02=${(E0 * 1.02).toFixed(0)}`, maxE <= E0 * 1.02);
})();
// ---------- 3. 静止稳定:不沉陷、不抖动 ----------
(function testRest() {
console.log('3) 水平面静止稳定性');
const p = baseParams({ e: 0.55, mu: 0.2 });
const b = PC.createBall(0, 200, 11);
const statics = [FLOOR];
for (let t = 0; t < 3; t += DT) PC.stepWorld([b], statics, p, DT);
const sp = Math.hypot(b.vx, b.vy);
check(`静止速度 ${sp.toFixed(2)} < 20`, sp < 20);
check(`不沉陷 y=${b.y.toFixed(2)} <= 489+1`, b.y <= 500 - 11 + 1);
})();
// ---------- 4. 等质量弹性碰撞速度交换 ----------
(function testMomentum() {
console.log('4) 等质量 e=1 正碰速度交换');
const p = baseParams({ g: 0, e: 1, mu: 0 });
const a = PC.createBall(0, 0, 11); a.vx = 100;
const b = PC.createBall(30, 0, 11);
for (let t = 0; t < 0.5; t += DT) PC.stepWorld([a, b], [], p, DT);
check(`a.vx=${a.vx.toFixed(1)}≈0`, Math.abs(a.vx) < 3, `got=${a.vx}`);
check(`b.vx=${b.vx.toFixed(1)}≈100`, Math.abs(b.vx - 100) < 3, `got=${b.vx}`);
})();
// ---------- 5. 高速不穿透 ----------
(function testNoTunnel() {
console.log('5) 3000px/s 不穿透薄壁');
const p = baseParams({ g: 0, e: 0.5, mu: 0 });
const wall = { type: 'seg', x1: 100, y1: -1000, x2: 100, y2: 1000 };
const b = PC.createBall(0, 0, 11); b.vx = 3000;
let maxX = -1e9;
for (let t = 0; t < 0.5; t += DT) {
PC.stepWorld([b], [wall], p, DT);
maxX = Math.max(maxX, b.x);
}
check(`maxX=${maxX.toFixed(1)} <= 100`, maxX <= 100 + 0.5);
})();
// ---------- 6. 摩擦减速 + 滚动建立 ----------
(function testFrictionRoll() {
console.log('6) 摩擦使滑动减速并建立滚动');
const mk = (mu) => {
const p = baseParams({ e: 0.3, mu, rollR: 0 });
const b = PC.createBall(0, 500 - 11, 11); b.vx = 500;
for (let t = 0; t < 0.5; t += DT) PC.stepWorld([b], [FLOOR], p, DT);
return b;
};
const noF = mk(0), withF = mk(0.4);
check(`无摩擦保持滑动 |vx|=${noF.vx.toFixed(0)} > 400`, noF.vx > 400);
check(`有摩擦减速 vx=${withF.vx.toFixed(0)} < ${noF.vx.toFixed(0)}`, withF.vx < noF.vx - 50);
check(`滚动建立 omega=${withF.omega.toFixed(1)} > 5`, withF.omega > 5);
})();
// ---------- 7. 斜坡:无摩擦加速下滑 ----------
(function testSlope() {
console.log('7) 斜坡下滑加速度 ≈ g·sinθ');
const theta = Math.PI / 6; // 30°
const p = baseParams({ g: 2000, e: 0, mu: 0 });
const slope = { type: 'seg', x1: 0, y1: 0, x2: 300, y2: 300 * Math.tan(theta) };
const b = PC.createBall(50, 50 * Math.tan(theta) - 11, 11);
const v0 = Math.hypot(b.vx, b.vy);
for (let t = 0; t < 0.3; t += DT) PC.stepWorld([b], [slope], p, DT);
const v1 = Math.hypot(b.vx, b.vy);
const dv = v1 - v0;
const expect = p.g * Math.sin(theta) * 0.3;
check(`dv=${dv.toFixed(0)} ≈ g·sinθ·t=${expect.toFixed(0)} (±8%)`, Math.abs(dv - expect) <= expect * 0.08);
})();
console.log(`\n结果: ${passed} passed, ${failed} failed`);
process.exit(failed > 0 ? 1 : 0);