二十一 - 四十阶魔方

目录

一,自动复原程序

[二,二十一 - 三十阶魔方(无界面版)](#二,二十一 - 三十阶魔方(无界面版))

1,生成程序

2,生成结果

三,三十阶魔方(网页版)

1,网页版程序

2,自动复原

[四,三十一 - 四十阶魔方(无界面版)](#四,三十一 - 四十阶魔方(无界面版))

1,生成程序

2,生成结果

五,四十阶魔方(网页版)

1,网页版程序

2,自动复原


一,自动复原程序

参考自动复原(终极版本)

二,二十一 - 三十阶魔方(无界面版)

1,生成程序

自动复原(终极版本)、无界面版

把这篇文章里面的2个程序放一起运行,即可得到各阶魔方的随机打乱和复原序列。

2,生成结果

cube21-30

三,三十阶魔方(网页版)

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,生成结果

cube31-40

五,四十阶魔方(网页版)

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,自动复原

相关推荐
专业抄代码选手20 分钟前
06|双缓冲与 Commit:让 Fiber 支持第二次渲染
前端·javascript·react.js
天才熊猫君26 分钟前
通用虚拟滚动表格行拖拽排序:从思路到完整实现
前端·javascript
xiaominlaopodaren30 分钟前
three.js最小地图运行时(九):TileKey 线框与 cursor 坐标探针
javascript·gis·three.js
CarIise31 分钟前
JavaScript进阶与轮播图实现 课堂笔记
开发语言·javascript·笔记
专业抄代码选手32 分钟前
05|把递归渲染拆成 Fiber:让一棵大树可以暂停
前端·javascript·react.js
mayaairi36 分钟前
JS DOM节点操作完全指南:增删改查与性能优化
开发语言·javascript·性能优化
天才熊猫君38 分钟前
Vue 3 插槽机制深度解析:两条线与三层树
前端·javascript
李溪白43 分钟前
事件循环(Event Loop):JavaScript 里的“时间管理大师”与“插队狂魔”
javascript
mONESY44 分钟前
告别 Vibe Coding:用 SDD 把一个 Chrome 翻译插件从 0 做到 1
javascript