/** * 调试:retro_pinball 拱形候选的力度扫描 + 精确参数输出 * 运行:node dev_test_scripts/debug/debug_retro_pinball_arch_sweep.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') const html = fs.readFileSync(HTML_PATH, 'utf-8') const grab = (a, b) => { const s = html.indexOf(a), e = html.indexOf(b) if (s < 0 || e < 0 || e <= s) throw new Error('未找到标记块: ' + a) return html.slice(s, e) } const code = grab('/* ==PHYSICS_CORE_START== */', '/* ==PHYSICS_CORE_END== */') + '\n' + grab('/* ==TABLE_GEOM_START== */', '/* ==TABLE_GEOM_END== */') const sandbox = { Math, console } vm.createContext(sandbox) vm.runInContext(code, sandbox) const PC = sandbox.PhysicsCore const { BALL_R, G_PX, SPRING_X, SPRING_TOP, LANE_X, buildTableStatics, slotIndexAt, SLOT_X0, SLOT_X1 } = sandbox const DT = 1 / 360 function params() { return { g: G_PX * Math.sin(14 * Math.PI / 180), e: 0.55, mu: 0.12, airK: 0.02, rollR: 0.25, restV: 50, iterations: 8, slop: 0.3, correction: 0.8, maxV: 4200 }; } function fitCircle(p1, p2, p3) { const [x1, y1] = p1, [x2, y2] = p2, [x3, y3] = p3 const d = 2 * (x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) const ux = ((x1 * x1 + y1 * y1) * (y2 - y3) + (x2 * x2 + y2 * y2) * (y3 - y1) + (x3 * x3 + y3 * y3) * (y1 - y2)) / d const uy = ((x1 * x1 + y1 * y1) * (x3 - x2) + (x2 * x2 + y2 * y2) * (x1 - x3) + (x3 * x3 + y3 * y3) * (x2 - x1)) / d return { cx: ux, cy: uy, r: Math.hypot(x1 - ux, y1 - uy) } } const deg = a => a * 180 / Math.PI function makeArch(leftExit, top, rightEnd) { const c = fitCircle(leftExit, top, rightEnd) let aR = deg(Math.atan2(rightEnd[1] - c.cy, rightEnd[0] - c.cx)) let aL = deg(Math.atan2(leftExit[1] - c.cy, leftExit[0] - c.cx)) while (aL > aR) aL -= 360 return { cx: c.cx, cy: c.cy, r: c.r, a0: aL, a1: aR } } function simulate(arch, v0, seedVx) { sandbox.ARCH = arch const table = buildTableStatics(5, 1) const b = PC.createBall(SPRING_X, SPRING_TOP - BALL_R, BALL_R) b.vy = -v0; b.vx = seedVx || 0 let minY = 1e9, enteredLeft = false, nan = false for (let t = 0; t < 10; t += DT) { PC.stepWorld([b], table.statics, params(), DT) minY = Math.min(minY, b.y) if (b.x < 90 && b.y > 200 && b.y < 450) enteredLeft = true if (!isFinite(b.x) || !isFinite(b.y)) { nan = true; break } } const sp = Math.hypot(b.vx, b.vy) return { minY, enteredLeft, nan, x: b.x, y: b.y, sp, inField: b.x >= SLOT_X0 && b.x <= SLOT_X1, slot: slotIndexAt(b.x, b.y), fellBackLane: b.x > LANE_X }; } // 聚焦最优候选 C4,输出精确值 const CHOSEN = { leftExit: [60, 340], top: [240, 50], rightEnd: [442, 220] } const arch = makeArch(CHOSEN.leftExit, CHOSEN.top, CHOSEN.rightEnd) console.log('选中拱形精确参数:') console.log(` cx=${arch.cx.toFixed(3)} cy=${arch.cy.toFixed(3)} r=${arch.r.toFixed(3)} a0=${arch.a0.toFixed(3)} a1=${arch.a1.toFixed(3)}`) console.log(` 左出口=(${CHOSEN.leftExit}) 弧顶=(${CHOSEN.top}) 右端=(${CHOSEN.rightEnd})`) console.log('') console.log('力度扫描(seedVx=5):') const powers = [900, 1200, 1500, 1800, 2100, 2400, 2700, 2900, 3200] for (const v0 of powers) { const r = simulate(arch, v0, 5) const tag = r.nan ? 'NaN!' : r.fellBackLane ? '落回发射道' : r.enteredLeft ? '绕弧左侧落入' : '其他' console.log(` v0=${v0} minY=${r.minY.toFixed(0)} ${tag} final=(${r.x.toFixed(0)},${r.y.toFixed(0)}) sp=${r.sp.toFixed(0)} slot=${r.slot}`) } console.log('') console.log('满力微扰扫描(v0=2900,落点分布):') const slots = {} for (let sv = -8; sv <= 8; sv += 2) { const r = simulate(arch, 2900, sv) slots[r.slot] = (slots[r.slot] || 0) + 1 console.log(` seedVx=${sv} 落点=(${r.x.toFixed(0)},${r.y.toFixed(0)}) slot=${r.slot} enteredLeft=${r.enteredLeft}`) } console.log(' 落坑分布:', JSON.stringify(slots))