目录
[二,二十一 - 三十阶魔方(无界面版)](#二,二十一 - 三十阶魔方(无界面版))
[四,三十一 - 四十阶魔方(无界面版)](#四,三十一 - 四十阶魔方(无界面版))
一,自动复原程序
二,二十一 - 三十阶魔方(无界面版)
1,生成程序
把这篇文章里面的2个程序放一起运行,即可得到各阶魔方的随机打乱和复原序列。
2,生成结果
三,三十阶魔方(网页版)
1,网页版程序
JS部分参考三阶魔方
HTML部分:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>三十阶魔方</title>
<style>
:root {
--bg1: #161b26;
--bg2: #1f2635;
--txt: #e8edf5;
--dim: #8fa0bf;
--panel: #1b2231;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
background: radial-gradient(1200px 700px at 50% -10%, var(--bg2), var(--bg1));
color: var(--txt);
padding: 6px 8px 8px;
overflow: hidden;
height: 100vh;
display: flex;
flex-direction: column;
}
h1 {
margin: 0 0 2px;
font-size: 20px;
letter-spacing: 2px;
text-align: center;
flex-shrink: 0;
}
#status {
min-height: 18px;
margin: 0 0 4px;
font-size: 12px;
color: var(--dim);
transition: color .3s;
text-align: center;
flex-shrink: 0;
}
#status.solved { color: #7ee08a; font-weight: bold; animation: pop .55s ease; }
#status.error { color: #ff7a7a; font-weight: bold; }
@keyframes pop {
0% { transform: scale(.6); opacity: 0; }
70% { transform: scale(1.18); }
100% { transform: scale(1); opacity: 1; }
}
#main {
display: flex;
gap: 6px;
flex: 1;
min-height: 0;
width: 100%;
max-width: 100%;
margin: 0 auto;
}
#leftCol {
flex: 0 0 58%;
min-width: 0;
height: 100%;
display: flex;
flex-direction: column;
}
#stage {
width: 100%;
height: 100%;
min-height: 0;
background: transparent;
border-radius: 8px;
overflow: hidden;
}
#rightCol {
flex: 1;
min-width: 0;
height: 100%;
display: flex;
flex-direction: column;
gap: 4px;
overflow: hidden;
}
#controls {
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 4px;
align-items: center;
padding: 4px 6px 6px;
background: var(--panel);
border-radius: 8px;
border: 1px solid #2a3344;
}
.move-row {
display: flex;
gap: 4px;
flex-wrap: wrap;
justify-content: center;
width: 100%;
}
button {
border: none;
border-radius: 6px;
padding: 6px 10px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
background: #37425c;
color: #fff;
box-shadow: 0 2px 0 #1a2130;
transition: transform .06s, background .15s, box-shadow .06s;
min-width: 38px;
letter-spacing: .3px;
flex-shrink: 0;
}
button:hover { background: #46557a; }
button:active { transform: translateY(2px); box-shadow: 0 0 0 #1a2130; }
button.tool { background: #2f7a55; }
button.tool:hover { background: #3b9768; }
button.tool.warn { background: #8a3d3d; }
button.tool.warn:hover { background: #a84c4c; }
button.copy { background: #2a6e8f; }
button.copy:hover { background: #3588ad; }
#autoRunBtn { background: #7a3f8a; }
#autoRunBtn:hover { background: #9550a8; }
#seqInput {
flex: 1;
min-width: 60px;
padding: 5px 8px;
border-radius: 6px;
border: 1px solid #4a5a7a;
background: #161b26;
color: #e8edf5;
font-size: 12px;
font-family: "Cascadia Code", "Consolas", monospace;
width: 100%;
}
#seqInput:focus { outline: none; border-color: #6d88c8; }
#seqRow {
flex-wrap: nowrap;
width: 100%;
}
#recordBox {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
gap: 2px;
overflow: hidden;
}
#recordTitle {
font-size: 13px;
font-weight: 700;
color: #cfd9ea;
letter-spacing: 0.5px;
text-align: center;
flex-shrink: 0;
margin: 0;
}
#recordPanel {
background: var(--panel);
border: 1px solid #2a3344;
border-radius: 8px;
padding: 6px 10px;
font-family: "Cascadia Code", "Consolas", "Courier New", monospace;
font-size: 11px;
line-height: 1.5;
color: #cfd9ea;
white-space: pre-wrap;
word-break: break-all;
flex: 1;
min-height: 0;
overflow: auto;
margin: 0;
}
#copyBtn { width: 100%; flex-shrink: 0; padding: 5px 0; font-size: 12px; }
@media (max-width: 860px) {
body { padding: 4px 6px 6px; }
#main { flex-direction: column; gap: 6px; }
#leftCol { flex: 0 0 55vh; width: 100%; }
#rightCol { flex: 1; min-height: 260px; }
#controls { padding: 4px; }
button { padding: 5px 8px; font-size: 12px; min-width: 34px; }
#stage { height: 100%; }
#recordPanel { font-size: 10px; padding: 4px 8px; }
h1 { font-size: 18px; }
}
@media (max-width: 480px) {
#leftCol { flex: 0 0 45vh; }
#seqRow { flex-wrap: wrap; }
#seqInput { flex: 1 1 100%; }
}
</style>
</head>
<body>
<h1>三十阶魔方</h1>
<div id="status">加载中...</div>
<div id="main">
<div id="leftCol">
<div id="stage"></div>
</div>
<div id="rightCol">
<div id="controls">
<div class="move-row">
<button class="tool" id="scrambleBtn">打乱</button>
<button class="tool warn" id="resetBtn">复原</button>
<button class="tool" id="autoRunBtn">自动运行</button>
</div>
<div class="move-row" id="seqRow">
<input id="seqInput" type="text" placeholder="操作序列,如 U2' F3 R29'" />
<button class="tool" id="seqBtn">执行</button>
</div>
</div>
<div id="recordBox">
<div id="recordTitle">📋 棋谱</div>
<button class="tool copy" id="copyBtn">📋 一键复制棋谱</button>
<pre id="recordPanel"></pre>
</div>
</div>
</div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js"
}
}
</script>
<script type="module">
import * as THREE from 'three';
// ---------- 场景 ----------
const stage = document.getElementById('stage');
const statusEl = document.getElementById('status');
const recordPanel = document.getElementById('recordPanel');
const copyBtn = document.getElementById('copyBtn');
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
function resizeRenderer() {
const w = stage.clientWidth;
const h = stage.clientHeight;
if (w > 0 && h > 0) {
renderer.setSize(w, h, false);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
}
stage.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(30, stage.clientWidth / stage.clientHeight, 0.1, 200);
camera.position.set(56, 43, 71);
camera.lookAt(0, 0, 0);
scene.add(new THREE.AmbientLight(0xffffff, 0.9));
const light1 = new THREE.DirectionalLight(0xffffff, 1.3);
light1.position.set(5, 9, 6);
scene.add(light1);
const light2 = new THREE.DirectionalLight(0xffffff, 0.45);
light2.position.set(-4, -3, -2);
scene.add(light2);
// ---------- 颜色 ----------
const COL_HEX = {
U: '#f5f5f5',
D: '#ffd500',
R: '#3b82f6',
L: '#3aab4e',
F: '#e33c3c',
B: '#ff8c1a'
};
const COL_CHAR = { U: 'W', D: 'Y', R: 'B', L: 'G', F: 'R', B: 'O' };
// ---------- 三十阶坐标 ----------
const N = 30;
const OUT = 14.5;
const POSITIONS = [];
for (let i = 0; i < N; i++) {
POSITIONS.push(-14.5 + i);
}
const SIZE = 0.82;
const AXIS_MAP = {
U: { axis: 'y', side: 1, vec: new THREE.Vector3(0, 1, 0) },
D: { axis: 'y', side: -1, vec: new THREE.Vector3(0, -1, 0) },
R: { axis: 'x', side: 1, vec: new THREE.Vector3(1, 0, 0) },
L: { axis: 'x', side: -1, vec: new THREE.Vector3(-1, 0, 0) },
F: { axis: 'z', side: 1, vec: new THREE.Vector3(0, 0, 1) },
B: { axis: 'z', side: -1, vec: new THREE.Vector3(0, 0, -1) }
};
const FACE_SIGN = {
'R': -1, 'L': 1, 'U': -1, 'D': 1, 'F': -1, 'B': 1
};
function buildLayerDefs() {
const defs = {};
const faces = ['U','D','R','L','F','B'];
for (const f of faces) {
const base = AXIS_MAP[f];
for (let layer = 1; layer <= N-1; layer++) {
const key = layer === 1 ? f : f + layer;
let targets;
if (base.side === 1) {
targets = POSITIONS.slice(N - layer);
} else {
targets = POSITIONS.slice(0, layer);
}
defs[key] = {
axis: base.axis,
targets: targets,
face: f,
sign: FACE_SIGN[f]
};
}
}
return defs;
}
const LAYER_DEFS = buildLayerDefs();
const ROT_DEF = {
x: { axis: 'x' },
y: { axis: 'z' },
z: { axis: 'y' }
};
const FACE_ORDER = ['U','D','L','R','F','B'];
// ---------- 贴纸纹理 ----------
function roundedRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}
function makeStickerTexture(hex) {
const size = 128;
const canvas = document.createElement('canvas');
canvas.width = canvas.height = size;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#0b0b0d';
roundedRect(ctx, 0, 0, size, size, 14);
ctx.fill();
const m = 17;
ctx.fillStyle = hex;
roundedRect(ctx, m, m, size - 2 * m, size - 2 * m, 9);
ctx.fill();
const tex = new THREE.CanvasTexture(canvas);
tex.colorSpace = THREE.SRGBColorSpace;
tex.anisotropy = 4;
return tex;
}
const TEX = {};
for (const f in COL_HEX) TEX[f] = makeStickerTexture(COL_HEX[f]);
const blackPlastic = new THREE.MeshStandardMaterial({ color: 0x0a0a0c, roughness: 0.62, metalness: 0.08 });
function stickerMaterial(face) {
return new THREE.MeshStandardMaterial({ map: TEX[face], roughness: 0.3, metalness: 0.05 });
}
// ---------- 表面块(含逻辑状态) ----------
let cubies = [];
function createCubie(x, y, z) {
const mats = [
x === OUT ? stickerMaterial('R') : blackPlastic,
x === -OUT ? stickerMaterial('L') : blackPlastic,
y === OUT ? stickerMaterial('U') : blackPlastic,
y === -OUT ? stickerMaterial('D') : blackPlastic,
z === OUT ? stickerMaterial('F') : blackPlastic,
z === -OUT ? stickerMaterial('B') : blackPlastic
];
const geo = new THREE.BoxGeometry(SIZE, SIZE, SIZE);
const mesh = new THREE.Mesh(geo, mats);
mesh.position.set(x, y, z);
scene.add(mesh);
const colors = {
px: x === OUT ? COL_CHAR.R : null,
nx: x === -OUT ? COL_CHAR.L : null,
py: y === OUT ? COL_CHAR.U : null,
ny: y === -OUT ? COL_CHAR.D : null,
pz: z === OUT ? COL_CHAR.F : null,
nz: z === -OUT ? COL_CHAR.B : null
};
return {
mesh,
home: new THREE.Vector3(x, y, z),
colors,
pos: new THREE.Vector3(x, y, z),
quat: new THREE.Quaternion()
};
}
function buildCube() {
cubies.forEach(c => scene.remove(c.mesh));
cubies = [];
for (const x of POSITIONS)
for (const y of POSITIONS)
for (const z of POSITIONS) {
if (Math.abs(x) === OUT || Math.abs(y) === OUT || Math.abs(z) === OUT) {
cubies.push(createCubie(x, y, z));
}
}
}
// ---------- 无动画即时旋转 ----------
const AXIS_VEC = {
x: new THREE.Vector3(1, 0, 0),
y: new THREE.Vector3(0, 1, 0),
z: new THREE.Vector3(0, 0, 1)
};
function snap(v) { return Math.round(v * 2) / 2; }
function rotateCubie(c, axis, angle) {
const q = new THREE.Quaternion().setFromAxisAngle(AXIS_VEC[axis], angle);
c.pos.applyQuaternion(q);
c.pos.x = snap(c.pos.x);
c.pos.y = snap(c.pos.y);
c.pos.z = snap(c.pos.z);
c.quat.premultiply(q);
}
function selectCubiesForLayer(faceKey) {
const def = LAYER_DEFS[faceKey];
if (!def) return [];
return cubies.filter(c => {
const val = c.pos[def.axis];
return def.targets.some(t => Math.abs(val - t) < 0.01);
});
}
function applyInstantMove(faceKey, dir) {
if (ROT_DEF[faceKey]) {
const axis = ROT_DEF[faceKey].axis;
const angle = -dir * Math.PI / 2;
cubies.forEach(c => rotateCubie(c, axis, angle));
return;
}
const def = LAYER_DEFS[faceKey];
if (!def) return;
const selected = selectCubiesForLayer(faceKey);
const angle = dir * def.sign * Math.PI / 2;
selected.forEach(c => rotateCubie(c, def.axis, angle));
}
function syncMeshes() {
for (const c of cubies) {
c.mesh.position.copy(c.pos);
c.mesh.quaternion.copy(c.quat);
}
}
// ---------- 工具函数 ----------
function axisToKey(v) {
const ax = Math.abs(v.x), ay = Math.abs(v.y), az = Math.abs(v.z);
if (ax >= ay && ax >= az) return v.x >= 0 ? 'px' : 'nx';
if (ay >= ax && ay >= az) return v.y >= 0 ? 'py' : 'ny';
return v.z >= 0 ? 'pz' : 'nz';
}
function isSolved() {
const st = getState();
for (const f of FACE_ORDER) {
const s = st[f];
if (!s || s.length !== N*N) return false;
for (let i = 1; i < N*N; i++) {
if (s[i] !== s[0]) return false;
}
}
return true;
}
// ---------- 局面读取(基于逻辑状态) ----------
function getFaceSlots(face) {
const s = [];
for (let r = 0; r < N; r++)
for (let c = 0; c < N; c++) {
const x = c - (N-1)/2;
const yTop = (N-1)/2 - r;
const z = r - (N-1)/2;
let p;
if (face === 'U') p = [x, OUT, z];
else if (face === 'D') p = [x, -OUT, z];
else if (face === 'F') p = [x, yTop, OUT];
else if (face === 'B') p = [-x, yTop, -OUT];
else if (face === 'R') p = [OUT, yTop, -x];
else p = [-OUT, yTop, x];
s.push(new THREE.Vector3(p[0], p[1], p[2]));
}
return s;
}
function faceColorString(face) {
const slots = getFaceSlots(face);
const n = AXIS_MAP[face].vec;
let out = '';
for (const p of slots) {
const cubie = cubies.find(c => c.pos.distanceTo(p) < 0.01);
if (!cubie) { out += '?'; continue; }
const n0 = n.clone().applyQuaternion(cubie.quat.clone().invert());
out += cubie.colors[axisToKey(n0)] || '?';
}
return out;
}
function getState() {
const state = {};
for (const f of FACE_ORDER) state[f] = faceColorString(f);
return state;
}
function stateToText(state) {
return FACE_ORDER.map(f => f + ': ' + state[f]).join('\n');
}
function moveNotation(m) {
return m.face + (m.dir === -1 ? "'" : '');
}
// 每 1000 个操作换行一次,单个操作(如 R29')不被换行符打断
function seqToText(seq) {
if (!seq.length) return '(无)';
const lines = [];
let cur = [];
for (const m of seq) {
cur.push(moveNotation(m));
if (cur.length === 1000) {
lines.push(cur.join(' '));
cur = [];
}
}
if (cur.length) lines.push(cur.join(' '));
return lines.join('\n');
}
// ---------- 棋谱 ----------
let moveCount = 0;
let scrambleSeq = [];
let userSeq = [];
let initialState = null;
function buildRecordText() {
const lines = [];
lines.push('三十阶魔方棋谱 · 层操作');
lines.push('');
lines.push('【打乱序列】');
lines.push(seqToText(scrambleSeq));
lines.push('');
lines.push('【初始局面(打乱后)】');
lines.push(initialState ? stateToText(initialState) : '(打乱中...)');
lines.push('');
lines.push('【当前局面】');
lines.push(stateToText(getState()));
lines.push('');
lines.push('【完整操作序列】');
lines.push(seqToText([...scrambleSeq, ...userSeq]));
return lines.join('\n');
}
function updateRecordPanel() {
recordPanel.textContent = buildRecordText();
}
function refreshVisual() {
syncMeshes();
updateRecordPanel();
}
async function copyRecord() {
const text = buildRecordText();
let ok = false;
try {
await navigator.clipboard.writeText(text);
ok = true;
} catch (e) {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
try { ok = document.execCommand('copy'); } catch (_) {}
ta.remove();
}
const old = copyBtn.textContent;
copyBtn.textContent = ok ? '✅ 已复制' : '❌ 复制失败';
setTimeout(() => { copyBtn.textContent = old; }, 1500);
}
// ---------- 序列解析与执行 ----------
function parseSequence(text) {
const normalized = text.replace(/['′]/g, "'");
const tokens = normalized.trim().split(/[\s,,]+/).filter(Boolean);
const moves = [];
const errors = [];
for (const raw of tokens) {
let m = raw.match(/^([URFDLB])(2[0-9]|1[0-9]|[2-9])?([''])?$/);
if (m) {
const letter = m[1];
const layerNum = m[2] || '';
const dir = m[3] === "'" ? -1 : 1;
let faceKey = layerNum === '' ? letter : letter + layerNum;
if (!LAYER_DEFS[faceKey]) {
errors.push('未知操作: ' + raw);
continue;
}
moves.push({ face: faceKey, dir });
continue;
}
const rotMatch = raw.match(/^([xyz])([''])?$/);
if (rotMatch) {
moves.push({ face: rotMatch[1], dir: rotMatch[2] === "'" ? -1 : 1 });
continue;
}
errors.push('无法解析: ' + raw);
}
return { moves, errors };
}
async function executeSequence(text) {
const { moves, errors } = parseSequence(text);
if (errors.length > 0) {
statusEl.textContent = '❌ 非法输入: ' + errors.join('; ');
statusEl.classList.add('error');
statusEl.classList.remove('solved');
return;
}
if (moves.length === 0) {
statusEl.textContent = '⚠️ 未检测到有效操作';
statusEl.classList.remove('solved', 'error');
return;
}
statusEl.classList.remove('error');
for (let i = 0; i < moves.length; i++) {
const mv = moves[i];
const isRot = !!ROT_DEF[mv.face];
applyInstantMove(mv.face, mv.dir);
userSeq.push({ face: mv.face, dir: mv.dir });
if (!isRot) moveCount++;
const isChunkEnd = (i + 1) % 100 === 0;
const isLast = i === moves.length - 1;
if (isChunkEnd || isLast) {
refreshVisual();
await new Promise(r => setTimeout(r, 0));
}
}
if (isSolved()) {
statusEl.textContent = '🎉 已复原!共用了 ' + moveCount + ' 步';
statusEl.classList.add('solved');
statusEl.classList.remove('error');
} else {
statusEl.textContent = '序列执行完成(' + moves.length + ' 步操作)';
statusEl.classList.remove('solved', 'error');
}
}
// ---------- 自动运行 ----------
const SERVER = 'http://localhost:3000';
async function autoRun() {
const stateText = stateToText(getState());
try {
const res = await fetch(SERVER + '/write-input', {
method: 'POST',
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
body: stateText
});
if (!res.ok) throw new Error('write failed');
} catch (e) {
statusEl.textContent = '❌ 无法连接本地服务,请先运行 server.js 并从 http://localhost:3000 打开页面';
statusEl.classList.add('error');
statusEl.classList.remove('solved');
return;
}
statusEl.textContent = '已写入 D:/cubeInput.txt,等待 D:/cubeOutput.txt...';
statusEl.classList.remove('solved', 'error');
let content = '';
const maxWait = 120000;
const start = performance.now();
while (true) {
if (performance.now() - start > maxWait) {
statusEl.textContent = '等待 D:/cubeOutput.txt 超时';
statusEl.classList.add('error');
statusEl.classList.remove('solved');
return;
}
try {
const res = await fetch(SERVER + '/read-output');
const data = await res.json();
content = (data.content || '').trim();
} catch (e) {}
if (content) break;
await new Promise(r => setTimeout(r, 500));
}
await executeSequence(content);
}
// ---------- 打乱 / 复原 ----------
function resetCubeInternal() {
cubies.forEach(c => {
c.pos.copy(c.home);
c.quat.identity();
});
syncMeshes();
}
function generateScramble(n = 100) {
const faces = Object.keys(LAYER_DEFS);
const seq = [];
let last = null;
for (let i = 0; i < n; i++) {
let f, d;
do {
f = faces[Math.floor(Math.random() * faces.length)];
d = Math.random() < 0.5 ? 1 : -1;
} while (last && last === f);
seq.push({ face: f, dir: d });
last = f;
}
return seq;
}
async function scramble() {
resetCubeInternal();
userSeq = [];
initialState = null;
moveCount = 0;
statusEl.textContent = '打乱中...';
statusEl.classList.remove('solved', 'error');
scrambleSeq = generateScramble(100);
if (scrambleSeq.length === 0) {
statusEl.textContent = '打乱序列为空';
return;
}
for (let i = 0; i < scrambleSeq.length; i++) {
applyInstantMove(scrambleSeq[i].face, scrambleSeq[i].dir);
const isChunkEnd = (i + 1) % 100 === 0;
const isLast = i === scrambleSeq.length - 1;
if (isChunkEnd || isLast) {
refreshVisual();
await new Promise(r => setTimeout(r, 0));
}
}
initialState = getState();
moveCount = 0;
statusEl.textContent = '已打乱,开始复原吧';
statusEl.classList.remove('error');
}
function resetCube() {
resetCubeInternal();
moveCount = 0;
scrambleSeq = [];
userSeq = [];
initialState = null;
statusEl.textContent = '已还原到初始状态';
statusEl.classList.remove('solved', 'error');
refreshVisual();
}
// ---------- 按钮 & 键盘 ----------
function buildButtons() {
document.getElementById('scrambleBtn').addEventListener('click', scramble);
document.getElementById('resetBtn').addEventListener('click', resetCube);
copyBtn.addEventListener('click', copyRecord);
const seqInput = document.getElementById('seqInput');
const seqBtn = document.getElementById('seqBtn');
seqBtn.addEventListener('click', () => executeSequence(seqInput.value));
seqInput.addEventListener('keydown', e => {
if (e.key === 'Enter') {
e.preventDefault();
executeSequence(seqInput.value);
}
});
document.getElementById('autoRunBtn').addEventListener('click', autoRun);
window.addEventListener('keydown', e => {
const key = e.key.toUpperCase();
if (/^[URFDLB]$/.test(key)) {
const dir = e.shiftKey ? -1 : 1;
applyInstantMove(key, dir);
userSeq.push({ face: key, dir });
moveCount++;
refreshVisual();
if (isSolved()) {
statusEl.textContent = '🎉 已复原!共用了 ' + moveCount + ' 步';
statusEl.classList.add('solved');
statusEl.classList.remove('error');
} else {
statusEl.textContent = '步数:' + moveCount;
statusEl.classList.remove('solved', 'error');
}
return;
}
if (/^[XYZ]$/.test(key)) {
const face = key.toLowerCase();
const dir = e.shiftKey ? -1 : 1;
applyInstantMove(face, dir);
userSeq.push({ face, dir });
refreshVisual();
return;
}
});
}
// ---------- 渲染循环 ----------
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
// ---------- 窗口自适应 ----------
function onResize() {
resizeRenderer();
}
window.addEventListener('resize', onResize);
const ro = new ResizeObserver(() => { resizeRenderer(); });
ro.observe(stage);
// ---------- 启动 ----------
buildCube();
buildButtons();
statusEl.textContent = '已就绪';
updateRecordPanel();
setTimeout(() => {
scramble();
}, 100);
animate();
setTimeout(resizeRenderer, 50);
</script>
</body>
</html>

2,自动复原

四,三十一 - 四十阶魔方(无界面版)
1,生成程序
把这篇文章里面的2个程序放一起运行,即可得到各阶魔方的随机打乱和复原序列。
2,生成结果
五,四十阶魔方(网页版)
1,网页版程序
JS部分参考三阶魔方
HTML部分:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>四十阶魔方</title>
<style>
:root {
--bg1: #161b26;
--bg2: #1f2635;
--txt: #e8edf5;
--dim: #8fa0bf;
--panel: #1b2231;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
background: radial-gradient(1200px 700px at 50% -10%, var(--bg2), var(--bg1));
color: var(--txt);
padding: 6px 8px 8px;
overflow: hidden;
height: 100vh;
display: flex;
flex-direction: column;
}
h1 {
margin: 0 0 2px;
font-size: 20px;
letter-spacing: 2px;
text-align: center;
flex-shrink: 0;
}
#status {
min-height: 18px;
margin: 0 0 4px;
font-size: 12px;
color: var(--dim);
transition: color .3s;
text-align: center;
flex-shrink: 0;
}
#status.solved { color: #7ee08a; font-weight: bold; animation: pop .55s ease; }
#status.error { color: #ff7a7a; font-weight: bold; }
@keyframes pop {
0% { transform: scale(.6); opacity: 0; }
70% { transform: scale(1.18); }
100% { transform: scale(1); opacity: 1; }
}
#main {
display: flex;
gap: 6px;
flex: 1;
min-height: 0;
width: 100%;
max-width: 100%;
margin: 0 auto;
}
#leftCol {
flex: 0 0 58%;
min-width: 0;
height: 100%;
display: flex;
flex-direction: column;
}
#stage {
width: 100%;
height: 100%;
min-height: 0;
background: transparent;
border-radius: 8px;
overflow: hidden;
}
#rightCol {
flex: 1;
min-width: 0;
height: 100%;
display: flex;
flex-direction: column;
gap: 4px;
overflow: hidden;
}
#controls {
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 4px;
align-items: center;
padding: 4px 6px 6px;
background: var(--panel);
border-radius: 8px;
border: 1px solid #2a3344;
}
.move-row {
display: flex;
gap: 4px;
flex-wrap: wrap;
justify-content: center;
width: 100%;
}
button {
border: none;
border-radius: 6px;
padding: 6px 10px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
background: #37425c;
color: #fff;
box-shadow: 0 2px 0 #1a2130;
transition: transform .06s, background .15s, box-shadow .06s;
min-width: 38px;
letter-spacing: .3px;
flex-shrink: 0;
}
button:hover { background: #46557a; }
button:active { transform: translateY(2px); box-shadow: 0 0 0 #1a2130; }
button.tool { background: #2f7a55; }
button.tool:hover { background: #3b9768; }
button.tool.warn { background: #8a3d3d; }
button.tool.warn:hover { background: #a84c4c; }
button.copy { background: #2a6e8f; }
button.copy:hover { background: #3588ad; }
#autoRunBtn { background: #7a3f8a; }
#autoRunBtn:hover { background: #9550a8; }
#seqInput {
flex: 1;
min-width: 60px;
padding: 5px 8px;
border-radius: 6px;
border: 1px solid #4a5a7a;
background: #161b26;
color: #e8edf5;
font-size: 12px;
font-family: "Cascadia Code", "Consolas", monospace;
width: 100%;
}
#seqInput:focus { outline: none; border-color: #6d88c8; }
#seqRow {
flex-wrap: nowrap;
width: 100%;
}
#recordBox {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
gap: 2px;
overflow: hidden;
}
#recordTitle {
font-size: 13px;
font-weight: 700;
color: #cfd9ea;
letter-spacing: 0.5px;
text-align: center;
flex-shrink: 0;
margin: 0;
}
#recordPanel {
background: var(--panel);
border: 1px solid #2a3344;
border-radius: 8px;
padding: 6px 10px;
font-family: "Cascadia Code", "Consolas", "Courier New", monospace;
font-size: 11px;
line-height: 1.5;
color: #cfd9ea;
white-space: pre-wrap;
word-break: break-all;
flex: 1;
min-height: 0;
overflow: auto;
margin: 0;
}
#copyBtn { width: 100%; flex-shrink: 0; padding: 5px 0; font-size: 12px; }
@media (max-width: 860px) {
body { padding: 4px 6px 6px; }
#main { flex-direction: column; gap: 6px; }
#leftCol { flex: 0 0 55vh; width: 100%; }
#rightCol { flex: 1; min-height: 260px; }
#controls { padding: 4px; }
button { padding: 5px 8px; font-size: 12px; min-width: 34px; }
#stage { height: 100%; }
#recordPanel { font-size: 10px; padding: 4px 8px; }
h1 { font-size: 18px; }
}
@media (max-width: 480px) {
#leftCol { flex: 0 0 45vh; }
#seqRow { flex-wrap: wrap; }
#seqInput { flex: 1 1 100%; }
}
</style>
</head>
<body>
<h1>四十阶魔方</h1>
<div id="status">加载中...</div>
<div id="main">
<div id="leftCol">
<div id="stage"></div>
</div>
<div id="rightCol">
<div id="controls">
<div class="move-row">
<button class="tool" id="scrambleBtn">打乱</button>
<button class="tool warn" id="resetBtn">复原</button>
<button class="tool" id="autoRunBtn">自动运行</button>
</div>
<div class="move-row" id="seqRow">
<input id="seqInput" type="text" placeholder="操作序列,如 U2' F3 R39'" />
<button class="tool" id="seqBtn">执行</button>
</div>
</div>
<div id="recordBox">
<div id="recordTitle">📋 棋谱</div>
<button class="tool copy" id="copyBtn">📋 一键复制棋谱</button>
<pre id="recordPanel"></pre>
</div>
</div>
</div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js"
}
}
</script>
<script type="module">
import * as THREE from 'three';
// ---------- 场景 ----------
const stage = document.getElementById('stage');
const statusEl = document.getElementById('status');
const recordPanel = document.getElementById('recordPanel');
const copyBtn = document.getElementById('copyBtn');
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
function resizeRenderer() {
const w = stage.clientWidth;
const h = stage.clientHeight;
if (w > 0 && h > 0) {
renderer.setSize(w, h, false);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
}
stage.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(30, stage.clientWidth / stage.clientHeight, 0.1, 300);
camera.position.set(76, 58, 96);
camera.lookAt(0, 0, 0);
scene.add(new THREE.AmbientLight(0xffffff, 0.9));
const light1 = new THREE.DirectionalLight(0xffffff, 1.3);
light1.position.set(5, 9, 6);
scene.add(light1);
const light2 = new THREE.DirectionalLight(0xffffff, 0.45);
light2.position.set(-4, -3, -2);
scene.add(light2);
// ---------- 颜色 ----------
const COL_HEX = {
U: '#f5f5f5',
D: '#ffd500',
R: '#3b82f6',
L: '#3aab4e',
F: '#e33c3c',
B: '#ff8c1a'
};
const COL_CHAR = { U: 'W', D: 'Y', R: 'B', L: 'G', F: 'R', B: 'O' };
// ---------- 四十阶坐标 ----------
const N = 40;
const OUT = 19.5;
const POSITIONS = [];
for (let i = 0; i < N; i++) {
POSITIONS.push(-19.5 + i);
}
const SIZE = 0.82;
const AXIS_MAP = {
U: { axis: 'y', side: 1, vec: new THREE.Vector3(0, 1, 0) },
D: { axis: 'y', side: -1, vec: new THREE.Vector3(0, -1, 0) },
R: { axis: 'x', side: 1, vec: new THREE.Vector3(1, 0, 0) },
L: { axis: 'x', side: -1, vec: new THREE.Vector3(-1, 0, 0) },
F: { axis: 'z', side: 1, vec: new THREE.Vector3(0, 0, 1) },
B: { axis: 'z', side: -1, vec: new THREE.Vector3(0, 0, -1) }
};
const FACE_SIGN = {
'R': -1, 'L': 1, 'U': -1, 'D': 1, 'F': -1, 'B': 1
};
function buildLayerDefs() {
const defs = {};
const faces = ['U','D','R','L','F','B'];
for (const f of faces) {
const base = AXIS_MAP[f];
for (let layer = 1; layer <= N-1; layer++) {
const key = layer === 1 ? f : f + layer;
let targets;
if (base.side === 1) {
targets = POSITIONS.slice(N - layer);
} else {
targets = POSITIONS.slice(0, layer);
}
defs[key] = {
axis: base.axis,
targets: targets,
face: f,
sign: FACE_SIGN[f]
};
}
}
return defs;
}
const LAYER_DEFS = buildLayerDefs();
const ROT_DEF = {
x: { axis: 'x' },
y: { axis: 'z' },
z: { axis: 'y' }
};
const FACE_ORDER = ['U','D','L','R','F','B'];
// ---------- 贴纸纹理 ----------
function roundedRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}
function makeStickerTexture(hex) {
const size = 128;
const canvas = document.createElement('canvas');
canvas.width = canvas.height = size;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#0b0b0d';
roundedRect(ctx, 0, 0, size, size, 14);
ctx.fill();
const m = 17;
ctx.fillStyle = hex;
roundedRect(ctx, m, m, size - 2 * m, size - 2 * m, 9);
ctx.fill();
const tex = new THREE.CanvasTexture(canvas);
tex.colorSpace = THREE.SRGBColorSpace;
tex.anisotropy = 4;
return tex;
}
const TEX = {};
for (const f in COL_HEX) TEX[f] = makeStickerTexture(COL_HEX[f]);
const blackPlastic = new THREE.MeshStandardMaterial({ color: 0x0a0a0c, roughness: 0.62, metalness: 0.08 });
function stickerMaterial(face) {
return new THREE.MeshStandardMaterial({ map: TEX[face], roughness: 0.3, metalness: 0.05 });
}
// ---------- 表面块(含逻辑状态) ----------
let cubies = [];
function createCubie(x, y, z) {
const mats = [
x === OUT ? stickerMaterial('R') : blackPlastic,
x === -OUT ? stickerMaterial('L') : blackPlastic,
y === OUT ? stickerMaterial('U') : blackPlastic,
y === -OUT ? stickerMaterial('D') : blackPlastic,
z === OUT ? stickerMaterial('F') : blackPlastic,
z === -OUT ? stickerMaterial('B') : blackPlastic
];
const geo = new THREE.BoxGeometry(SIZE, SIZE, SIZE);
const mesh = new THREE.Mesh(geo, mats);
mesh.position.set(x, y, z);
scene.add(mesh);
const colors = {
px: x === OUT ? COL_CHAR.R : null,
nx: x === -OUT ? COL_CHAR.L : null,
py: y === OUT ? COL_CHAR.U : null,
ny: y === -OUT ? COL_CHAR.D : null,
pz: z === OUT ? COL_CHAR.F : null,
nz: z === -OUT ? COL_CHAR.B : null
};
return {
mesh,
home: new THREE.Vector3(x, y, z),
colors,
pos: new THREE.Vector3(x, y, z),
quat: new THREE.Quaternion()
};
}
function buildCube() {
cubies.forEach(c => scene.remove(c.mesh));
cubies = [];
for (const x of POSITIONS)
for (const y of POSITIONS)
for (const z of POSITIONS) {
if (Math.abs(x) === OUT || Math.abs(y) === OUT || Math.abs(z) === OUT) {
cubies.push(createCubie(x, y, z));
}
}
}
// ---------- 无动画即时旋转 ----------
const AXIS_VEC = {
x: new THREE.Vector3(1, 0, 0),
y: new THREE.Vector3(0, 1, 0),
z: new THREE.Vector3(0, 0, 1)
};
function snap(v) { return Math.round(v * 2) / 2; }
function rotateCubie(c, axis, angle) {
const q = new THREE.Quaternion().setFromAxisAngle(AXIS_VEC[axis], angle);
c.pos.applyQuaternion(q);
c.pos.x = snap(c.pos.x);
c.pos.y = snap(c.pos.y);
c.pos.z = snap(c.pos.z);
c.quat.premultiply(q);
}
function selectCubiesForLayer(faceKey) {
const def = LAYER_DEFS[faceKey];
if (!def) return [];
return cubies.filter(c => {
const val = c.pos[def.axis];
return def.targets.some(t => Math.abs(val - t) < 0.01);
});
}
function applyInstantMove(faceKey, dir) {
if (ROT_DEF[faceKey]) {
const axis = ROT_DEF[faceKey].axis;
const angle = -dir * Math.PI / 2;
cubies.forEach(c => rotateCubie(c, axis, angle));
return;
}
const def = LAYER_DEFS[faceKey];
if (!def) return;
const selected = selectCubiesForLayer(faceKey);
const angle = dir * def.sign * Math.PI / 2;
selected.forEach(c => rotateCubie(c, def.axis, angle));
}
function syncMeshes() {
for (const c of cubies) {
c.mesh.position.copy(c.pos);
c.mesh.quaternion.copy(c.quat);
}
}
// ---------- 工具函数 ----------
function axisToKey(v) {
const ax = Math.abs(v.x), ay = Math.abs(v.y), az = Math.abs(v.z);
if (ax >= ay && ax >= az) return v.x >= 0 ? 'px' : 'nx';
if (ay >= ax && ay >= az) return v.y >= 0 ? 'py' : 'ny';
return v.z >= 0 ? 'pz' : 'nz';
}
function isSolved() {
const st = getState();
for (const f of FACE_ORDER) {
const s = st[f];
if (!s || s.length !== N*N) return false;
for (let i = 1; i < N*N; i++) {
if (s[i] !== s[0]) return false;
}
}
return true;
}
// ---------- 局面读取(基于逻辑状态) ----------
function getFaceSlots(face) {
const s = [];
for (let r = 0; r < N; r++)
for (let c = 0; c < N; c++) {
const x = c - (N-1)/2;
const yTop = (N-1)/2 - r;
const z = r - (N-1)/2;
let p;
if (face === 'U') p = [x, OUT, z];
else if (face === 'D') p = [x, -OUT, z];
else if (face === 'F') p = [x, yTop, OUT];
else if (face === 'B') p = [-x, yTop, -OUT];
else if (face === 'R') p = [OUT, yTop, -x];
else p = [-OUT, yTop, x];
s.push(new THREE.Vector3(p[0], p[1], p[2]));
}
return s;
}
function faceColorString(face) {
const slots = getFaceSlots(face);
const n = AXIS_MAP[face].vec;
let out = '';
for (const p of slots) {
const cubie = cubies.find(c => c.pos.distanceTo(p) < 0.01);
if (!cubie) { out += '?'; continue; }
const n0 = n.clone().applyQuaternion(cubie.quat.clone().invert());
out += cubie.colors[axisToKey(n0)] || '?';
}
return out;
}
function getState() {
const state = {};
for (const f of FACE_ORDER) state[f] = faceColorString(f);
return state;
}
function stateToText(state) {
return FACE_ORDER.map(f => f + ': ' + state[f]).join('\n');
}
function moveNotation(m) {
return m.face + (m.dir === -1 ? "'" : '');
}
// 每 1000 个操作换行一次,单个操作(如 R39')不被换行符打断
function seqToText(seq) {
if (!seq.length) return '(无)';
const lines = [];
let cur = [];
for (const m of seq) {
cur.push(moveNotation(m));
if (cur.length === 1000) {
lines.push(cur.join(' '));
cur = [];
}
}
if (cur.length) lines.push(cur.join(' '));
return lines.join('\n');
}
// ---------- 棋谱 ----------
let moveCount = 0;
let scrambleSeq = [];
let userSeq = [];
let initialState = null;
function buildRecordText() {
const lines = [];
lines.push('四十阶魔方棋谱 · 层操作');
lines.push('');
lines.push('【打乱序列】');
lines.push(seqToText(scrambleSeq));
lines.push('');
lines.push('【初始局面(打乱后)】');
lines.push(initialState ? stateToText(initialState) : '(打乱中...)');
lines.push('');
lines.push('【当前局面】');
lines.push(stateToText(getState()));
lines.push('');
lines.push('【完整操作序列】');
lines.push(seqToText([...scrambleSeq, ...userSeq]));
return lines.join('\n');
}
function updateRecordPanel() {
recordPanel.textContent = buildRecordText();
}
function refreshVisual() {
syncMeshes();
updateRecordPanel();
}
async function copyRecord() {
const text = buildRecordText();
let ok = false;
try {
await navigator.clipboard.writeText(text);
ok = true;
} catch (e) {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
try { ok = document.execCommand('copy'); } catch (_) {}
ta.remove();
}
const old = copyBtn.textContent;
copyBtn.textContent = ok ? '✅ 已复制' : '❌ 复制失败';
setTimeout(() => { copyBtn.textContent = old; }, 1500);
}
// ---------- 序列解析与执行 ----------
function parseSequence(text) {
const normalized = text.replace(/['′]/g, "'");
const tokens = normalized.trim().split(/[\s,,]+/).filter(Boolean);
const moves = [];
const errors = [];
for (const raw of tokens) {
let m = raw.match(/^([URFDLB])(3[0-9]|[12][0-9]|[2-9])?([''])?$/);
if (m) {
const letter = m[1];
const layerNum = m[2] || '';
const dir = m[3] === "'" ? -1 : 1;
let faceKey = layerNum === '' ? letter : letter + layerNum;
if (!LAYER_DEFS[faceKey]) {
errors.push('未知操作: ' + raw);
continue;
}
moves.push({ face: faceKey, dir });
continue;
}
const rotMatch = raw.match(/^([xyz])([''])?$/);
if (rotMatch) {
moves.push({ face: rotMatch[1], dir: rotMatch[2] === "'" ? -1 : 1 });
continue;
}
errors.push('无法解析: ' + raw);
}
return { moves, errors };
}
async function executeSequence(text) {
const { moves, errors } = parseSequence(text);
if (errors.length > 0) {
statusEl.textContent = '❌ 非法输入: ' + errors.join('; ');
statusEl.classList.add('error');
statusEl.classList.remove('solved');
return;
}
if (moves.length === 0) {
statusEl.textContent = '⚠️ 未检测到有效操作';
statusEl.classList.remove('solved', 'error');
return;
}
statusEl.classList.remove('error');
for (let i = 0; i < moves.length; i++) {
const mv = moves[i];
const isRot = !!ROT_DEF[mv.face];
applyInstantMove(mv.face, mv.dir);
userSeq.push({ face: mv.face, dir: mv.dir });
if (!isRot) moveCount++;
const isChunkEnd = (i + 1) % 100 === 0;
const isLast = i === moves.length - 1;
if (isChunkEnd || isLast) {
refreshVisual();
await new Promise(r => setTimeout(r, 0));
}
}
if (isSolved()) {
statusEl.textContent = '🎉 已复原!共用了 ' + moveCount + ' 步';
statusEl.classList.add('solved');
statusEl.classList.remove('error');
} else {
statusEl.textContent = '序列执行完成(' + moves.length + ' 步操作)';
statusEl.classList.remove('solved', 'error');
}
}
// ---------- 自动运行 ----------
const SERVER = 'http://localhost:3000';
async function autoRun() {
const stateText = stateToText(getState());
try {
const res = await fetch(SERVER + '/write-input', {
method: 'POST',
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
body: stateText
});
if (!res.ok) throw new Error('write failed');
} catch (e) {
statusEl.textContent = '❌ 无法连接本地服务,请先运行 server.js 并从 http://localhost:3000 打开页面';
statusEl.classList.add('error');
statusEl.classList.remove('solved');
return;
}
statusEl.textContent = '已写入 D:/cubeInput.txt,等待 D:/cubeOutput.txt...';
statusEl.classList.remove('solved', 'error');
let content = '';
const maxWait = 120000;
const start = performance.now();
while (true) {
if (performance.now() - start > maxWait) {
statusEl.textContent = '等待 D:/cubeOutput.txt 超时';
statusEl.classList.add('error');
statusEl.classList.remove('solved');
return;
}
try {
const res = await fetch(SERVER + '/read-output');
const data = await res.json();
content = (data.content || '').trim();
} catch (e) {}
if (content) break;
await new Promise(r => setTimeout(r, 500));
}
await executeSequence(content);
}
// ---------- 打乱 / 复原 ----------
function resetCubeInternal() {
cubies.forEach(c => {
c.pos.copy(c.home);
c.quat.identity();
});
syncMeshes();
}
function generateScramble(n = 100) {
const faces = Object.keys(LAYER_DEFS);
const seq = [];
let last = null;
for (let i = 0; i < n; i++) {
let f, d;
do {
f = faces[Math.floor(Math.random() * faces.length)];
d = Math.random() < 0.5 ? 1 : -1;
} while (last && last === f);
seq.push({ face: f, dir: d });
last = f;
}
return seq;
}
async function scramble() {
resetCubeInternal();
userSeq = [];
initialState = null;
moveCount = 0;
statusEl.textContent = '打乱中...';
statusEl.classList.remove('solved', 'error');
scrambleSeq = generateScramble(100);
if (scrambleSeq.length === 0) {
statusEl.textContent = '打乱序列为空';
return;
}
for (let i = 0; i < scrambleSeq.length; i++) {
applyInstantMove(scrambleSeq[i].face, scrambleSeq[i].dir);
const isChunkEnd = (i + 1) % 100 === 0;
const isLast = i === scrambleSeq.length - 1;
if (isChunkEnd || isLast) {
refreshVisual();
await new Promise(r => setTimeout(r, 0));
}
}
initialState = getState();
moveCount = 0;
statusEl.textContent = '已打乱,开始复原吧';
statusEl.classList.remove('error');
}
function resetCube() {
resetCubeInternal();
moveCount = 0;
scrambleSeq = [];
userSeq = [];
initialState = null;
statusEl.textContent = '已还原到初始状态';
statusEl.classList.remove('solved', 'error');
refreshVisual();
}
// ---------- 按钮 & 键盘 ----------
function buildButtons() {
document.getElementById('scrambleBtn').addEventListener('click', scramble);
document.getElementById('resetBtn').addEventListener('click', resetCube);
copyBtn.addEventListener('click', copyRecord);
const seqInput = document.getElementById('seqInput');
const seqBtn = document.getElementById('seqBtn');
seqBtn.addEventListener('click', () => executeSequence(seqInput.value));
seqInput.addEventListener('keydown', e => {
if (e.key === 'Enter') {
e.preventDefault();
executeSequence(seqInput.value);
}
});
document.getElementById('autoRunBtn').addEventListener('click', autoRun);
window.addEventListener('keydown', e => {
const key = e.key.toUpperCase();
if (/^[URFDLB]$/.test(key)) {
const dir = e.shiftKey ? -1 : 1;
applyInstantMove(key, dir);
userSeq.push({ face: key, dir });
moveCount++;
refreshVisual();
if (isSolved()) {
statusEl.textContent = '🎉 已复原!共用了 ' + moveCount + ' 步';
statusEl.classList.add('solved');
statusEl.classList.remove('error');
} else {
statusEl.textContent = '步数:' + moveCount;
statusEl.classList.remove('solved', 'error');
}
return;
}
if (/^[XYZ]$/.test(key)) {
const face = key.toLowerCase();
const dir = e.shiftKey ? -1 : 1;
applyInstantMove(face, dir);
userSeq.push({ face, dir });
refreshVisual();
return;
}
});
}
// ---------- 渲染循环 ----------
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
// ---------- 窗口自适应 ----------
function onResize() {
resizeRenderer();
}
window.addEventListener('resize', onResize);
const ro = new ResizeObserver(() => { resizeRenderer(); });
ro.observe(stage);
// ---------- 启动 ----------
buildCube();
buildButtons();
statusEl.textContent = '已就绪';
updateRecordPanel();
setTimeout(() => {
scramble();
}, 100);
animate();
setTimeout(resizeRenderer, 50);
</script>
</body>
</html>

2,自动复原
