/* 454(433-21 ②) 방향키 측점 No. 걸음 — 무작위 짧은 다리 노선(14 IP · 고정 R) 교점마다 네 방향 한 걸음 * 누르고 측점 밖 막힘 수를 JSON 한 줄로(`test_454_arrow_station_step.py` 가 판정). * 누름 판정은 편집기 방향키 길과 같음 — 하한 깸 = 막힘 · 미리보기(`fitDrag`) 측점 밖이 늘면 막힘. */ const fs = require("fs"); const path = require("path"); const { makeLoader } = require("./helper_ts_stub_loader.cjs"); const ROOT = path.join(__dirname, "..", ".."); const { loadTs } = makeLoader({ root: ROOT, requireFor: (full) => (request) => request.startsWith(".") ? loadTs(path.join(path.dirname(full), `${request}.ts`)) : require(request), }); const LOGIC = path.join(ROOT, "B05_Profile", "logic"); const fit = loadTs(path.join(LOGIC, "B05_Profile_RouteEdit_StationFit.ts")); const edits = loadTs(path.join(LOGIC, "B05_Profile_UI_RouteEdit_Edits.ts")); const INTERVAL = 20; const LIMITS = { minRadiusM: 30, limitRadiusM: 12, limitArcM: 0, intervalM: INTERVAL }; const columns = (p) => ({ planned: p, curveOn: p.map(() => true), curveRadius: p.map(() => null), curveLock: p.map(() => null), curveArc: p.map(() => null), apexLock: p.map((_, i) => i === 0 || i === p.length - 1), }); const copy = (c) => ({ ...c, planned: c.planned.map(([x, y]) => [x, y]), curveRadius: [...c.curveRadius], }); const off = (b) => fit.offStationCount(b.vertices, b.curves, INTERVAL); const floor = (b) => edits.curveShortfalls(b.nodes, LIMITS.limitRadiusM, LIMITS.limitArcM); function route(seed) { const rnd = () => (seed = (seed * 16807) % 2147483647) / 2147483647; const line = [[0, 0]]; let a = 0; for (let i = 0; i < 14; i += 1) { a += (rnd() - 0.5) * 1.6; const length = 35 + rnd() * 60; const p = line[line.length - 1]; line.push([p[0] + Math.cos(a) * length, p[1] + Math.sin(a) * length]); } return line; } const out = { press: 0, floorBlock: 0, offBlock: 0, offStart: 0, offAfterRelease: 0 }; for (let seed = 1; seed <= 10; seed += 1) { const hist = columns(fit.fitCurveStations(columns(route(seed)), LIMITS, 1)); hist.curveRadius = edits .rebuildLine(copy(hist), LIMITS) .nodes.map((n) => (n.radius_m === null ? null : Math.round(n.radius_m * 100) / 100)); out.offStart += off(edits.rebuildLine(copy(hist), LIMITS)); for (let k = 1; k < hist.planned.length - 1; k += 1) for (const [dx, dy] of [ [1, 0], [-1, 0], [0, 1], [0, -1], ]) { const st = copy(hist); const b0 = edits.rebuildLine(st, LIMITS); const node = st.planned[k]; st.planned[k] = [node[0] + dx * INTERVAL, node[1] + dy * INTERVAL]; out.press += 1; if (edits.shortfallCrossed(floor(b0), floor(edits.rebuildLine(st, LIMITS)))) { out.floorBlock += 1; continue; } const pv = { ...st, planned: fit.fitDrag(st, hist.planned, k, LIMITS) }; if (off(edits.rebuildLine(copy(pv), LIMITS)) > off(b0)) { out.offBlock += 1; continue; } const rel = { ...pv, planned: fit.fitCurveStations(pv, LIMITS, fit.fitStart(hist, pv)) }; if (off(edits.rebuildLine(copy(rel), LIMITS))) out.offAfterRelease += 1; } } process.stdout.write(JSON.stringify(out));