/** * 调试:retro_pinball 拱形导轨几何搜索 * 目的:找到一组 ARCH(cx,cy,r,a0,a1),让满力发射的球能绕弧 -> 左侧落入 -> 静止落球区 * 运行:node dev_test_scripts/debug/debug_retro_pinball_arch_search.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 defaultParams() { 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)) if (Math.abs(d) < 1e-9) return null 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 const r = Math.hypot(x1 - ux, y1 - uy) return { cx: ux, cy: uy, r: r } } const deg = a => a * 180 / Math.PI function angleOf(c, p) { return deg(Math.atan2(p[1] - c.cy, p[0] - c.cx)) } // 候选:三点(左出口 / 弧顶 / 右端),右端放在发射道上方,让球顺势切入 // laneTop = 发射道左壁顶 (LANE_X,190);右端在右外壁 x=442 附近 const candidates = [] function addCandidate(leftExit, top, rightEnd, label) { const c = fitCircle(leftExit, top, rightEnd) if (!c) return // 角度范围:从右端 -> 顶部 -> 左出口(逆时针,y 向下故角度递减) let aR = angleOf(c, rightEnd) let aT = angleOf(c, top) let aL = angleOf(c, leftExit) // 归一化:弧要从左出口经弧顶(-90°)扫到右端,需 aL < -90 < aR(角度递增) while (aL > aR) aL -= 360 candidates.push({ label, arch: { cx: c.cx, cy: c.cy, r: c.r, a0: aL, a1: aR }, _top: aT, c }) } // 右端扫过发射道:从 (442,180) 到 (442,230),左出口落在钉阵左侧,顶部留 >=30 余量 addCandidate([56, 330], [230, 42], [442, 180], 'C1 右端(442,180)') addCandidate([56, 330], [235, 45], [442, 200], 'C2 右端(442,200)') addCandidate([60, 340], [235, 48], [442, 210], 'C3 右端(442,210)') addCandidate([60, 340], [240, 50], [442, 220], 'C4 右端(442,220)') addCandidate([50, 320], [225, 40], [442, 190], 'C5 右端(442,190)') addCandidate([56, 335], [230, 44], [440, 195], 'C6 右端(440,195)') function simulate(arch, v0, seconds, 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 const tr = { minY: 1e9, enteredLeft: false, nan: false } for (let t = 0; t < seconds; t += DT) { PC.stepWorld([b], table.statics, defaultParams(), DT) tr.minY = Math.min(tr.minY, b.y) if (b.x < 90 && b.y > 200 && b.y < 450) tr.enteredLeft = true if (!isFinite(b.x) || !isFinite(b.y)) { tr.nan = true; break } } tr.b = b tr.sp = Math.hypot(b.vx, b.vy) tr.inField = b.x >= SLOT_X0 && b.x <= SLOT_X1 tr.slot = slotIndexAt(b.x, b.y) return tr } for (const cd of candidates) { const a = cd.arch const tr = simulate(a, 2900, 10, 5) const topOk = tr.minY < 90 const ok = !tr.nan && topOk && tr.enteredLeft && tr.inField && tr.sp < 30 console.log(`${ok ? '★' : ' '} ${cd.label} cx=${a.cx.toFixed(0)} cy=${a.cy.toFixed(0)} r=${a.r.toFixed(0)} a:[${a.a0.toFixed(0)}..${a.a1.toFixed(0)}] topAng=${cd._top.toFixed(0)}`) console.log(` minY=${tr.minY.toFixed(0)} enteredLeft=${tr.enteredLeft} final=(${tr.b.x.toFixed(0)},${tr.b.y.toFixed(0)}) sp=${tr.sp.toFixed(1)} inField=${tr.inField} slot=${tr.slot} nan=${tr.nan}`) }