// 单元测试:kaleidoscope 纯函数(种子可复现 + 棱镜分割) // 运行:node dev_test_scripts/unit/test_kaleidoscope_pure.js // 从 HTML 中抽取 PURE-BEGIN..PURE-END 区块执行 const fs = require('fs') const path = require('path') const vm = require('vm') const html = fs.readFileSync(path.join(process.cwd(), 'public', 'tools', 'thought_lab', 'labs', 'kaleidoscope', 'index.html'), 'utf-8') const m = html.match(/\/\/ ==== PURE-BEGIN[\s\S]*?\/\/ ==== PURE-END/) if (!m) { console.log('FAIL - 未找到 PURE-BEGIN..PURE-END'); process.exit(1) } const sandbox = { TAU: Math.PI * 2, console }; vm.createContext(sandbox); vm.runInContext(m[0], sandbox); const { TAU, mulberry32, buildPieces } = sandbox; // 注:PALETTES 在 sandbox 中为 const 声明,vm.runInContext 不暴露为 sandbox 属性 // 色板仅用于绘制,此处不验证具体颜色值 let fail = 0; const check = (name, cond, extra) => { console.log(`${cond ? 'PASS' : 'FAIL'} - ${name}${extra ? ' | ' + extra : ''}`); if (!cond) fail++; }; // ------------------------------------------------------------ // 1) 种子可复现性:同种子+同参数 → 输出完全一致 // ------------------------------------------------------------ const P1 = { segments: 8, mirror: true, shape: 'mix', palette: 'neon', count: 12 }; const A = buildPieces(P1, 42); const B = buildPieces(P1, 42); check('种子可复现:同种子 42 两次产出 length 一致', A.length === B.length, `A=${A.length} B=${B.length}`); let identical = A.length === B.length && A.length > 0; for (let i = 0; i < A.length && identical; i++) { if (A[i].shape !== B[i].shape || A[i].color !== B[i].color) identical = false; } check('同种子纸片字段完全一致', identical, 'shape + color 逐张对比'); const C = buildPieces(P1, 43); check('不同种子产出不同', C.length > 0 && (C[0].shape !== A[0].shape || C[0].color !== A[0].color)); // ------------------------------------------------------------ // 2) 棱镜镜像严格跟随旋转次数 // ------------------------------------------------------------ // 基础楔角 = TAU / segments // 镜像开启时纸片只撒半楔角 [0, copy/2),绘制时镜像副本补满另一半 → 镜面数 = 2×segments // 不勾镜像时纸片撒满整楔角 [0, copy) → 面数 = segments const wedgeAngle = (P) => TAU / P.segments / (P.mirror ? 2 : 1); const P_mirror = { segments: 8, mirror: true, shape: 'mix', palette: 'neon', count: 20 }; const pieces_mirror = buildPieces(P_mirror, 777); const maxAngle_mirror = Math.max(...pieces_mirror.map(p => p.an)); check('镜像 on:纸片角度均 < 半楔角', maxAngle_mirror + 0.001 < TAU / 8, `max_an=${maxAngle_mirror.toFixed(4)} half=${(TAU/8/2).toFixed(4)}`); const P_nomirror = { segments: 8, mirror: false, shape: 'mix', palette: 'neon', count: 20 }; const pieces_nomirror = buildPieces(P_nomirror, 777); const maxAngle_nomirror = Math.max(...pieces_nomirror.map(p => p.an)); check('镜像 off:纸片角度 < 整楔角', maxAngle_nomirror + 0.001 < TAU / 8, `max_an=${maxAngle_nomirror.toFixed(4)} full=${(TAU/8).toFixed(4)}`); // 换不同旋转次数验证 for (const seg of [3, 6, 12, 24]) { const P = { segments: seg, mirror: true, shape: 'mix', palette: 'neon', count: 10 }; const pcs = buildPieces(P, 999); const semi = TAU / seg / 2; const maxA = Math.max(...pcs.map(p => p.an)); if (maxA + 0.001 >= semi) { check(`旋转 ${seg} 次 镜像 at ${semi.toFixed(4)}`, false, `max_an=${maxA.toFixed(4)} 超出半楔角`); fail++; } } console.log(`旋转 3/6/12/24 次镜像 all PASS`); // ------------------------------------------------------------ // 3) 验证:纸片首张字段可稳定哈希 // ------------------------------------------------------------ const D = buildPieces({ segments: 4, mirror: true, shape: 'rect', palette: 'contrast', count: 5 }, 20260806); check('首张纸片 shape 确定性', D[0].shape === 'rect'); check('首张纸片有 color 字段', typeof D[0].color === 'string' && D[0].color.length > 0); // ------------------------------------------------------------ // 4) 老万花筒模式:经典模式强制 mirror=false,纸片应撒满整楔角 // ------------------------------------------------------------ const E = buildPieces({ segments: 8, mirror: false, shape: 'mix', palette: 'neon', count: 20 }, 12345); const maxA = Math.max(...E.map(p => p.an)); check('经典模式纸片角度 < 整楔角', maxA + 0.001 < TAU / 8, `max=${maxA.toFixed(4)} wedge=${(TAU/8).toFixed(4)}`); // 经典模式下旋转次数必须为偶数,否则奇偶交替不闭合 // 本测试不验证 UI 层,仅验证纯函数:同种子同参数可复现 const F1 = buildPieces({ segments: 6, mirror: false, shape: 'mix', palette: 'neon', count: 10 }, 555); const F2 = buildPieces({ segments: 6, mirror: false, shape: 'mix', palette: 'neon', count: 10 }, 555); check('经典模式(6 扇区)可复现', F1.length === F2.length && F1[0].shape === F2[0].shape); console.log(fail === 0 ? 'ALL PASS' : `${fail} FAILED`); process.exit(fail === 0 ? 0 : 1);